From 6f4452f4e0ebc7052b8c74760fe949a730a7a4ba Mon Sep 17 00:00:00 2001 From: zemf4you Date: Sat, 6 Apr 2024 03:26:51 +0700 Subject: [PATCH] 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. --- aiogram/methods/base.py | 27 +++++++++++++++++++++--- aiogram/types/base.py | 26 +++++++++++++++++++++-- tests/test_api/test_methods/test_base.py | 25 ++++++++++++++++++++-- 3 files changed, 71 insertions(+), 7 deletions(-) diff --git a/aiogram/methods/base.py b/aiogram/methods/base.py index b2d35bec..66d4dde6 100644 --- a/aiogram/methods/base.py +++ b/aiogram/methods/base.py @@ -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] diff --git a/aiogram/types/base.py b/aiogram/types/base.py index f97cf03a..aa13c80a 100644 --- a/aiogram/types/base.py +++ b/aiogram/types/base.py @@ -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( diff --git a/tests/test_api/test_methods/test_base.py b/tests/test_api/test_methods/test_base.py index 085650f3..66cfd101 100644 --- a/tests/test_api/test_methods/test_base.py +++ b/tests/test_api/test_methods/test_base.py @@ -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(