refactor(backend): implement default value handling in serialization

The `json_serialize` method has been added to the `TelegramMethod` and
`TelegramObject` classes to replace `Default` placeholders with actual
values from the bot's defaults during JSON serialization. This change
ensures that non-standard objects are handled correctly, maintaining
backward compatibility for built-in pydantic json serialization.

This modification is beneficial as it centralizes the handling of
default values when serializing objects to JSON, making the code more
maintainable and robust against future changes in serialization logic.
This commit is contained in:
zemf4you 2024-04-06 03:26:51 +07:00
parent 7653895dd2
commit 6f4452f4e0
3 changed files with 71 additions and 7 deletions

View file

@ -12,11 +12,16 @@ from typing import (
TypeVar,
)
from pydantic import BaseModel, ConfigDict
from pydantic.functional_validators import model_validator
from pydantic import (
BaseModel,
ConfigDict,
SerializerFunctionWrapHandler,
model_serializer,
model_validator,
)
from aiogram.client.context_controller import BotContextController
from aiogram.client.default import Default, DefaultBotProperties
from ..types import InputFile, ResponseParameters
from ..types.base import UNSET_TYPE
@ -65,6 +70,22 @@ class TelegramMethod(BotContextController, BaseModel, Generic[TelegramType], ABC
return values
return {k: v for k, v in values.items() if not isinstance(v, UNSET_TYPE)}
@model_serializer(mode="wrap", when_used="json")
def json_serialize(self, handler: SerializerFunctionWrapHandler) -> Dict[str, Any]:
"""
Replacing `Default` placeholders with actual values from bot defaults.
Ensures JSON serialization backward compatibility by handling non-standard objects.
"""
if not isinstance(self, TelegramMethod):
return handler(self)
properties = self.bot.default if self.bot else DefaultBotProperties()
default_fields = {
field: properties[value.name]
for field in self.model_fields.keys()
if isinstance(value := getattr(self, field), Default)
}
return handler(self.model_copy(update=default_fields))
if TYPE_CHECKING:
__returning__: ClassVar[type]
__api_method__: ClassVar[str]

View file

@ -1,10 +1,16 @@
from typing import Any, Dict
from unittest.mock import sentinel
from pydantic import BaseModel, ConfigDict, model_validator
from pydantic import (
BaseModel,
ConfigDict,
SerializerFunctionWrapHandler,
model_serializer,
model_validator,
)
from aiogram.client.context_controller import BotContextController
from aiogram.client.default import Default
from aiogram.client.default import Default, DefaultBotProperties
class TelegramObject(BotContextController, BaseModel):
@ -36,6 +42,22 @@ class TelegramObject(BotContextController, BaseModel):
return values
return {k: v for k, v in values.items() if not isinstance(v, UNSET_TYPE)}
@model_serializer(mode="wrap", when_used="json")
def json_serialize(self, handler: SerializerFunctionWrapHandler) -> Dict[str, Any]:
"""
Replacing `Default` placeholders with actual values from bot defaults.
Ensures JSON serialization backward compatibility by handling non-standard objects.
"""
if not isinstance(self, TelegramObject):
return handler(self)
properties = self.bot.default if self.bot else DefaultBotProperties()
default_fields = {
field: properties[value.name]
for field in self.model_fields.keys()
if isinstance(value := getattr(self, field), Default)
}
return handler(self.model_copy(update=default_fields))
class MutableTelegramObject(TelegramObject):
model_config = ConfigDict(

View file

@ -1,9 +1,11 @@
from typing import Any, Dict
from unittest.mock import sentinel
import pytest
from aiogram.methods import GetMe, TelegramMethod
from aiogram.types import TelegramObject, User
from aiogram.client.default import Default
from aiogram.methods import GetMe, SendMessage, TelegramMethod
from aiogram.types import LinkPreviewOptions, TelegramObject, User
from tests.mocked_bot import MockedBot
@ -26,6 +28,25 @@ class TestTelegramMethodRemoveUnset:
assert obj.remove_unset("") == ""
class TestTelegramMethodJsonSerialize:
@pytest.mark.parametrize(
"obj",
[
SendMessage(
chat_id=1,
text="test",
),
LinkPreviewOptions(),
],
)
def test_json_serialize(self, obj):
def has_defaults(dump: Dict[str, Any]) -> bool:
return any(isinstance(value, Default) for value in dump.values())
assert has_defaults(obj.model_dump())
assert not has_defaults(obj.model_dump(mode="json"))
class TestTelegramMethodCall:
async def test_async_emit_unsuccessful(self, bot: MockedBot):
with pytest.raises(