aiogram/tests/test_dispatcher/test_event/test_handler.py
Alex Root Junior 3d63bf3b99
PoC Scenes (#1280)
* Base implementation

* Small refactoring + added possibility to specify post-action on handlers

* Move scene properties to config object

* Revise aiogram/scenes with wizard-based design pattern

Modified files in aiogram/scenes to incorporate the Wizard design pattern. Files affected are _marker.py, _registry.py, _wizard.py and __init__.py. The changes introduced a SceneWizard Class and ScenesManager, both of which aid in controlling navigation between different scenes or states. This helps clarifying the codebase, streamline scene transitions and offer more control over the app flow.

* Added example

* Small optimizations

* Replace ValueError with SceneException in scenes. Added error safety in scene resolver.

* str

* Added possibility to reset context on scene entered and to handle callback query in any state

* Remove inline markup in example

* Small changes

* Docs + example

* Small refactoring

* Remove scene inclusion methods from router

The methods for including scenes as sub-routers have been removed from the router.py file. Instead, the SceneRegistry class is now set to register scenes by default upon initializing. This streamlines the scene management process by removing redundant routers and making registration automatic.

* Init tests

* Small fix in tests

* Add support for State instance in the scene

The aiogram FSM scene now allows the use of State instance as an argument, enabling more customization. Modified the 'as_handler' method to receive **kwargs arguments, allowing passing of attributes to the handler. An additional type check has been also added to ensure the 'scene' is either a subclass of Scene or a string.

* Fixed test

* Expand test coverage for test_fsm module

The commit enhances tests for the test_fsm module to improve code reliability. It includes additional unit tests for the ObserverDecorator and ActionContainer classes and introduces new tests for the SceneHandlerWrapper class. This ensures the correct functionality of the decorator methods, the action container execution, and the handler wrapper.

* Reformat code

* Fixed long line in the example

* Skip some tests on PyPy

* Change mock return_value

* Compatibility...

* Compatibility...

* Compatibility...

* Added base changes description

* Scenes Tests (#1369)

* ADD tests for `SceneRegistry`

* ADD tests for `ScenesManager`

* ADD Changelog

* Revert "ADD Changelog"

This reverts commit 6dd9301252.

* Remove `@pytest.mark.asyncio`, Reformat code

* Scenes Tests. Part 2 (#1371)

* ADD tests for `SceneWizard`

* ADD tests for `Scene`

* Refactor ObserverDecorator to use on.message syntax in test_scene.py
Cover `Scene::__init_subclass__::if isinstance(value, ObserverDecorator):`

* Refactor `HistoryManager` in `aiogram/fsm/scene.py`
Removed condition that checked if 'history' is empty before calling 'update_data' in 'Scene'.

* ADD tests for `HistoryManager`

* Small changes in the documentation

* Small changes in the documentation

* Small changes in the documentation

---------

Co-authored-by: Andrew <11490628+andrew000@users.noreply.github.com>
2023-11-23 00:41:21 +02:00

217 lines
6.9 KiB
Python

import functools
from typing import Any, Callable, Dict, Set, Union
import pytest
from magic_filter import F as A
from aiogram import F
from aiogram.dispatcher.event.handler import CallableObject, FilterObject, HandlerObject
from aiogram.filters import Filter
from aiogram.handlers import BaseHandler
from aiogram.types import Update
from aiogram.utils.warnings import Recommendation
def callback1(foo: int, bar: int, baz: int):
return locals()
async def callback2(foo: int, bar: int, baz: int):
return locals()
async def callback3(foo: int, **kwargs):
return locals()
async def callback4(foo: int, *, bar: int, baz: int):
return locals()
class TestFilter(Filter):
async def __call__(self, foo: int, bar: int, baz: int) -> Union[bool, Dict[str, Any]]:
return locals()
class SyncCallable:
def __call__(self, foo, bar, baz):
return locals()
class TestCallableObject:
@pytest.mark.parametrize("callback", [callback2, TestFilter()])
def test_init_awaitable(self, callback):
obj = CallableObject(callback)
assert obj.awaitable
assert obj.callback == callback
@pytest.mark.parametrize("callback", [callback1, SyncCallable()])
def test_init_not_awaitable(self, callback):
obj = CallableObject(callback)
assert not obj.awaitable
assert obj.callback == callback
@pytest.mark.parametrize(
"callback,args",
[
pytest.param(callback1, {"foo", "bar", "baz"}),
pytest.param(callback2, {"foo", "bar", "baz"}),
pytest.param(callback3, {"foo"}),
pytest.param(TestFilter(), {"self", "foo", "bar", "baz"}),
pytest.param(SyncCallable(), {"self", "foo", "bar", "baz"}),
],
)
def test_init_args_spec(self, callback: Callable, args: Set[str]):
obj = CallableObject(callback)
assert set(obj.params) == args
def test_init_decorated(self):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
@decorator
def callback1(foo, bar, baz):
pass
@decorator
@decorator
def callback2(foo, bar, baz):
pass
obj1 = CallableObject(callback1)
obj2 = CallableObject(callback2)
assert set(obj1.params) == {"foo", "bar", "baz"}
assert obj1.callback == callback1
assert set(obj2.params) == {"foo", "bar", "baz"}
assert obj2.callback == callback2
@pytest.mark.parametrize(
"callback,kwargs,result",
[
pytest.param(
callback1, {"foo": 42, "spam": True, "baz": "fuz"}, {"foo": 42, "baz": "fuz"}
),
pytest.param(
callback2,
{"foo": 42, "spam": True, "baz": "fuz", "bar": "test"},
{"foo": 42, "baz": "fuz", "bar": "test"},
),
pytest.param(
functools.partial(callback2, bar="test"),
{"foo": 42, "spam": True, "baz": "fuz"},
{"foo": 42, "baz": "fuz"},
),
pytest.param(
callback3,
{"foo": 42, "spam": True, "baz": "fuz", "bar": "test"},
{"foo": 42, "spam": True, "baz": "fuz", "bar": "test"},
),
pytest.param(
callback4,
{"foo": 42, "spam": True, "baz": "fuz", "bar": "test"},
{"foo": 42, "baz": "fuz", "bar": "test"},
),
pytest.param(
TestFilter(), {"foo": 42, "spam": True, "baz": "fuz"}, {"foo": 42, "baz": "fuz"}
),
pytest.param(
SyncCallable(), {"foo": 42, "spam": True, "baz": "fuz"}, {"foo": 42, "baz": "fuz"}
),
],
)
def test_prepare_kwargs(
self, callback: Callable, kwargs: Dict[str, Any], result: Dict[str, Any]
):
obj = CallableObject(callback)
assert obj._prepare_kwargs(kwargs) == result
async def test_sync_call(self):
obj = CallableObject(callback1)
result = await obj.call(foo=42, bar="test", baz="fuz", spam=True)
assert result == {"foo": 42, "bar": "test", "baz": "fuz"}
async def test_async_call(self):
obj = CallableObject(callback2)
result = await obj.call(foo=42, bar="test", baz="fuz", spam=True)
assert result == {"foo": 42, "bar": "test", "baz": "fuz"}
class TestFilterObject:
def test_post_init(self):
case = F.test
filter_obj = FilterObject(callback=case)
print(filter_obj.callback)
assert filter_obj.callback == case.resolve
async def simple_handler(*args, **kwargs):
return args, kwargs
class TestHandlerObject:
async def test_check_with_bool_result(self):
handler = HandlerObject(simple_handler, [FilterObject(lambda value: True)] * 3)
result, data = await handler.check(42, foo=True)
assert result
assert data == {"foo": True}
async def test_check_with_dict_result(self):
handler = HandlerObject(
simple_handler,
[
FilterObject(
functools.partial(lambda value, index: {f"test{index}": "ok"}, index=item)
)
for item in range(3)
],
)
result, data = await handler.check(42, foo=True)
assert result
assert data == {"foo": True, "test0": "ok", "test1": "ok", "test2": "ok"}
async def test_check_with_combined_result(self):
handler = HandlerObject(
simple_handler,
[FilterObject(lambda value: True), FilterObject(lambda value: {"test": value})],
)
result, data = await handler.check(42, foo=True)
assert result
assert data == {"foo": True, "test": 42}
async def test_check_rejected(self):
handler = HandlerObject(simple_handler, [FilterObject(lambda value: False)])
result, data = await handler.check(42, foo=True)
assert not result
async def test_check_partial_rejected(self):
handler = HandlerObject(
simple_handler, [FilterObject(lambda value: True), FilterObject(lambda value: False)]
)
result, data = await handler.check(42, foo=True)
assert not result
async def test_class_based_handler(self):
class MyHandler(BaseHandler):
event: Update
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) == 1
result = await handler.call(Update(update_id=42))
assert result == 42
def test_warn_another_magic(self):
with pytest.warns(Recommendation):
FilterObject(callback=A.test.is_(True))