mirror of
https://github.com/aiogram/aiogram.git
synced 2025-12-11 18:01:04 +00:00
Add prototype of class-based handlers
This commit is contained in:
parent
2a731f7ce2
commit
b82a1a6fb0
11 changed files with 178 additions and 8 deletions
|
|
@ -4,16 +4,18 @@ from functools import partial
|
||||||
from typing import Any, Awaitable, Callable, Dict, List, Tuple, Union
|
from typing import Any, Awaitable, Callable, Dict, List, Tuple, Union
|
||||||
|
|
||||||
from aiogram.dispatcher.filters.base import BaseFilter
|
from aiogram.dispatcher.filters.base import BaseFilter
|
||||||
|
from aiogram.dispatcher.handler.base import BaseHandler
|
||||||
|
|
||||||
CallbackType = Callable[[Any], Awaitable[Any]]
|
CallbackType = Callable[[Any], Awaitable[Any]]
|
||||||
SyncFilter = Callable[[Any], Any]
|
SyncFilter = Callable[[Any], Any]
|
||||||
AsyncFilter = Callable[[Any], Awaitable[Any]]
|
AsyncFilter = Callable[[Any], Awaitable[Any]]
|
||||||
FilterType = Union[SyncFilter, AsyncFilter, BaseFilter]
|
FilterType = Union[SyncFilter, AsyncFilter, BaseFilter]
|
||||||
|
HandlerType = Union[CallbackType, BaseHandler]
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class CallableMixin:
|
class CallableMixin:
|
||||||
callback: Callable
|
callback: HandlerType
|
||||||
awaitable: bool = field(init=False)
|
awaitable: bool = field(init=False)
|
||||||
spec: inspect.FullArgSpec = field(init=False)
|
spec: inspect.FullArgSpec = field(init=False)
|
||||||
|
|
||||||
|
|
@ -44,9 +46,19 @@ class FilterObject(CallableMixin):
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class HandlerObject(CallableMixin):
|
class HandlerObject(CallableMixin):
|
||||||
callback: CallbackType
|
callback: HandlerType
|
||||||
filters: List[FilterObject]
|
filters: List[FilterObject]
|
||||||
|
|
||||||
|
def __post_init__(self):
|
||||||
|
super(HandlerObject, self).__post_init__()
|
||||||
|
|
||||||
|
if inspect.isclass(self.callback) and issubclass(self.callback, BaseHandler):
|
||||||
|
self.awaitable = True
|
||||||
|
if hasattr(self.callback, "filters"):
|
||||||
|
self.filters.extend(
|
||||||
|
FilterObject(event_filter) for event_filter in self.callback.filters
|
||||||
|
)
|
||||||
|
|
||||||
async def check(self, *args: Any, **kwargs: Any) -> Tuple[bool, Dict[str, Any]]:
|
async def check(self, *args: Any, **kwargs: Any) -> Tuple[bool, Dict[str, Any]]:
|
||||||
for event_filter in self.filters:
|
for event_filter in self.filters:
|
||||||
check = await event_filter.call(*args, **kwargs)
|
check = await event_filter.call(*args, **kwargs)
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ from typing import TYPE_CHECKING, Any, Dict, List, Type
|
||||||
from pydantic import ValidationError
|
from pydantic import ValidationError
|
||||||
|
|
||||||
from ..filters.base import BaseFilter
|
from ..filters.base import BaseFilter
|
||||||
from .handler import CallbackType, FilterObject, FilterType, HandlerObject
|
from .handler import CallbackType, FilterObject, FilterType, HandlerObject, HandlerType
|
||||||
|
|
||||||
if TYPE_CHECKING: # pragma: no cover
|
if TYPE_CHECKING: # pragma: no cover
|
||||||
from aiogram.dispatcher.router import Router
|
from aiogram.dispatcher.router import Router
|
||||||
|
|
@ -24,7 +24,7 @@ class EventObserver:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.handlers: List[HandlerObject] = []
|
self.handlers: List[HandlerObject] = []
|
||||||
|
|
||||||
def register(self, callback: CallbackType, *filters: FilterType):
|
def register(self, callback: HandlerType, *filters: FilterType):
|
||||||
"""
|
"""
|
||||||
Register callback with filters
|
Register callback with filters
|
||||||
|
|
||||||
|
|
@ -91,7 +91,7 @@ class TelegramEventObserver(EventObserver):
|
||||||
yield filter_
|
yield filter_
|
||||||
registry.append(filter_)
|
registry.append(filter_)
|
||||||
|
|
||||||
def register(self, callback: CallbackType, *filters: FilterType, **bound_filters: Any):
|
def register(self, callback: HandlerType, *filters: FilterType, **bound_filters: Any):
|
||||||
resolved_filters = self.resolve_filters(bound_filters)
|
resolved_filters = self.resolve_filters(bound_filters)
|
||||||
return super().register(callback, *filters, *resolved_filters)
|
return super().register(callback, *filters, *resolved_filters)
|
||||||
|
|
||||||
|
|
|
||||||
0
aiogram/dispatcher/handler/__init__.py
Normal file
0
aiogram/dispatcher/handler/__init__.py
Normal file
37
aiogram/dispatcher/handler/base.py
Normal file
37
aiogram/dispatcher/handler/base.py
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
from typing import TYPE_CHECKING, Any, Dict, List, Tuple, Union
|
||||||
|
|
||||||
|
from aiogram import Bot
|
||||||
|
from aiogram.api.types import TelegramObject
|
||||||
|
|
||||||
|
if TYPE_CHECKING: # pragma: no cover
|
||||||
|
from aiogram.dispatcher.event.handler import FilterType # NOQA: F401
|
||||||
|
|
||||||
|
|
||||||
|
class BaseHandlerMixin:
|
||||||
|
event: TelegramObject
|
||||||
|
data: Dict[str, Any]
|
||||||
|
|
||||||
|
|
||||||
|
class HandlerBotMixin(BaseHandlerMixin):
|
||||||
|
@property
|
||||||
|
def bot(self) -> Bot:
|
||||||
|
if "bot" in self.data:
|
||||||
|
return self.data["bot"]
|
||||||
|
return Bot.get_current()
|
||||||
|
|
||||||
|
|
||||||
|
class BaseHandler(HandlerBotMixin, ABC):
|
||||||
|
event: TelegramObject
|
||||||
|
filters: Union[List["FilterType"], Tuple["FilterType"]]
|
||||||
|
|
||||||
|
def __init__(self, event: TelegramObject, **kwargs: Any) -> None:
|
||||||
|
self.event = event
|
||||||
|
self.data = kwargs
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
async def handle(self) -> Any: # pragma: no cover
|
||||||
|
pass
|
||||||
|
|
||||||
|
def __await__(self):
|
||||||
|
return self.handle().__await__()
|
||||||
16
aiogram/dispatcher/handler/message.py
Normal file
16
aiogram/dispatcher/handler/message.py
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
from abc import ABC
|
||||||
|
|
||||||
|
from aiogram.api.types import Message
|
||||||
|
from aiogram.dispatcher.handler.base import BaseHandler
|
||||||
|
|
||||||
|
|
||||||
|
class MessageHandler(BaseHandler, ABC):
|
||||||
|
event: Message
|
||||||
|
|
||||||
|
@property
|
||||||
|
def from_user(self):
|
||||||
|
return self.event.from_user
|
||||||
|
|
||||||
|
@property
|
||||||
|
def chat(self):
|
||||||
|
return self.event.chat
|
||||||
|
|
@ -45,9 +45,13 @@ class ContextInstanceMixin:
|
||||||
return cls.__context_instance.get()
|
return cls.__context_instance.get()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def set_current(cls: Type[T], value: T):
|
def set_current(cls: Type[T], value: T) -> contextvars.Token:
|
||||||
if not isinstance(value, cls):
|
if not isinstance(value, cls):
|
||||||
raise TypeError(
|
raise TypeError(
|
||||||
f"Value should be instance of {cls.__name__!r} not {type(value).__name__!r}"
|
f"Value should be instance of {cls.__name__!r} not {type(value).__name__!r}"
|
||||||
)
|
)
|
||||||
cls.__context_instance.set(value)
|
return cls.__context_instance.set(value)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def reset_current(cls: Type[T], token: contextvars.Token):
|
||||||
|
cls.__context_instance.reset(token)
|
||||||
|
|
|
||||||
|
|
@ -7,5 +7,6 @@ from tests.mocked_bot import MockedBot
|
||||||
@pytest.fixture()
|
@pytest.fixture()
|
||||||
def bot():
|
def bot():
|
||||||
bot = MockedBot()
|
bot = MockedBot()
|
||||||
Bot.set_current(bot)
|
token = Bot.set_current(bot)
|
||||||
yield bot
|
yield bot
|
||||||
|
Bot.reset_current(token)
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,11 @@ from typing import Any, Dict, Union
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
from aiogram.api.types import Update
|
||||||
from aiogram.dispatcher.event.handler import CallableMixin, FilterObject, HandlerObject
|
from aiogram.dispatcher.event.handler import CallableMixin, FilterObject, HandlerObject
|
||||||
|
from aiogram.dispatcher.filters import Text
|
||||||
from aiogram.dispatcher.filters.base import BaseFilter
|
from aiogram.dispatcher.filters.base import BaseFilter
|
||||||
|
from aiogram.dispatcher.handler.base import BaseHandler
|
||||||
|
|
||||||
|
|
||||||
def callback1(foo: int, bar: int, baz: int):
|
def callback1(foo: int, bar: int, baz: int):
|
||||||
|
|
@ -174,3 +177,25 @@ class TestHandlerObject:
|
||||||
)
|
)
|
||||||
result, data = await handler.check(42, foo=True)
|
result, data = await handler.check(42, foo=True)
|
||||||
assert not result
|
assert not result
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_class_based_handler(self):
|
||||||
|
class MyFilter(BaseFilter):
|
||||||
|
async def __call__(self, event):
|
||||||
|
return True
|
||||||
|
|
||||||
|
class MyHandler(BaseHandler):
|
||||||
|
event: Update
|
||||||
|
filters = [MyFilter()]
|
||||||
|
|
||||||
|
async def handle(self) -> Any:
|
||||||
|
return self.event.update_id
|
||||||
|
|
||||||
|
handler = HandlerObject(MyHandler, filters=[FilterObject(lambda event: True)])
|
||||||
|
|
||||||
|
assert handler.awaitable
|
||||||
|
assert handler.callback == MyHandler
|
||||||
|
assert len(handler.filters) == 2
|
||||||
|
assert handler.filters[1].callback == MyFilter()
|
||||||
|
result = await handler.call(Update(update_id=42))
|
||||||
|
assert result == 42
|
||||||
|
|
|
||||||
0
tests/test_dispatcher/test_handler/__init__.py
Normal file
0
tests/test_dispatcher/test_handler/__init__.py
Normal file
47
tests/test_dispatcher/test_handler/test_base.py
Normal file
47
tests/test_dispatcher/test_handler/test_base.py
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
import asyncio
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from aiogram import Bot
|
||||||
|
from aiogram.api.types import Update
|
||||||
|
from aiogram.dispatcher.handler.base import BaseHandler
|
||||||
|
|
||||||
|
|
||||||
|
class MyHandler(BaseHandler):
|
||||||
|
async def handle(self) -> Any:
|
||||||
|
await asyncio.sleep(0.1)
|
||||||
|
return 42
|
||||||
|
|
||||||
|
|
||||||
|
class TestBaseClassBasedHandler:
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_base_handler(self):
|
||||||
|
event = Update(update_id=42)
|
||||||
|
handler = MyHandler(event=event, key=42)
|
||||||
|
|
||||||
|
assert handler.event == event
|
||||||
|
assert handler.data["key"] == 42
|
||||||
|
assert hasattr(handler, "bot")
|
||||||
|
assert not hasattr(handler, "filters")
|
||||||
|
assert await handler == 42
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_bot_mixin_from_context(self):
|
||||||
|
event = Update(update_id=42)
|
||||||
|
handler = MyHandler(event=event, key=42)
|
||||||
|
bot = Bot("42:TEST")
|
||||||
|
|
||||||
|
assert handler.bot is None
|
||||||
|
|
||||||
|
Bot.set_current(bot)
|
||||||
|
assert handler.bot == bot
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_bot_mixin_from_data(self):
|
||||||
|
event = Update(update_id=42)
|
||||||
|
bot = Bot("42:TEST")
|
||||||
|
handler = MyHandler(event=event, key=42, bot=bot)
|
||||||
|
|
||||||
|
assert "bot" in handler.data
|
||||||
|
assert handler.bot == bot
|
||||||
28
tests/test_dispatcher/test_handler/test_message.py
Normal file
28
tests/test_dispatcher/test_handler/test_message.py
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
import datetime
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from aiogram.api.types import Chat, Message, User
|
||||||
|
from aiogram.dispatcher.handler.message import MessageHandler
|
||||||
|
|
||||||
|
|
||||||
|
class MyHandler(MessageHandler):
|
||||||
|
async def handle(self) -> Any:
|
||||||
|
return self.event.text
|
||||||
|
|
||||||
|
|
||||||
|
class TestClassBasedMessageHandler:
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_message_handler(self):
|
||||||
|
event = Message(
|
||||||
|
message_id=42,
|
||||||
|
date=datetime.datetime.now(),
|
||||||
|
text="test",
|
||||||
|
chat=Chat(id=42, type="private"),
|
||||||
|
from_user=User(id=42, is_bot=False, first_name="Test"),
|
||||||
|
)
|
||||||
|
handler = MyHandler(event=event)
|
||||||
|
|
||||||
|
assert handler.from_user == event.from_user
|
||||||
|
assert handler.chat == event.chat
|
||||||
Loading…
Add table
Add a link
Reference in a new issue