diff --git a/CHANGES/1210.misc.rst b/CHANGES/1210.misc.rst new file mode 100644 index 00000000..e4d3589c --- /dev/null +++ b/CHANGES/1210.misc.rst @@ -0,0 +1,6 @@ +Replaced ContextVar's with a new feature called `Validation Context `_ +in Pydantic to improve the clarity, usability, and versatility of handling the Bot instance within method shortcuts. + +.. danger:: + + **Breaking**: The 'bot' argument now is required in `URLInputFile` diff --git a/aiogram/client/context_controller.py b/aiogram/client/context_controller.py new file mode 100644 index 00000000..d2402018 --- /dev/null +++ b/aiogram/client/context_controller.py @@ -0,0 +1,27 @@ +from typing import TYPE_CHECKING, Any, Optional + +from pydantic import BaseModel, PrivateAttr +from typing_extensions import Self + +if TYPE_CHECKING: + from aiogram.client.bot import Bot + + +class BotContextController(BaseModel): + _bot: Optional["Bot"] = PrivateAttr() + + def model_post_init(self, __context: Any) -> None: + if not __context: + self._bot = None + else: + self._bot = __context.get("bot") + + def as_(self, bot: Optional["Bot"]) -> Self: + """ + Bind object to a bot instance. + + :param bot: Bot instance + :return: self + """ + self._bot = bot + return self diff --git a/aiogram/client/session/aiohttp.py b/aiogram/client/session/aiohttp.py index b4c791e8..79e2fa4f 100644 --- a/aiogram/client/session/aiohttp.py +++ b/aiogram/client/session/aiohttp.py @@ -167,7 +167,9 @@ class AiohttpSession(BaseSession): raise TelegramNetworkError(method=method, message="Request timeout error") except ClientError as e: raise TelegramNetworkError(method=method, message=f"{type(e).__name__}: {e}") - response = self.check_response(method=method, status_code=resp.status, content=raw_result) + response = self.check_response( + bot=bot, method=method, status_code=resp.status, content=raw_result + ) return cast(TelegramType, response.result) async def stream_content( diff --git a/aiogram/client/session/base.py b/aiogram/client/session/base.py index afbb2edb..9342cbcc 100644 --- a/aiogram/client/session/base.py +++ b/aiogram/client/session/base.py @@ -75,7 +75,7 @@ class BaseSession(abc.ABC): self.middleware = RequestMiddlewareManager() def check_response( - self, method: TelegramMethod[TelegramType], status_code: int, content: str + self, bot: Bot, method: TelegramMethod[TelegramType], status_code: int, content: str ) -> Response[TelegramType]: """ Check response status @@ -89,7 +89,8 @@ class BaseSession(abc.ABC): raise ClientDecodeError("Failed to decode object", e, content) try: - response = method.build_response(json_data) + response_type = Response[method.__returning__] # type: ignore + response = response_type.model_validate(json_data, context={"bot": bot}) except ValidationError as e: raise ClientDecodeError("Failed to deserialize object", e, json_data) diff --git a/aiogram/methods/base.py b/aiogram/methods/base.py index 53175784..221ad9c0 100644 --- a/aiogram/methods/base.py +++ b/aiogram/methods/base.py @@ -1,11 +1,22 @@ from __future__ import annotations from abc import ABC, abstractmethod -from typing import TYPE_CHECKING, Any, Dict, Generator, Generic, Optional, TypeVar +from typing import ( + TYPE_CHECKING, + Any, + ClassVar, + Dict, + Generator, + Generic, + Optional, + TypeVar, +) from pydantic import BaseModel, ConfigDict from pydantic.functional_validators import model_validator +from aiogram.client.context_controller import BotContextController + from ..types import InputFile, ResponseParameters from ..types.base import UNSET_TYPE @@ -32,7 +43,7 @@ class Response(BaseModel, Generic[TelegramType]): parameters: Optional[ResponseParameters] = None -class TelegramMethod(BaseModel, Generic[TelegramType], ABC): +class TelegramMethod(BotContextController, BaseModel, Generic[TelegramType], ABC): model_config = ConfigDict( extra="allow", populate_by_name=True, @@ -40,6 +51,7 @@ class TelegramMethod(BaseModel, Generic[TelegramType], ABC): ) @model_validator(mode="before") + @classmethod def remove_unset(cls, values: Dict[str, Any]) -> Dict[str, Any]: """ Remove UNSET before fields validation. @@ -51,25 +63,31 @@ class TelegramMethod(BaseModel, Generic[TelegramType], ABC): """ return {k: v for k, v in values.items() if not isinstance(v, UNSET_TYPE)} - @property - @abstractmethod - def __returning__(self) -> type: # pragma: no cover - pass + if TYPE_CHECKING: + __returning__: ClassVar[type] + __api_method__: ClassVar[str] + else: - @property - @abstractmethod - def __api_method__(self) -> str: - pass + @property + @abstractmethod + def __returning__(self) -> type: + pass - def build_response(self, data: Dict[str, Any]) -> Response[TelegramType]: - # noinspection PyTypeChecker - return Response[self.__returning__](**data) # type: ignore + @property + @abstractmethod + def __api_method__(self) -> str: + pass async def emit(self, bot: Bot) -> TelegramType: return await bot(self) def __await__(self) -> Generator[Any, None, TelegramType]: - from aiogram.client.bot import Bot - - bot = Bot.get_current(no_error=False) + bot = self._bot + if not bot: + raise RuntimeError( + "This method is not mounted to a any bot instance, please call it explicilty " + "with bot instance `await bot(method)`\n" + "or mount method to a bot instance `method.as_(bot)` " + "and then call it `await method()`" + ) return self.emit(bot).__await__() diff --git a/aiogram/types/base.py b/aiogram/types/base.py index a9d3eb1a..9d24b51e 100644 --- a/aiogram/types/base.py +++ b/aiogram/types/base.py @@ -3,10 +3,10 @@ from unittest.mock import sentinel from pydantic import BaseModel, ConfigDict -from aiogram.utils.mixins import ContextInstanceMixin +from aiogram.client.context_controller import BotContextController -class TelegramObject(ContextInstanceMixin["TelegramObject"], BaseModel): +class TelegramObject(BotContextController, BaseModel): model_config = ConfigDict( use_enum_values=True, extra="allow", diff --git a/aiogram/types/callback_query.py b/aiogram/types/callback_query.py index 5812bb13..59f9ea66 100644 --- a/aiogram/types/callback_query.py +++ b/aiogram/types/callback_query.py @@ -74,4 +74,4 @@ class CallbackQuery(TelegramObject): url=url, cache_time=cache_time, **kwargs, - ) + ).as_(self._bot) diff --git a/aiogram/types/chat.py b/aiogram/types/chat.py index 4bb6d688..d5d0dfcc 100644 --- a/aiogram/types/chat.py +++ b/aiogram/types/chat.py @@ -164,7 +164,7 @@ class Chat(TelegramObject): chat_id=self.id, sender_chat_id=sender_chat_id, **kwargs, - ) + ).as_(self._bot) def unban_sender_chat( self, @@ -193,7 +193,7 @@ class Chat(TelegramObject): chat_id=self.id, sender_chat_id=sender_chat_id, **kwargs, - ) + ).as_(self._bot) def get_administrators( self, @@ -219,7 +219,7 @@ class Chat(TelegramObject): return GetChatAdministrators( chat_id=self.id, **kwargs, - ) + ).as_(self._bot) def delete_message( self, @@ -266,7 +266,7 @@ class Chat(TelegramObject): chat_id=self.id, message_id=message_id, **kwargs, - ) + ).as_(self._bot) def revoke_invite_link( self, @@ -295,7 +295,7 @@ class Chat(TelegramObject): chat_id=self.id, invite_link=invite_link, **kwargs, - ) + ).as_(self._bot) def edit_invite_link( self, @@ -336,7 +336,7 @@ class Chat(TelegramObject): member_limit=member_limit, creates_join_request=creates_join_request, **kwargs, - ) + ).as_(self._bot) def create_invite_link( self, @@ -374,7 +374,7 @@ class Chat(TelegramObject): member_limit=member_limit, creates_join_request=creates_join_request, **kwargs, - ) + ).as_(self._bot) def export_invite_link( self, @@ -402,7 +402,7 @@ class Chat(TelegramObject): return ExportChatInviteLink( chat_id=self.id, **kwargs, - ) + ).as_(self._bot) def do( self, @@ -438,7 +438,7 @@ class Chat(TelegramObject): action=action, message_thread_id=message_thread_id, **kwargs, - ) + ).as_(self._bot) def delete_sticker_set( self, @@ -464,7 +464,7 @@ class Chat(TelegramObject): return DeleteChatStickerSet( chat_id=self.id, **kwargs, - ) + ).as_(self._bot) def set_sticker_set( self, @@ -493,7 +493,7 @@ class Chat(TelegramObject): chat_id=self.id, sticker_set_name=sticker_set_name, **kwargs, - ) + ).as_(self._bot) def get_member( self, @@ -522,7 +522,7 @@ class Chat(TelegramObject): chat_id=self.id, user_id=user_id, **kwargs, - ) + ).as_(self._bot) def get_member_count( self, @@ -548,7 +548,7 @@ class Chat(TelegramObject): return GetChatMemberCount( chat_id=self.id, **kwargs, - ) + ).as_(self._bot) def leave( self, @@ -574,7 +574,7 @@ class Chat(TelegramObject): return LeaveChat( chat_id=self.id, **kwargs, - ) + ).as_(self._bot) def unpin_all_messages( self, @@ -600,7 +600,7 @@ class Chat(TelegramObject): return UnpinAllChatMessages( chat_id=self.id, **kwargs, - ) + ).as_(self._bot) def unpin_message( self, @@ -629,7 +629,7 @@ class Chat(TelegramObject): chat_id=self.id, message_id=message_id, **kwargs, - ) + ).as_(self._bot) def pin_message( self, @@ -661,7 +661,7 @@ class Chat(TelegramObject): message_id=message_id, disable_notification=disable_notification, **kwargs, - ) + ).as_(self._bot) def set_administrator_custom_title( self, @@ -693,7 +693,7 @@ class Chat(TelegramObject): user_id=user_id, custom_title=custom_title, **kwargs, - ) + ).as_(self._bot) def set_permissions( self, @@ -725,7 +725,7 @@ class Chat(TelegramObject): permissions=permissions, use_independent_chat_permissions=use_independent_chat_permissions, **kwargs, - ) + ).as_(self._bot) def promote( self, @@ -790,7 +790,7 @@ class Chat(TelegramObject): can_pin_messages=can_pin_messages, can_manage_topics=can_manage_topics, **kwargs, - ) + ).as_(self._bot) def restrict( self, @@ -828,7 +828,7 @@ class Chat(TelegramObject): use_independent_chat_permissions=use_independent_chat_permissions, until_date=until_date, **kwargs, - ) + ).as_(self._bot) def unban( self, @@ -860,7 +860,7 @@ class Chat(TelegramObject): user_id=user_id, only_if_banned=only_if_banned, **kwargs, - ) + ).as_(self._bot) def ban( self, @@ -895,7 +895,7 @@ class Chat(TelegramObject): until_date=until_date, revoke_messages=revoke_messages, **kwargs, - ) + ).as_(self._bot) def set_description( self, @@ -924,7 +924,7 @@ class Chat(TelegramObject): chat_id=self.id, description=description, **kwargs, - ) + ).as_(self._bot) def set_title( self, @@ -953,7 +953,7 @@ class Chat(TelegramObject): chat_id=self.id, title=title, **kwargs, - ) + ).as_(self._bot) def delete_photo( self, @@ -979,7 +979,7 @@ class Chat(TelegramObject): return DeleteChatPhoto( chat_id=self.id, **kwargs, - ) + ).as_(self._bot) def set_photo( self, @@ -1008,4 +1008,4 @@ class Chat(TelegramObject): chat_id=self.id, photo=photo, **kwargs, - ) + ).as_(self._bot) diff --git a/aiogram/types/chat_join_request.py b/aiogram/types/chat_join_request.py index ca4f8bb7..94ef61ba 100644 --- a/aiogram/types/chat_join_request.py +++ b/aiogram/types/chat_join_request.py @@ -62,7 +62,7 @@ class ChatJoinRequest(TelegramObject): chat_id=self.chat.id, user_id=self.from_user.id, **kwargs, - ) + ).as_(self._bot) def decline( self, @@ -90,4 +90,4 @@ class ChatJoinRequest(TelegramObject): chat_id=self.chat.id, user_id=self.from_user.id, **kwargs, - ) + ).as_(self._bot) diff --git a/aiogram/types/inline_query.py b/aiogram/types/inline_query.py index 55eab72f..afb76a8b 100644 --- a/aiogram/types/inline_query.py +++ b/aiogram/types/inline_query.py @@ -81,4 +81,4 @@ class InlineQuery(TelegramObject): switch_pm_parameter=switch_pm_parameter, switch_pm_text=switch_pm_text, **kwargs, - ) + ).as_(self._bot) diff --git a/aiogram/types/input_file.py b/aiogram/types/input_file.py index e13c3379..ed0a2433 100644 --- a/aiogram/types/input_file.py +++ b/aiogram/types/input_file.py @@ -4,10 +4,21 @@ import io import os from abc import ABC, abstractmethod from pathlib import Path -from typing import Any, AsyncGenerator, AsyncIterator, Dict, Iterator, Optional, Union +from typing import ( + TYPE_CHECKING, + Any, + AsyncGenerator, + AsyncIterator, + Dict, + Optional, + Union, +) import aiofiles +if TYPE_CHECKING: + from aiogram.client.bot import Bot + DEFAULT_CHUNK_SIZE = 64 * 1024 # 64 kb @@ -110,6 +121,7 @@ class URLInputFile(InputFile): def __init__( self, url: str, + bot: "Bot", headers: Optional[Dict[str, Any]] = None, filename: Optional[str] = None, chunk_size: int = DEFAULT_CHUNK_SIZE, @@ -122,6 +134,9 @@ class URLInputFile(InputFile): :param headers: HTTP Headers :param filename: Filename to be propagated to telegram. :param chunk_size: Uploading chunk size + :param timeout: Timeout for downloading + :param bot: Bot instance to use HTTP session from. + If not specified, will be used current bot from context. """ super().__init__(filename=filename, chunk_size=chunk_size) if headers is None: @@ -130,12 +145,10 @@ class URLInputFile(InputFile): self.url = url self.headers = headers self.timeout = timeout + self.bot = bot async def read(self, chunk_size: int) -> AsyncGenerator[bytes, None]: - from aiogram.client.bot import Bot - - bot = Bot.get_current(no_error=False) - stream = bot.session.stream_content( + stream = self.bot.session.stream_content( url=self.url, headers=self.headers, timeout=self.timeout, diff --git a/aiogram/types/message.py b/aiogram/types/message.py index a057ef97..9339fe7f 100644 --- a/aiogram/types/message.py +++ b/aiogram/types/message.py @@ -418,7 +418,7 @@ class Message(TelegramObject): allow_sending_without_reply=allow_sending_without_reply, reply_markup=reply_markup, **kwargs, - ) + ).as_(self._bot) def answer_animation( self, @@ -490,7 +490,7 @@ class Message(TelegramObject): allow_sending_without_reply=allow_sending_without_reply, reply_markup=reply_markup, **kwargs, - ) + ).as_(self._bot) def reply_audio( self, @@ -559,7 +559,7 @@ class Message(TelegramObject): allow_sending_without_reply=allow_sending_without_reply, reply_markup=reply_markup, **kwargs, - ) + ).as_(self._bot) def answer_audio( self, @@ -629,7 +629,7 @@ class Message(TelegramObject): allow_sending_without_reply=allow_sending_without_reply, reply_markup=reply_markup, **kwargs, - ) + ).as_(self._bot) def reply_contact( self, @@ -685,7 +685,7 @@ class Message(TelegramObject): allow_sending_without_reply=allow_sending_without_reply, reply_markup=reply_markup, **kwargs, - ) + ).as_(self._bot) def answer_contact( self, @@ -742,7 +742,7 @@ class Message(TelegramObject): allow_sending_without_reply=allow_sending_without_reply, reply_markup=reply_markup, **kwargs, - ) + ).as_(self._bot) def reply_document( self, @@ -804,7 +804,7 @@ class Message(TelegramObject): allow_sending_without_reply=allow_sending_without_reply, reply_markup=reply_markup, **kwargs, - ) + ).as_(self._bot) def answer_document( self, @@ -867,7 +867,7 @@ class Message(TelegramObject): allow_sending_without_reply=allow_sending_without_reply, reply_markup=reply_markup, **kwargs, - ) + ).as_(self._bot) def reply_game( self, @@ -912,7 +912,7 @@ class Message(TelegramObject): allow_sending_without_reply=allow_sending_without_reply, reply_markup=reply_markup, **kwargs, - ) + ).as_(self._bot) def answer_game( self, @@ -958,7 +958,7 @@ class Message(TelegramObject): allow_sending_without_reply=allow_sending_without_reply, reply_markup=reply_markup, **kwargs, - ) + ).as_(self._bot) def reply_invoice( self, @@ -1063,7 +1063,7 @@ class Message(TelegramObject): allow_sending_without_reply=allow_sending_without_reply, reply_markup=reply_markup, **kwargs, - ) + ).as_(self._bot) def answer_invoice( self, @@ -1169,7 +1169,7 @@ class Message(TelegramObject): allow_sending_without_reply=allow_sending_without_reply, reply_markup=reply_markup, **kwargs, - ) + ).as_(self._bot) def reply_location( self, @@ -1231,7 +1231,7 @@ class Message(TelegramObject): allow_sending_without_reply=allow_sending_without_reply, reply_markup=reply_markup, **kwargs, - ) + ).as_(self._bot) def answer_location( self, @@ -1294,7 +1294,7 @@ class Message(TelegramObject): allow_sending_without_reply=allow_sending_without_reply, reply_markup=reply_markup, **kwargs, - ) + ).as_(self._bot) def reply_media_group( self, @@ -1336,7 +1336,7 @@ class Message(TelegramObject): protect_content=protect_content, allow_sending_without_reply=allow_sending_without_reply, **kwargs, - ) + ).as_(self._bot) def answer_media_group( self, @@ -1379,7 +1379,7 @@ class Message(TelegramObject): reply_to_message_id=reply_to_message_id, allow_sending_without_reply=allow_sending_without_reply, **kwargs, - ) + ).as_(self._bot) def reply( self, @@ -1435,7 +1435,7 @@ class Message(TelegramObject): allow_sending_without_reply=allow_sending_without_reply, reply_markup=reply_markup, **kwargs, - ) + ).as_(self._bot) def answer( self, @@ -1492,7 +1492,7 @@ class Message(TelegramObject): allow_sending_without_reply=allow_sending_without_reply, reply_markup=reply_markup, **kwargs, - ) + ).as_(self._bot) def reply_photo( self, @@ -1551,7 +1551,7 @@ class Message(TelegramObject): allow_sending_without_reply=allow_sending_without_reply, reply_markup=reply_markup, **kwargs, - ) + ).as_(self._bot) def answer_photo( self, @@ -1611,7 +1611,7 @@ class Message(TelegramObject): allow_sending_without_reply=allow_sending_without_reply, reply_markup=reply_markup, **kwargs, - ) + ).as_(self._bot) def reply_poll( self, @@ -1691,7 +1691,7 @@ class Message(TelegramObject): allow_sending_without_reply=allow_sending_without_reply, reply_markup=reply_markup, **kwargs, - ) + ).as_(self._bot) def answer_poll( self, @@ -1772,7 +1772,7 @@ class Message(TelegramObject): allow_sending_without_reply=allow_sending_without_reply, reply_markup=reply_markup, **kwargs, - ) + ).as_(self._bot) def reply_dice( self, @@ -1819,7 +1819,7 @@ class Message(TelegramObject): allow_sending_without_reply=allow_sending_without_reply, reply_markup=reply_markup, **kwargs, - ) + ).as_(self._bot) def answer_dice( self, @@ -1867,7 +1867,7 @@ class Message(TelegramObject): allow_sending_without_reply=allow_sending_without_reply, reply_markup=reply_markup, **kwargs, - ) + ).as_(self._bot) def reply_sticker( self, @@ -1917,7 +1917,7 @@ class Message(TelegramObject): allow_sending_without_reply=allow_sending_without_reply, reply_markup=reply_markup, **kwargs, - ) + ).as_(self._bot) def answer_sticker( self, @@ -1968,7 +1968,7 @@ class Message(TelegramObject): allow_sending_without_reply=allow_sending_without_reply, reply_markup=reply_markup, **kwargs, - ) + ).as_(self._bot) def reply_venue( self, @@ -2036,7 +2036,7 @@ class Message(TelegramObject): allow_sending_without_reply=allow_sending_without_reply, reply_markup=reply_markup, **kwargs, - ) + ).as_(self._bot) def answer_venue( self, @@ -2105,7 +2105,7 @@ class Message(TelegramObject): allow_sending_without_reply=allow_sending_without_reply, reply_markup=reply_markup, **kwargs, - ) + ).as_(self._bot) def reply_video( self, @@ -2179,7 +2179,7 @@ class Message(TelegramObject): allow_sending_without_reply=allow_sending_without_reply, reply_markup=reply_markup, **kwargs, - ) + ).as_(self._bot) def answer_video( self, @@ -2254,7 +2254,7 @@ class Message(TelegramObject): allow_sending_without_reply=allow_sending_without_reply, reply_markup=reply_markup, **kwargs, - ) + ).as_(self._bot) def reply_video_note( self, @@ -2310,7 +2310,7 @@ class Message(TelegramObject): allow_sending_without_reply=allow_sending_without_reply, reply_markup=reply_markup, **kwargs, - ) + ).as_(self._bot) def answer_video_note( self, @@ -2367,7 +2367,7 @@ class Message(TelegramObject): allow_sending_without_reply=allow_sending_without_reply, reply_markup=reply_markup, **kwargs, - ) + ).as_(self._bot) def reply_voice( self, @@ -2426,7 +2426,7 @@ class Message(TelegramObject): allow_sending_without_reply=allow_sending_without_reply, reply_markup=reply_markup, **kwargs, - ) + ).as_(self._bot) def answer_voice( self, @@ -2486,7 +2486,7 @@ class Message(TelegramObject): allow_sending_without_reply=allow_sending_without_reply, reply_markup=reply_markup, **kwargs, - ) + ).as_(self._bot) def send_copy( # noqa: C901 self: Message, @@ -2684,7 +2684,7 @@ class Message(TelegramObject): allow_sending_without_reply=allow_sending_without_reply, reply_markup=reply_markup, **kwargs, - ) + ).as_(self._bot) def edit_text( self, @@ -2730,7 +2730,7 @@ class Message(TelegramObject): disable_web_page_preview=disable_web_page_preview, reply_markup=reply_markup, **kwargs, - ) + ).as_(self._bot) def forward( self, @@ -2770,7 +2770,7 @@ class Message(TelegramObject): disable_notification=disable_notification, protect_content=protect_content, **kwargs, - ) + ).as_(self._bot) def edit_media( self, @@ -2807,7 +2807,7 @@ class Message(TelegramObject): inline_message_id=inline_message_id, reply_markup=reply_markup, **kwargs, - ) + ).as_(self._bot) def edit_reply_markup( self, @@ -2841,7 +2841,7 @@ class Message(TelegramObject): inline_message_id=inline_message_id, reply_markup=reply_markup, **kwargs, - ) + ).as_(self._bot) def delete_reply_markup(self) -> EditMessageReplyMarkup: return self.edit_reply_markup(reply_markup=None) @@ -2893,7 +2893,7 @@ class Message(TelegramObject): proximity_alert_radius=proximity_alert_radius, reply_markup=reply_markup, **kwargs, - ) + ).as_(self._bot) def stop_live_location( self, @@ -2927,7 +2927,7 @@ class Message(TelegramObject): inline_message_id=inline_message_id, reply_markup=reply_markup, **kwargs, - ) + ).as_(self._bot) def edit_caption( self, @@ -2970,7 +2970,7 @@ class Message(TelegramObject): caption_entities=caption_entities, reply_markup=reply_markup, **kwargs, - ) + ).as_(self._bot) def delete( self, @@ -3016,7 +3016,7 @@ class Message(TelegramObject): chat_id=self.chat.id, message_id=self.message_id, **kwargs, - ) + ).as_(self._bot) def pin( self, @@ -3047,7 +3047,7 @@ class Message(TelegramObject): message_id=self.message_id, disable_notification=disable_notification, **kwargs, - ) + ).as_(self._bot) def unpin( self, @@ -3075,7 +3075,7 @@ class Message(TelegramObject): chat_id=self.chat.id, message_id=self.message_id, **kwargs, - ) + ).as_(self._bot) def get_url(self, force_private: bool = False) -> Optional[str]: """ diff --git a/aiogram/types/sticker.py b/aiogram/types/sticker.py index 1bac276f..cb453b32 100644 --- a/aiogram/types/sticker.py +++ b/aiogram/types/sticker.py @@ -76,7 +76,7 @@ class Sticker(TelegramObject): sticker=self.file_id, position=position, **kwargs, - ) + ).as_(self._bot) def delete_from_set( self, @@ -102,4 +102,4 @@ class Sticker(TelegramObject): return DeleteStickerFromSet( sticker=self.file_id, **kwargs, - ) + ).as_(self._bot) diff --git a/aiogram/types/user.py b/aiogram/types/user.py index 3b71af19..de1941fb 100644 --- a/aiogram/types/user.py +++ b/aiogram/types/user.py @@ -90,4 +90,4 @@ class User(TelegramObject): offset=offset, limit=limit, **kwargs, - ) + ).as_(self._bot) diff --git a/tests/mocked_bot.py b/tests/mocked_bot.py index 680e4883..af86098f 100644 --- a/tests/mocked_bot.py +++ b/tests/mocked_bot.py @@ -35,7 +35,7 @@ class MockedSession(BaseSession): self.requests.append(method) response: Response[TelegramType] = self.responses.pop() self.check_response( - method=method, status_code=response.error_code, content=response.json() + bot=bot, method=method, status_code=response.error_code, content=response.json() ) return response.result # type: ignore diff --git a/tests/test_api/test_client/test_context_controller.py b/tests/test_api/test_client/test_context_controller.py new file mode 100644 index 00000000..25643b27 --- /dev/null +++ b/tests/test_api/test_client/test_context_controller.py @@ -0,0 +1,36 @@ +from aiogram.client.context_controller import BotContextController +from tests.mocked_bot import MockedBot + + +class MyModel(BotContextController): + id: int + + +class TestBotContextController: + def test_via_model_validate(self, bot: MockedBot): + my_model = MyModel.model_validate({"id": 1}, context={"bot": bot}) + assert my_model.id == 1 + assert my_model._bot == bot + + def test_via_model_validate_none(self): + my_model = MyModel.model_validate({"id": 1}, context={}) + assert my_model.id == 1 + assert my_model._bot is None + + def test_as(self, bot: MockedBot): + my_model = MyModel(id=1).as_(bot) + assert my_model.id == 1 + assert my_model._bot == bot + + def test_as_none(self): + my_model = MyModel(id=1).as_(None) + assert my_model.id == 1 + assert my_model._bot is None + + def test_replacement(self, bot: MockedBot): + my_model = MyModel(id=1).as_(bot) + assert my_model.id == 1 + assert my_model._bot == bot + my_model = my_model.as_(None) + assert my_model.id == 1 + assert my_model._bot is None diff --git a/tests/test_api/test_client/test_session/test_base_session.py b/tests/test_api/test_client/test_session/test_base_session.py index 5d1ebef1..3793f22a 100644 --- a/tests/test_api/test_client/test_session/test_base_session.py +++ b/tests/test_api/test_client/test_session/test_base_session.py @@ -170,9 +170,11 @@ class TestBaseSession: ) def test_check_response(self, status_code, content, error): session = CustomSession() + bot = MockedBot() method = DeleteMessage(chat_id=42, message_id=42) if error is None: session.check_response( + bot=bot, method=method, status_code=status_code, content=content, @@ -180,6 +182,7 @@ class TestBaseSession: else: with pytest.raises(error) as exc_info: session.check_response( + bot=bot, method=method, status_code=status_code, content=content, @@ -191,10 +194,12 @@ class TestBaseSession: def test_check_response_json_decode_error(self): session = CustomSession() + bot = MockedBot() method = DeleteMessage(chat_id=42, message_id=42) with pytest.raises(ClientDecodeError, match="JSONDecodeError"): session.check_response( + bot=bot, method=method, status_code=200, content="is not a JSON object", @@ -202,10 +207,12 @@ class TestBaseSession: def test_check_response_validation_error(self): session = CustomSession() + bot = MockedBot() method = DeleteMessage(chat_id=42, message_id=42) with pytest.raises(ClientDecodeError, match="ValidationError"): session.check_response( + bot=bot, method=method, status_code=200, content='{"ok": "test"}', diff --git a/tests/test_api/test_methods/test_base.py b/tests/test_api/test_methods/test_base.py index f2351d40..9626c9b7 100644 --- a/tests/test_api/test_methods/test_base.py +++ b/tests/test_api/test_methods/test_base.py @@ -22,6 +22,14 @@ class TestTelegramMethodRemoveUnset: class TestTelegramMethodCall: + async def test_async_emit_unsuccessful(self, bot: MockedBot): + with pytest.raises( + RuntimeError, + match="This method is not mounted to a any bot instance.+", + ): + await GetMe() + async def test_async_emit(self, bot: MockedBot): bot.add_result_for(GetMe, ok=True, result=User(id=42, is_bot=True, first_name="Test")) - assert isinstance(await GetMe(), User) + method = GetMe().as_(bot) + assert isinstance(await method, User) diff --git a/tests/test_api/test_types/test_input_file.py b/tests/test_api/test_types/test_input_file.py index 05391a8e..81e80ad5 100644 --- a/tests/test_api/test_types/test_input_file.py +++ b/tests/test_api/test_types/test_input_file.py @@ -4,6 +4,7 @@ from aresponses import ResponsesMockServer from aiogram import Bot from aiogram.types import BufferedInputFile, FSInputFile, InputFile, URLInputFile +from tests.mocked_bot import MockedBot class TestInputFile: @@ -72,10 +73,8 @@ class TestInputFile: aresponses.add( aresponses.ANY, aresponses.ANY, "get", aresponses.Response(status=200, body=b"\f" * 10) ) - - Bot.set_current(Bot("42:TEST")) - - file = URLInputFile("https://test.org/", chunk_size=1) + bot = Bot(token="42:TEST") + file = URLInputFile("https://test.org/", bot, chunk_size=1) size = 0 async for chunk in file: