Deprecate filters factory (#976)

* Deprecate filters factory

* Added changelog

* Update filters usage in docs and examples
This commit is contained in:
Alex Root Junior 2022-08-14 18:40:41 +03:00 committed by GitHub
parent c1341ba2df
commit 0e0dbe7e59
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 67 additions and 23 deletions

1
CHANGES/942.misc.rst Normal file
View file

@ -0,0 +1 @@
Deprecated filters factory. It will be removed in next Beta (3.0b5)

View file

@ -11,6 +11,7 @@ from aiogram.dispatcher.middlewares.manager import MiddlewareManager
from aiogram.filters.base import BaseFilter from aiogram.filters.base import BaseFilter
from ...exceptions import FiltersResolveError from ...exceptions import FiltersResolveError
from ...filters import BUILTIN_FILTERS_SET
from ...types import TelegramObject from ...types import TelegramObject
from .bases import REJECTED, UNHANDLED, MiddlewareType, SkipHandler from .bases import REJECTED, UNHANDLED, MiddlewareType, SkipHandler
from .handler import CallbackType, FilterObject, HandlerObject from .handler import CallbackType, FilterObject, HandlerObject
@ -24,7 +25,7 @@ class TelegramEventObserver:
Event observer for Telegram events 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. 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: def __init__(self, router: Router, event_name: str) -> None:
@ -41,14 +42,16 @@ class TelegramEventObserver:
# with dummy callback which never will be used # with dummy callback which never will be used
self._handler = HandlerObject(callback=lambda: True, filters=[]) 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 Register filter for all handlers of this event observer
:param filters: positional filters :param filters: positional filters
:param bound_filters: keyword 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: if self._handler.filters is None:
self._handler.filters = [] self._handler.filters = []
self._handler.filters.extend( self._handler.filters.extend(
@ -67,14 +70,18 @@ class TelegramEventObserver:
:param bound_filter: :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): if not isclass(bound_filter) or not issubclass(bound_filter, BaseFilter):
raise TypeError( raise TypeError(
"bound_filter() argument 'bound_filter' must be subclass of BaseFilter" "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) self.filters.append(bound_filter)
def _resolve_filters_chain(self) -> Generator[Type[BaseFilter], None, None]: def _resolve_filters_chain(self) -> Generator[Type[BaseFilter], None, None]:
@ -106,6 +113,7 @@ class TelegramEventObserver:
filters: Tuple[CallbackType, ...], filters: Tuple[CallbackType, ...],
full_config: Dict[str, Any], full_config: Dict[str, Any],
ignore_default: bool = True, ignore_default: bool = True,
_stacklevel: int = 2,
) -> List[BaseFilter]: ) -> List[BaseFilter]:
""" """
Resolve keyword filters via filters factory Resolve keyword filters via filters factory
@ -164,11 +172,11 @@ class TelegramEventObserver:
if bound_filters: if bound_filters:
warnings.warn( warnings.warn(
category=DeprecationWarning, 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 " "Use filters directly, for example instead of "
"`@router.message(commands=['help']')` " "`@router.message(commands=['help']')` "
"use `@router.message(Command(commands=['help'])`", "use `@router.message(Command(commands=['help'])`",
stacklevel=3, stacklevel=_stacklevel,
) )
return bound_filters return bound_filters
@ -177,6 +185,7 @@ class TelegramEventObserver:
callback: CallbackType, callback: CallbackType,
*filters: CallbackType, *filters: CallbackType,
flags: Optional[Dict[str, Any]] = None, flags: Optional[Dict[str, Any]] = None,
_stacklevel: int = 2,
**bound_filters: Any, **bound_filters: Any,
) -> CallbackType: ) -> CallbackType:
""" """
@ -184,7 +193,12 @@ class TelegramEventObserver:
""" """
if flags is None: if flags is None:
flags = {} 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: for resolved_filter in resolved_filters:
resolved_filter.update_handler_flags(flags=flags) resolved_filter.update_handler_flags(flags=flags)
self.handlers.append( self.handlers.append(
@ -238,14 +252,20 @@ class TelegramEventObserver:
return UNHANDLED return UNHANDLED
def __call__( 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]: ) -> Callable[[CallbackType], CallbackType]:
""" """
Decorator for registering event handlers Decorator for registering event handlers
""" """
def wrapper(callback: CallbackType) -> CallbackType: 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 callback
return wrapper return wrapper

View file

@ -1,3 +1,4 @@
from itertools import chain
from typing import Dict, Tuple, Type from typing import Dict, Tuple, Type
from .base import BaseFilter from .base import BaseFilter
@ -134,3 +135,5 @@ BUILTIN_FILTERS: Dict[str, Tuple[Type[BaseFilter], ...]] = {
*_ALL_EVENTS_FILTERS, *_ALL_EVENTS_FILTERS,
), ),
} }
BUILTIN_FILTERS_SET = set(chain.from_iterable(BUILTIN_FILTERS.values()))

