diff --git a/aiogram/dispatcher/handler/__init__.py b/aiogram/dispatcher/handler/__init__.py index 15dddab6..49ae18d2 100644 --- a/aiogram/dispatcher/handler/__init__.py +++ b/aiogram/dispatcher/handler/__init__.py @@ -1,4 +1,21 @@ from .base import BaseHandler, BaseHandlerMixin +from .callback_query import CallbackQueryHandler +from .chosen_inline_result import ChosenInlineResultHandler +from .inline_query import InlineQueryHandler from .message import MessageHandler, MessageHandlerCommandMixin +from .poll import PollHandler +from .pre_checkout_query import PreCheckoutQueryHandler +from .shipping_query import ShippingQueryHandler -__all__ = ("BaseHandler", "BaseHandlerMixin", "MessageHandler", "MessageHandlerCommandMixin") +__all__ = ( + "BaseHandler", + "BaseHandlerMixin", + "CallbackQueryHandler", + "ChosenInlineResultHandler", + "InlineQueryHandler", + "MessageHandler", + "MessageHandlerCommandMixin", + "PollHandler", + "PreCheckoutQueryHandler", + "ShippingQueryHandler", +) diff --git a/aiogram/dispatcher/handler/base.py b/aiogram/dispatcher/handler/base.py index 017f4c65..6df413a4 100644 --- a/aiogram/dispatcher/handler/base.py +++ b/aiogram/dispatcher/handler/base.py @@ -1,23 +1,25 @@ from abc import ABC, abstractmethod -from typing import Any, Dict +from typing import TYPE_CHECKING, Any, Dict, Generic, TypeVar from aiogram import Bot -from aiogram.api.types import TelegramObject + +T = TypeVar("T") -class BaseHandlerMixin: +class BaseHandlerMixin(Generic[T]): + if TYPE_CHECKING: # pragma: no cover + event: T + data: Dict[str, Any] + + +class BaseHandler(BaseHandlerMixin[T], ABC): """ - Typed mixin. Do nothing. + Base class for all class-based handlers """ - event: TelegramObject - data: Dict[str, Any] - - -class _HandlerBotMixin(BaseHandlerMixin): - """ - Mixin adds bot attribute - """ + def __init__(self, event: T, **kwargs: Any) -> None: + self.event: T = event + self.data: Dict[str, Any] = kwargs @property def bot(self) -> Bot: @@ -25,16 +27,6 @@ class _HandlerBotMixin(BaseHandlerMixin): return self.data["bot"] return Bot.get_current() - -class BaseHandler(_HandlerBotMixin, ABC): - """ - Base class for all class-based handlers - """ - - def __init__(self, event: TelegramObject, **kwargs: Any) -> None: - self.event = event - self.data = kwargs - @abstractmethod async def handle(self) -> Any: # pragma: no cover pass diff --git a/aiogram/dispatcher/handler/callback_query.py b/aiogram/dispatcher/handler/callback_query.py new file mode 100644 index 00000000..a7faa0d3 --- /dev/null +++ b/aiogram/dispatcher/handler/callback_query.py @@ -0,0 +1,23 @@ +from abc import ABC +from typing import Optional + +from aiogram.api.types import CallbackQuery, Message, User +from aiogram.dispatcher.handler import BaseHandler + + +class CallbackQueryHandler(BaseHandler[CallbackQuery], ABC): + """ + Base class for callback query handlers + """ + + @property + def from_user(self) -> User: + return self.event.from_user + + @property + def message(self) -> Optional[Message]: + return self.event.message + + @property + def callback_data(self) -> Optional[str]: + return self.event.data diff --git a/aiogram/dispatcher/handler/chosen_inline_result.py b/aiogram/dispatcher/handler/chosen_inline_result.py new file mode 100644 index 00000000..b15469bf --- /dev/null +++ b/aiogram/dispatcher/handler/chosen_inline_result.py @@ -0,0 +1,18 @@ +from abc import ABC + +from aiogram.api.types import ChosenInlineResult, User +from aiogram.dispatcher.handler import BaseHandler + + +class ChosenInlineResultHandler(BaseHandler[ChosenInlineResult], ABC): + """ + Base class for chosen inline result handlers + """ + + @property + def from_user(self) -> User: + return self.event.from_user + + @property + def query(self) -> str: + return self.event.query diff --git a/aiogram/dispatcher/handler/inline_query.py b/aiogram/dispatcher/handler/inline_query.py new file mode 100644 index 00000000..faae2d23 --- /dev/null +++ b/aiogram/dispatcher/handler/inline_query.py @@ -0,0 +1,18 @@ +from abc import ABC + +from aiogram.api.types import InlineQuery, User +from aiogram.dispatcher.handler import BaseHandler + + +class InlineQueryHandler(BaseHandler[InlineQuery], ABC): + """ + Base class for inline query handlers + """ + + @property + def from_user(self) -> User: + return self.event.from_user + + @property + def query(self) -> str: + return self.event.query diff --git a/aiogram/dispatcher/handler/message.py b/aiogram/dispatcher/handler/message.py index 92b4b486..d881a75b 100644 --- a/aiogram/dispatcher/handler/message.py +++ b/aiogram/dispatcher/handler/message.py @@ -6,8 +6,10 @@ from aiogram.dispatcher.filters import CommandObject from aiogram.dispatcher.handler.base import BaseHandler, BaseHandlerMixin -class MessageHandler(BaseHandler, ABC): - event: Message +class MessageHandler(BaseHandler[Message], ABC): + """ + Base class for message handlers + """ @property def from_user(self) -> Optional[User]: @@ -18,7 +20,7 @@ class MessageHandler(BaseHandler, ABC): return self.event.chat -class MessageHandlerCommandMixin(BaseHandlerMixin): +class MessageHandlerCommandMixin(BaseHandlerMixin[Message]): @property def command(self) -> Optional[CommandObject]: if "command" in self.data: diff --git a/aiogram/dispatcher/handler/poll.py b/aiogram/dispatcher/handler/poll.py new file mode 100644 index 00000000..27883738 --- /dev/null +++ b/aiogram/dispatcher/handler/poll.py @@ -0,0 +1,19 @@ +from abc import ABC +from typing import List + +from aiogram.api.types import Poll, PollOption +from aiogram.dispatcher.handler import BaseHandler + + +class PollHandler(BaseHandler[Poll], ABC): + """ + Base class for poll handlers + """ + + @property + def question(self) -> str: + return self.event.question + + @property + def options(self) -> List[PollOption]: + return self.event.options diff --git a/aiogram/dispatcher/handler/pre_checkout_query.py b/aiogram/dispatcher/handler/pre_checkout_query.py new file mode 100644 index 00000000..02fae71c --- /dev/null +++ b/aiogram/dispatcher/handler/pre_checkout_query.py @@ -0,0 +1,14 @@ +from abc import ABC + +from aiogram.api.types import PreCheckoutQuery, User +from aiogram.dispatcher.handler import BaseHandler + + +class PreCheckoutQueryHandler(BaseHandler[PreCheckoutQuery], ABC): + """ + Base class for pre-checkout handlers + """ + + @property + def from_user(self) -> User: + return self.event.from_user diff --git a/aiogram/dispatcher/handler/shipping_query.py b/aiogram/dispatcher/handler/shipping_query.py new file mode 100644 index 00000000..ea079a23 --- /dev/null +++ b/aiogram/dispatcher/handler/shipping_query.py @@ -0,0 +1,14 @@ +from abc import ABC + +from aiogram.api.types import ShippingQuery, User +from aiogram.dispatcher.handler import BaseHandler + + +class ShippingQueryHandler(BaseHandler[ShippingQuery], ABC): + """ + Base class for shipping query handlers + """ + + @property + def from_user(self) -> User: + return self.event.from_user diff --git a/docs/dispatcher/class_based_handlers/basics.md b/docs/dispatcher/class_based_handlers/basics.md index a6e4c1f8..57e1f50c 100644 --- a/docs/dispatcher/class_based_handlers/basics.md +++ b/docs/dispatcher/class_based_handlers/basics.md @@ -7,11 +7,16 @@ There are some base class based handlers what you need to use in your own handle - [BaseHandler](#basehandler) - [MessageHandler](message.md) - +- [CallbackQueryHandler](callback_query.md) +- [ChosenInlineResultHandler](chosen_inline_result.md) +- [InlineQueryHandler](inline_query.md) +- [PollHandler](poll.md) +- [PreCheckoutQueryHandler](pre_checkout_query.md) +- [ShippingQueryHandler](shipping_query.md) ## BaseHandler -Base handler is abstract class and should be used in all other class-based handlers. +Base handler is generic abstract class and should be used in all other class-based handlers. Import: `#!python3 from aiogram.hanler import BaseHandler` @@ -21,3 +26,11 @@ This class is also have an default initializer and you don't need to change it. Initializer accepts current event and all contextual data and which can be accessed from the handler through attributes: `event: TelegramEvent` and `data: Dict[Any, str]` If instance of the bot is specified in context data or current context it can be accessed through `bot` class attribute. + + +### For example: +```python3 +class MyHandler(BaseHandler[Message]): + async def handle(self) -> Any: + await self.event.answer("Hello!") +``` diff --git a/docs/dispatcher/class_based_handlers/callback_query.md b/docs/dispatcher/class_based_handlers/callback_query.md new file mode 100644 index 00000000..a9b5549f --- /dev/null +++ b/docs/dispatcher/class_based_handlers/callback_query.md @@ -0,0 +1,29 @@ +# CallbackQueryHandler + +There is base class for callback query handlers. + +## Simple usage: +```pyhton3 +from aiogram.handlers import CallbackQueryHandler + +... + +@router.callback_query_handler() +class MyHandler(CallbackQueryHandler): + async def handle(self) -> Any: ... + +``` + +## Extension + +This base handler is subclass of [BaseHandler](basics.md#basehandler) with some extensions: + +- `self.from_user` is alias for `self.event.from_user` +- `self.message` is alias for `self.event.message` +- `self.callback_data` is alias for `self.event.data` + +## Related pages + +- [BaseHandler](basics.md#basehandler) +- [CallbackQuery](../../api/types/callback_query.md) +- [Router.callback_query_handler](../router.md#callback-query) diff --git a/docs/dispatcher/class_based_handlers/chosen_inline_result.md b/docs/dispatcher/class_based_handlers/chosen_inline_result.md new file mode 100644 index 00000000..44dd353d --- /dev/null +++ b/docs/dispatcher/class_based_handlers/chosen_inline_result.md @@ -0,0 +1,28 @@ +# ChosenInlineResultHandler + +There is base class for chosen inline result handlers. + +## Simple usage: +```pyhton3 +from aiogram.handlers import ChosenInlineResultHandler + +... + +@router.chosen_inline_result_handler() +class MyHandler(ChosenInlineResultHandler): + async def handle(self) -> Any: ... + +``` + +## Extension + +This base handler is subclass of [BaseHandler](basics.md#basehandler) with some extensions: + +- `self.chat` is alias for `self.event.chat` +- `self.from_user` is alias for `self.event.from_user` + +## Related pages + +- [BaseHandler](basics.md#basehandler) +- [ChosenInlineResult](../../api/types/chosen_inline_result.md) +- [Router.chosen_inline_result_handler](../router.md#chosen-inline-query) diff --git a/docs/dispatcher/class_based_handlers/inline_query.md b/docs/dispatcher/class_based_handlers/inline_query.md new file mode 100644 index 00000000..c348be43 --- /dev/null +++ b/docs/dispatcher/class_based_handlers/inline_query.md @@ -0,0 +1,27 @@ +# InlineQueryHandler +There is base class for inline query handlers. + +## Simple usage: +```pyhton3 +from aiogram.handlers import InlineQueryHandler + +... + +@router.inline_query_handler() +class MyHandler(InlineQueryHandler): + async def handle(self) -> Any: ... + +``` + +## Extension + +This base handler is subclass of [BaseHandler](basics.md#basehandler) with some extensions: + +- `self.chat` is alias for `self.event.chat` +- `self.query` is alias for `self.event.query` + +## Related pages + +- [BaseHandler](basics.md#basehandler) +- [InlineQuery](../../api/types/inline_query.md) +- [Router.inline_query_handler](../router.md#inline-query) diff --git a/docs/dispatcher/class_based_handlers/message.md b/docs/dispatcher/class_based_handlers/message.md index 978cafab..093e07eb 100644 --- a/docs/dispatcher/class_based_handlers/message.md +++ b/docs/dispatcher/class_based_handlers/message.md @@ -9,8 +9,8 @@ from aiogram.handlers import MessageHandler ... @router.message_handler() -class MyTestMessageHandler(MessageHandler): - async def handle() -> Any: +class MyHandler(MessageHandler): + async def handle(self) -> Any: return SendMessage(chat_id=self.chat.id, text="PASS") ``` @@ -21,3 +21,12 @@ This base handler is subclass of [BaseHandler](basics.md#basehandler) with some - `self.chat` is alias for `self.event.chat` - `self.from_user` is alias for `self.event.from_user` + +## Related pages + +- [BaseHandler](basics.md#basehandler) +- [Message](../../api/types/message.md) +- [Router.message_handler](../router.md#message) +- [Router.edited_message_handler](../router.md#edited-message) +- [Router.channel_post_handler](../router.md#channel-post) +- [Router.edited_channel_post_handler](../router.md#edited-channel-post) diff --git a/docs/dispatcher/class_based_handlers/poll.md b/docs/dispatcher/class_based_handlers/poll.md new file mode 100644 index 00000000..88dc02aa --- /dev/null +++ b/docs/dispatcher/class_based_handlers/poll.md @@ -0,0 +1,28 @@ +# PollHandler + +There is base class for poll handlers. + +## Simple usage: +```pyhton3 +from aiogram.handlers import PollHandler + +... + +@router.poll_handler() +class MyHandler(PollHandler): + async def handle(self) -> Any: ... + +``` + +## Extension + +This base handler is subclass of [BaseHandler](basics.md#basehandler) with some extensions: + +- `self.question` is alias for `self.event.question` +- `self.options` is alias for `self.event.options` + +## Related pages + +- [BaseHandler](basics.md#basehandler) +- [Poll](../../api/types/poll.md) +- [Router.poll_handler](../router.md#poll) diff --git a/docs/dispatcher/class_based_handlers/pre_checkout_query.md b/docs/dispatcher/class_based_handlers/pre_checkout_query.md new file mode 100644 index 00000000..90cf6c2f --- /dev/null +++ b/docs/dispatcher/class_based_handlers/pre_checkout_query.md @@ -0,0 +1,27 @@ +# PreCheckoutQueryHandler + +There is base class for callback query handlers. + +## Simple usage: +```pyhton3 +from aiogram.handlers import PreCheckoutQueryHandler + +... + +@router.pre_checkout_query_handler() +class MyHandler(PreCheckoutQueryHandler): + async def handle(self) -> Any: ... + +``` + +## Extension + +This base handler is subclass of [BaseHandler](basics.md#basehandler) with some extensions: + +- `self.from_user` is alias for `self.event.from_user` + +## Related pages + +- [BaseHandler](basics.md#basehandler) +- [PreCheckoutQuery](../../api/types/pre_checkout_query.md) +- [Router.pre_checkout_query_handler](../router.md#pre-checkout-query) diff --git a/docs/dispatcher/class_based_handlers/shipping_query.md b/docs/dispatcher/class_based_handlers/shipping_query.md new file mode 100644 index 00000000..d6d70555 --- /dev/null +++ b/docs/dispatcher/class_based_handlers/shipping_query.md @@ -0,0 +1,27 @@ +# ShippingQueryHandler + +There is base class for callback query handlers. + +## Simple usage: +```pyhton3 +from aiogram.handlers import ShippingQueryHandler + +... + +@router.shipping_query_handler() +class MyHandler(ShippingQueryHandler): + async def handle(self) -> Any: ... + +``` + +## Extension + +This base handler is subclass of [BaseHandler](basics.md#basehandler) with some extensions: + +- `self.from_user` is alias for `self.event.from_user` + +## Related pages + +- [BaseHandler](basics.md#basehandler) +- [ShippingQuery](../../api/types/shipping_query.md) +- [Router.shipping_query_handler](../router.md#shipping-query) diff --git a/mkdocs.yml b/mkdocs.yml index 3d5d70b4..1f1955f3 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -231,6 +231,12 @@ nav: - Class based handlers: - dispatcher/class_based_handlers/basics.md - dispatcher/class_based_handlers/message.md + - dispatcher/class_based_handlers/callback_query.md + - dispatcher/class_based_handlers/chosen_inline_result.md + - dispatcher/class_based_handlers/inline_query.md + - dispatcher/class_based_handlers/poll.md + - dispatcher/class_based_handlers/pre_checkout_query.md + - dispatcher/class_based_handlers/shipping_query.md - Build reports: - reports.md - Tests result: /reports/tests diff --git a/tests/test_dispatcher/test_handler/test_callback_query.py b/tests/test_dispatcher/test_handler/test_callback_query.py new file mode 100644 index 00000000..d41ffb9e --- /dev/null +++ b/tests/test_dispatcher/test_handler/test_callback_query.py @@ -0,0 +1,26 @@ +from typing import Any + +import pytest +from aiogram.api.types import CallbackQuery, User +from aiogram.dispatcher.handler import CallbackQueryHandler + + +class TestCallbackQueryHandler: + @pytest.mark.asyncio + async def test_attributes_aliases(self): + event = CallbackQuery( + id="chosen", + from_user=User(id=42, is_bot=False, first_name="Test"), + data="test", + chat_instance="test", + ) + + class MyHandler(CallbackQueryHandler): + async def handle(self) -> Any: + assert self.event == event + assert self.from_user == self.event.from_user + assert self.callback_data == self.event.data + assert self.message == self.message + return True + + assert await MyHandler(event) diff --git a/tests/test_dispatcher/test_handler/test_chosen_inline_result.py b/tests/test_dispatcher/test_handler/test_chosen_inline_result.py new file mode 100644 index 00000000..e92f2f11 --- /dev/null +++ b/tests/test_dispatcher/test_handler/test_chosen_inline_result.py @@ -0,0 +1,24 @@ +from typing import Any + +import pytest +from aiogram.api.types import CallbackQuery, ChosenInlineResult, User +from aiogram.dispatcher.handler import ChosenInlineResultHandler + + +class TestChosenInlineResultHandler: + @pytest.mark.asyncio + async def test_attributes_aliases(self): + event = ChosenInlineResult( + result_id="chosen", + from_user=User(id=42, is_bot=False, first_name="Test"), + query="test", + ) + + class MyHandler(ChosenInlineResultHandler): + async def handle(self) -> Any: + assert self.event == event + assert self.from_user == self.event.from_user + assert self.query == self.event.query + return True + + assert await MyHandler(event) diff --git a/tests/test_dispatcher/test_handler/test_inline_query.py b/tests/test_dispatcher/test_handler/test_inline_query.py new file mode 100644 index 00000000..045d014e --- /dev/null +++ b/tests/test_dispatcher/test_handler/test_inline_query.py @@ -0,0 +1,25 @@ +from typing import Any + +import pytest +from aiogram.api.types import CallbackQuery, InlineQuery, User +from aiogram.dispatcher.handler import InlineQueryHandler + + +class TestCallbackQueryHandler: + @pytest.mark.asyncio + async def test_attributes_aliases(self): + event = InlineQuery( + id="query", + from_user=User(id=42, is_bot=False, first_name="Test"), + query="query", + offset="0", + ) + + class MyHandler(InlineQueryHandler): + async def handle(self) -> Any: + assert self.event == event + assert self.from_user == self.event.from_user + assert self.query == self.event.query + return True + + assert await MyHandler(event) diff --git a/tests/test_dispatcher/test_handler/test_poll.py b/tests/test_dispatcher/test_handler/test_poll.py new file mode 100644 index 00000000..6a84dda4 --- /dev/null +++ b/tests/test_dispatcher/test_handler/test_poll.py @@ -0,0 +1,34 @@ +from typing import Any + +import pytest +from aiogram.api.types import ( + CallbackQuery, + InlineQuery, + Poll, + PollOption, + ShippingAddress, + ShippingQuery, + User, +) +from aiogram.dispatcher.handler import PollHandler + + +class TestShippingQueryHandler: + @pytest.mark.asyncio + async def test_attributes_aliases(self): + event = Poll( + id="query", + question="Q?", + options=[PollOption(text="A1", voter_count=1)], + is_closed=True, + ) + + class MyHandler(PollHandler): + async def handle(self) -> Any: + assert self.event == event + assert self.question == self.event.question + assert self.options == self.event.options + + return True + + assert await MyHandler(event) diff --git a/tests/test_dispatcher/test_handler/test_pre_checkout_query.py b/tests/test_dispatcher/test_handler/test_pre_checkout_query.py new file mode 100644 index 00000000..7bab017a --- /dev/null +++ b/tests/test_dispatcher/test_handler/test_pre_checkout_query.py @@ -0,0 +1,25 @@ +from typing import Any + +import pytest +from aiogram.api.types import PreCheckoutQuery, User +from aiogram.dispatcher.handler import PreCheckoutQueryHandler + + +class TestPreCheckoutQueryHandler: + @pytest.mark.asyncio + async def test_attributes_aliases(self): + event = PreCheckoutQuery( + id="query", + from_user=User(id=42, is_bot=False, first_name="Test"), + currency="BTC", + total_amount=7, + invoice_payload="payload", + ) + + class MyHandler(PreCheckoutQueryHandler): + async def handle(self) -> Any: + assert self.event == event + assert self.from_user == self.event.from_user + return True + + assert await MyHandler(event) diff --git a/tests/test_dispatcher/test_handler/test_shipping_query.py b/tests/test_dispatcher/test_handler/test_shipping_query.py new file mode 100644 index 00000000..d0902fdf --- /dev/null +++ b/tests/test_dispatcher/test_handler/test_shipping_query.py @@ -0,0 +1,31 @@ +from typing import Any + +import pytest +from aiogram.api.types import CallbackQuery, InlineQuery, ShippingAddress, ShippingQuery, User +from aiogram.dispatcher.handler import ShippingQueryHandler + + +class TestShippingQueryHandler: + @pytest.mark.asyncio + async def test_attributes_aliases(self): + event = ShippingQuery( + id="query", + from_user=User(id=42, is_bot=False, first_name="Test"), + invoice_payload="payload", + shipping_address=ShippingAddress( + country_code="country_code", + state="state", + city="city", + street_line1="street_line1", + street_line2="street_line2", + post_code="post_code", + ), + ) + + class MyHandler(ShippingQueryHandler): + async def handle(self) -> Any: + assert self.event == event + assert self.from_user == self.event.from_user + return True + + assert await MyHandler(event)