Repository: aiogram/aiogram
Pull request #1230 (2023-07-28 19:18:12)

Removed the use of the context instance (Bot.get_current) from all placements that were used previously.
# Description

Removed the use of the context instance (Bot.get_current) from all placements that were used previously.

github-actions[bot]: # :x: Changelog is required!

You need to add a brief description of the changes to the `CHANGES` directory.

For example, you can run `towncrier create <issue>.<type>` to create a file in the change directory and then write a description on that file.

Read more at [Towncrier docs](https://towncrier.readthedocs.io/en/latest/quickstart.html#creating-news-fragments)

Patch:
diff --git a/CHANGES/1230.removal.rst b/CHANGES/1230.removal.rst
new file mode 100644
index 00000000..bc1bc5b8
--- /dev/null
+++ b/CHANGES/1230.removal.rst
@@ -0,0 +1,2 @@
+Removed the use of the context instance (Bot.get_current) from all placements that were used previously.
+This is to avoid the use of the context instance in the wrong place.
diff --git a/aiogram/client/bot.py b/aiogram/client/bot.py
index c6721c6f..b7cda3ea 100644
--- a/aiogram/client/bot.py
+++ b/aiogram/client/bot.py
@@ -230,7 +230,7 @@ from .session.base import BaseSession
 T = TypeVar("T")
 
 
-class Bot(ContextInstanceMixin["Bot"]):
+class Bot:
     def __init__(
         self,
         token: str,
@@ -284,16 +284,14 @@ class Bot(ContextInstanceMixin["Bot"]):
         """
         Generate bot context
 
-        :param auto_close:
+        :param auto_close: close session on exit
         :return:
         """
-        token = self.set_current(self)
         try:
             yield self
         finally:
             if auto_close:
                 await self.session.close()
-            self.reset_current(token)
 
     async def me(self) -> User:
         """
diff --git a/aiogram/client/context_controller.py b/aiogram/client/context_controller.py
index f998252f..97795a73 100644
--- a/aiogram/client/context_controller.py
+++ b/aiogram/client/context_controller.py
@@ -13,13 +13,6 @@ class BotContextController(BaseModel):
     def model_post_init(self, __context: Any) -> None:
         self._bot = __context.get("bot") if __context else None
 
-    def get_mounted_bot(self) -> Optional["Bot"]:
-        # Properties are not supported in pydantic BaseModel
-        # @computed_field decorator is not a solution for this case in due to
-        # it produces an additional field in model with validation and serialization that
-        # we don't need here
-        return self._bot
-
     def as_(self, bot: Optional["Bot"]) -> Self:
         """
         Bind object to a bot instance.
@@ -29,3 +22,12 @@ class BotContextController(BaseModel):
         """
         self._bot = bot
         return self
+
+    @property
+    def bot(self) -> Optional["Bot"]:
+        """
+        Get bot instance.
+
+        :return: Bot instance
+        """
+        return self._bot
diff --git a/aiogram/dispatcher/dispatcher.py b/aiogram/dispatcher/dispatcher.py
index 64dfac72..eb2d55f2 100644
--- a/aiogram/dispatcher/dispatcher.py
+++ b/aiogram/dispatcher/dispatcher.py
@@ -18,7 +18,7 @@ from ..fsm.strategy import FSMStrategy
 from ..methods import GetUpdates, TelegramMethod
 from ..methods.base import TelegramType
 from ..types import Update, User
-from ..types.base import UNSET_TYPE, UNSET
+from ..types.base import UNSET, UNSET_TYPE
 from ..types.update import UpdateTypeLookupError
 from ..utils.backoff import Backoff, BackoffConfig
 from .event.bases import UNHANDLED, SkipHandler
@@ -143,7 +143,7 @@ class Dispatcher(Router):
         handled = False
         start_time = loop.time()
 
-        if update.get_mounted_bot() != bot:
+        if update.bot != bot:
             # Re-mounting update to the current bot instance for making possible to
             # use it in shortcuts.
             # Here is update is re-created because we need to propagate context to
@@ -184,7 +184,7 @@ class Dispatcher(Router):
         :param update:
         :param kwargs:
         """
-        parsed_update = Update(**update)
+        parsed_update = Update.model_validate(update, context={"bot": bot})
         return await self.feed_update(bot=bot, update=parsed_update, **kwargs)
 
     @classmethod
@@ -558,7 +558,7 @@ class Dispatcher(Router):
         polling_timeout: int = 10,
         handle_as_tasks: bool = True,
         backoff_config: BackoffConfig = DEFAULT_BACKOFF_CONFIG,
-        allowed_updates: Optional[List[str]] = None,
+        allowed_updates: Optional[Union[List[str], UNSET_TYPE]] = UNSET,
         handle_signals: bool = True,
         close_bot_session: bool = True,
         **kwargs: Any,
diff --git a/aiogram/filters/state.py b/aiogram/filters/state.py
index 5ad65ae5..82a141c9 100644
--- a/aiogram/filters/state.py
+++ b/aiogram/filters/state.py
@@ -27,7 +27,7 @@ class StateFilter(Filter):
         )
 
     async def __call__(
-        self, obj: Union[TelegramObject], raw_state: Optional[str] = None
+        self, obj: TelegramObject, raw_state: Optional[str] = None
     ) -> Union[bool, Dict[str, Any]]:
         allowed_states = cast(Sequence[StateType], self.states)
         for allowed_state in allowed_states:
diff --git a/aiogram/handlers/base.py b/aiogram/handlers/base.py
index 076016fa..0eb1b420 100644
--- a/aiogram/handlers/base.py
+++ b/aiogram/handlers/base.py
@@ -32,7 +32,7 @@ class BaseHandler(BaseHandlerMixin[T], ABC):
 
         if "bot" in self.data:
             return cast(Bot, self.data["bot"])
-        return Bot.get_current(no_error=False)
+        raise RuntimeError("Bot instance not found in the context")
 
     @property
     def update(self) -> Update:
diff --git a/aiogram/types/base.py b/aiogram/types/base.py
index b057f59a..641b16df 100644
--- a/aiogram/types/base.py
+++ b/aiogram/types/base.py
@@ -14,6 +14,7 @@ class TelegramObject(BotContextController, BaseModel):
         frozen=True,
         populate_by_name=True,
         arbitrary_types_allowed=True,
+        defer_build=True,
     )
 
 
