From 7f26ec99358639ac1dcde68bb668e91c20941a54 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Tue, 26 May 2020 00:23:35 +0300 Subject: [PATCH 1/3] Implement new middlewares --- Makefile | 24 +- aiogram/__init__.py | 6 +- aiogram/dispatcher/dispatcher.py | 50 ++- aiogram/dispatcher/event/bases.py | 29 ++ aiogram/dispatcher/event/event.py | 39 +++ aiogram/dispatcher/event/handler.py | 18 +- .../event/{observer.py => telegram.py} | 190 ++++------- aiogram/dispatcher/middlewares/abstract.py | 61 ---- aiogram/dispatcher/middlewares/base.py | 322 +----------------- aiogram/dispatcher/middlewares/error.py | 31 ++ aiogram/dispatcher/middlewares/manager.py | 71 ---- aiogram/dispatcher/middlewares/types.py | 35 -- .../middlewares/update_processing_context.py | 62 ++++ aiogram/dispatcher/router.py | 117 +------ aiogram/utils/mixins.py | 5 +- docs/dispatcher/middlewares/basics.md | 8 +- docs/dispatcher/middlewares/index.md | 24 +- poetry.lock | 22 +- pyproject.toml | 3 +- .../test_api/test_types/test_inline_query.py | 8 +- .../test_types/test_shipping_query.py | 5 +- tests/test_dispatcher/test_deprecated.py | 2 +- tests/test_dispatcher/test_dispatcher.py | 42 +-- .../test_dispatcher/test_event/test_event.py | 59 ++++ .../{test_observer.py => test_telegram.py} | 114 +++---- .../test_middlewares/__init__.py | 0 .../test_middlewares/test_base.py | 257 -------------- .../test_middlewares/test_manager.py | 82 ----- tests/test_dispatcher/test_router.py | 98 +++--- 29 files changed, 532 insertions(+), 1252 deletions(-) create mode 100644 aiogram/dispatcher/event/bases.py create mode 100644 aiogram/dispatcher/event/event.py rename aiogram/dispatcher/event/{observer.py => telegram.py} (53%) delete mode 100644 aiogram/dispatcher/middlewares/abstract.py create mode 100644 aiogram/dispatcher/middlewares/error.py delete mode 100644 aiogram/dispatcher/middlewares/manager.py delete mode 100644 aiogram/dispatcher/middlewares/types.py create mode 100644 aiogram/dispatcher/middlewares/update_processing_context.py create mode 100644 tests/test_dispatcher/test_event/test_event.py rename tests/test_dispatcher/test_event/{test_observer.py => test_telegram.py} (72%) delete mode 100644 tests/test_dispatcher/test_middlewares/__init__.py delete mode 100644 tests/test_dispatcher/test_middlewares/test_base.py delete mode 100644 tests/test_dispatcher/test_middlewares/test_manager.py diff --git a/Makefile b/Makefile index c14a3781..cf2e405c 100644 --- a/Makefile +++ b/Makefile @@ -6,7 +6,6 @@ python := $(py) python reports_dir := reports -.PHONY: help help: @echo "=======================================================================================" @echo " aiogram build tools " @@ -45,12 +44,10 @@ help: # Environment # ================================================================================================= -.PHONY: install install: $(base_python) -m pip install --user -U poetry poetry install -.PHONY: clean clean: rm -rf `find . -name __pycache__` rm -f `find . -type f -name '*.py[co]' ` @@ -68,65 +65,56 @@ clean: # Code quality # ================================================================================================= -.PHONY: isort isort: $(py) isort -rc aiogram tests -.PHONY: black black: $(py) black aiogram tests -.PHONY: flake8 flake8: $(py) flake8 aiogram test -.PHONY: flake8-report flake8-report: mkdir -p $(reports_dir)/flake8 $(py) flake8 --format=html --htmldir=$(reports_dir)/flake8 aiogram test -.PHONY: mypy mypy: $(py) mypy aiogram -.PHONY: mypy-report mypy-report: $(py) mypy aiogram --html-report $(reports_dir)/typechecking -.PHONY: lint lint: isort black flake8 mypy # ================================================================================================= # Tests # ================================================================================================= -.PHONY: test test: $(py) pytest --cov=aiogram --cov-config .coveragerc tests/ -.PHONY: test-coverage test-coverage: mkdir -p $(reports_dir)/tests/ $(py) pytest --cov=aiogram --cov-config .coveragerc --html=$(reports_dir)/tests/index.html tests/ + + +test-coverage-report: $(py) coverage html -d $(reports_dir)/coverage -.PHONY: test-coverage-report -test-coverage-report: +test-coverage-view: + $(py) coverage html -d $(reports_dir)/coverage python -c "import webbrowser; webbrowser.open('file://$(shell pwd)/reports/coverage/index.html')" # ================================================================================================= # Docs # ================================================================================================= -.PHONY: docs docs: $(py) mkdocs build -.PHONY: docs-serve docs-serve: $(py) mkdocs serve -.PHONY: docs-copy-reports docs-copy-reports: mv $(reports_dir)/* site/reports @@ -134,9 +122,7 @@ docs-copy-reports: # Project # ================================================================================================= -.PHONY: build build: clean flake8-report mypy-report test-coverage docs docs-copy-reports mkdir -p site/simple poetry build mv dist site/simple/aiogram - diff --git a/aiogram/__init__.py b/aiogram/__init__.py index 0b2a8cc7..8ab84aa6 100644 --- a/aiogram/__init__.py +++ b/aiogram/__init__.py @@ -1,9 +1,10 @@ +from pkg_resources import get_distribution + from .api import methods, types from .api.client import session from .api.client.bot import Bot from .dispatcher import filters, handler from .dispatcher.dispatcher import Dispatcher -from .dispatcher.middlewares.base import BaseMiddleware from .dispatcher.router import Router try: @@ -23,10 +24,9 @@ __all__ = ( "session", "Dispatcher", "Router", - "BaseMiddleware", "filters", "handler", ) -__version__ = "3.0.0a4" +__version__ = get_distribution(dist=__package__).version __api_version__ = "4.8" diff --git a/aiogram/dispatcher/dispatcher.py b/aiogram/dispatcher/dispatcher.py index 8960769d..8b475316 100644 --- a/aiogram/dispatcher/dispatcher.py +++ b/aiogram/dispatcher/dispatcher.py @@ -11,6 +11,8 @@ from ..api.client.bot import Bot from ..api.methods import TelegramMethod from ..api.types import Update, User from ..utils.exceptions import TelegramAPIError +from .event.bases import NOT_HANDLED +from .middlewares.update_processing_context import UserContextMiddleware from .router import Router @@ -23,6 +25,9 @@ class Dispatcher(Router): super(Dispatcher, self).__init__(**kwargs) self._running_lock = Lock() + # Default middleware is needed for contextual features + self.update.outer_middleware(UserContextMiddleware()) + @property def parent_router(self) -> None: """ @@ -42,9 +47,7 @@ class Dispatcher(Router): """ raise RuntimeError("Dispatcher can not be attached to another Router.") - async def feed_update( - self, bot: Bot, update: Update, **kwargs: Any - ) -> AsyncGenerator[Any, None]: + async def feed_update(self, bot: Bot, update: Update, **kwargs: Any) -> Any: """ Main entry point for incoming updates @@ -57,9 +60,9 @@ class Dispatcher(Router): Bot.set_current(bot) try: - async for result in self.update.trigger(update, bot=bot, **kwargs): - handled = True - yield result + response = await self.update.trigger(update, bot=bot, **kwargs) + handled = response is not NOT_HANDLED + return response finally: finish_time = loop.time() duration = (finish_time - start_time) * 1000 @@ -71,9 +74,7 @@ class Dispatcher(Router): bot.id, ) - async def feed_raw_update( - self, bot: Bot, update: Dict[str, Any], **kwargs: Any - ) -> AsyncGenerator[Any, None]: + async def feed_raw_update(self, bot: Bot, update: Dict[str, Any], **kwargs: Any) -> Any: """ Main entry point for incoming updates with automatic Dict->Update serializer @@ -82,8 +83,7 @@ class Dispatcher(Router): :param kwargs: """ parsed_update = Update(**update) - async for result in self.feed_update(bot=bot, update=parsed_update, **kwargs): - yield result + return await self.feed_update(bot=bot, update=parsed_update, **kwargs) @classmethod async def _listen_updates(cls, bot: Bot) -> AsyncGenerator[Update, None]: @@ -114,7 +114,7 @@ class Dispatcher(Router): # For debugging here is added logging. loggers.dispatcher.error("Failed to make answer: %s: %s", e.__class__.__name__, e) - async def process_update( + async def _process_update( self, bot: Bot, update: Update, call_answer: bool = True, **kwargs: Any ) -> bool: """ @@ -126,11 +126,13 @@ class Dispatcher(Router): :param kwargs: contextual data for middlewares, filters and handlers :return: status """ + handled = False try: - async for result in self.feed_update(bot, update, **kwargs): - if call_answer and isinstance(result, TelegramMethod): - await self._silent_call_request(bot=bot, result=result) - return True + response = await self.feed_update(bot, update, **kwargs) + handled = handled is not NOT_HANDLED + if call_answer and isinstance(response, TelegramMethod): + await self._silent_call_request(bot=bot, result=response) + return handled except Exception as e: loggers.dispatcher.exception( @@ -142,8 +144,6 @@ class Dispatcher(Router): ) return True # because update was processed but unsuccessful - return False - async def _polling(self, bot: Bot, **kwargs: Any) -> None: """ Internal polling process @@ -153,16 +153,14 @@ class Dispatcher(Router): :return: """ async for update in self._listen_updates(bot): - await self.process_update(bot=bot, update=update, **kwargs) + await self._process_update(bot=bot, update=update, **kwargs) async def _feed_webhook_update(self, bot: Bot, update: Update, **kwargs: Any) -> Any: """ The same with `Dispatcher.process_update()` but returns real response instead of bool """ try: - async for result in self.feed_update(bot, update, **kwargs): - return result - + return await self.feed_update(bot, update, **kwargs) except Exception as e: loggers.dispatcher.exception( "Cause exception while process update id=%d by bot id=%d\n%s: %s", @@ -196,10 +194,10 @@ class Dispatcher(Router): def process_response(task: Future[Any]) -> None: warnings.warn( - f"Detected slow response into webhook.\n" - f"Telegram is waiting for response only first 60 seconds and then re-send update.\n" - f"For preventing this situation response into webhook returned immediately " - f"and handler is moved to background and still processing update.", + "Detected slow response into webhook.\n" + "Telegram is waiting for response only first 60 seconds and then re-send update.\n" + "For preventing this situation response into webhook returned immediately " + "and handler is moved to background and still processing update.", RuntimeWarning, ) try: diff --git a/aiogram/dispatcher/event/bases.py b/aiogram/dispatcher/event/bases.py new file mode 100644 index 00000000..d255e3ae --- /dev/null +++ b/aiogram/dispatcher/event/bases.py @@ -0,0 +1,29 @@ +from __future__ import annotations + +from typing import Any, Awaitable, Callable, Dict, NoReturn, Optional, Union +from unittest.mock import sentinel + +from ...api.types import TelegramObject +from ..middlewares.base import BaseMiddleware + +NextMiddlewareType = Callable[[TelegramObject, Dict[str, Any]], Awaitable[Any]] +MiddlewareType = Union[ + BaseMiddleware, Callable[[NextMiddlewareType, TelegramObject, Dict[str, Any]], Awaitable[Any]] +] + +NOT_HANDLED = sentinel.NOT_HANDLED + + +class SkipHandler(Exception): + pass + + +class CancelHandler(Exception): + pass + + +def skip(message: Optional[str] = None) -> NoReturn: + """ + Raise an SkipHandler + """ + raise SkipHandler(message or "Event skipped") diff --git a/aiogram/dispatcher/event/event.py b/aiogram/dispatcher/event/event.py new file mode 100644 index 00000000..29aa4580 --- /dev/null +++ b/aiogram/dispatcher/event/event.py @@ -0,0 +1,39 @@ +from __future__ import annotations + +from typing import Any, Callable, List + +from .handler import CallbackType, HandlerObject, HandlerType + + +class EventObserver: + """ + Simple events observer + """ + + def __init__(self) -> None: + self.handlers: List[HandlerObject] = [] + + def register(self, callback: HandlerType) -> None: + """ + Register callback with filters + """ + self.handlers.append(HandlerObject(callback=callback)) + + async def trigger(self, *args: Any, **kwargs: Any) -> None: + """ + Propagate event to handlers. + Handler will be called when all its filters is pass. + """ + for handler in self.handlers: + await handler.call(*args, **kwargs) + + def __call__(self) -> Callable[[CallbackType], CallbackType]: + """ + Decorator for registering event handlers + """ + + def wrapper(callback: CallbackType) -> CallbackType: + self.register(callback) + return callback + + return wrapper diff --git a/aiogram/dispatcher/event/handler.py b/aiogram/dispatcher/event/handler.py index d5a59277..6c2a5e57 100644 --- a/aiogram/dispatcher/event/handler.py +++ b/aiogram/dispatcher/event/handler.py @@ -1,3 +1,5 @@ +import asyncio +import contextvars import inspect from dataclasses import dataclass, field from functools import partial @@ -6,9 +8,9 @@ from typing import Any, Awaitable, Callable, Dict, List, Optional, Tuple, Type, from aiogram.dispatcher.filters.base import BaseFilter from aiogram.dispatcher.handler.base import BaseHandler -CallbackType = Callable[[Any], Awaitable[Any]] -SyncFilter = Callable[[Any], Any] -AsyncFilter = Callable[[Any], Awaitable[Any]] +CallbackType = Callable[..., Awaitable[Any]] +SyncFilter = Callable[..., Any] +AsyncFilter = Callable[..., Awaitable[Any]] FilterType = Union[SyncFilter, AsyncFilter, BaseFilter] HandlerType = Union[FilterType, Type[BaseHandler]] @@ -40,7 +42,11 @@ class CallableMixin: wrapped = partial(self.callback, *args, **self._prepare_kwargs(kwargs)) if self.awaitable: return await wrapped() - return wrapped() + + loop = asyncio.get_event_loop() + context = contextvars.copy_context() + wrapped = partial(context.run, wrapped) + return await loop.run_in_executor(None, wrapped) @dataclass @@ -60,11 +66,11 @@ class HandlerObject(CallableMixin): async def check(self, *args: Any, **kwargs: Any) -> Tuple[bool, Dict[str, Any]]: if not self.filters: - return True, {} + return True, kwargs for event_filter in self.filters: check = await event_filter.call(*args, **kwargs) if not check: - return False, {} + return False, kwargs if isinstance(check, dict): kwargs.update(check) return True, kwargs diff --git a/aiogram/dispatcher/event/observer.py b/aiogram/dispatcher/event/telegram.py similarity index 53% rename from aiogram/dispatcher/event/observer.py rename to aiogram/dispatcher/event/telegram.py index cea2eb6a..e72d5db3 100644 --- a/aiogram/dispatcher/event/observer.py +++ b/aiogram/dispatcher/event/telegram.py @@ -1,93 +1,33 @@ from __future__ import annotations +import functools from itertools import chain -from typing import ( - TYPE_CHECKING, - Any, - AsyncGenerator, - Callable, - Dict, - Generator, - List, - NoReturn, - Optional, - Type, -) +from typing import TYPE_CHECKING, Any, Callable, Dict, Generator, List, Optional, Type, Union from pydantic import ValidationError +from ...api.types import TelegramObject from ..filters.base import BaseFilter -from ..middlewares.types import MiddlewareStep, UpdateType +from .bases import NOT_HANDLED, MiddlewareType, NextMiddlewareType, SkipHandler from .handler import CallbackType, FilterObject, FilterType, HandlerObject, HandlerType if TYPE_CHECKING: # pragma: no cover from aiogram.dispatcher.router import Router -class SkipHandler(Exception): - pass - - -class CancelHandler(Exception): - pass - - -def skip(message: Optional[str] = None) -> NoReturn: - """ - Raise an SkipHandler - """ - raise SkipHandler(message or "Event skipped") - - -class EventObserver: - """ - Base events observer - """ - - def __init__(self) -> None: - self.handlers: List[HandlerObject] = [] - - def register(self, callback: HandlerType) -> HandlerType: - """ - Register callback with filters - """ - self.handlers.append(HandlerObject(callback=callback)) - return callback - - async def trigger(self, *args: Any, **kwargs: Any) -> AsyncGenerator[Any, None]: - """ - Propagate event to handlers. - Handler will be called when all its filters is pass. - """ - for handler in self.handlers: - try: - yield await handler.call(*args, **kwargs) - except SkipHandler: - continue - - def __call__(self) -> Callable[[CallbackType], CallbackType]: - """ - Decorator for registering event handlers - """ - - def wrapper(callback: CallbackType) -> CallbackType: - self.register(callback) - return callback - - return wrapper - - -class TelegramEventObserver(EventObserver): +class TelegramEventObserver: """ Event observer for Telegram events """ def __init__(self, router: Router, event_name: str) -> None: - super().__init__() - self.router: Router = router self.event_name: str = event_name + + self.handlers: List[HandlerObject] = [] self.filters: List[Type[BaseFilter]] = [] + self.outer_middlewares: List[MiddlewareType] = [] + self.middlewares: List[MiddlewareType] = [] def bind_filter(self, bound_filter: Type[BaseFilter]) -> None: """ @@ -144,37 +84,6 @@ class TelegramEventObserver(EventObserver): return filters - async def trigger_middleware( - self, step: MiddlewareStep, event: UpdateType, data: Dict[str, Any], result: Any = None, - ) -> None: - """ - Trigger middlewares chain - - :param step: - :param event: - :param data: - :param result: - :return: - """ - reverse = step == MiddlewareStep.POST_PROCESS - recursive = self.event_name == "update" or step == MiddlewareStep.PROCESS - - if self.event_name == "update": - routers = self.router.chain - else: - routers = self.router.chain_head - for router in routers: - await router.middleware.trigger( - step=step, - event_name=self.event_name, - event=event, - data=data, - result=result, - reverse=reverse, - ) - if not recursive: - break - def register( self, callback: HandlerType, *filters: FilterType, **bound_filters: Any ) -> HandlerType: @@ -190,32 +99,39 @@ class TelegramEventObserver(EventObserver): ) return callback - async def trigger(self, *args: Any, **kwargs: Any) -> AsyncGenerator[Any, None]: + @classmethod + def _wrap_middleware( + cls, middlewares: List[MiddlewareType], handler: HandlerType + ) -> NextMiddlewareType: + @functools.wraps(handler) + def mapper(event: TelegramObject, kwargs: Dict[str, Any]) -> Any: + return handler(event, **kwargs) + + middleware = mapper + for m in reversed(middlewares): + middleware = functools.partial(m, middleware) + return middleware + + async def trigger(self, event: TelegramObject, **kwargs: Any) -> Any: """ Propagate event to handlers and stops propagation on first match. Handler will be called when all its filters is pass. """ - event = args[0] - await self.trigger_middleware(step=MiddlewareStep.PRE_PROCESS, event=event, data=kwargs) + wrapped_outer = self._wrap_middleware(self.outer_middlewares, self._trigger) + return await wrapped_outer(event, kwargs) + + async def _trigger(self, event: TelegramObject, **kwargs: Any) -> Any: for handler in self.handlers: - result, data = await handler.check(*args, **kwargs) + result, data = await handler.check(event, **kwargs) if result: kwargs.update(data) - await self.trigger_middleware( - step=MiddlewareStep.PROCESS, event=event, data=kwargs - ) try: - response = await handler.call(*args, **kwargs) - await self.trigger_middleware( - step=MiddlewareStep.POST_PROCESS, - event=event, - data=kwargs, - result=response, - ) - yield response + wrapped_inner = self._wrap_middleware(self.middlewares, handler.call) + return await wrapped_inner(event, kwargs) except SkipHandler: continue - break + + return NOT_HANDLED def __call__( self, *args: FilterType, **bound_filters: BaseFilter @@ -229,3 +145,45 @@ class TelegramEventObserver(EventObserver): return callback return wrapper + + def middleware( + self, middleware: Optional[MiddlewareType] = None, + ) -> Union[Callable[[MiddlewareType], MiddlewareType], MiddlewareType]: + """ + Decorator for registering inner middlewares + + Usage: + >>> @.middleware() # via decorator (variant 1) + >>> @.middleware # via decorator (variant 2) + >>> async def my_middleware(handler, event, data): ... + >>> .middleware(middleware) # via method + """ + + def wrapper(m: MiddlewareType) -> MiddlewareType: + self.middlewares.append(m) + return m + + if middleware is None: + return wrapper + return wrapper(middleware) + + def outer_middleware( + self, middleware: Optional[MiddlewareType] = None, + ) -> Union[Callable[[MiddlewareType], MiddlewareType], MiddlewareType]: + """ + Decorator for registering outer middlewares + + Usage: + >>> @.outer_middleware() # via decorator (variant 1) + >>> @.outer_middleware # via decorator (variant 2) + >>> async def my_middleware(handler, event, data): ... + >>> .outer_middleware(my_middleware) # via method + """ + + def wrapper(m: MiddlewareType) -> MiddlewareType: + self.outer_middlewares.append(m) + return m + + if middleware is None: + return wrapper + return wrapper(middleware) diff --git a/aiogram/dispatcher/middlewares/abstract.py b/aiogram/dispatcher/middlewares/abstract.py deleted file mode 100644 index eac16534..00000000 --- a/aiogram/dispatcher/middlewares/abstract.py +++ /dev/null @@ -1,61 +0,0 @@ -from __future__ import annotations - -from abc import ABC, abstractmethod -from typing import TYPE_CHECKING, Any, Dict, Optional - -from aiogram.dispatcher.middlewares.types import MiddlewareStep, UpdateType - -if TYPE_CHECKING: # pragma: no cover - from aiogram.dispatcher.middlewares.manager import MiddlewareManager - - -class AbstractMiddleware(ABC): - """ - Abstract class for middleware. - """ - - def __init__(self) -> None: - self._manager: Optional[MiddlewareManager] = None - - @property - def manager(self) -> MiddlewareManager: - """ - Instance of MiddlewareManager - """ - if self._manager is None: - raise RuntimeError("Middleware is not configured!") - return self._manager - - def setup(self, manager: MiddlewareManager, _stack_level: int = 1) -> AbstractMiddleware: - """ - Mark middleware as configured - - :param manager: - :param _stack_level: - :return: - """ - if self.configured: - return manager.setup(self, _stack_level=_stack_level + 1) - - self._manager = manager - return self - - @property - def configured(self) -> bool: - """ - Check middleware is configured - - :return: - """ - return bool(self._manager) - - @abstractmethod - async def trigger( - self, - step: MiddlewareStep, - event_name: str, - event: UpdateType, - data: Dict[str, Any], - result: Any = None, - ) -> Any: # pragma: no cover - pass diff --git a/aiogram/dispatcher/middlewares/base.py b/aiogram/dispatcher/middlewares/base.py index 8766f9dc..f0db86ec 100644 --- a/aiogram/dispatcher/middlewares/base.py +++ b/aiogram/dispatcher/middlewares/base.py @@ -1,317 +1,15 @@ -from __future__ import annotations +from abc import ABC, abstractmethod +from typing import Any, Awaitable, Callable, Dict, Generic, TypeVar -from typing import TYPE_CHECKING, Any, Dict - -from aiogram.dispatcher.middlewares.abstract import AbstractMiddleware -from aiogram.dispatcher.middlewares.types import MiddlewareStep, UpdateType - -if TYPE_CHECKING: # pragma: no cover - from aiogram.api.types import ( - CallbackQuery, - ChosenInlineResult, - InlineQuery, - Message, - Poll, - PollAnswer, - PreCheckoutQuery, - ShippingQuery, - Update, - ) +T = TypeVar("T") -class BaseMiddleware(AbstractMiddleware): - """ - Base class for middleware. - - All methods on the middle always must be coroutines and name starts with "on_" like "on_process_message". - """ - - async def trigger( +class BaseMiddleware(ABC, Generic[T]): + @abstractmethod + async def __call__( self, - step: MiddlewareStep, - event_name: str, - event: UpdateType, + handler: Callable[[T, Dict[str, Any]], Awaitable[Any]], + event: T, data: Dict[str, Any], - result: Any = None, - ) -> Any: - """ - Trigger action. - - :param step: - :param event_name: - :param event: - :param data: - :param result: - :return: - """ - handler_name = f"on_{step.value}_{event_name}" - handler = getattr(self, handler_name, None) - if not handler: - return None - args = (event, result, data) if step == MiddlewareStep.POST_PROCESS else (event, data) - return await handler(*args) - - if TYPE_CHECKING: # pragma: no cover - # ============================================================================================= - # Event that triggers before process - # ============================================================================================= - async def on_pre_process_update(self, update: Update, data: Dict[str, Any]) -> Any: - """ - Event that triggers before process update - """ - - async def on_pre_process_message(self, message: Message, data: Dict[str, Any]) -> Any: - """ - Event that triggers before process message - """ - - async def on_pre_process_edited_message( - self, edited_message: Message, data: Dict[str, Any] - ) -> Any: - """ - Event that triggers before process edited_message - """ - - async def on_pre_process_channel_post( - self, channel_post: Message, data: Dict[str, Any] - ) -> Any: - """ - Event that triggers before process channel_post - """ - - async def on_pre_process_edited_channel_post( - self, edited_channel_post: Message, data: Dict[str, Any] - ) -> Any: - """ - Event that triggers before process edited_channel_post - """ - - async def on_pre_process_inline_query( - self, inline_query: InlineQuery, data: Dict[str, Any] - ) -> Any: - """ - Event that triggers before process inline_query - """ - - async def on_pre_process_chosen_inline_result( - self, chosen_inline_result: ChosenInlineResult, data: Dict[str, Any] - ) -> Any: - """ - Event that triggers before process chosen_inline_result - """ - - async def on_pre_process_callback_query( - self, callback_query: CallbackQuery, data: Dict[str, Any] - ) -> Any: - """ - Event that triggers before process callback_query - """ - - async def on_pre_process_shipping_query( - self, shipping_query: ShippingQuery, data: Dict[str, Any] - ) -> Any: - """ - Event that triggers before process shipping_query - """ - - async def on_pre_process_pre_checkout_query( - self, pre_checkout_query: PreCheckoutQuery, data: Dict[str, Any] - ) -> Any: - """ - Event that triggers before process pre_checkout_query - """ - - async def on_pre_process_poll(self, poll: Poll, data: Dict[str, Any]) -> Any: - """ - Event that triggers before process poll - """ - - async def on_pre_process_poll_answer( - self, poll_answer: PollAnswer, data: Dict[str, Any] - ) -> Any: - """ - Event that triggers before process poll_answer - """ - - async def on_pre_process_error(self, exception: Exception, data: Dict[str, Any]) -> Any: - """ - Event that triggers before process error - """ - - # ============================================================================================= - # Event that triggers on process after filters. - # ============================================================================================= - async def on_process_update(self, update: Update, data: Dict[str, Any]) -> Any: - """ - Event that triggers on process update - """ - - async def on_process_message(self, message: Message, data: Dict[str, Any]) -> Any: - """ - Event that triggers on process message - """ - - async def on_process_edited_message( - self, edited_message: Message, data: Dict[str, Any] - ) -> Any: - """ - Event that triggers on process edited_message - """ - - async def on_process_channel_post( - self, channel_post: Message, data: Dict[str, Any] - ) -> Any: - """ - Event that triggers on process channel_post - """ - - async def on_process_edited_channel_post( - self, edited_channel_post: Message, data: Dict[str, Any] - ) -> Any: - """ - Event that triggers on process edited_channel_post - """ - - async def on_process_inline_query( - self, inline_query: InlineQuery, data: Dict[str, Any] - ) -> Any: - """ - Event that triggers on process inline_query - """ - - async def on_process_chosen_inline_result( - self, chosen_inline_result: ChosenInlineResult, data: Dict[str, Any] - ) -> Any: - """ - Event that triggers on process chosen_inline_result - """ - - async def on_process_callback_query( - self, callback_query: CallbackQuery, data: Dict[str, Any] - ) -> Any: - """ - Event that triggers on process callback_query - """ - - async def on_process_shipping_query( - self, shipping_query: ShippingQuery, data: Dict[str, Any] - ) -> Any: - """ - Event that triggers on process shipping_query - """ - - async def on_process_pre_checkout_query( - self, pre_checkout_query: PreCheckoutQuery, data: Dict[str, Any] - ) -> Any: - """ - Event that triggers on process pre_checkout_query - """ - - async def on_process_poll(self, poll: Poll, data: Dict[str, Any]) -> Any: - """ - Event that triggers on process poll - """ - - async def on_process_poll_answer( - self, poll_answer: PollAnswer, data: Dict[str, Any] - ) -> Any: - """ - Event that triggers on process poll_answer - """ - - async def on_process_error(self, exception: Exception, data: Dict[str, Any]) -> Any: - """ - Event that triggers on process error - """ - - # ============================================================================================= - # Event that triggers after process . - # ============================================================================================= - async def on_post_process_update( - self, update: Update, data: Dict[str, Any], result: Any - ) -> Any: - """ - Event that triggers after processing update - """ - - async def on_post_process_message( - self, message: Message, data: Dict[str, Any], result: Any - ) -> Any: - """ - Event that triggers after processing message - """ - - async def on_post_process_edited_message( - self, edited_message: Message, data: Dict[str, Any], result: Any - ) -> Any: - """ - Event that triggers after processing edited_message - """ - - async def on_post_process_channel_post( - self, channel_post: Message, data: Dict[str, Any], result: Any - ) -> Any: - """ - Event that triggers after processing channel_post - """ - - async def on_post_process_edited_channel_post( - self, edited_channel_post: Message, data: Dict[str, Any], result: Any - ) -> Any: - """ - Event that triggers after processing edited_channel_post - """ - - async def on_post_process_inline_query( - self, inline_query: InlineQuery, data: Dict[str, Any], result: Any - ) -> Any: - """ - Event that triggers after processing inline_query - """ - - async def on_post_process_chosen_inline_result( - self, chosen_inline_result: ChosenInlineResult, data: Dict[str, Any], result: Any - ) -> Any: - """ - Event that triggers after processing chosen_inline_result - """ - - async def on_post_process_callback_query( - self, callback_query: CallbackQuery, data: Dict[str, Any], result: Any - ) -> Any: - """ - Event that triggers after processing callback_query - """ - - async def on_post_process_shipping_query( - self, shipping_query: ShippingQuery, data: Dict[str, Any], result: Any - ) -> Any: - """ - Event that triggers after processing shipping_query - """ - - async def on_post_process_pre_checkout_query( - self, pre_checkout_query: PreCheckoutQuery, data: Dict[str, Any], result: Any - ) -> Any: - """ - Event that triggers after processing pre_checkout_query - """ - - async def on_post_process_poll(self, poll: Poll, data: Dict[str, Any], result: Any) -> Any: - """ - Event that triggers after processing poll - """ - - async def on_post_process_poll_answer( - self, poll_answer: PollAnswer, data: Dict[str, Any], result: Any - ) -> Any: - """ - Event that triggers after processing poll_answer - """ - - async def on_post_process_error( - self, exception: Exception, data: Dict[str, Any], result: Any - ) -> Any: - """ - Event that triggers after processing error - """ + ) -> Any: # pragma: no cover + pass diff --git a/aiogram/dispatcher/middlewares/error.py b/aiogram/dispatcher/middlewares/error.py new file mode 100644 index 00000000..438277b1 --- /dev/null +++ b/aiogram/dispatcher/middlewares/error.py @@ -0,0 +1,31 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, Awaitable, Callable, Dict + +from ...api.types import Update +from ..event.bases import NOT_HANDLED, CancelHandler, SkipHandler +from .base import BaseMiddleware + +if TYPE_CHECKING: # pragma: no cover + from ..router import Router + + +class ErrorsMiddleware(BaseMiddleware[Update]): + def __init__(self, router: Router): + self.router = router + + async def __call__( + self, + handler: Callable[[Any, Dict[str, Any]], Awaitable[Any]], + event: Any, + data: Dict[str, Any], + ) -> Any: + try: + return await handler(event, data) + except (SkipHandler, CancelHandler): # pragma: no cover + raise + except Exception as e: + response = await self.router.errors.trigger(event, exception=e, **data) + if response is NOT_HANDLED: + raise + return response diff --git a/aiogram/dispatcher/middlewares/manager.py b/aiogram/dispatcher/middlewares/manager.py deleted file mode 100644 index 39a6230d..00000000 --- a/aiogram/dispatcher/middlewares/manager.py +++ /dev/null @@ -1,71 +0,0 @@ -from __future__ import annotations - -from typing import TYPE_CHECKING, Any, Dict, List -from warnings import warn - -from .abstract import AbstractMiddleware -from .types import MiddlewareStep, UpdateType - -if TYPE_CHECKING: # pragma: no cover - from aiogram.dispatcher.router import Router - - -class MiddlewareManager: - """ - Middleware manager. - """ - - def __init__(self, router: Router) -> None: - self.router = router - self.middlewares: List[AbstractMiddleware] = [] - - def setup(self, middleware: AbstractMiddleware, _stack_level: int = 1) -> AbstractMiddleware: - """ - Setup middleware - - :param middleware: - :param _stack_level: - :return: - """ - if not isinstance(middleware, AbstractMiddleware): - raise TypeError( - f"`middleware` should be instance of BaseMiddleware, not {type(middleware)}" - ) - if middleware.configured: - if middleware.manager is self: - warn( - f"Middleware {middleware} is already configured for this Router " - "That's mean re-installing of this middleware has no effect.", - category=RuntimeWarning, - stacklevel=_stack_level + 1, - ) - return middleware - raise ValueError( - f"Middleware is already configured for another manager {middleware.manager} " - f"in router {middleware.manager.router}!" - ) - - self.middlewares.append(middleware) - middleware.setup(self) - return middleware - - async def trigger( - self, - step: MiddlewareStep, - event_name: str, - event: UpdateType, - data: Dict[str, Any], - result: Any = None, - reverse: bool = False, - ) -> Any: - """ - Call action to middlewares with args lilt. - """ - middlewares = reversed(self.middlewares) if reverse else self.middlewares - for middleware in middlewares: - await middleware.trigger( - step=step, event_name=event_name, event=event, data=data, result=result - ) - - def __contains__(self, item: AbstractMiddleware) -> bool: - return item in self.middlewares diff --git a/aiogram/dispatcher/middlewares/types.py b/aiogram/dispatcher/middlewares/types.py deleted file mode 100644 index bc173025..00000000 --- a/aiogram/dispatcher/middlewares/types.py +++ /dev/null @@ -1,35 +0,0 @@ -from __future__ import annotations - -from enum import Enum -from typing import Union - -from aiogram.api.types import ( - CallbackQuery, - ChosenInlineResult, - InlineQuery, - Message, - Poll, - PollAnswer, - PreCheckoutQuery, - ShippingQuery, - Update, -) - -UpdateType = Union[ - CallbackQuery, - ChosenInlineResult, - InlineQuery, - Message, - Poll, - PollAnswer, - PreCheckoutQuery, - ShippingQuery, - Update, - BaseException, -] - - -class MiddlewareStep(Enum): - PRE_PROCESS = "pre_process" - PROCESS = "process" - POST_PROCESS = "post_process" diff --git a/aiogram/dispatcher/middlewares/update_processing_context.py b/aiogram/dispatcher/middlewares/update_processing_context.py new file mode 100644 index 00000000..09abd10f --- /dev/null +++ b/aiogram/dispatcher/middlewares/update_processing_context.py @@ -0,0 +1,62 @@ +from contextlib import contextmanager +from typing import Any, Awaitable, Callable, Dict, Iterator, Optional, Tuple + +from aiogram.api.types import Chat, Update, User +from aiogram.dispatcher.middlewares.base import BaseMiddleware + + +class UserContextMiddleware(BaseMiddleware[Update]): + async def __call__( + self, + handler: Callable[[Update, Dict[str, Any]], Awaitable[Any]], + event: Update, + data: Dict[str, Any], + ) -> Any: + chat, user = self.resolve_event_context(event=event) + with self.context(chat=chat, user=user): + return await handler(event, data) + + @contextmanager + def context(self, chat: Optional[Chat] = None, user: Optional[User] = None) -> Iterator[None]: + chat_token = None + user_token = None + if chat: + chat_token = chat.set_current(chat) + if user: + user_token = user.set_current(user) + try: + yield + finally: + if chat and chat_token: + chat.reset_current(chat_token) + if user and user_token: + user.reset_current(user_token) + + @classmethod + def resolve_event_context(cls, event: Update) -> Tuple[Optional[Chat], Optional[User]]: + """ + Resolve chat and user instance from Update object + """ + if event.message: + return event.message.chat, event.message.from_user + if event.edited_message: + return event.edited_message.chat, event.edited_message.from_user + if event.channel_post: + return event.channel_post.chat, None + if event.edited_channel_post: + return event.edited_channel_post.chat, None + if event.inline_query: + return None, event.inline_query.from_user + if event.chosen_inline_result: + return None, event.chosen_inline_result.from_user + if event.callback_query: + if event.callback_query.message: + return event.callback_query.message.chat, event.callback_query.from_user + return None, event.callback_query.from_user + if event.shipping_query: + return None, event.shipping_query.from_user + if event.pre_checkout_query: + return None, event.pre_checkout_query.from_user + if event.poll_answer: + return None, event.poll_answer.user + return None, None diff --git a/aiogram/dispatcher/router.py b/aiogram/dispatcher/router.py index 371c490d..7680454e 100644 --- a/aiogram/dispatcher/router.py +++ b/aiogram/dispatcher/router.py @@ -3,13 +3,14 @@ from __future__ import annotations import warnings from typing import Any, Dict, Generator, List, Optional, Union -from ..api.types import Chat, TelegramObject, Update, User +from ..api.types import TelegramObject, Update from ..utils.imports import import_module from ..utils.warnings import CodeHasNoEffect -from .event.observer import EventObserver, SkipHandler, TelegramEventObserver +from .event.bases import NOT_HANDLED, SkipHandler +from .event.event import EventObserver +from .event.telegram import TelegramEventObserver from .filters import BUILTIN_FILTERS -from .middlewares.abstract import AbstractMiddleware -from .middlewares.manager import MiddlewareManager +from .middlewares.error import ErrorsMiddleware class Router: @@ -44,8 +45,6 @@ class Router: self.poll_answer = TelegramEventObserver(router=self, event_name="poll_answer") self.errors = TelegramEventObserver(router=self, event_name="error") - self.middleware = MiddlewareManager(router=self) - self.startup = EventObserver() self.shutdown = EventObserver() @@ -68,6 +67,8 @@ class Router: # Root handler self.update.register(self._listen_update) + self.update.outer_middleware(ErrorsMiddleware(self)) + # Builtin filters if use_builtin_filters: for name, observer in self.observers.items(): @@ -94,16 +95,6 @@ class Router: next(tail) # Skip self yield from tail - def use(self, middleware: AbstractMiddleware, _stack_level: int = 1) -> AbstractMiddleware: - """ - Use middleware - - :param middleware: - :param _stack_level: - :return: - """ - return self.middleware.setup(middleware, _stack_level=_stack_level + 1) - @property def parent_router(self) -> Optional[Router]: return self._parent_router @@ -176,53 +167,40 @@ class Router: :param kwargs: :return: """ - chat: Optional[Chat] = None - from_user: Optional[User] = None - event: TelegramObject if update.message: update_type = "message" - from_user = update.message.from_user - chat = update.message.chat event = update.message elif update.edited_message: update_type = "edited_message" - from_user = update.edited_message.from_user - chat = update.edited_message.chat event = update.edited_message elif update.channel_post: update_type = "channel_post" - chat = update.channel_post.chat event = update.channel_post elif update.edited_channel_post: update_type = "edited_channel_post" - chat = update.edited_channel_post.chat event = update.edited_channel_post elif update.inline_query: update_type = "inline_query" - from_user = update.inline_query.from_user event = update.inline_query elif update.chosen_inline_result: update_type = "chosen_inline_result" - from_user = update.chosen_inline_result.from_user event = update.chosen_inline_result elif update.callback_query: update_type = "callback_query" - if update.callback_query.message: - chat = update.callback_query.message.chat - from_user = update.callback_query.from_user event = update.callback_query elif update.shipping_query: update_type = "shipping_query" - from_user = update.shipping_query.from_user event = update.shipping_query elif update.pre_checkout_query: update_type = "pre_checkout_query" - from_user = update.pre_checkout_query.from_user 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 else: warnings.warn( "Detected unknown update type.\n" @@ -232,76 +210,17 @@ class Router: ) raise SkipHandler - return await self.listen_update( - update_type=update_type, - update=update, - event=event, - from_user=from_user, - chat=chat, - **kwargs, - ) - - async def listen_update( - self, - update_type: str, - update: Update, - event: TelegramObject, - from_user: Optional[User] = None, - chat: Optional[Chat] = None, - **kwargs: Any, - ) -> Any: - """ - Listen update by current and child routers - - :param update_type: - :param update: - :param event: - :param from_user: - :param chat: - :param kwargs: - :return: - """ - user_token = None - if from_user: - user_token = User.set_current(from_user) - chat_token = None - if chat: - chat_token = Chat.set_current(chat) - kwargs.update(event_update=update, event_router=self) observer = self.observers[update_type] - try: - async for result in observer.trigger(event, update=update, **kwargs): - return result + response = await observer.trigger(event, update=update, **kwargs) + if response is NOT_HANDLED: # Resolve nested routers for router in self.sub_routers: - try: - return await router.listen_update( - update_type=update_type, - update=update, - event=event, - from_user=from_user, - chat=chat, - **kwargs, - ) - except SkipHandler: + response = await router.update.trigger(event=update, **kwargs) + if response is NOT_HANDLED: continue - raise SkipHandler - - except SkipHandler: - raise - - except Exception as e: - async for result in self.errors.trigger(e, **kwargs): - return result - raise - - finally: - if user_token: - User.reset_current(user_token) - if chat_token: - Chat.reset_current(chat_token) + return response async def emit_startup(self, *args: Any, **kwargs: Any) -> None: """ @@ -312,8 +231,7 @@ class Router: :return: """ kwargs.update(router=self) - async for _ in self.startup.trigger(*args, **kwargs): # pragma: no cover - pass + await self.startup.trigger(*args, **kwargs) for router in self.sub_routers: await router.emit_startup(*args, **kwargs) @@ -326,8 +244,7 @@ class Router: :return: """ kwargs.update(router=self) - async for _ in self.shutdown.trigger(*args, **kwargs): # pragma: no cover - pass + await self.shutdown.trigger(*args, **kwargs) for router in self.sub_routers: await router.emit_shutdown(*args, **kwargs) diff --git a/aiogram/utils/mixins.py b/aiogram/utils/mixins.py index 0c4834f4..156339d6 100644 --- a/aiogram/utils/mixins.py +++ b/aiogram/utils/mixins.py @@ -1,9 +1,10 @@ from __future__ import annotations import contextvars -from typing import Any, ClassVar, Dict, Generic, Optional, TypeVar, cast, overload +from typing import TYPE_CHECKING, Any, ClassVar, Dict, Generic, Optional, TypeVar, cast, overload -from typing_extensions import Literal +if TYPE_CHECKING: # pragma: no cover + from typing_extensions import Literal __all__ = ("ContextInstanceMixin", "DataMixin") diff --git a/docs/dispatcher/middlewares/basics.md b/docs/dispatcher/middlewares/basics.md index 83b58f07..51ef8b4f 100644 --- a/docs/dispatcher/middlewares/basics.md +++ b/docs/dispatcher/middlewares/basics.md @@ -1,5 +1,7 @@ # Basics + + All middlewares should be made with `BaseMiddleware` (`#!python3 from aiogram import BaseMiddleware`) as base class. For example: @@ -13,9 +15,9 @@ And then use next pattern in naming callback functions in middleware: `on_{step} Where is: - `#!python3 step`: - - `#!python3 pre_process` - - `#!python3 process` - - `#!python3 post_process` + - `#!python3 pre_process` + - `#!python3 process` + - `#!python3 post_process` - `#!python3 event`: - `#!python3 update` - `#!python3 message` diff --git a/docs/dispatcher/middlewares/index.md b/docs/dispatcher/middlewares/index.md index 6815a565..7752b5d1 100644 --- a/docs/dispatcher/middlewares/index.md +++ b/docs/dispatcher/middlewares/index.md @@ -2,25 +2,25 @@ **aiogram** provides powerful mechanism for customizing event handlers via middlewares. -Middlewares in bot framework seems like Middlewares mechanism in web-frameworks -(like [aiohttp](https://docs.aiohttp.org/en/stable/web_advanced.html#aiohttp-web-middlewares), -[fastapi](https://fastapi.tiangolo.com/tutorial/middleware/), -[Django](https://docs.djangoproject.com/en/3.0/topics/http/middleware/) or etc.) -with small difference - here is implemented many layers of processing +Middlewares in bot framework seems like Middlewares mechanism in web-frameworks +(like [aiohttp](https://docs.aiohttp.org/en/stable/web_advanced.html#aiohttp-web-middlewares), +[fastapi](https://fastapi.tiangolo.com/tutorial/middleware/), +[Django](https://docs.djangoproject.com/en/3.0/topics/http/middleware/) or etc.) +with small difference - here is implemented two layers of processing (named as [pipeline](#event-pipeline)). !!! info - Middleware is function that triggered on every event received from + Middleware is function that triggered on every event received from Telegram Bot API in many points on processing pipeline. ## Base theory -As many books and other literature in internet says: -> Middleware is reusable software that leverages patterns and frameworks to bridge +As many books and other literature in internet says: +> Middleware is reusable software that leverages patterns and frameworks to bridge >the gap between the functional requirements of applications and the underlying operating systems, > network protocol stacks, and databases. -Middleware can modify, extend or reject processing event before-, +Middleware can modify, extend or reject processing event before-, on- or after- processing of that event. [![middlewares](../../assets/images/basics_middleware.png)](../../assets/images/basics_middleware.png) @@ -34,7 +34,7 @@ As described below middleware an interact with event in many stages of pipeline. Simple workflow: 1. Dispatcher receive an [Update](../../api/types/update.md) -1. Call **pre-process** update middleware in all routers tree +1. Call **pre-process** update middleware in all routers tree 1. Filter Update over handlers 1. Call **process** update middleware in all routers tree 1. Router detects event type (Message, Callback query, etc.) @@ -47,11 +47,11 @@ Simple workflow: 1. Emit response into webhook (when it needed) !!! warning - When filters does not match any handler with this event the `#!python3 process` + When filters does not match any handler with this event the `#!python3 process` step will not be called. !!! warning - When exception will be caused in handlers pipeline will be stopped immediately + When exception will be caused in handlers pipeline will be stopped immediately and then start processing error via errors handler and it own middleware callbacks. !!! warning diff --git a/poetry.lock b/poetry.lock index ba3c3590..3ccb6507 100644 --- a/poetry.lock +++ b/poetry.lock @@ -434,6 +434,14 @@ html5 = ["html5lib"] htmlsoup = ["beautifulsoup4"] source = ["Cython (>=0.29.7)"] +[[package]] +category = "main" +description = "This package provides magic filter based on dynamic attribute getter" +name = "magic-filter" +optional = false +python-versions = ">=3.6.1,<4.0.0" +version = "0.1.2" + [[package]] category = "dev" description = "Python implementation of Markdown." @@ -884,7 +892,7 @@ category = "dev" description = "YAML parser and emitter for Python" name = "pyyaml" optional = false -python-versions = "*" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" version = "5.3.1" [[package]] @@ -1031,8 +1039,7 @@ fast = ["uvloop"] proxy = ["aiohttp-socks"] [metadata] -content-hash = "57137b60a539ba01e8df533db976e2f3eadec37e717cbefbe775dc021a8c2714" - +content-hash = "5c53527f09e65af097aa3d3a25e41646e8b8a0dda25e96445ceef969c19297e5" python-versions = "^3.7" [metadata.files] @@ -1249,6 +1256,10 @@ lxml = [ {file = "lxml-4.5.0-cp38-cp38-win_amd64.whl", hash = "sha256:d5b3c4b7edd2e770375a01139be11307f04341ec709cf724e0f26ebb1eef12c3"}, {file = "lxml-4.5.0.tar.gz", hash = "sha256:8620ce80f50d023d414183bf90cc2576c2837b88e00bea3f33ad2630133bbb60"}, ] +magic-filter = [ + {file = "magic-filter-0.1.2.tar.gz", hash = "sha256:dfd1a778493083ac1355791d1716c3beb6629598739f2c2ec078815952282c1d"}, + {file = "magic_filter-0.1.2-py3-none-any.whl", hash = "sha256:16d0c96584f0660fd7fa94b6cd16f92383616208a32568bf8f95a57fc1a69e9d"}, +] markdown = [ {file = "Markdown-3.2.1-py2.py3-none-any.whl", hash = "sha256:e4795399163109457d4c5af2183fbe6b60326c17cfdf25ce6e7474c6624f725d"}, {file = "Markdown-3.2.1.tar.gz", hash = "sha256:90fee683eeabe1a92e149f7ba74e5ccdc81cd397bd6c516d93a8da0ef90b6902"}, @@ -1284,11 +1295,6 @@ markupsafe = [ {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6"}, {file = "MarkupSafe-1.1.1-cp37-cp37m-win32.whl", hash = "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2"}, {file = "MarkupSafe-1.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c"}, - {file = "MarkupSafe-1.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15"}, - {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2"}, - {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42"}, - {file = "MarkupSafe-1.1.1-cp38-cp38-win32.whl", hash = "sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b"}, - {file = "MarkupSafe-1.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be"}, {file = "MarkupSafe-1.1.1.tar.gz", hash = "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b"}, ] mccabe = [ diff --git a/pyproject.toml b/pyproject.toml index 60a9c0e3..88878b56 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,6 +41,7 @@ uvloop = {version = "^0.14.0", markers = "sys_platform == 'darwin' or sys_platfo async_lru = "^1.0" aiohttp-socks = {version = "^0.3.8", optional = true} typing-extensions = "^3.7.4" +magic-filter = "^0.1.2" [tool.poetry.dev-dependencies] uvloop = {version = "^0.14.0", markers = "sys_platform == 'darwin' or sys_platform == 'linux'"} @@ -94,7 +95,7 @@ include_trailing_comma = true force_grid_wrap = 0 use_parentheses = true line_length = 99 -known_third_party = ["aiofiles", "aiohttp", "aiohttp_socks", "aresponses", "async_lru", "packaging", "pydantic", "pytest", "typing_extensions"] +known_third_party = ["aiofiles", "aiohttp", "aiohttp_socks", "aresponses", "async_lru", "packaging", "pkg_resources", "pydantic", "pytest"] [build-system] requires = ["poetry>=0.12"] diff --git a/tests/test_api/test_types/test_inline_query.py b/tests/test_api/test_types/test_inline_query.py index 2ef76685..c828c17a 100644 --- a/tests/test_api/test_types/test_inline_query.py +++ b/tests/test_api/test_types/test_inline_query.py @@ -11,7 +11,13 @@ class TestInlineQuery: offset="", ) - kwargs = dict(results=[], cache_time=123, next_offset="123", switch_pm_text="foo", switch_pm_parameter="foo") + kwargs = dict( + results=[], + cache_time=123, + next_offset="123", + switch_pm_text="foo", + switch_pm_parameter="foo", + ) api_method = inline_query.answer(**kwargs) diff --git a/tests/test_api/test_types/test_shipping_query.py b/tests/test_api/test_types/test_shipping_query.py index 94e60640..939bb6c5 100644 --- a/tests/test_api/test_types/test_shipping_query.py +++ b/tests/test_api/test_types/test_shipping_query.py @@ -1,5 +1,5 @@ from aiogram.api.methods import AnswerShippingQuery -from aiogram.api.types import ShippingAddress, ShippingQuery, User, ShippingOption, LabeledPrice +from aiogram.api.types import LabeledPrice, ShippingAddress, ShippingOption, ShippingQuery, User class TestInlineQuery: @@ -19,7 +19,8 @@ class TestInlineQuery: ) shipping_options = [ - ShippingOption(id="id", title="foo", prices=[LabeledPrice(label="foo", amount=123)])] + ShippingOption(id="id", title="foo", prices=[LabeledPrice(label="foo", amount=123)]) + ] kwargs = dict(ok=True, shipping_options=shipping_options, error_message="foo") diff --git a/tests/test_dispatcher/test_deprecated.py b/tests/test_dispatcher/test_deprecated.py index efe6dfda..edea4aac 100644 --- a/tests/test_dispatcher/test_deprecated.py +++ b/tests/test_dispatcher/test_deprecated.py @@ -1,6 +1,6 @@ import pytest -from aiogram.dispatcher.event.observer import TelegramEventObserver +from aiogram.dispatcher.event.telegram import TelegramEventObserver from aiogram.dispatcher.router import Router from tests.deprecated import check_deprecated diff --git a/tests/test_dispatcher/test_dispatcher.py b/tests/test_dispatcher/test_dispatcher.py index 46a53db7..e5b9b50f 100644 --- a/tests/test_dispatcher/test_dispatcher.py +++ b/tests/test_dispatcher/test_dispatcher.py @@ -9,6 +9,7 @@ from aiogram import Bot from aiogram.api.methods import GetMe, GetUpdates, SendMessage from aiogram.api.types import Chat, Message, Update, User from aiogram.dispatcher.dispatcher import Dispatcher +from aiogram.dispatcher.event.bases import NOT_HANDLED from aiogram.dispatcher.router import Router from tests.mocked_bot import MockedBot @@ -63,7 +64,7 @@ class TestDispatcher: return message.text results_count = 0 - async for result in dp.feed_update( + result = await dp.feed_update( bot=bot, update=Update( update_id=42, @@ -75,11 +76,9 @@ class TestDispatcher: from_user=User(id=42, is_bot=False, first_name="Test"), ), ), - ): - results_count += 1 - assert result == "test" - - assert results_count == 1 + ) + results_count += 1 + assert result == "test" @pytest.mark.asyncio async def test_feed_raw_update(self): @@ -91,8 +90,7 @@ class TestDispatcher: assert message.text == "test" return message.text - handled = False - async for result in dp.feed_raw_update( + result = await dp.feed_raw_update( bot=bot, update={ "update_id": 42, @@ -101,13 +99,11 @@ class TestDispatcher: "date": int(time.time()), "text": "test", "chat": {"id": 42, "type": "private"}, - "user": {"id": 42, "is_bot": False, "first_name": "Test"}, + "from": {"id": 42, "is_bot": False, "first_name": "Test"}, }, }, - ): - handled = True - assert result == "test" - assert handled + ) + assert result == "test" @pytest.mark.asyncio async def test_listen_updates(self, bot: MockedBot): @@ -136,7 +132,8 @@ class TestDispatcher: async def test_process_update_empty(self, bot: MockedBot): dispatcher = Dispatcher() - assert not await dispatcher.process_update(bot=bot, update=Update(update_id=42)) + result = await dispatcher._process_update(bot=bot, update=Update(update_id=42)) + assert result @pytest.mark.asyncio async def test_process_update_handled(self, bot: MockedBot): @@ -146,22 +143,25 @@ class TestDispatcher: async def update_handler(update: Update): pass - assert await dispatcher.process_update(bot=bot, update=Update(update_id=42)) + assert await dispatcher._process_update(bot=bot, update=Update(update_id=42)) @pytest.mark.asyncio async def test_process_update_call_request(self, bot: MockedBot): dispatcher = Dispatcher() @dispatcher.update() - async def update_handler(update: Update): + async def message_handler(update: Update): return GetMe() + dispatcher.update.handlers.reverse() + with patch( "aiogram.dispatcher.dispatcher.Dispatcher._silent_call_request", new_callable=CoroutineMock, ) as mocked_silent_call_request: - assert await dispatcher.process_update(bot=bot, update=Update(update_id=42)) - mocked_silent_call_request.assert_awaited_once() + result = await dispatcher._process_update(bot=bot, update=Update(update_id=42)) + print(result) + mocked_silent_call_request.assert_awaited() @pytest.mark.asyncio async def test_process_update_exception(self, bot: MockedBot, caplog): @@ -171,7 +171,7 @@ class TestDispatcher: async def update_handler(update: Update): raise Exception("Kaboom!") - assert await dispatcher.process_update(bot=bot, update=Update(update_id=42)) + assert await dispatcher._process_update(bot=bot, update=Update(update_id=42)) log_records = [rec.message for rec in caplog.records] assert len(log_records) == 1 assert "Cause exception while process update" in log_records[0] @@ -184,7 +184,7 @@ class TestDispatcher: yield Update(update_id=42) with patch( - "aiogram.dispatcher.dispatcher.Dispatcher.process_update", new_callable=CoroutineMock + "aiogram.dispatcher.dispatcher.Dispatcher._process_update", new_callable=CoroutineMock ) as mocked_process_update, patch( "aiogram.dispatcher.dispatcher.Dispatcher._listen_updates" ) as patched_listen_updates: @@ -203,7 +203,7 @@ class TestDispatcher: yield Update(update_id=42) with patch( - "aiogram.dispatcher.dispatcher.Dispatcher.process_update", new_callable=CoroutineMock + "aiogram.dispatcher.dispatcher.Dispatcher._process_update", new_callable=CoroutineMock ) as mocked_process_update, patch( "aiogram.dispatcher.router.Router.emit_startup", new_callable=CoroutineMock ) as mocked_emit_startup, patch( diff --git a/tests/test_dispatcher/test_event/test_event.py b/tests/test_dispatcher/test_event/test_event.py new file mode 100644 index 00000000..335b864c --- /dev/null +++ b/tests/test_dispatcher/test_event/test_event.py @@ -0,0 +1,59 @@ +import functools +from typing import Any + +import pytest + +from aiogram.dispatcher.event.event import EventObserver +from aiogram.dispatcher.event.handler import HandlerObject + +try: + from asynctest import CoroutineMock, patch +except ImportError: + from unittest.mock import AsyncMock as CoroutineMock, patch # type: ignore + + +async def my_handler(value: str, index: int = 0) -> Any: + return value + + +class TestEventObserver: + @pytest.mark.parametrize("via_decorator", [True, False]) + @pytest.mark.parametrize("count,handler", ([5, my_handler], [3, my_handler], [2, my_handler])) + def test_register_filters(self, via_decorator, count, handler): + observer = EventObserver() + + for index in range(count): + wrapped_handler = functools.partial(handler, index=index) + if via_decorator: + register_result = observer()(wrapped_handler) + assert register_result == wrapped_handler + else: + register_result = observer.register(wrapped_handler) + assert register_result is None + + registered_handler = observer.handlers[index] + + assert len(observer.handlers) == index + 1 + assert isinstance(registered_handler, HandlerObject) + assert registered_handler.callback == wrapped_handler + assert not registered_handler.filters + + @pytest.mark.asyncio + async def test_trigger(self): + observer = EventObserver() + + observer.register(my_handler) + observer.register(lambda e: True) + observer.register(my_handler) + + assert observer.handlers[0].awaitable + assert not observer.handlers[1].awaitable + assert observer.handlers[2].awaitable + + with patch( + "aiogram.dispatcher.event.handler.CallableMixin.call", new_callable=CoroutineMock, + ) as mocked_my_handler: + results = await observer.trigger("test") + assert results is None + mocked_my_handler.assert_awaited_with("test") + assert mocked_my_handler.call_count == 3 diff --git a/tests/test_dispatcher/test_event/test_observer.py b/tests/test_dispatcher/test_event/test_telegram.py similarity index 72% rename from tests/test_dispatcher/test_event/test_observer.py rename to tests/test_dispatcher/test_event/test_telegram.py index c1364676..5d4c6607 100644 --- a/tests/test_dispatcher/test_event/test_observer.py +++ b/tests/test_dispatcher/test_event/test_telegram.py @@ -5,11 +5,14 @@ from typing import Any, Awaitable, Callable, Dict, NoReturn, Union import pytest from aiogram.api.types import Chat, Message, User +from aiogram.dispatcher.event.bases import SkipHandler from aiogram.dispatcher.event.handler import HandlerObject -from aiogram.dispatcher.event.observer import EventObserver, SkipHandler, TelegramEventObserver +from aiogram.dispatcher.event.telegram import TelegramEventObserver from aiogram.dispatcher.filters.base import BaseFilter from aiogram.dispatcher.router import Router +# TODO: Test middlewares in routers tree + async def my_handler(event: Any, index: int = 0) -> Any: return event @@ -38,54 +41,6 @@ class MyFilter3(MyFilter1): pass -class TestEventObserver: - @pytest.mark.parametrize("count,handler", ([5, my_handler], [3, my_handler], [2, my_handler])) - def test_register_filters(self, count, handler): - observer = EventObserver() - - for index in range(count): - wrapped_handler = functools.partial(handler, index=index) - observer.register(wrapped_handler) - registered_handler = observer.handlers[index] - - assert len(observer.handlers) == index + 1 - assert isinstance(registered_handler, HandlerObject) - assert registered_handler.callback == wrapped_handler - assert not registered_handler.filters - - @pytest.mark.parametrize("count,handler", ([5, my_handler], [3, my_handler], [2, my_handler])) - def test_register_filters_via_decorator(self, count, handler): - observer = EventObserver() - - for index in range(count): - wrapped_handler = functools.partial(handler, index=index) - observer()(wrapped_handler) - registered_handler = observer.handlers[index] - - assert len(observer.handlers) == index + 1 - assert isinstance(registered_handler, HandlerObject) - assert registered_handler.callback == wrapped_handler - assert not registered_handler.filters - - @pytest.mark.asyncio - async def test_trigger_accepted_bool(self): - observer = EventObserver() - observer.register(my_handler) - - results = [result async for result in observer.trigger(42)] - assert results == [42] - - @pytest.mark.asyncio - async def test_trigger_with_skip(self): - observer = EventObserver() - observer.register(skip_my_handler) - observer.register(my_handler) - observer.register(my_handler) - - results = [result async for result in observer.trigger(42)] - assert results == [42, 42] - - class TestTelegramEventObserver: def test_bind_filter(self): event_observer = TelegramEventObserver(Router(), "test") @@ -198,8 +153,8 @@ class TestTelegramEventObserver: from_user=User(id=42, is_bot=False, first_name="Test"), ) - results = [result async for result in observer.trigger(message)] - assert results == [message] + results = await observer.trigger(message) + assert results is message @pytest.mark.parametrize( "count,handler,filters", @@ -223,15 +178,58 @@ class TestTelegramEventObserver: assert registered_handler.callback == wrapped_handler assert len(registered_handler.filters) == len(filters) - # @pytest.mark.asyncio async def test_trigger_right_context_in_handlers(self): router = Router(use_builtin_filters=False) observer = router.message - observer.register( - pipe_handler, lambda event: {"a": 1}, lambda event: False - ) # {"a": 1} should not be in result - observer.register(pipe_handler, lambda event: {"b": 2}) - results = [result async for result in observer.trigger(42)] - assert results == [((42,), {"b": 2})] + async def mix_unnecessary_data(event): + return {"a": 1} + + async def mix_data(event): + return {"b": 2} + + async def handler(event, **kwargs): + return False + + observer.register( + pipe_handler, mix_unnecessary_data, handler + ) # {"a": 1} should not be in result + observer.register(pipe_handler, mix_data) + + results = await observer.trigger(42) + assert results == ((42,), {"b": 2}) + + @pytest.mark.parametrize("middleware_type", ("middleware", "outer_middleware")) + def test_register_middleware(self, middleware_type): + event_observer = TelegramEventObserver(Router(), "test") + + middlewares = getattr(event_observer, f"{middleware_type}s") + decorator = getattr(event_observer, middleware_type) + + @decorator + async def my_middleware1(handler, event, data): + pass + + assert my_middleware1 is not None + assert my_middleware1.__name__ == "my_middleware1" + assert my_middleware1 in middlewares + + @decorator() + async def my_middleware2(handler, event, data): + pass + + assert my_middleware2 is not None + assert my_middleware2.__name__ == "my_middleware2" + assert my_middleware2 in middlewares + + async def my_middleware3(handler, event, data): + pass + + decorator(my_middleware3) + + assert my_middleware3 is not None + assert my_middleware3.__name__ == "my_middleware3" + assert my_middleware3 in middlewares + + assert middlewares == [my_middleware1, my_middleware2, my_middleware3] diff --git a/tests/test_dispatcher/test_middlewares/__init__.py b/tests/test_dispatcher/test_middlewares/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/tests/test_dispatcher/test_middlewares/test_base.py b/tests/test_dispatcher/test_middlewares/test_base.py deleted file mode 100644 index 7899324d..00000000 --- a/tests/test_dispatcher/test_middlewares/test_base.py +++ /dev/null @@ -1,257 +0,0 @@ -import datetime -from typing import Any, Dict, Type - -import pytest - -from aiogram.api.types import ( - CallbackQuery, - Chat, - ChosenInlineResult, - InlineQuery, - Message, - Poll, - PollAnswer, - PreCheckoutQuery, - ShippingQuery, - Update, - User, -) -from aiogram.dispatcher.middlewares.base import BaseMiddleware -from aiogram.dispatcher.middlewares.types import MiddlewareStep, UpdateType - -try: - from asynctest import CoroutineMock, patch -except ImportError: - from unittest.mock import AsyncMock as CoroutineMock, patch # type: ignore - - -class MyMiddleware(BaseMiddleware): - async def on_pre_process_update(self, update: Update, data: Dict[str, Any]) -> Any: - return "update" - - async def on_pre_process_message(self, message: Message, data: Dict[str, Any]) -> Any: - return "message" - - async def on_pre_process_edited_message( - self, edited_message: Message, data: Dict[str, Any] - ) -> Any: - return "edited_message" - - async def on_pre_process_channel_post( - self, channel_post: Message, data: Dict[str, Any] - ) -> Any: - return "channel_post" - - async def on_pre_process_edited_channel_post( - self, edited_channel_post: Message, data: Dict[str, Any] - ) -> Any: - return "edited_channel_post" - - async def on_pre_process_inline_query( - self, inline_query: InlineQuery, data: Dict[str, Any] - ) -> Any: - return "inline_query" - - async def on_pre_process_chosen_inline_result( - self, chosen_inline_result: ChosenInlineResult, data: Dict[str, Any] - ) -> Any: - return "chosen_inline_result" - - async def on_pre_process_callback_query( - self, callback_query: CallbackQuery, data: Dict[str, Any] - ) -> Any: - return "callback_query" - - async def on_pre_process_shipping_query( - self, shipping_query: ShippingQuery, data: Dict[str, Any] - ) -> Any: - return "shipping_query" - - async def on_pre_process_pre_checkout_query( - self, pre_checkout_query: PreCheckoutQuery, data: Dict[str, Any] - ) -> Any: - return "pre_checkout_query" - - async def on_pre_process_poll(self, poll: Poll, data: Dict[str, Any]) -> Any: - return "poll" - - async def on_pre_process_poll_answer( - self, poll_answer: PollAnswer, data: Dict[str, Any] - ) -> Any: - return "poll_answer" - - async def on_pre_process_error(self, exception: Exception, data: Dict[str, Any]) -> Any: - return "error" - - async def on_process_update(self, update: Update, data: Dict[str, Any]) -> Any: - return "update" - - async def on_process_message(self, message: Message, data: Dict[str, Any]) -> Any: - return "message" - - async def on_process_edited_message( - self, edited_message: Message, data: Dict[str, Any] - ) -> Any: - return "edited_message" - - async def on_process_channel_post(self, channel_post: Message, data: Dict[str, Any]) -> Any: - return "channel_post" - - async def on_process_edited_channel_post( - self, edited_channel_post: Message, data: Dict[str, Any] - ) -> Any: - return "edited_channel_post" - - async def on_process_inline_query( - self, inline_query: InlineQuery, data: Dict[str, Any] - ) -> Any: - return "inline_query" - - async def on_process_chosen_inline_result( - self, chosen_inline_result: ChosenInlineResult, data: Dict[str, Any] - ) -> Any: - return "chosen_inline_result" - - async def on_process_callback_query( - self, callback_query: CallbackQuery, data: Dict[str, Any] - ) -> Any: - return "callback_query" - - async def on_process_shipping_query( - self, shipping_query: ShippingQuery, data: Dict[str, Any] - ) -> Any: - return "shipping_query" - - async def on_process_pre_checkout_query( - self, pre_checkout_query: PreCheckoutQuery, data: Dict[str, Any] - ) -> Any: - return "pre_checkout_query" - - async def on_process_poll(self, poll: Poll, data: Dict[str, Any]) -> Any: - return "poll" - - async def on_process_poll_answer(self, poll_answer: PollAnswer, data: Dict[str, Any]) -> Any: - return "poll_answer" - - async def on_process_error(self, exception: Exception, data: Dict[str, Any]) -> Any: - return "error" - - async def on_post_process_update( - self, update: Update, data: Dict[str, Any], result: Any - ) -> Any: - return "update" - - async def on_post_process_message( - self, message: Message, data: Dict[str, Any], result: Any - ) -> Any: - return "message" - - async def on_post_process_edited_message( - self, edited_message: Message, data: Dict[str, Any], result: Any - ) -> Any: - return "edited_message" - - async def on_post_process_channel_post( - self, channel_post: Message, data: Dict[str, Any], result: Any - ) -> Any: - return "channel_post" - - async def on_post_process_edited_channel_post( - self, edited_channel_post: Message, data: Dict[str, Any], result: Any - ) -> Any: - return "edited_channel_post" - - async def on_post_process_inline_query( - self, inline_query: InlineQuery, data: Dict[str, Any], result: Any - ) -> Any: - return "inline_query" - - async def on_post_process_chosen_inline_result( - self, chosen_inline_result: ChosenInlineResult, data: Dict[str, Any], result: Any - ) -> Any: - return "chosen_inline_result" - - async def on_post_process_callback_query( - self, callback_query: CallbackQuery, data: Dict[str, Any], result: Any - ) -> Any: - return "callback_query" - - async def on_post_process_shipping_query( - self, shipping_query: ShippingQuery, data: Dict[str, Any], result: Any - ) -> Any: - return "shipping_query" - - async def on_post_process_pre_checkout_query( - self, pre_checkout_query: PreCheckoutQuery, data: Dict[str, Any], result: Any - ) -> Any: - return "pre_checkout_query" - - async def on_post_process_poll(self, poll: Poll, data: Dict[str, Any], result: Any) -> Any: - return "poll" - - async def on_post_process_poll_answer( - self, poll_answer: PollAnswer, data: Dict[str, Any], result: Any - ) -> Any: - return "poll_answer" - - async def on_post_process_error( - self, exception: Exception, data: Dict[str, Any], result: Any - ) -> Any: - return "error" - - -UPDATE = Update(update_id=42) -MESSAGE = Message(message_id=42, date=datetime.datetime.now(), chat=Chat(id=42, type="private")) -POLL_ANSWER = PollAnswer( - poll_id="poll", user=User(id=42, is_bot=False, first_name="Test"), option_ids=[0] -) - - -class TestBaseMiddleware: - @pytest.mark.asyncio - @pytest.mark.parametrize( - "middleware_cls,should_be_awaited", [[MyMiddleware, True], [BaseMiddleware, False]] - ) - @pytest.mark.parametrize( - "step", [MiddlewareStep.PRE_PROCESS, MiddlewareStep.PROCESS, MiddlewareStep.POST_PROCESS] - ) - @pytest.mark.parametrize( - "event_name,event", - [ - ["update", UPDATE], - ["message", MESSAGE], - ["poll_answer", POLL_ANSWER], - ["error", Exception("KABOOM")], - ], - ) - async def test_trigger( - self, - step: MiddlewareStep, - event_name: str, - event: UpdateType, - middleware_cls: Type[BaseMiddleware], - should_be_awaited: bool, - ): - middleware = middleware_cls() - - with patch( - f"tests.test_dispatcher.test_middlewares.test_base." - f"MyMiddleware.on_{step.value}_{event_name}", - new_callable=CoroutineMock, - ) as mocked_call: - response = await middleware.trigger( - step=step, event_name=event_name, event=event, data={} - ) - if should_be_awaited: - mocked_call.assert_awaited() - assert response is not None - else: - mocked_call.assert_not_awaited() - assert response is None - - def test_not_configured(self): - middleware = BaseMiddleware() - assert not middleware.configured - - with pytest.raises(RuntimeError): - manager = middleware.manager diff --git a/tests/test_dispatcher/test_middlewares/test_manager.py b/tests/test_dispatcher/test_middlewares/test_manager.py deleted file mode 100644 index 0e23f1b2..00000000 --- a/tests/test_dispatcher/test_middlewares/test_manager.py +++ /dev/null @@ -1,82 +0,0 @@ -import pytest - -from aiogram import Router -from aiogram.api.types import Update -from aiogram.dispatcher.middlewares.base import BaseMiddleware -from aiogram.dispatcher.middlewares.manager import MiddlewareManager -from aiogram.dispatcher.middlewares.types import MiddlewareStep - -try: - from asynctest import CoroutineMock, patch -except ImportError: - from unittest.mock import AsyncMock as CoroutineMock, patch # type: ignore - - -@pytest.fixture("function") -def router(): - return Router() - - -@pytest.fixture("function") -def manager(router: Router): - return MiddlewareManager(router) - - -class TestManager: - def test_setup(self, manager: MiddlewareManager): - middleware = BaseMiddleware() - returned = manager.setup(middleware) - assert returned is middleware - assert middleware.configured - assert middleware.manager is manager - assert middleware in manager - - @pytest.mark.parametrize("obj", [object, object(), None, BaseMiddleware]) - def test_setup_invalid_type(self, manager: MiddlewareManager, obj): - with pytest.raises(TypeError): - assert manager.setup(obj) - - def test_configure_twice_different_managers(self, manager: MiddlewareManager, router: Router): - middleware = BaseMiddleware() - manager.setup(middleware) - - assert middleware.configured - - new_manager = MiddlewareManager(router) - with pytest.raises(ValueError): - new_manager.setup(middleware) - with pytest.raises(ValueError): - middleware.setup(new_manager) - - def test_configure_twice(self, manager: MiddlewareManager): - middleware = BaseMiddleware() - manager.setup(middleware) - - assert middleware.configured - - with pytest.warns(RuntimeWarning, match="is already configured for this Router"): - manager.setup(middleware) - - with pytest.warns(RuntimeWarning, match="is already configured for this Router"): - middleware.setup(manager) - - @pytest.mark.asyncio - @pytest.mark.parametrize("count", range(5)) - async def test_trigger(self, manager: MiddlewareManager, count: int): - for _ in range(count): - manager.setup(BaseMiddleware()) - - with patch( - "aiogram.dispatcher.middlewares.base.BaseMiddleware.trigger", - new_callable=CoroutineMock, - ) as mocked_call: - await manager.trigger( - step=MiddlewareStep.PROCESS, - event_name="update", - event=Update(update_id=42), - data={}, - result=None, - reverse=True, - ) - - assert mocked_call.await_count == count diff --git a/tests/test_dispatcher/test_router.py b/tests/test_dispatcher/test_router.py index 303efbb3..c5ba6277 100644 --- a/tests/test_dispatcher/test_router.py +++ b/tests/test_dispatcher/test_router.py @@ -10,6 +10,7 @@ from aiogram.api.types import ( InlineQuery, Message, Poll, + PollAnswer, PollOption, PreCheckoutQuery, ShippingAddress, @@ -17,8 +18,8 @@ from aiogram.api.types import ( Update, User, ) -from aiogram.dispatcher.event.observer import SkipHandler, skip -from aiogram.dispatcher.middlewares.base import BaseMiddleware +from aiogram.dispatcher.event.bases import NOT_HANDLED, SkipHandler, skip +from aiogram.dispatcher.middlewares.update_processing_context import UserContextMiddleware from aiogram.dispatcher.router import Router from aiogram.utils.warnings import CodeHasNoEffect @@ -274,12 +275,26 @@ class TestRouter: False, False, ), + pytest.param( + "poll_answer", + Update( + update_id=42, + poll_answer=PollAnswer( + poll_id="poll id", + user=User(id=42, is_bot=False, first_name="Test"), + option_ids=[42], + ), + ), + False, + True, + ), ], ) async def test_listen_update( self, event_type: str, update: Update, has_chat: bool, has_user: bool ): router = Router() + router.update.outer_middleware(UserContextMiddleware()) observer = router.observers[event_type] @observer() @@ -291,7 +306,7 @@ class TestRouter: assert User.get_current(False) return kwargs - result = await router._listen_update(update, test="PASS") + result = await router.update.trigger(update, test="PASS") assert isinstance(result, dict) assert result["event_update"] == update assert result["event_router"] == router @@ -313,26 +328,26 @@ class TestRouter: async def handler(event: Any): pass - with pytest.raises(SkipHandler): - await router._listen_update( - Update( - update_id=42, - poll=Poll( - id="poll id", - question="Q?", - options=[ - PollOption(text="A1", voter_count=2), - PollOption(text="A2", voter_count=3), - ], - is_closed=False, - is_anonymous=False, - type="quiz", - allows_multiple_answers=False, - total_voter_count=0, - correct_option_id=0, - ), - ) + response = await router._listen_update( + Update( + update_id=42, + poll=Poll( + id="poll id", + question="Q?", + options=[ + PollOption(text="A1", voter_count=2), + PollOption(text="A2", voter_count=3), + ], + is_closed=False, + is_anonymous=False, + type="quiz", + allows_multiple_answers=False, + total_voter_count=0, + correct_option_id=0, + ), ) + ) + assert response is NOT_HANDLED @pytest.mark.asyncio async def test_nested_router_listen_update(self): @@ -345,8 +360,6 @@ class TestRouter: @observer() async def my_handler(event: Message, **kwargs: Any): - assert Chat.get_current(False) - assert User.get_current(False) return kwargs update = Update( @@ -409,14 +422,6 @@ class TestRouter: await router1.emit_shutdown() assert results == [2, 1, 2] - def test_use(self): - router = Router() - - middleware = router.use(BaseMiddleware()) - assert isinstance(middleware, BaseMiddleware) - assert middleware.configured - assert middleware.manager == router.middleware - def test_skip(self): with pytest.raises(SkipHandler): skip() @@ -444,37 +449,20 @@ class TestRouter: ), ) with pytest.raises(Exception, match="KABOOM"): - await root_router.listen_update( - update_type="message", - update=update, - event=update.message, - from_user=update.message.from_user, - chat=update.message.chat, - ) + await root_router.update.trigger(update) @root_router.errors() - async def root_error_handler(exception: Exception): + async def root_error_handler(event: Update, exception: Exception): return exception - response = await root_router.listen_update( - update_type="message", - update=update, - event=update.message, - from_user=update.message.from_user, - chat=update.message.chat, - ) + response = await root_router.update.trigger(update) + assert isinstance(response, Exception) assert str(response) == "KABOOM" @router.errors() - async def error_handler(exception: Exception): + async def error_handler(event: Update, exception: Exception): return "KABOOM" - response = await root_router.listen_update( - update_type="message", - update=update, - event=update.message, - from_user=update.message.from_user, - chat=update.message.chat, - ) + response = await root_router.update.trigger(update) assert response == "KABOOM" From bafc2ff341332baccc82983ae5c94565905c61a2 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Tue, 26 May 2020 20:26:29 +0300 Subject: [PATCH 2/3] Update middlewares docs --- aiogram/__init__.py | 2 + docs/assets/images/basics_middleware.png | Bin 32505 -> 52245 bytes docs/dispatcher/dispatcher.md | 2 +- docs/dispatcher/middlewares.md | 95 ++++++++++++++++++ docs/dispatcher/middlewares/basics.md | 117 ----------------------- docs/dispatcher/middlewares/index.md | 77 --------------- docs/index.md | 2 +- docs/stylesheets/extra.css | 12 +++ mkdocs.yml | 8 +- poetry.lock | 16 +--- pyproject.toml | 3 +- 11 files changed, 119 insertions(+), 215 deletions(-) create mode 100644 docs/dispatcher/middlewares.md delete mode 100644 docs/dispatcher/middlewares/basics.md delete mode 100644 docs/dispatcher/middlewares/index.md create mode 100644 docs/stylesheets/extra.css diff --git a/aiogram/__init__.py b/aiogram/__init__.py index 8ab84aa6..3df77008 100644 --- a/aiogram/__init__.py +++ b/aiogram/__init__.py @@ -5,6 +5,7 @@ from .api.client import session from .api.client.bot import Bot from .dispatcher import filters, handler from .dispatcher.dispatcher import Dispatcher +from .dispatcher.middlewares.base import BaseMiddleware from .dispatcher.router import Router try: @@ -24,6 +25,7 @@ __all__ = ( "session", "Dispatcher", "Router", + "BaseMiddleware", "filters", "handler", ) diff --git a/docs/assets/images/basics_middleware.png b/docs/assets/images/basics_middleware.png index a797fd384a1d630c1d2a631dc88a8856dc374313..b4165e2ec2401143a74714e27699cca195e1d30b 100644 GIT binary patch literal 52245 zcmeFYz@(XkE3;k=NrmUx-&d4VR zy>@YQwuAmC+1WU|{c9oz@^o{BmMDtwbMbHq@bd6;3GqV9b%FLkPsjfu<)89)-av={ zC@uu>)b(?9_fS#L0Sf8sI_VfXK>njJ#Lm;p(GC2cYP|ehyj=YMdEo8uZug&32RAnd z7igb+jQk2v+x@qZpzfU%sPj@N&LfPqkD|I57EP|I*a zHT@@q*TCM|#@o$TL7NAt?4_+@AQ+%5qwXxCDXR@M(NuR;)79`Y)&uCcIQppTLdWDJ z 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%0D`)^icMb+xAUgy!x%;?Ig@DY|Y2@ZP>vn56Fu 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 ZqWl}EBMTb=eA51tw77y;mB_b%{{x-X#IOJW diff --git a/docs/dispatcher/dispatcher.md b/docs/dispatcher/dispatcher.md index 462d0748..74f018e4 100644 --- a/docs/dispatcher/dispatcher.md +++ b/docs/dispatcher/dispatcher.md @@ -39,7 +39,7 @@ dp.include_router(router1) ## Handling updates All updates can be propagated to the dispatcher by `feed_update` method: -``` +```python3 bot = Bot(...) dp = Dispathcher() diff --git a/docs/dispatcher/middlewares.md b/docs/dispatcher/middlewares.md new file mode 100644 index 00000000..a0649ce5 --- /dev/null +++ b/docs/dispatcher/middlewares.md @@ -0,0 +1,95 @@ +# Middlewares + +**aiogram** provides powerful mechanism for customizing event handlers via middlewares. + +Middlewares in bot framework seems like Middlewares mechanism in web-frameworks +(like [aiohttp](https://docs.aiohttp.org/en/stable/web_advanced.html#aiohttp-web-middlewares), +[fastapi](https://fastapi.tiangolo.com/tutorial/middleware/), +[Django](https://docs.djangoproject.com/en/3.0/topics/http/middleware/) or etc.) +with small difference - here is implemented two layers of middlewares (before and after filters). + +!!! info + Middleware is function that triggered on every event received from + Telegram Bot API in many points on processing pipeline. + +## Base theory + +As many books and other literature in internet says: +> Middleware is reusable software that leverages patterns and frameworks to bridge +> the gap between the functional requirements of applications and the underlying operating systems, +> network protocol stacks, and databases. + +Middleware can modify, extend or reject processing event in many places of pipeline. + +## Basics + +Middleware instance can be applied for every type of Telegram Event (Update, Message, etc.) in two places + +1. Outer scope - before processing filters (`#!python ..outer_middleware(...)`) +2. Inner scope - after processing filters but before handler (`#!python ..middleware(...)`) + +[![middlewares](../assets/images/basics_middleware.png)](../assets/images/basics_middleware.png) + +_(Click on image to zoom it)_ + +!!! warning + + Middleware should be subclass of `BaseMiddleware` (`#!python3 from aiogram import BaseMiddleware`) or any async callable + +## Arguments specification +| Argument | Type | Description | +| - | - | - | +| `handler` | `#!python Callable[[T, Dict[str, Any]], Awaitable[Any]]` | Wrapped handler in middlewares chain | +| `event` | `#!python T` | Incoming event (Subclass of `TelegramObject`) | +| `data` | `#!python Dict[str, Any]` | Contextual data. Will be mapped to handler arguments | + +## Examples + +!!! danger + + Middleware should always call `#!python await handler(event, data)` to propagate event for next middleware/handler + +### Class-based +```python3 +from aiogram import BaseMiddleware +from aiogram.api.types import Message + + +class CounterMiddleware(BaseMiddleware[Message]): + def __init__(self) -> None: + self.counter = 0 + + async def __call__( + self, + handler: Callable[[Message, Dict[str, Any]], Awaitable[Any]], + event: Message, + data: Dict[str, Any] + ) -> Any: + self.counter += 1 + data['counter'] = self.counter + return await handler(event, data) +``` +and then +```python3 +router = Router() +router.message.middleware(CounterMiddleware()) +``` + +### Function-based +```python3 +@dispatcher.update.outer_middleware() +async def database_transaction_middleware( + handler: Callable[[Update, Dict[str, Any]], Awaitable[Any]], + event: Update, + data: Dict[str, Any] +) -> Any: + async with database.transaction(): + return await handler(event, data) +``` + +## Facts + +1. Middlewares from outer scope will be called on every incoming event +1. Middlewares from inner scope will be called only when filters pass +1. Inner middlewares is always calls for `Update` event type in due to all incoming updates going to specific event type handler through built in update handler + diff --git a/docs/dispatcher/middlewares/basics.md b/docs/dispatcher/middlewares/basics.md deleted file mode 100644 index 51ef8b4f..00000000 --- a/docs/dispatcher/middlewares/basics.md +++ /dev/null @@ -1,117 +0,0 @@ -# Basics - - - -All middlewares should be made with `BaseMiddleware` (`#!python3 from aiogram import BaseMiddleware`) as base class. - -For example: - -```python3 -class MyMiddleware(BaseMiddleware): ... -``` - -And then use next pattern in naming callback functions in middleware: `on_{step}_{event}` - -Where is: - -- `#!python3 step`: - - `#!python3 pre_process` - - `#!python3 process` - - `#!python3 post_process` -- `#!python3 event`: - - `#!python3 update` - - `#!python3 message` - - `#!python3 edited_message` - - `#!python3 channel_post` - - `#!python3 edited_channel_post` - - `#!python3 inline_query` - - `#!python3 chosen_inline_result` - - `#!python3 callback_query` - - `#!python3 shipping_query` - - `#!python3 pre_checkout_query` - - `#!python3 poll` - - `#!python3 poll_answer` - - `#!python3 error` - -## Connecting middleware with router - -Middlewares can be connected with router by next ways: - -1. `#!python3 router.use(MyMiddleware())` (**recommended**) -1. `#!python3 router.middleware.setup(MyMiddleware())` -1. `#!python3 MyMiddleware().setup(router.middleware)` (**not recommended**) - -!!! warning - One instance of middleware **can't** be registered twice in single or many middleware managers - -## The specification of step callbacks - -### Pre-process step - -| Argument | Type | Description | -| --- | --- | --- | -| event name | Any of event type (Update, Message and etc.) | Event | -| `#!python3 data` | `#!python3 Dict[str, Any]` | Contextual data (Will be mapped to handler arguments) | - -Returns `#!python3 Any` - -### Process step - -| Argument | Type | Description | -| --- | --- | --- | -| event name | Any of event type (Update, Message and etc.) | Event | -| `#!python3 data` | `#!python3 Dict[str, Any]` | Contextual data (Will be mapped to handler arguments) | - -Returns `#!python3 Any` - -### Post-Process step - -| Argument | Type | Description | -| --- | --- | --- | -| event name | Any of event type (Update, Message and etc.) | Event | -| `#!python3 data` | `#!python3 Dict[str, Any]` | Contextual data (Will be mapped to handler arguments) | -| `#!python3 result` | `#!python3 Dict[str, Any]` | Response from handlers | - -Returns `#!python3 Any` - -## Full list of available callbacks - -- `#!python3 on_pre_process_update` - will be triggered on **pre process** `#!python3 update` event -- `#!python3 on_process_update` - will be triggered on **process** `#!python3 update` event -- `#!python3 on_post_process_update` - will be triggered on **post process** `#!python3 update` event -- `#!python3 on_pre_process_message` - will be triggered on **pre process** `#!python3 message` event -- `#!python3 on_process_message` - will be triggered on **process** `#!python3 message` event -- `#!python3 on_post_process_message` - will be triggered on **post process** `#!python3 message` event -- `#!python3 on_pre_process_edited_message` - will be triggered on **pre process** `#!python3 edited_message` event -- `#!python3 on_process_edited_message` - will be triggered on **process** `#!python3 edited_message` event -- `#!python3 on_post_process_edited_message` - will be triggered on **post process** `#!python3 edited_message` event -- `#!python3 on_pre_process_channel_post` - will be triggered on **pre process** `#!python3 channel_post` event -- `#!python3 on_process_channel_post` - will be triggered on **process** `#!python3 channel_post` event -- `#!python3 on_post_process_channel_post` - will be triggered on **post process** `#!python3 channel_post` event -- `#!python3 on_pre_process_edited_channel_post` - will be triggered on **pre process** `#!python3 edited_channel_post` event -- `#!python3 on_process_edited_channel_post` - will be triggered on **process** `#!python3 edited_channel_post` event -- `#!python3 on_post_process_edited_channel_post` - will be triggered on **post process** `#!python3 edited_channel_post` event -- `#!python3 on_pre_process_inline_query` - will be triggered on **pre process** `#!python3 inline_query` event -- `#!python3 on_process_inline_query` - will be triggered on **process** `#!python3 inline_query` event -- `#!python3 on_post_process_inline_query` - will be triggered on **post process** `#!python3 inline_query` event -- `#!python3 on_pre_process_chosen_inline_result` - will be triggered on **pre process** `#!python3 chosen_inline_result` event -- `#!python3 on_process_chosen_inline_result` - will be triggered on **process** `#!python3 chosen_inline_result` event -- `#!python3 on_post_process_chosen_inline_result` - will be triggered on **post process** `#!python3 chosen_inline_result` event -- `#!python3 on_pre_process_callback_query` - will be triggered on **pre process** `#!python3 callback_query` event -- `#!python3 on_process_callback_query` - will be triggered on **process** `#!python3 callback_query` event -- `#!python3 on_post_process_callback_query` - will be triggered on **post process** `#!python3 callback_query` event -- `#!python3 on_pre_process_shipping_query` - will be triggered on **pre process** `#!python3 shipping_query` event -- `#!python3 on_process_shipping_query` - will be triggered on **process** `#!python3 shipping_query` event -- `#!python3 on_post_process_shipping_query` - will be triggered on **post process** `#!python3 shipping_query` event -- `#!python3 on_pre_process_pre_checkout_query` - will be triggered on **pre process** `#!python3 pre_checkout_query` event -- `#!python3 on_process_pre_checkout_query` - will be triggered on **process** `#!python3 pre_checkout_query` event -- `#!python3 on_post_process_pre_checkout_query` - will be triggered on **post process** `#!python3 pre_checkout_query` event -- `#!python3 on_pre_process_poll` - will be triggered on **pre process** `#!python3 poll` event -- `#!python3 on_process_poll` - will be triggered on **process** `#!python3 poll` event -- `#!python3 on_post_process_poll` - will be triggered on **post process** `#!python3 poll` event -- `#!python3 on_pre_process_poll_answer` - will be triggered on **pre process** `#!python3 poll_answer` event -- `#!python3 on_process_poll_answer` - will be triggered on **process** `#!python3 poll_answer` event -- `#!python3 on_post_process_poll_answer` - will be triggered on **post process** `#!python3 poll_answer` event -- `#!python3 on_pre_process_error` - will be triggered on **pre process** `#!python3 error` event -- `#!python3 on_process_error` - will be triggered on **process** `#!python3 error` event -- `#!python3 on_post_process_error` - will be triggered on **post process** `#!python3 error` event diff --git a/docs/dispatcher/middlewares/index.md b/docs/dispatcher/middlewares/index.md deleted file mode 100644 index 7752b5d1..00000000 --- a/docs/dispatcher/middlewares/index.md +++ /dev/null @@ -1,77 +0,0 @@ -# Overview - -**aiogram** provides powerful mechanism for customizing event handlers via middlewares. - -Middlewares in bot framework seems like Middlewares mechanism in web-frameworks -(like [aiohttp](https://docs.aiohttp.org/en/stable/web_advanced.html#aiohttp-web-middlewares), -[fastapi](https://fastapi.tiangolo.com/tutorial/middleware/), -[Django](https://docs.djangoproject.com/en/3.0/topics/http/middleware/) or etc.) -with small difference - here is implemented two layers of processing -(named as [pipeline](#event-pipeline)). - -!!! info - Middleware is function that triggered on every event received from - Telegram Bot API in many points on processing pipeline. - -## Base theory - -As many books and other literature in internet says: -> Middleware is reusable software that leverages patterns and frameworks to bridge ->the gap between the functional requirements of applications and the underlying operating systems, -> network protocol stacks, and databases. - -Middleware can modify, extend or reject processing event before-, -on- or after- processing of that event. - -[![middlewares](../../assets/images/basics_middleware.png)](../../assets/images/basics_middleware.png) - -_(Click on image to zoom it)_ - -## Event pipeline - -As described below middleware an interact with event in many stages of pipeline. - -Simple workflow: - -1. Dispatcher receive an [Update](../../api/types/update.md) -1. Call **pre-process** update middleware in all routers tree -1. Filter Update over handlers -1. Call **process** update middleware in all routers tree -1. Router detects event type (Message, Callback query, etc.) -1. Router triggers **pre-process** middleware of specific type -1. Pass event over [filters](../filters/index.md) to detect specific handler -1. Call **process** middleware for specific type (only when handler for this event exists) -1. *Do magick*. Call handler (Read more [Event observers](../router.md#event-observers)) -1. Call **post-process** middleware -1. Call **post-process** update middleware in all routers tree -1. Emit response into webhook (when it needed) - -!!! warning - When filters does not match any handler with this event the `#!python3 process` - step will not be called. - -!!! warning - When exception will be caused in handlers pipeline will be stopped immediately - and then start processing error via errors handler and it own middleware callbacks. - -!!! warning - Middlewares for updates will be called for all routers in tree but callbacks for events - will be called only for specific branch of routers. - -### Pipeline in pictures: - -#### Simple pipeline - -[![middlewares](../../assets/images/middleware_pipeline.png)](../../assets/images/middleware_pipeline.png) - -_(Click on image to zoom it)_ - -#### Nested routers pipeline - -[![middlewares](../../assets/images/middleware_pipeline_nested.png)](../../assets/images/middleware_pipeline_nested.png) - -_(Click on image to zoom it)_ - -## Read more - -- [Middleware Basics](basics.md) diff --git a/docs/index.md b/docs/index.md index 9529d0ac..5f6b5bce 100644 --- a/docs/index.md +++ b/docs/index.md @@ -20,7 +20,7 @@ Documentation for version 3.0 [WIP] [^1] - [Supports Telegram Bot API v{!_api_version.md!}](api/index.md) - [Updates router](dispatcher/index.md) (Blueprints) - Finite State Machine -- [Middlewares](dispatcher/middlewares/index.md) +- [Middlewares](dispatcher/middlewares.md) - [Replies into Webhook](https://core.telegram.org/bots/faq#how-can-i-make-requests-in-response-to-updates) diff --git a/docs/stylesheets/extra.css b/docs/stylesheets/extra.css new file mode 100644 index 00000000..b35d291c --- /dev/null +++ b/docs/stylesheets/extra.css @@ -0,0 +1,12 @@ +@font-face { + font-family: 'JetBrainsMono'; + src: url('https://cdn.jsdelivr.net/gh/JetBrains/JetBrainsMono/web/woff2/JetBrainsMono-Regular.woff2') format('woff2'), + url('https://cdn.jsdelivr.net/gh/JetBrains/JetBrainsMono/web/woff/JetBrainsMono-Regular.woff') format('woff'), + url('https://cdn.jsdelivr.net/gh/JetBrains/JetBrainsMono/ttf/JetBrainsMono-Regular.ttf') format('truetype'); + font-weight: 400; + font-style: normal; +} + +code, kbd, pre { + font-family: "JetBrainsMono", "Roboto Mono", "Courier New", Courier, monospace; +} diff --git a/mkdocs.yml b/mkdocs.yml index 527d0561..2c50c6db 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -16,6 +16,9 @@ theme: favicon: 'assets/images/logo.png' logo: 'assets/images/logo.png' +extra_css: + - stylesheets/extra.css + extra: version: 3.0.0a3 @@ -255,9 +258,8 @@ nav: - dispatcher/class_based_handlers/pre_checkout_query.md - dispatcher/class_based_handlers/shipping_query.md - dispatcher/class_based_handlers/error.md - - Middlewares: - - dispatcher/middlewares/index.md - - dispatcher/middlewares/basics.md + - dispatcher/middlewares.md + - todo.md - Build reports: - reports.md diff --git a/poetry.lock b/poetry.lock index 3ccb6507..cdd78f22 100644 --- a/poetry.lock +++ b/poetry.lock @@ -434,14 +434,6 @@ html5 = ["html5lib"] htmlsoup = ["beautifulsoup4"] source = ["Cython (>=0.29.7)"] -[[package]] -category = "main" -description = "This package provides magic filter based on dynamic attribute getter" -name = "magic-filter" -optional = false -python-versions = ">=3.6.1,<4.0.0" -version = "0.1.2" - [[package]] category = "dev" description = "Python implementation of Markdown." @@ -963,7 +955,7 @@ python-versions = "*" version = "1.4.1" [[package]] -category = "main" +category = "dev" description = "Backported and Experimental Type Hints for Python 3.5+" name = "typing-extensions" optional = false @@ -1039,7 +1031,7 @@ fast = ["uvloop"] proxy = ["aiohttp-socks"] [metadata] -content-hash = "5c53527f09e65af097aa3d3a25e41646e8b8a0dda25e96445ceef969c19297e5" +content-hash = "768759359beca8b84811bfc21adac9649925cd22b87427a10608c9d1e16a0923" python-versions = "^3.7" [metadata.files] @@ -1256,10 +1248,6 @@ lxml = [ {file = "lxml-4.5.0-cp38-cp38-win_amd64.whl", hash = "sha256:d5b3c4b7edd2e770375a01139be11307f04341ec709cf724e0f26ebb1eef12c3"}, {file = "lxml-4.5.0.tar.gz", hash = "sha256:8620ce80f50d023d414183bf90cc2576c2837b88e00bea3f33ad2630133bbb60"}, ] -magic-filter = [ - {file = "magic-filter-0.1.2.tar.gz", hash = "sha256:dfd1a778493083ac1355791d1716c3beb6629598739f2c2ec078815952282c1d"}, - {file = "magic_filter-0.1.2-py3-none-any.whl", hash = "sha256:16d0c96584f0660fd7fa94b6cd16f92383616208a32568bf8f95a57fc1a69e9d"}, -] markdown = [ {file = "Markdown-3.2.1-py2.py3-none-any.whl", hash = "sha256:e4795399163109457d4c5af2183fbe6b60326c17cfdf25ce6e7474c6624f725d"}, {file = "Markdown-3.2.1.tar.gz", hash = "sha256:90fee683eeabe1a92e149f7ba74e5ccdc81cd397bd6c516d93a8da0ef90b6902"}, diff --git a/pyproject.toml b/pyproject.toml index 88878b56..807bab18 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,8 +40,6 @@ aiofiles = "^0.4.0" uvloop = {version = "^0.14.0", markers = "sys_platform == 'darwin' or sys_platform == 'linux'", optional = true} async_lru = "^1.0" aiohttp-socks = {version = "^0.3.8", optional = true} -typing-extensions = "^3.7.4" -magic-filter = "^0.1.2" [tool.poetry.dev-dependencies] uvloop = {version = "^0.14.0", markers = "sys_platform == 'darwin' or sys_platform == 'linux'"} @@ -69,6 +67,7 @@ markdown-include = "^0.5.1" aiohttp-socks = "^0.3.4" pre-commit = "^2.3.0" packaging = "^20.3" +typing-extensions = "^3.7.4" [tool.poetry.extras] fast = ["uvloop"] From b69bd74d0ce7d43ba96ac10bdf30de0165afd0eb Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Tue, 26 May 2020 22:00:51 +0300 Subject: [PATCH 3/3] Rename `update_processing_context.py` to `user_context.py` --- aiogram/dispatcher/dispatcher.py | 2 +- .../{update_processing_context.py => user_context.py} | 0 tests/test_dispatcher/test_router.py | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) rename aiogram/dispatcher/middlewares/{update_processing_context.py => user_context.py} (100%) diff --git a/aiogram/dispatcher/dispatcher.py b/aiogram/dispatcher/dispatcher.py index 8b475316..1c4b08aa 100644 --- a/aiogram/dispatcher/dispatcher.py +++ b/aiogram/dispatcher/dispatcher.py @@ -12,7 +12,7 @@ from ..api.methods import TelegramMethod from ..api.types import Update, User from ..utils.exceptions import TelegramAPIError from .event.bases import NOT_HANDLED -from .middlewares.update_processing_context import UserContextMiddleware +from .middlewares.user_context import UserContextMiddleware from .router import Router diff --git a/aiogram/dispatcher/middlewares/update_processing_context.py b/aiogram/dispatcher/middlewares/user_context.py similarity index 100% rename from aiogram/dispatcher/middlewares/update_processing_context.py rename to aiogram/dispatcher/middlewares/user_context.py diff --git a/tests/test_dispatcher/test_router.py b/tests/test_dispatcher/test_router.py index c5ba6277..9d425388 100644 --- a/tests/test_dispatcher/test_router.py +++ b/tests/test_dispatcher/test_router.py @@ -19,7 +19,7 @@ from aiogram.api.types import ( User, ) from aiogram.dispatcher.event.bases import NOT_HANDLED, SkipHandler, skip -from aiogram.dispatcher.middlewares.update_processing_context import UserContextMiddleware +from aiogram.dispatcher.middlewares.user_context import UserContextMiddleware from aiogram.dispatcher.router import Router from aiogram.utils.warnings import CodeHasNoEffect