View file

@ -86,10 +86,10 @@ Handle user leave or join events
from aiogram.filters import IS_MEMBER, IS_NOT_MEMBER 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): ... 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): ... async def on_user_join(event: ChatMemberUpdated): ...
Or construct your own terms via using pre-defined set of statuses and transitions. Or construct your own terms via using pre-defined set of statuses and transitions.

View file

@ -21,8 +21,7 @@ Usage
1. Filter single variant of commands: :code:`Command(commands=["start"])` or :code:`Command(commands="start")` 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+)")])` 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+)")])` 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)` 4. Handle commands in public chats intended for other bots: :code:`Command(commands=["command"], commands_ignore_mention=True)`
5. As keyword argument in registerer: :code:`@router.message(commands=["help"])`
.. warning:: .. warning::

View file

@ -2,6 +2,13 @@
Filtering events 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. Filters is needed for routing updates to the specific handler.
Searching of handler is always stops on first match set of filters are pass. Searching of handler is always stops on first match set of filters are pass.

View file

@ -17,7 +17,7 @@ Or used from filters factory by passing corresponding arguments to handler regis
Usage 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 Allowed handlers

View file

@ -1,6 +1,7 @@
import logging import logging
from aiogram import Bot, Dispatcher, types from aiogram import Bot, Dispatcher, types
from aiogram.filters import Command
from aiogram.types import Message from aiogram.types import Message
TOKEN = "42:TOKEN" TOKEN = "42:TOKEN"
@ -9,7 +10,7 @@ dp = Dispatcher()
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@dp.message(commands=["start"]) @dp.message(Command(commands=["start"]))
async def command_start_handler(message: Message) -> None: async def command_start_handler(message: Message) -> None:
""" """
This handler receive messages with `/start` command This handler receive messages with `/start` command

View file

@ -5,6 +5,7 @@ from os import getenv
from typing import Any, Dict from typing import Any, Dict
from aiogram import Bot, Dispatcher, F, Router, html from aiogram import Bot, Dispatcher, F, Router, html
from aiogram.filters import Command
from aiogram.fsm.context import FSMContext from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup from aiogram.fsm.state import State, StatesGroup
from aiogram.types import KeyboardButton, Message, ReplyKeyboardMarkup, ReplyKeyboardRemove from aiogram.types import KeyboardButton, Message, ReplyKeyboardMarkup, ReplyKeyboardRemove
@ -18,7 +19,7 @@ class Form(StatesGroup):
language = State() language = State()
@form_router.message(commands=["start"]) @form_router.message(Command(commands=["start"]))
async def command_start(message: Message, state: FSMContext) -> None: async def command_start(message: Message, state: FSMContext) -> None:
await state.set_state(Form.name) await state.set_state(Form.name)
await message.answer( 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") @form_router.message(F.text.casefold() == "cancel")
async def cancel_handler(message: Message, state: FSMContext) -> None: async def cancel_handler(message: Message, state: FSMContext) -> None:
""" """

View file

@ -1,6 +1,7 @@
import logging import logging
from aiogram import Bot, Dispatcher, Router from aiogram import Bot, Dispatcher, Router
from aiogram.filters import Command
from aiogram.types import ( from aiogram.types import (
CallbackQuery, CallbackQuery,
ChatMemberUpdated, ChatMemberUpdated,
@ -16,7 +17,7 @@ logger = logging.getLogger(__name__)
logging.basicConfig(level=logging.INFO) logging.basicConfig(level=logging.INFO)
@dp.message(commands=["start"]) @dp.message(Command(commands=["start"]))
async def command_start_handler(message: Message) -> None: async def command_start_handler(message: Message) -> None:
""" """
This handler receive messages with `/start` command 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: 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") bot = Bot(TOKEN, parse_mode="HTML")
sub_router.include_router(deep_dark_router) sub_router.include_router(deep_dark_router)

View file

@ -9,8 +9,9 @@ from aiogram.dispatcher.event.handler import HandlerObject
from aiogram.dispatcher.event.telegram import TelegramEventObserver from aiogram.dispatcher.event.telegram import TelegramEventObserver
from aiogram.dispatcher.router import Router from aiogram.dispatcher.router import Router
from aiogram.exceptions import FiltersResolveError from aiogram.exceptions import FiltersResolveError
from aiogram.filters import BaseFilter from aiogram.filters import BaseFilter, Command
from aiogram.types import Chat, Message, User from aiogram.types import Chat, Message, User
from tests.deprecated import check_deprecated
pytestmark = pytest.mark.asyncio pytestmark = pytest.mark.asyncio
@ -368,3 +369,13 @@ class TestTelegramEventObserver:
r2.message.register(handler) r2.message.register(handler)
assert await r1.message.trigger(None) is REJECTED 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"]})