Repository: aiogram/aiogram
Pull request #976 (2022-08-14 14:05:10)

Deprecate filters factory
# Description

Deprecate filters factory as described in #942

## Type of change

- Documentation (typos, code examples or any documentation update)

# Checklist:

- [x] My code follows the style guidelines of this project
- [ ] I have performed a self-review of my own code
- [x] I have added tests that prove my fix is effective or that my feature works as expected
- [x] New and existing unit tests pass locally with my changes
- [x] My changes generate no new warnings or errors
- [x] My changes are compatible with minimum requirements of the project
- [ ] I have made corresponding changes to the documentation


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)
codecov[bot]: # [Codecov](https://codecov.io/gh/aiogram/aiogram/pull/976?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aiogram) Report
> Merging [#976](https://codecov.io/gh/aiogram/aiogram/pull/976?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aiogram) (1a76d86) into [dev-3.x](https://codecov.io/gh/aiogram/aiogram/commit/c1341ba2df24b4a51c9df138e19d45e12cd6f02b?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aiogram) (c1341ba) will **not change** coverage.
> The diff coverage is `100.00%`.

[![Impacted file tree graph](https://codecov.io/gh/aiogram/aiogram/pull/976/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/976?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      #976   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files          303       303           
  Lines         8211      8216    +5     
=========================================
+ Hits          8211      8216    +5     
```

| 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/976?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aiogram) | Coverage Δ | |
|---|---|---|
| [aiogram/dispatcher/event/telegram.py](https://codecov.io/gh/aiogram/aiogram/pull/976/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aiogram#diff-YWlvZ3JhbS9kaXNwYXRjaGVyL2V2ZW50L3RlbGVncmFtLnB5) | `100.00% <100.00%> (ø)` | |
| [aiogram/filters/\_\_init\_\_.py](https://codecov.io/gh/aiogram/aiogram/pull/976/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aiogram#diff-YWlvZ3JhbS9maWx0ZXJzL19faW5pdF9fLnB5) | `100.00% <100.00%> (ø)` | |


Patch:
diff --git a/CHANGES/942.misc.rst b/CHANGES/942.misc.rst
new file mode 100644
index 00000000..09aa341c
--- /dev/null
+++ b/CHANGES/942.misc.rst
@@ -0,0 +1,1 @@
+Deprecated filters factory. It will be removed in next Beta (3.0b5)
diff --git a/aiogram/dispatcher/event/telegram.py b/aiogram/dispatcher/event/telegram.py
index 8dd438bc..d5170594 100644
--- a/aiogram/dispatcher/event/telegram.py
+++ b/aiogram/dispatcher/event/telegram.py
@@ -11,6 +11,7 @@ from aiogram.dispatcher.middlewares.manager import MiddlewareManager
 from aiogram.filters.base import BaseFilter
 
 from ...exceptions import FiltersResolveError
+from ...filters import BUILTIN_FILTERS_SET
 from ...types import TelegramObject
 from .bases import REJECTED, UNHANDLED, MiddlewareType, SkipHandler
 from .handler import CallbackType, FilterObject, HandlerObject
@@ -24,7 +25,7 @@ class TelegramEventObserver:
     Event observer for Telegram events
 
     Here you can register handler with filters or bounded filters which can be used as keyword arguments instead of writing full references when you register new handlers.
-    This observer will stops event propagation when first handler is pass.
+    This observer will stop event propagation when first handler is pass.
     """
 
     def __init__(self, router: Router, event_name: str) -> None:
@@ -41,14 +42,16 @@ class TelegramEventObserver:
         # with dummy callback which never will be used
         self._handler = HandlerObject(callback=lambda: True, filters=[])
 
-    def filter(self, *filters: CallbackType, **bound_filters: Any) -> None:
+    def filter(self, *filters: CallbackType, _stacklevel: int = 2, **bound_filters: Any) -> None:
         """
         Register filter for all handlers of this event observer
 
         :param filters: positional filters
         :param bound_filters: keyword filters
         """
-        resolved_filters = self.resolve_filters(filters, bound_filters)
+        resolved_filters = self.resolve_filters(
+            filters, bound_filters, _stacklevel=_stacklevel + 1
+        )
         if self._handler.filters is None:
             self._handler.filters = []
         self._handler.filters.extend(
@@ -67,14 +70,18 @@ class TelegramEventObserver:
 
         :param bound_filter:
         """
-        # TODO: This functionality should be deprecated in the future
-        #  in due to bound filter has uncontrollable ordering and
-        #  makes debugging process is harder that explicit using filters
-
         if not isclass(bound_filter) or not issubclass(bound_filter, BaseFilter):
             raise TypeError(
                 "bound_filter() argument 'bound_filter' must be subclass of BaseFilter"
             )
+        if bound_filter not in BUILTIN_FILTERS_SET:
+            warnings.warn(
+                category=DeprecationWarning,
+                message="filters factory deprecated and will be removed in 3.0b5,"
+                " use filters directly instead (Example: "
+                f"`{bound_filter.__name__}(<argument>=<value>)` instead of `<argument>=<value>`)",
+                stacklevel=2,
+            )
         self.filters.append(bound_filter)
 
     def _resolve_filters_chain(self) -> Generator[Type[BaseFilter], None, None]:
@@ -106,6 +113,7 @@ class TelegramEventObserver:
         filters: Tuple[CallbackType, ...],
         full_config: Dict[str, Any],
         ignore_default: bool = True,
+        _stacklevel: int = 2,
     ) -> List[BaseFilter]:
         """
         Resolve keyword filters via filters factory
@@ -164,11 +172,11 @@ class TelegramEventObserver:
         if bound_filters:
             warnings.warn(
                 category=DeprecationWarning,
-                message="Filters factory deprecated and will be removed in Beta 5. "
+                message="Filters factory deprecated and will be removed in 3.0b5.\n"
                 "Use filters directly, for example instead of "
                 "`@router.message(commands=['help']')` "
                 "use `@router.message(Command(commands=['help'])`",
-                stacklevel=3,
+                stacklevel=_stacklevel,
             )
         return bound_filters
 
@@ -177,6 +185,7 @@ class TelegramEventObserver:
         callback: CallbackType,
         *filters: CallbackType,
         flags: Optional[Dict[str, Any]] = None,
+        _stacklevel: int = 2,
         **bound_filters: Any,
     ) -> CallbackType:
         """
@@ -184,7 +193,12 @@ class TelegramEventObserver:
         """
         if flags is None:
             flags = {}
-        resolved_filters = self.resolve_filters(filters, bound_filters, ignore_default=False)
+        resolved_filters = self.resolve_filters(
+            filters,
+            bound_filters,
+            ignore_default=False,
+            _stacklevel=_stacklevel + 1,
+        )
         for resolved_filter in resolved_filters:
             resolved_filter.update_handler_flags(flags=flags)
         self.handlers.append(
@@ -238,14 +252,20 @@ class TelegramEventObserver:
         return UNHANDLED
 
     def __call__(
-        self, *args: CallbackType, flags: Optional[Dict[str, Any]] = None, **bound_filters: Any
+        self,
+        *args: CallbackType,
+        flags: Optional[Dict[str, Any]] = None,
+        _stacklevel: int = 2,
+        **bound_filters: Any,
     ) -> Callable[[CallbackType], CallbackType]:
         """
         Decorator for registering event handlers
         """
 
         def wrapper(callback: CallbackType) -> CallbackType:
-            self.register(callback, *args, flags=flags, **bound_filters)
+            self.register(
+                callback, *args, flags=flags, **bound_filters, _stacklevel=_stacklevel + 1
+            )
             return callback
 
         return wrapper
diff --git a/aiogram/filters/__init__.py b/aiogram/filters/__init__.py
index 6c73bacf..2626d51e 100644
--- a/aiogram/filters/__init__.py
+++ b/aiogram/filters/__init__.py
@@ -1,3 +1,4 @@
+from itertools import chain
 from typing import Dict, Tuple, Type
 
 from .base import BaseFilter
@@ -134,3 +135,5 @@ BUILTIN_FILTERS: Dict[str, Tuple[Type[BaseFilter], ...]] = {
         *_ALL_EVENTS_FILTERS,
     ),
 }
+
+BUILTIN_FILTERS_SET = set(chain.from_iterable(BUILTIN_FILTERS.values()))
diff --git a/docs/dispatcher/filters/chat_member_updated.rst b/docs/dispatcher/filters/chat_member_updated.rst
index d6ba10c9..dd283171 100644
--- a/docs/dispatcher/filters/chat_member_updated.rst
+++ b/docs/dispatcher/filters/chat_member_updated.rst
@@ -86,10 +86,10 @@ Handle user leave or join events
 
     from aiogram.filters import IS_MEMBER, IS_NOT_MEMBER
 
-    @router.chat_member(member_status_changed=IS_MEMBER >> IS_NOT_MEMBER)
+    @router.chat_member(ChatMemberUpdatedFilter(member_status_changed=IS_MEMBER >> IS_NOT_MEMBER))
     async def on_user_leave(event: ChatMemberUpdated): ...
 
-    @router.chat_member(member_status_changed=IS_NOT_MEMBER >> IS_MEMBER)
+    @router.chat_member(ChatMemberUpdatedFilter(member_status_changed=IS_NOT_MEMBER >> IS_MEMBER))
     async def on_user_join(event: ChatMemberUpdated): ...
 
 Or construct your own terms via using pre-defined set of statuses and transitions.
diff --git a/docs/dispatcher/filters/command.rst b/docs/dispatcher/filters/command.rst
index ecded283..7281d751 100644
--- a/docs/dispatcher/filters/command.rst
+++ b/docs/dispatcher/filters/command.rst
@@ -21,8 +21,7 @@ Usage
 1. Filter single variant of commands: :code:`Command(commands=["start"])` or :code:`Command(commands="start")`
 2. Handle command by regexp pattern: :code:`Command(commands=[re.compile(r"item_(\d+)")])`
 3. Match command by multiple variants: :code:`Command(commands=["item", re.compile(r"item_(\d+)")])`
-4. Handle commands in public chats intended for other bots: :code:`Command(commands=["command"], commands)`
-5. As keyword argument in registerer: :code:`@router.message(commands=["help"])`
+4. Handle commands in public chats intended for other bots: :code:`Command(commands=["command"], commands_ignore_mention=True)`
 
 .. warning::
 
diff --git a/docs/dispatcher/filters/index.rst b/docs/dispatcher/filters/index.rst
index e7507ba1..d9e56c75 100644
--- a/docs/dispatcher/filters/index.rst
+++ b/docs/dispatcher/filters/index.rst
@@ -2,6 +2,13 @@
 Filtering events
 ================
 
+
+.. danger::
+
+    Note that the design of filters will be changed in 3.0b5
+
+    `Read more >> <https://github.com/aiogram/aiogram/issues/942>`_
+
 Filters is needed for routing updates to the specific handler.
 Searching of handler is always stops on first match set of filters are pass.
 
diff --git a/docs/dispatcher/filters/magic_data.rst b/docs/dispatcher/filters/magic_data.rst
index 20336d17..86294a69 100644
--- a/docs/dispatcher/filters/magic_data.rst
+++ b/docs/dispatcher/filters/magic_data.rst
@@ -17,7 +17,7 @@ Or used from filters factory by passing corresponding arguments to handler regis
 Usage
 =====
 
-#. :code:`magic_data=F.event.from_user.id == F.config.admin_id`  (Note that :code:`config` should be passed from middleware)
+#. :code:`MagicData(magic_data=F.event.from_user.id == F.config.admin_id)`  (Note that :code:`config` should be passed from middleware)
 
 
 Allowed handlers
diff --git a/examples/echo_bot.py b/examples/echo_bot.py
index 9097c7ac..29cf3b52 100644
--- a/examples/echo_bot.py
+++ b/examples/echo_bot.py
@@ -1,6 +1,7 @@
 import logging
 
 from aiogram import Bot, Dispatcher, types
+from aiogram.filters import Command
 from aiogram.types import Message
 
 TOKEN = "42:TOKEN"
@@ -9,7 +10,7 @@ dp = Dispatcher()
 logger = logging.getLogger(__name__)
 
 
-@dp.message(commands=["start"])
+@dp.message(Command(commands=["start"]))
 async def command_start_handler(message: Message) -> None:
     """
     This handler receive messages with `/start` command
diff --git a/examples/finite_state_machine.py b/examples/finite_state_machine.py
index 197e8f66..8addfec0 100644
--- a/examples/finite_state_machine.py
+++ b/examples/finite_state_machine.py
@@ -5,6 +5,7 @@ from os import getenv
 from typing import Any, Dict
 
 from aiogram import Bot, Dispatcher, F, Router, html
+from aiogram.filters import Command
 from aiogram.fsm.context import FSMContext
 from aiogram.fsm.state import State, StatesGroup
 from aiogram.types import KeyboardButton, Message, ReplyKeyboardMarkup, ReplyKeyboardRemove
@@ -18,7 +19,7 @@ class Form(StatesGroup):
     language = State()
 
 
-@form_router.message(commands=["start"])
+@form_router.message(Command(commands=["start"]))
 async def command_start(message: Message, state: FSMContext) -> None:
     await state.set_state(Form.name)
     await message.answer(
@@ -27,7 +28,7 @@ async def command_start(message: Message, state: FSMContext) -> None:
     )
 
 
-@form_router.message(commands=["cancel"])
+@form_router.message(Command(commands=["cancel"]))
 @form_router.message(F.text.casefold() == "cancel")
 async def cancel_handler(message: Message, state: FSMContext) -> None:
     """
diff --git a/examples/specify_updates.py b/examples/specify_updates.py
index 40e241bf..6a6abb65 100644
--- a/examples/specify_updates.py
+++ b/examples/specify_updates.py
@@ -1,6 +1,7 @@
 import logging
 
 from aiogram import Bot, Dispatcher, Router
+from aiogram.filters import Command
 from aiogram.types import (
     CallbackQuery,
     ChatMemberUpdated,
@@ -16,7 +17,7 @@ logger = logging.getLogger(__name__)
 logging.basicConfig(level=logging.INFO)
 
 
-@dp.message(commands=["start"])
+@dp.message(Command(commands=["start"]))
 async def command_start_handler(message: Message) -> None:
     """
     This handler receive messages with `/start` command
@@ -71,7 +72,7 @@ async def my_chat_member_change(chat_member: ChatMemberUpdated, bot: Bot) -> Non
 
 
 def main() -> None:
-    # Initialize Bot instance with an default parse mode which will be passed to all API calls
+    # Initialize Bot instance with a default parse mode which will be passed to all API calls
     bot = Bot(TOKEN, parse_mode="HTML")
 
     sub_router.include_router(deep_dark_router)


Test patch:
diff --git a/tests/test_dispatcher/test_event/test_telegram.py b/tests/test_dispatcher/test_event/test_telegram.py
index a03d83d7..ebb4ae87 100644
--- a/tests/test_dispatcher/test_event/test_telegram.py
+++ b/tests/test_dispatcher/test_event/test_telegram.py
@@ -9,8 +9,9 @@ from aiogram.dispatcher.event.handler import HandlerObject
 from aiogram.dispatcher.event.telegram import TelegramEventObserver
 from aiogram.dispatcher.router import Router
 from aiogram.exceptions import FiltersResolveError
-from aiogram.filters import BaseFilter
+from aiogram.filters import BaseFilter, Command
 from aiogram.types import Chat, Message, User
+from tests.deprecated import check_deprecated
 
 pytestmark = pytest.mark.asyncio
 
@@ -368,3 +369,13 @@ class TestTelegramEventObserver:
         r2.message.register(handler)
 
         assert await r1.message.trigger(None) is REJECTED
+
+    def test_deprecated_bind_filter(self):
+        router = Router()
+        with check_deprecated("3.0b5", exception=AttributeError):
+            router.message.bind_filter(MyFilter1)
+
+    def test_deprecated_resolve_filters(self):
+        router = Router()
+        with check_deprecated("3.0b5", exception=AttributeError):
+            router.message.resolve_filters([Command], full_config={"commands": ["test"]})