Global filters for router (#644)

* Bump version

* Added more comments

* Cover registering global filters

* Reformat code

* Add more tests

* Rework event propagation to routers mechanism. Fixed compatibility with Python 3.10 syntax (match keyword)

* Fixed tests

* Fixed coverage

Co-authored-by: evgfilim1 <evgfilim1@yandex.ru>
This commit is contained in:
Alex Root Junior 2021-07-31 23:34:09 +03:00 committed by GitHub
parent a70ecb767f
commit 4f2cc75951
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 176 additions and 31 deletions

View file

@ -232,20 +232,11 @@ class Dispatcher(Router):
"installed not latest version of aiogram framework",
RuntimeWarning,
)
raise SkipHandler
raise SkipHandler()
kwargs.update(event_update=update)
for router in self.chain:
kwargs.update(event_router=router)
observer = router.observers[update_type]
response = await observer.trigger(event, update=update, **kwargs)
if response is not UNHANDLED:
break
else:
response = UNHANDLED
return response
return await self.propagate_event(update_type=update_type, event=event, **kwargs)
@classmethod
async def _silent_call_request(cls, bot: Bot, result: TelegramMethod[Any]) -> None:

View file

@ -12,6 +12,7 @@ MiddlewareType = Union[
]
UNHANDLED = sentinel.UNHANDLED
REJECTED = sentinel.REJECTED
class SkipHandler(Exception):

View file

@ -8,7 +8,7 @@ from pydantic import ValidationError
from ...types import TelegramObject
from ..filters.base import BaseFilter
from .bases import UNHANDLED, MiddlewareType, NextMiddlewareType, SkipHandler
from .bases import REJECTED, UNHANDLED, MiddlewareType, NextMiddlewareType, SkipHandler
from .handler import CallbackType, FilterObject, FilterType, HandlerObject, HandlerType
if TYPE_CHECKING: # pragma: no cover
@ -32,6 +32,24 @@ class TelegramEventObserver:
self.outer_middlewares: List[MiddlewareType] = []
self.middlewares: List[MiddlewareType] = []
# Re-used filters check method from already implemented handler object
# with dummy callback which never will be used
self._handler = HandlerObject(callback=lambda: True, filters=[])
def filter(self, *filters: FilterType, **bound_filters: Any) -> None:
"""
Register filter for all handlers of this event observer
:param filters: positional filters
:param bound_filters: keyword filters
"""
resolved_filters = self.resolve_filters(bound_filters)
if self._handler.filters is None:
self._handler.filters = []
self._handler.filters.extend(
[FilterObject(filter_) for filter_ in chain(resolved_filters, filters)]
)
def bind_filter(self, bound_filter: Type[BaseFilter]) -> None:
"""
Register filter class in factory
@ -139,6 +157,12 @@ class TelegramEventObserver:
return await wrapped_outer(event, kwargs)
async def _trigger(self, event: TelegramObject, **kwargs: Any) -> Any:
# Check globally defined filters before any other handler will be checked
result, data = await self._handler.check(event, **kwargs)
if not result:
return REJECTED
kwargs.update(data)
for handler in self.handlers:
result, data = await handler.check(event, **kwargs)
if result:

View file

@ -89,7 +89,7 @@ class Command(BaseFilter):
if isinstance(allowed_command, Pattern): # Regexp
result = allowed_command.match(command.command)
if result:
return replace(command, match=result)
return replace(command, regexp_match=result)
elif command.command == allowed_command: # String
return command
raise CommandException("Command did not match pattern")
@ -134,7 +134,7 @@ class CommandObject:
"""Mention (if available)"""
args: Optional[str] = field(repr=False, default=None)
"""Command argument"""
match: Optional[Match[str]] = field(repr=False, default=None)
regexp_match: Optional[Match[str]] = field(repr=False, default=None)
"""Will be presented match result if the command is presented as regexp in filter"""
@property

View file

@ -26,20 +26,20 @@ class ExceptionMessageFilter(BaseFilter):
Allow to match exception by message
"""
match: Union[str, Pattern[str]]
pattern: Union[str, Pattern[str]]
"""Regexp pattern"""
class Config:
arbitrary_types_allowed = True
@validator("match")
@validator("pattern")
def _validate_match(cls, value: Union[str, Pattern[str]]) -> Union[str, Pattern[str]]:
if isinstance(value, str):
return re.compile(value)
return value
async def __call__(self, exception: Exception) -> Union[bool, Dict[str, Any]]:
pattern = cast(Pattern[str], self.match)
pattern = cast(Pattern[str], self.pattern)
result = pattern.match(str(exception))
if not result:
return False