diff --git a/aiogram/utils/chat_action.py b/aiogram/utils/chat_action.py
index b6bda916..d4778443 100644
--- a/aiogram/utils/chat_action.py
+++ b/aiogram/utils/chat_action.py
@@ -31,22 +31,19 @@ class ChatActionSender:
     def __init__(
         self,
         *,
+        bot: Bot,
         chat_id: Union[str, int],
         action: str = "typing",
         interval: float = DEFAULT_INTERVAL,
         initial_sleep: float = DEFAULT_INITIAL_SLEEP,
-        bot: Optional[Bot] = None,
     ) -> None:
         """
+        :param bot: instance of the bot
         :param chat_id: target chat id
         :param action: chat action type
         :param interval: interval between iterations
         :param initial_sleep: sleep before first iteration
-        :param bot: instance of the bot, can be omitted from the context
         """
-        if bot is None:
-            bot = Bot.get_current(False)
-
         self.chat_id = chat_id
         self.action = action
         self.interval = interval
@@ -132,7 +129,7 @@ class ChatActionSender:
     def typing(
         cls,
         chat_id: Union[int, str],
-        bot: Optional[Bot] = None,
+        bot: Bot,
         interval: float = DEFAULT_INTERVAL,
         initial_sleep: float = DEFAULT_INITIAL_SLEEP,
     ) -> "ChatActionSender":
@@ -149,7 +146,7 @@ class ChatActionSender:
     def upload_photo(
         cls,
         chat_id: Union[int, str],
-        bot: Optional[Bot] = None,
+        bot: Bot,
         interval: float = DEFAULT_INTERVAL,
         initial_sleep: float = DEFAULT_INITIAL_SLEEP,
     ) -> "ChatActionSender":
@@ -166,7 +163,7 @@ class ChatActionSender:
     def record_video(
         cls,
         chat_id: Union[int, str],
-        bot: Optional[Bot] = None,
+        bot: Bot,
         interval: float = DEFAULT_INTERVAL,
         initial_sleep: float = DEFAULT_INITIAL_SLEEP,
     ) -> "ChatActionSender":
@@ -183,7 +180,7 @@ class ChatActionSender:
     def upload_video(
         cls,
         chat_id: Union[int, str],
-        bot: Optional[Bot] = None,
+        bot: Bot,
         interval: float = DEFAULT_INTERVAL,
         initial_sleep: float = DEFAULT_INITIAL_SLEEP,
     ) -> "ChatActionSender":
@@ -200,7 +197,7 @@ class ChatActionSender:
     def record_voice(
         cls,
         chat_id: Union[int, str],
-        bot: Optional[Bot] = None,
+        bot: Bot,
         interval: float = DEFAULT_INTERVAL,
         initial_sleep: float = DEFAULT_INITIAL_SLEEP,
     ) -> "ChatActionSender":
