From 7c0e229301daa7773908ec561141163ad748e9f0 Mon Sep 17 00:00:00 2001 From: Daniil Kovalenko <40635760+WhiteMemory99@users.noreply.github.com> Date: Fri, 27 Aug 2021 18:36:03 +0700 Subject: [PATCH 1/3] Fix incorrect type checking in KeyboardBuilder (#674) * Fix incorrect type checking in KeyboardBuilder * Add a patch note * Update CHANGES/674.bugfix Co-authored-by: Alex Root Junior Co-authored-by: Alex Root Junior --- CHANGES/674.bugfix | 1 + aiogram/utils/keyboard.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 CHANGES/674.bugfix diff --git a/CHANGES/674.bugfix b/CHANGES/674.bugfix new file mode 100644 index 00000000..61d3a816 --- /dev/null +++ b/CHANGES/674.bugfix @@ -0,0 +1 @@ +Fixed incorrect type checking in the :class:`aiogram.utils.keyboard.KeyboardBuilder` diff --git a/aiogram/utils/keyboard.py b/aiogram/utils/keyboard.py index a8e31c17..56c5f1ff 100644 --- a/aiogram/utils/keyboard.py +++ b/aiogram/utils/keyboard.py @@ -232,7 +232,7 @@ class KeyboardBuilder(Generic[ButtonType]): return self.add(button) def as_markup(self, **kwargs: Any) -> Union[InlineKeyboardMarkup, ReplyKeyboardMarkup]: - if self._button_type is ReplyKeyboardMarkup: + if self._button_type is KeyboardButton: return ReplyKeyboardMarkup(keyboard=self.export(), **kwargs) return InlineKeyboardMarkup(inline_keyboard=self.export()) From 714ac8896c97fc63bdee20d68b1436af4f1f9836 Mon Sep 17 00:00:00 2001 From: darksidecat <58224121+darksidecat@users.noreply.github.com> Date: Sun, 5 Sep 2021 23:49:23 +0300 Subject: [PATCH 2/3] Move update type detecting from `Dispatcher` to `Update` (#669) * move update type detecting to Update * requested changes * fix typo * requested changes * add docstring * Update CHANGES/669.misc Co-authored-by: Alex Root Junior * move mypy hack to utils, add lru_cache configuration * More accurate description of the exception, thanks @uwinx * Update CHANGES/669.misc Co-authored-by: evgfilim1 * Callable import fix Co-authored-by: evgfilim1 Co-authored-by: Alex Root Junior Co-authored-by: evgfilim1 --- CHANGES/669.misc | 1 + aiogram/dispatcher/dispatcher.py | 48 ++++------------------------- aiogram/types/update.py | 52 +++++++++++++++++++++++++++++++- aiogram/utils/mypy_hacks.py | 16 ++++++++++ 4 files changed, 74 insertions(+), 43 deletions(-) create mode 100644 CHANGES/669.misc create mode 100644 aiogram/utils/mypy_hacks.py diff --git a/CHANGES/669.misc b/CHANGES/669.misc new file mode 100644 index 00000000..2410f4c1 --- /dev/null +++ b/CHANGES/669.misc @@ -0,0 +1 @@ +Moved update type detection from Dispatcher to Update object diff --git a/aiogram/dispatcher/dispatcher.py b/aiogram/dispatcher/dispatcher.py index 2f4bb1ba..fffe0262 100644 --- a/aiogram/dispatcher/dispatcher.py +++ b/aiogram/dispatcher/dispatcher.py @@ -9,7 +9,8 @@ from typing import Any, AsyncGenerator, Dict, List, Optional, Union from .. import loggers from ..client.bot import Bot from ..methods import GetUpdates, TelegramMethod -from ..types import TelegramObject, Update, User +from ..types import Update, User +from ..types.update import UpdateTypeLookupError from ..utils.backoff import Backoff, BackoffConfig from ..utils.exceptions.base import TelegramAPIError from ..utils.exceptions.network import NetworkError @@ -186,47 +187,10 @@ class Dispatcher(Router): :param kwargs: :return: """ - event: TelegramObject - if update.message: - update_type = "message" - event = update.message - elif update.edited_message: - update_type = "edited_message" - event = update.edited_message - elif update.channel_post: - update_type = "channel_post" - event = update.channel_post - elif update.edited_channel_post: - update_type = "edited_channel_post" - event = update.edited_channel_post - elif update.inline_query: - update_type = "inline_query" - event = update.inline_query - elif update.chosen_inline_result: - update_type = "chosen_inline_result" - event = update.chosen_inline_result - elif update.callback_query: - update_type = "callback_query" - event = update.callback_query - elif update.shipping_query: - update_type = "shipping_query" - event = update.shipping_query - elif update.pre_checkout_query: - update_type = "pre_checkout_query" - event = update.pre_checkout_query - elif update.poll: - update_type = "poll" - event = update.poll - elif update.poll_answer: - update_type = "poll_answer" - event = update.poll_answer - elif update.my_chat_member: - update_type = "my_chat_member" - event = update.my_chat_member - elif update.chat_member: - update_type = "chat_member" - event = update.chat_member - else: + try: + update_type = update.event_type + event = update.event + except UpdateTypeLookupError: warnings.warn( "Detected unknown update type.\n" "Seems like Telegram Bot API was updated and you have " diff --git a/aiogram/types/update.py b/aiogram/types/update.py index 3e43a316..7b54195c 100644 --- a/aiogram/types/update.py +++ b/aiogram/types/update.py @@ -1,7 +1,8 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING, Optional, cast +from ..utils.mypy_hacks import lru_cache from .base import TelegramObject if TYPE_CHECKING: # pragma: no cover @@ -53,3 +54,52 @@ class Update(TelegramObject): """*Optional*. The bot's chat member status was updated in a chat. For private chats, this update is received only when the bot is blocked or unblocked by the user.""" chat_member: Optional[ChatMemberUpdated] = None """*Optional*. A chat member's status was updated in a chat. The bot must be an administrator in the chat and must explicitly specify 'chat_member' in the list of *allowed_updates* to receive these updates.""" + + def __hash__(self) -> int: + return hash((type(self), self.update_id)) + + @property # type: ignore + @lru_cache() + def event_type(self) -> str: + """ + Detect update type + If update type is unknown, raise UpdateTypeLookupError + + :return: + """ + if self.message: + return "message" + if self.edited_message: + return "edited_message" + if self.channel_post: + return "channel_post" + if self.edited_channel_post: + return "edited_channel_post" + if self.inline_query: + return "inline_query" + if self.chosen_inline_result: + return "chosen_inline_result" + if self.callback_query: + return "callback_query" + if self.shipping_query: + return "shipping_query" + if self.pre_checkout_query: + return "pre_checkout_query" + if self.poll: + return "poll" + if self.poll_answer: + return "poll_answer" + if self.my_chat_member: + return "my_chat_member" + if self.chat_member: + return "chat_member" + + raise UpdateTypeLookupError("Update does not contain any known event type.") + + @property + def event(self) -> TelegramObject: + return cast(TelegramObject, getattr(self, self.event_type)) + + +class UpdateTypeLookupError(LookupError): + """Update does not contain any known event type.""" diff --git a/aiogram/utils/mypy_hacks.py b/aiogram/utils/mypy_hacks.py new file mode 100644 index 00000000..ea47a9dc --- /dev/null +++ b/aiogram/utils/mypy_hacks.py @@ -0,0 +1,16 @@ +import functools +from typing import Callable, TypeVar + +T = TypeVar("T") + + +def lru_cache(maxsize: int = 128, typed: bool = False) -> Callable[[T], T]: + """ + fix: lru_cache annotation doesn't work with a property + this hack is only needed for the property, so type annotations are as they are + """ + + def wrapper(func: T) -> T: + return functools.lru_cache(maxsize, typed)(func) # type: ignore + + return wrapper From 90b3a990396dc65ecc1029997a349c5555ea382c Mon Sep 17 00:00:00 2001 From: Andrey Tikhonov Date: Sun, 5 Sep 2021 23:55:38 +0300 Subject: [PATCH 3/3] iter states in states group (#666) * iter states in states group * fix type hint * remove empty line * add changes for doc --- CHANGES/666.feature | 2 ++ aiogram/dispatcher/fsm/state.py | 5 ++++- tests/test_dispatcher/test_fsm/test_state.py | 7 +++++++ 3 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 CHANGES/666.feature diff --git a/CHANGES/666.feature b/CHANGES/666.feature new file mode 100644 index 00000000..35a6572e --- /dev/null +++ b/CHANGES/666.feature @@ -0,0 +1,2 @@ +Ability to iterate over all states in StatesGroup. +Aiogram already had in check for states group so this is relative feature. diff --git a/aiogram/dispatcher/fsm/state.py b/aiogram/dispatcher/fsm/state.py index ced9779a..a034e003 100644 --- a/aiogram/dispatcher/fsm/state.py +++ b/aiogram/dispatcher/fsm/state.py @@ -1,5 +1,5 @@ import inspect -from typing import Any, Optional, Tuple, Type, no_type_check +from typing import Any, Iterator, Optional, Tuple, Type, no_type_check from ...types import TelegramObject @@ -118,6 +118,9 @@ class StatesGroupMeta(type): def __str__(self) -> str: return f"" + def __iter__(self) -> Iterator[State]: + return iter(self.__all_states__) + class StatesGroup(metaclass=StatesGroupMeta): @classmethod diff --git a/tests/test_dispatcher/test_fsm/test_state.py b/tests/test_dispatcher/test_fsm/test_state.py index 04c8e448..3d41e1fa 100644 --- a/tests/test_dispatcher/test_fsm/test_state.py +++ b/tests/test_dispatcher/test_fsm/test_state.py @@ -150,6 +150,13 @@ class TestStatesGroup: assert MyGroup.MyNestedGroup.get_root() is MyGroup + def test_iterable(self): + class Group(StatesGroup): + x = State() + y = State() + + assert set(Group) == {Group.x, Group.y} + def test_empty_filter(self): class MyGroup(StatesGroup): pass