View file

@ -45,7 +45,7 @@ class RedisStorage(BaseStorage):
return cls(redis=redis, **kwargs)
async def close(self) -> None:
await self.redis.close()
await self.redis.close() # type: ignore
def generate_key(self, bot: Bot, *parts: Any) -> str:
prefix_parts = [self.prefix]
@ -73,7 +73,7 @@ class RedisStorage(BaseStorage):
await self.redis.delete(key)
else:
await self.redis.set(
key, state.state if isinstance(state, State) else state, ex=self.state_ttl
key, state.state if isinstance(state, State) else state, ex=self.state_ttl # type: ignore[arg-type]
)
async def get_state(self, bot: Bot, chat_id: int, user_id: int) -> Optional[str]:
@ -89,7 +89,7 @@ class RedisStorage(BaseStorage):
await self.redis.delete(key)
return
json_data = bot.session.json_dumps(data)
await self.redis.set(key, json_data, ex=self.data_ttl)
await self.redis.set(key, json_data, ex=self.data_ttl) # type: ignore[arg-type]
async def get_data(self, bot: Bot, chat_id: int, user_id: int) -> Dict[str, Any]:
key = self.generate_key(bot, chat_id, user_id, STATE_DATA_KEY)

View file

@ -3,8 +3,10 @@ from __future__ import annotations
import warnings
from typing import Any, Dict, Generator, List, Optional, Union
from ..types import TelegramObject
from ..utils.imports import import_module
from ..utils.warnings import CodeHasNoEffect
from .event.bases import REJECTED, UNHANDLED
from .event.event import EventObserver
from .event.telegram import TelegramEventObserver
from .filters import BUILTIN_FILTERS
@ -82,6 +84,22 @@ class Router:
for builtin_filter in BUILTIN_FILTERS.get(name, ()):
observer.bind_filter(builtin_filter)
async def propagate_event(self, update_type: str, event: TelegramObject, **kwargs: Any) -> Any:
kwargs.update(event_router=self)
observer = self.observers[update_type]
response = await observer.trigger(event, **kwargs)
if response is REJECTED:
return UNHANDLED
if response is not UNHANDLED:
return response
for router in self.sub_routers:
response = await router.propagate_event(update_type=update_type, event=event, **kwargs)
if response is not UNHANDLED:
break
return response
@property
def chain_head(self) -> Generator[Router, None, None]:
router: Optional[Router] = self

59
poetry.lock generated
View file