@@ -217,7 +214,7 @@ class ChatActionSender:
     def upload_voice(
         cls,
         chat_id: Union[int, str],
-        bot: Optional[Bot] = None,
+        bot: Bot,
         interval: float = DEFAULT_INTERVAL,
         initial_sleep: float = DEFAULT_INITIAL_SLEEP,
     ) -> "ChatActionSender":
@@ -234,7 +231,7 @@ class ChatActionSender:
     def upload_document(
         cls,
         chat_id: Union[int, str],
-        bot: Optional[Bot] = None,
+        bot: Bot,
         interval: float = DEFAULT_INTERVAL,
         initial_sleep: float = DEFAULT_INITIAL_SLEEP,
     ) -> "ChatActionSender":
@@ -251,7 +248,7 @@ class ChatActionSender:
     def choose_sticker(
         cls,
         chat_id: Union[int, str],
-        bot: Optional[Bot] = None,
+        bot: Bot,
         interval: float = DEFAULT_INTERVAL,
         initial_sleep: float = DEFAULT_INITIAL_SLEEP,
     ) -> "ChatActionSender":
@@ -268,7 +265,7 @@ class ChatActionSender:
     def find_location(
         cls,
         chat_id: Union[int, str],
-        bot: Optional[Bot] = None,
+        bot: Bot,
         interval: float = DEFAULT_INTERVAL,
         initial_sleep: float = DEFAULT_INITIAL_SLEEP,
     ) -> "ChatActionSender":
@@ -285,7 +282,7 @@ class ChatActionSender:
     def record_video_note(
         cls,
         chat_id: Union[int, str],
-        bot: Optional[Bot] = None,
+        bot: Bot,
         interval: float = DEFAULT_INTERVAL,
         initial_sleep: float = DEFAULT_INITIAL_SLEEP,
     ) -> "ChatActionSender":
@@ -302,7 +299,7 @@ class ChatActionSender:
     def upload_video_note(
         cls,
         chat_id: Union[int, str],
-        bot: Optional[Bot] = None,
+        bot: Bot,
         interval: float = DEFAULT_INTERVAL,
         initial_sleep: float = DEFAULT_INITIAL_SLEEP,
     ) -> "ChatActionSender":


Test patch:
diff --git a/tests/conftest.py b/tests/conftest.py
index b7b7774a..1c72f386 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -4,7 +4,7 @@ import pytest
 from _pytest.config import UsageError
 from redis.asyncio.connection import parse_url as parse_redis_url
 
-from aiogram import Bot, Dispatcher
+from aiogram import Dispatcher
 from aiogram.fsm.storage.memory import (
     DisabledEventIsolation,
     MemoryStorage,
@@ -109,12 +109,7 @@ async def disabled_isolation():
 
 @pytest.fixture()
 def bot():
-    bot = MockedBot()
-    token = Bot.set_current(bot)
-    try:
-        yield bot
-    finally:
-        Bot.reset_current(token)
+    return MockedBot()
 
 
 @pytest.fixture()
diff --git a/tests/test_handler/test_base.py b/tests/test_handler/test_base.py
index f79a6a6b..5f3b1225 100644
--- a/tests/test_handler/test_base.py
+++ b/tests/test_handler/test_base.py
@@ -29,14 +29,16 @@ class TestBaseClassBasedHandler:
 
     async def test_bot_from_context(self):
         event = Update(update_id=42)
-        handler = MyHandler(event=event, key=42)
         bot = Bot("42:TEST")
+        handler = MyHandler(event=event, key=42, bot=bot)
+        assert handler.bot == bot
 
-        with pytest.raises(LookupError):
-            handler.bot
+    async def test_bot_from_context_missing(self):
+        event = Update(update_id=42)
+        handler = MyHandler(event=event, key=42)
 
-        Bot.set_current(bot)
-        assert handler.bot == bot
+        with pytest.raises(RuntimeError):
+            handler.bot
 
     async def test_bot_from_data(self):
         event = Update(update_id=42)
diff --git a/tests/test_utils/test_chat_action.py b/tests/test_utils/test_chat_action.py
index 517b8e90..84cb8abb 100644
--- a/tests/test_utils/test_chat_action.py
+++ b/tests/test_utils/test_chat_action.py
@@ -36,10 +36,9 @@ class TestChatActionSender:
             "upload_video_note",
         ],
     )
-    @pytest.mark.parametrize("pass_bot", [True, False])
-    async def test_factory(self, action: str, bot: MockedBot, pass_bot: bool):
+    async def test_factory(self, action: str, bot: MockedBot):
         sender_factory = getattr(ChatActionSender, action)
-        sender = sender_factory(chat_id=42, bot=bot if pass_bot else None)
+        sender = sender_factory(chat_id=42, bot=bot)
         assert isinstance(sender, ChatActionSender)
         assert sender.action == action
         assert sender.chat_id == 42