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 @@ +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__}(=)` instead of `=`)", + 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 >> `_ + 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) 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"]})