@ -40,7 +40,7 @@ python-socks = {version = ">=1.0.1", extras = ["asyncio"]}
[[package]]
name = "aioredis"
version = "2.0.0a1"
version = "2.0.0"
description = "asyncio (PEP 3156) Redis support"
category = "main"
optional = false
@ -296,6 +296,14 @@ importlib-metadata = "*"
jinja2 = ">=2.9.0"
pygments = ">=2.2.0"
[[package]]
name = "frozenlist"
version = "1.1.1"
description = "A list-like structure which implements collections.abc.MutableSequence"
category = "main"
optional = false
python-versions = ">=3.6"
[[package]]
name = "furo"
version = "2021.6.18b36"
@ -1201,7 +1209,7 @@ redis = ["aioredis"]
[metadata]
lock-version = "1.1"
python-versions = "^3.8"
content-hash = "ef3571030ff35c2a05e01dca86e9347239e98ad0f45bed6f5d9a73121013f376"
content-hash = "c51e22cdb0e17fb996fda81c5484d34f3dff0e57511380b1103a1d53c9416440"
[metadata.files]
aiofiles = [
@ -1252,8 +1260,8 @@ aiohttp-socks = [
{file = "aiohttp_socks-0.5.5.tar.gz", hash = "sha256:2eb2059756bde34c55bb429541cbf2eba3fd53e36ac80875b461221e2858b04a"},
]
aioredis = [
{file = "aioredis-2.0.0a1-py3-none-any.whl", hash = "sha256:32d7910724282a475c91b8b34403867069a4f07bf0c5ad5fe66cd797322f9a0d"},
{file = "aioredis-2.0.0a1.tar.gz", hash = "sha256:5884f384b8ecb143bb73320a96e7c464fd38e117950a7d48340a35db8e35e7d2"},
{file = "aioredis-2.0.0-py3-none-any.whl", hash = "sha256:9921d68a3df5c5cdb0d5b49ad4fc88a4cfdd60c108325df4f0066e8410c55ffb"},
{file = "aioredis-2.0.0.tar.gz", hash = "sha256:3a2de4b614e6a5f8e104238924294dc4e811aefbe17ddf52c04a93cbf06e67db"},
]
alabaster = [
{file = "alabaster-0.7.12-py2.py3-none-any.whl", hash = "sha256:446438bdcca0e05bd45ea2de1668c1d9b032e1a9154c2c259092d77031ddd359"},
@ -1401,6 +1409,49 @@ flake8-html = [
{file = "flake8-html-0.4.1.tar.gz", hash = "sha256:2fb436cbfe1e109275bc8fb7fdd0cb00e67b3b48cfeb397309b6b2c61eeb4cb4"},
{file = "flake8_html-0.4.1-py2.py3-none-any.whl", hash = "sha256:17324eb947e7006807e4184ee26953e67baf421b3cf9e646a38bfec34eec5a94"},
]
frozenlist = [
{file = "frozenlist-1.1.1-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:968b520ef969541b2c8f47d9a13c78e080806dc97862434d29163d44c2c1d709"},
{file = "frozenlist-1.1.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:451b445120ea95d86af3817bbd4d67ab77269fe7f055dc67b8c70bf4633f4efe"},
{file = "frozenlist-1.1.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:313384e54a7285a6f20ca6530b207a0a9cf6ebcda6c7b074ee802e4a82a0a6dc"},
{file = "frozenlist-1.1.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:3c4f7399e7338a5788d32802017f94aaab3267afa8b1a663272b81eee7193e66"},
{file = "frozenlist-1.1.1-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:1e7b18fdf6682028f512d3e6142b79ca95b9b66f30c1bec2be237160d9eb6518"},
{file = "frozenlist-1.1.1-cp36-cp36m-manylinux2014_ppc64le.whl", hash = "sha256:7622f5c4c3dfaa09b9c6a62fb1af94da124626bb30f3ad9095f9cec6328074c1"},
{file = "frozenlist-1.1.1-cp36-cp36m-manylinux2014_s390x.whl", hash = "sha256:657341b9bc166d3f7418d37e1decf6d95485501e0d0e7da1a26a881e624216c6"},
{file = "frozenlist-1.1.1-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:fc6d994de78b11e1f465f2224c56858eb52cb51c8f9faf0c33e5799184d414a7"},
{file = "frozenlist-1.1.1-cp36-cp36m-win32.whl", hash = "sha256:08428f9d0178b6fa0da95a42ab87a5b20ed2a707bacc97e3689e96ae6cab13fa"},
{file = "frozenlist-1.1.1-cp36-cp36m-win_amd64.whl", hash = "sha256:5c4c42bdbf5754010e0cc5cc0f91019437839bc6b7e585262bcc126557a244bc"},
{file = "frozenlist-1.1.1-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:32491ac26e72e5f35913887bc3ab7bcfe562b4fb65b0e58350fce6efa22fec75"},
{file = "frozenlist-1.1.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:d160a73e4a034a857a98384b5e05204c375489d2bbb6ecf1ee8fc124735028fa"},
{file = "frozenlist-1.1.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:b3cf6737afc4347092a0c8392b4c0e77acc5594e73f4aef355705117a945743a"},
{file = "frozenlist-1.1.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:efb805e383836250bef3c99f1857c432a8941c802d0ed7767751315617a54794"},
{file = "frozenlist-1.1.1-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:2f1f56a36962e28c304872797e226cd646395381de97517870fb819ff7b4f496"},
{file = "frozenlist-1.1.1-cp37-cp37m-manylinux2014_ppc64le.whl", hash = "sha256:7443d815fd9ff2de75b810e192cfa92854bada43aed47ec1598766c7bc9d4a40"},
{file = "frozenlist-1.1.1-cp37-cp37m-manylinux2014_s390x.whl", hash = "sha256:fe463e7b3cd089d221f33bd9c22cfad2726622b2a96c3af56a8eb5a71c0943bd"},
{file = "frozenlist-1.1.1-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:c228886891dc0170d21acbfb62fea801856c3fa207619c973e17d96455ab83e3"},
{file = "frozenlist-1.1.1-cp37-cp37m-win32.whl", hash = "sha256:de170ea97e7b5051a13989ea457300b8159c00455d2207d22afb6b129a433152"},
{file = "frozenlist-1.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:158c6258ab4ee8a01470d86e75a7514091391b27bb400ba28a7f6a30466cc8e0"},
{file = "frozenlist-1.1.1-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:572a5a0977b1bd2f15183a352df907726b20da5f91cd1242343b0d72ac677be6"},
{file = "frozenlist-1.1.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:748150da8bbd9cbe1b29f0965a675b5732337ae874eed47ccb48dfa75815d0d7"},
{file = "frozenlist-1.1.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:caba5ae97c40020771502866dee5024b0031187293185ca5c7714ea52a824a92"},
{file = "frozenlist-1.1.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:aea1b84bbebec7c46cd59da13aff90e23bece13bba91974a305bd555f66a72f3"},
{file = "frozenlist-1.1.1-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:d07f08268b8d37c357f4d34272f1f7588a0618d3fa509a87ad614b5e1cf7109f"},
{file = "frozenlist-1.1.1-cp38-cp38-manylinux2014_ppc64le.whl", hash = "sha256:66f7ba888fe51685502be51ad548b226eb4214fcab0ef48672a2a91a4de08417"},
{file = "frozenlist-1.1.1-cp38-cp38-manylinux2014_s390x.whl", hash = "sha256:4282d897ea190b5e38a18fc3b70295e20e00af7734892250876e1e1b452a7dc8"},
{file = "frozenlist-1.1.1-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:84ef9f6f6f8e2dd9cf828367c61715202a781ef6d32caa9a016d9055a7daef8b"},
{file = "frozenlist-1.1.1-cp38-cp38-win32.whl", hash = "sha256:d5cba2a537bcf8b4abffc9e01b037eb4ca5c9d1cb29d575fd433f82919a04c68"},
{file = "frozenlist-1.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:6c1cefdc3666507f7241b120b828e223c1dfb18e599f960ebbd0558de5010efe"},
{file = "frozenlist-1.1.1-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:a6d2d80222eefe6e08b8167005e5a0c1a05ce784ce97de4d6d693be7e2a99862"},
{file = "frozenlist-1.1.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:8fbdc86968f71d1d1e216f1f3467da96571b092378ad55b7eb6fc9f3ff877902"},
{file = "frozenlist-1.1.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:1032a7eb76ca47cb94dcfd05a289dfb2f31b5e155c9cd845f97a56526eca9800"},
{file = "frozenlist-1.1.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:4fdfb300d205f3d007462d66c9e8ffa89d7b1b3699e538ae7344845223291ff0"},
{file = "frozenlist-1.1.1-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:e2d35804cf42b58e42e9b2cca6a2a5bb7155bb545808ff652503a8bacab2be5d"},
{file = "frozenlist-1.1.1-cp39-cp39-manylinux2014_ppc64le.whl", hash = "sha256:a2ebc6dd4f73f39212073add6b3a629a4274ed0a5e43c2fa87bd91957f511450"},
{file = "frozenlist-1.1.1-cp39-cp39-manylinux2014_s390x.whl", hash = "sha256:9ee0bca9801eea5431680bdf22817b1b07310474ac284a3aa7a3902d0dba2382"},
{file = "frozenlist-1.1.1-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:08a4f1bd182659416c8ae518ef8a63c37953eb2d4bd77cf8b45941a90e87d27c"},
{file = "frozenlist-1.1.1-cp39-cp39-win32.whl", hash = "sha256:803bc0fdb904a762b0a49572fe2f1cb2a03ade5514b265971da5c3e7a8b14798"},
{file = "frozenlist-1.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:301220a5752fc2585ef97794b1dc88f87d77f5c1951782488896fcb6a732f883"},
{file = "frozenlist-1.1.1.tar.gz", hash = "sha256:32fa8c86eee5c6f11b44863c0d3e945b2b1a03df3bf218617e832f1f04ba3146"},
]
furo = [
{file = "furo-2021.6.18b36-py3-none-any.whl", hash = "sha256:a4c00634afeb5896a34d141a5dffb62f20c5eca7831b78269823a8cd8b09a5e4"},
{file = "furo-2021.6.18b36.tar.gz", hash = "sha256:46a30bc597a9067088d39d730e7d9bf6c1a1d71967e4af062f796769f66b3bdb"},

View file

@ -37,8 +37,9 @@ pydantic = "^1.8.1"
Babel = "^2.9.1"
aiofiles = "^0.6.0"
async_lru = "^1.0.2"
frozenlist = "^1.1.1"
aiohttp-socks = { version = "^0.5.5", optional = true }
aioredis = { version = "^2.0.0a1", allow-prereleases = true, optional = true }
aioredis = { version = "^2.0.0", allow-prereleases = true, optional = true }
magic-filter = { version = "1.0.0a1", allow-prereleases = true }
sphinx = { version = "^3.1.0", optional = true }
sphinx-intl = { version = "^2.0.1", optional = true }

View file

@ -1,6 +1,6 @@
import pytest
from aiogram.methods import BanChatMember, DeleteMyCommands, Request
from aiogram.methods import DeleteMyCommands, Request
from tests.mocked_bot import MockedBot

View file

@ -4,7 +4,7 @@ from typing import Any, Awaitable, Callable, Dict, NoReturn, Union
import pytest
from aiogram.dispatcher.event.bases import SkipHandler
from aiogram.dispatcher.event.bases import REJECTED, SkipHandler
from aiogram.dispatcher.event.handler import HandlerObject
from aiogram.dispatcher.event.telegram import TelegramEventObserver
from aiogram.dispatcher.filters.base import BaseFilter
@ -233,3 +233,48 @@ class TestTelegramEventObserver:
assert my_middleware3 in middlewares
assert middlewares == [my_middleware1, my_middleware2, my_middleware3]
def test_register_global_filters(self):
router = Router(use_builtin_filters=False)
assert isinstance(router.message._handler.filters, list)
assert not router.message._handler.filters
my_filter = MyFilter1(test="pass")
router.message.filter(my_filter)
assert len(router.message._handler.filters) == 1
assert router.message._handler.filters[0].callback is my_filter
router.message._handler.filters = None
router.message.filter(my_filter)
assert len(router.message._handler.filters) == 1
assert router.message._handler.filters[0].callback is my_filter
@pytest.mark.asyncio
async def test_global_filter(self):
r1 = Router()
r2 = Router()
async def handler(evt):
return evt
r1.message.filter(lambda evt: False)
r1.message.register(handler)
r2.message.register(handler)
assert await r1.message.trigger(None) is REJECTED
assert await r2.message.trigger(None) is None
@pytest.mark.asyncio
async def test_global_filter_in_nested_router(self):
r1 = Router()
r2 = Router()
async def handler(evt):
return evt
r1.include_router(r2)
r1.message.filter(lambda evt: False)
r2.message.register(handler)
assert await r1.message.trigger(None) is REJECTED

View file

@ -8,12 +8,12 @@ from aiogram.dispatcher.filters import ExceptionMessageFilter, ExceptionTypeFilt
class TestExceptionMessageFilter:
@pytest.mark.parametrize("value", ["value", re.compile("value")])
def test_converter(self, value):
obj = ExceptionMessageFilter(match=value)
assert isinstance(obj.match, re.Pattern)
obj = ExceptionMessageFilter(pattern=value)
assert isinstance(obj.pattern, re.Pattern)
@pytest.mark.asyncio
async def test_match(self):
obj = ExceptionMessageFilter(match="KABOOM")
obj = ExceptionMessageFilter(pattern="KABOOM")
result = await obj(Exception())
assert not result

View file

@ -1,6 +1,6 @@
import pytest
from aiogram.dispatcher.event.bases import SkipHandler, skip
from aiogram.dispatcher.event.bases import SkipHandler, skip, UNHANDLED
from aiogram.dispatcher.router import Router
from aiogram.utils.warnings import CodeHasNoEffect
@ -122,3 +122,17 @@ class TestRouter:
skip()
with pytest.raises(SkipHandler, match="KABOOM"):
skip("KABOOM")
@pytest.mark.asyncio
async def test_global_filter_in_nested_router(self):
r1 = Router()
r2 = Router()
async def handler(evt):
return evt
r1.include_router(r2)
r1.message.filter(lambda evt: False)
r2.message.register(handler)
assert await r1.propagate_event(update_type="message", event=None) is UNHANDLED