Repository: aiogram/aiogram
Pull request #666 (2021-08-17 20:41:02)

iter states in states group
# Description

This allows to iterate over all states in StatesGroup. 
Aiogram already had `in` check for states group so this is relative feature.


## Type of change

Please delete options that are not relevant.

- [ ] Documentation (typos, code examples or any documentation update)
- [ ] Bug fix (non-breaking change which fixes an issue)
- [x] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [ ] This change requires a documentation update

# How Has This Been Tested?

Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration

- [x] test_iterable

**Test Configuration**:
* Operating System: Ubuntu 20.04.2 LTS
* Python version: 3.8

# Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my own code
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my feature works
- [x] New and existing unit tests pass locally with my changes


codecov[bot]: # [Codecov](https://codecov.io/gh/aiogram/aiogram/pull/666?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aiogram) Report
> Merging [#666](https://codecov.io/gh/aiogram/aiogram/pull/666?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aiogram) (9f9cfdb) into [dev-3.x](https://codecov.io/gh/aiogram/aiogram/commit/5851e32266f6232538f6212284363b7e5c83378d?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aiogram) (5851e32) will **not change** coverage.
> The diff coverage is `100.00%`.

[![Impacted file tree graph](https://codecov.io/gh/aiogram/aiogram/pull/666/graphs/tree.svg?width=650&height=150&src=pr&token=cFqEr3p9MX&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aiogram)](https://codecov.io/gh/aiogram/aiogram/pull/666?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aiogram)

```diff
@@            Coverage Diff            @@
##           dev-3.x      #666   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files          269       269           
  Lines         5658      5660    +2     
=========================================
+ Hits          5658      5660    +2     
```

| Flag | Coverage Δ | |
|---|---|---|
| unittests | `100.00% <100.00%> (ø)` | |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aiogram#carryforward-flags-in-the-pull-request-comment) to find out more.

| [Impacted Files](https://codecov.io/gh/aiogram/aiogram/pull/666?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aiogram) | Coverage Δ | |
|---|---|---|
| [aiogram/dispatcher/fsm/state.py](https://codecov.io/gh/aiogram/aiogram/pull/666/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aiogram#diff-YWlvZ3JhbS9kaXNwYXRjaGVyL2ZzbS9zdGF0ZS5weQ==) | `100.00% <100.00%> (ø)` | |


Patch:
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/CHANGES/668.misc b/CHANGES/668.misc
new file mode 100644
index 00000000..22fc86e9
--- /dev/null
+++ b/CHANGES/668.misc
@@ -0,0 +1,1 @@
+Disable ContentType filter by default
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,1 @@
+Moved update type detection from Dispatcher to Update object
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,1 @@
+Fixed incorrect type checking in the :class:`aiogram.utils.keyboard.KeyboardBuilder`
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/dispatcher/filters/content_types.py b/aiogram/dispatcher/filters/content_types.py
index 177ecdae..dc96ca23 100644
--- a/aiogram/dispatcher/filters/content_types.py
+++ b/aiogram/dispatcher/filters/content_types.py
@@ -11,10 +11,9 @@ from .base import BaseFilter
 class ContentTypesFilter(BaseFilter):
     """
     Is useful for handling specific types of messages (For example separate text and stickers handlers).
-    This is always automatically adds to the filters list for message handlers.
     """
 
-    content_types: Optional[Union[Sequence[str], str]] = None
+    content_types: Union[Sequence[str], str]
     """Sequence of allowed content types"""
 
     @validator("content_types")
@@ -32,7 +31,4 @@ class ContentTypesFilter(BaseFilter):
         return value
 
     async def __call__(self, message: Message) -> Union[bool, Dict[str, Any]]:
-        if not self.content_types:  # pragma: no cover
-            # Is impossible but needed for valid typechecking
-            self.content_types = [ContentType.TEXT]
         return ContentType.ANY in self.content_types or message.content_type in self.content_types
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"<StatesGroup '{self.__full_group_name__}'>"
 
+    def __iter__(self) -> Iterator[State]:
+        return iter(self.__all_states__)
+
 
 class StatesGroup(metaclass=StatesGroupMeta):
     @classmethod
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/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())
 
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
diff --git a/docs/dispatcher/filters/content_types.rst b/docs/dispatcher/filters/content_types.rst
index 639f2996..c28e6b8a 100644
--- a/docs/dispatcher/filters/content_types.rst
+++ b/docs/dispatcher/filters/content_types.rst
@@ -15,11 +15,6 @@ Can be imported:
 
 Or used from filters factory by passing corresponding arguments to handler registration line
 
-.. warning::
-    **Please be patient!**
-
-    If no one content type filter is specified the :code:`["text"]` value is automatically will be used.
-
 Usage
 =====
 
diff --git a/docs/dispatcher/filters/index.rst b/docs/dispatcher/filters/index.rst
index 010cb0c5..1b970d86 100644
--- a/docs/dispatcher/filters/index.rst
+++ b/docs/dispatcher/filters/index.rst
@@ -37,6 +37,8 @@ Filters can be:
 
 - Subclass of :ref:`BaseFilter <filters-base>`
 
+- Instances of :ref:`MagicFilter <magic-filters>`
+
 Filters should return bool or dict. 
 If the dictionary is passed as result of filter - resulted data will be propagated to the next 
 filters and handler as keywords arguments.
@@ -70,4 +72,7 @@ For example if you need to make simple text filter:
 
 .. note::
 
-    Bound filters is always recursive propagates to the nested routers.
+    Bound filters is always recursive propagates to the nested routers but will be available 
+    in nested routers only after attaching routers so that's mean you will need to
+    include routers before registering handlers.
+
diff --git a/docs/dispatcher/router.rst b/docs/dispatcher/router.rst
index 3e1a1b69..ea13eea7 100644
--- a/docs/dispatcher/router.rst
+++ b/docs/dispatcher/router.rst
@@ -35,6 +35,19 @@ Update
 Message
 -------
 
+
+.. attention::
+
+    Be attentive with filtering this event
+    
+    You should expect than this event can be with different set's of attributes in different cases 
+
+    (For example text, sticker and document is always is different content types of message)
+
+    Recommended way to check field availability before usage or use
+    :class:`aiogram.dispatcher.filters.content_types.ContentTypesFilter`
+
+
 .. code-block:: python
 
     @router.message()


Test patch:
diff --git a/tests/test_dispatcher/test_filters/test_content_types.py b/tests/test_dispatcher/test_filters/test_content_types.py
index 8d1706b2..046073c4 100644
--- a/tests/test_dispatcher/test_filters/test_content_types.py
+++ b/tests/test_dispatcher/test_filters/test_content_types.py
@@ -16,12 +16,6 @@ class MinimalMessage:
 
 
 class TestContentTypesFilter:
-    async def test_validator_empty(self):
-        filter_ = ContentTypesFilter()
-        assert not filter_.content_types
-        await filter_(cast(Message, MinimalMessage(ContentType.TEXT)))
-        assert filter_.content_types == [ContentType.TEXT]
-
     def test_validator_empty_list(self):
         filter_ = ContentTypesFilter(content_types=[])
         assert filter_.content_types == []
@@ -46,7 +40,6 @@ class TestContentTypesFilter:
     @pytest.mark.parametrize(
         "values,content_type,result",
         [
-            [[], ContentType.TEXT, True],
             [[ContentType.TEXT], ContentType.TEXT, True],
             [[ContentType.PHOTO], ContentType.TEXT, False],
             [[ContentType.ANY], ContentType.TEXT, True],
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