From d7be55bc58f7a3c9a55ab6ce65fb1efebe421fe7 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Wed, 24 Nov 2021 06:00:37 +0200 Subject: [PATCH] Extended MagicFilter with aiogram-specific operation (#759) * Extend MagicFilter with aiogram-specific operation * Added tests * Added changes annotation and update docs --- CHANGES/759.feature | 13 +++++++++++++ aiogram/__init__.py | 3 +-- aiogram/utils/magic_filter.py | 21 +++++++++++++++++++++ docs/dispatcher/filters/magic_filters.rst | 21 ++++++++++++++++++++- poetry.lock | 15 ++++++++------- pyproject.toml | 2 +- tests/test_utils/test_magic_filter.py | 21 +++++++++++++++++++++ 7 files changed, 85 insertions(+), 11 deletions(-) create mode 100644 CHANGES/759.feature create mode 100644 aiogram/utils/magic_filter.py create mode 100644 tests/test_utils/test_magic_filter.py diff --git a/CHANGES/759.feature b/CHANGES/759.feature new file mode 100644 index 00000000..da4a0d99 --- /dev/null +++ b/CHANGES/759.feature @@ -0,0 +1,13 @@ +Added new custom operation for MagicFilter named :code:`as_` + +Now you can use it to get magic filter result as handler argument + +.. code-block:: python + + from aiogram import F + + ... + + @router.message(F.text.regexp(r"^(\d+)$").as_("digits")) + async def any_digits_handler(message: Message, digits: Match[str]): + await message.answer(html.quote(str(digits))) diff --git a/aiogram/__init__.py b/aiogram/__init__.py index f1c4813e..a6f60991 100644 --- a/aiogram/__init__.py +++ b/aiogram/__init__.py @@ -1,11 +1,10 @@ -from magic_filter import MagicFilter - from .client import session from .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 +from .utils.magic_filter import MagicFilter from .utils.text_decorations import html_decoration as _html_decoration from .utils.text_decorations import markdown_decoration as _markdown_decoration diff --git a/aiogram/utils/magic_filter.py b/aiogram/utils/magic_filter.py new file mode 100644 index 00000000..a00d0010 --- /dev/null +++ b/aiogram/utils/magic_filter.py @@ -0,0 +1,21 @@ +from typing import Any + +from magic_filter import MagicFilter as _MagicFilter +from magic_filter import MagicT as _MagicT +from magic_filter.operations import BaseOperation + + +class AsFilterResultOperation(BaseOperation): + __slots__ = ("name",) + + def __init__(self, name: str) -> None: + self.name = name + + def resolve(self, value: Any, initial_value: Any) -> Any: + if value: + return {self.name: value} + + +class MagicFilter(_MagicFilter): + def as_(self: _MagicT, name: str) -> _MagicT: + return self._extend(AsFilterResultOperation(name=name)) diff --git a/docs/dispatcher/filters/magic_filters.rst b/docs/dispatcher/filters/magic_filters.rst index f60dca5d..3117aeb3 100644 --- a/docs/dispatcher/filters/magic_filters.rst +++ b/docs/dispatcher/filters/magic_filters.rst @@ -16,7 +16,11 @@ That's mean you can install it and use with any other libraries and in own proje Usage ===== -The **magic_filter** package implements class shortly named :class:`magic_filter.F` that's mean :code:`F` can be imported from :code:`magic_filter`. :class:`F` is alias for :class:`MagicFilter`. +The **magic_filter** package implements class shortly named :class:`magic_filter.F` that's mean :code:`F` can be imported from :code:`aiogram` or :code:`magic_filter`. :class:`F` is alias for :class:`MagicFilter`. + +.. note:: + + Note that *aiogram* has an small extension over magic-filter and if you want to use this extension you should import magic from *aiogram* instead of *magic_filter* package The :class:`MagicFilter` object is callable, supports :ref:`some actions ` and memorize the attributes chain and the action which should be checked on demand. @@ -130,6 +134,21 @@ Can be used only with string attributes. F.text.len() == 5 # lambda message: len(message.text) == 5 +Get filter result as handler argument +------------------------------------- + +This part is not available in *magic-filter* directly but can be used with *aiogram* + +.. code-block:: python + + from aiogram import F + + ... + + @router.message(F.text.regexp(r"^(\d+)$").as_("digits")) + async def any_digits_handler(message: Message, digits: Match[str]): + await message.answer(html.quote(str(digits))) + Usage in *aiogram* ================== diff --git a/poetry.lock b/poetry.lock index 20905344..3ed1df7c 100644 --- a/poetry.lock +++ b/poetry.lock @@ -513,7 +513,7 @@ tornado = {version = "*", markers = "python_version > \"2.7\""} [[package]] name = "magic-filter" -version = "1.0.3" +version = "1.0.4" description = "This package provides magic filter based on dynamic attribute getter" category = "main" optional = false @@ -917,7 +917,7 @@ pytest = ">=3.5" [[package]] name = "python-socks" -version = "1.2.4" +version = "2.0.0" description = "Core proxy (SOCKS4, SOCKS5, HTTP tunneling) functionality for Python" category = "main" optional = true @@ -927,6 +927,7 @@ python-versions = "*" async-timeout = {version = ">=3.0.1", optional = true, markers = "extra == \"asyncio\""} [package.extras] +anyio = ["anyio (>=3.3.4)"] asyncio = ["async-timeout (>=3.0.1)"] curio = ["curio (>=1.4)"] trio = ["trio (>=0.16.0)"] @@ -1323,7 +1324,7 @@ redis = ["aioredis"] [metadata] lock-version = "1.1" python-versions = "^3.8" -content-hash = "de8ae82fb5f86da3377cc87aab35239ec5baddac4e0151595b5c4f6152d95ac8" +content-hash = "27d602728b6dab256d184fbd44953030676edf320652ad828c6e1b4beaa80f8b" [metadata.files] aiofiles = [ @@ -1681,8 +1682,8 @@ livereload = [ {file = "livereload-2.6.3.tar.gz", hash = "sha256:776f2f865e59fde56490a56bcc6773b6917366bce0c267c60ee8aaf1a0959869"}, ] magic-filter = [ - {file = "magic-filter-1.0.3.tar.gz", hash = "sha256:f39c1a27bdbdf5bec2e9ccfc4269557f9812345559ddd656926220f28921492b"}, - {file = "magic_filter-1.0.3-py3-none-any.whl", hash = "sha256:e1139dad0b0f6426894d3fd214fb7d5bb1f964e8748141cb902414020e5ceb71"}, + {file = "magic-filter-1.0.4.tar.gz", hash = "sha256:2b77de98bfef16a990c22b2a68a904b4a99913b656d48cf877367da8ae5f5c84"}, + {file = "magic_filter-1.0.4-py3-none-any.whl", hash = "sha256:daf869025b9490c4438f1c2d3f4f9bcb8295fa0ffa7fec643bc20486f519f349"}, ] markdown = [ {file = "Markdown-3.3.4-py3-none-any.whl", hash = "sha256:96c3ba1261de2f7547b46a00ea8463832c921d3f9d6aba3f255a6f71386db20c"}, @@ -2002,8 +2003,8 @@ pytest-mypy = [ {file = "pytest_mypy-0.8.1-py3-none-any.whl", hash = "sha256:6e68e8eb7ceeb7d1c83a1590912f784879f037b51adfb9c17b95c6b2fc57466b"}, ] python-socks = [ - {file = "python-socks-1.2.4.tar.gz", hash = "sha256:7d0ef2578cead9f762b71317d25a6c118fabaf79535555e75b3e102f5158ddd8"}, - {file = "python_socks-1.2.4-py3-none-any.whl", hash = "sha256:9f12e8fe78629b87543fad0e4ea0ccf103a4fad6a7872c5d0ecb36d9903fa548"}, + {file = "python-socks-2.0.0.tar.gz", hash = "sha256:7944dad882846ac73e5f79e180c841e3895ee058e16855b7e8fff24f4cd0b90b"}, + {file = "python_socks-2.0.0-py3-none-any.whl", hash = "sha256:aac65671cbd3b0eb55b20f8558c8de3894a315536aaab3ec0a7b9d46ff89c1bf"}, ] pytz = [ {file = "pytz-2021.3-py2.py3-none-any.whl", hash = "sha256:3672058bc3453457b622aab7a1c3bfd5ab0bdae451512f6cf25f64ed37f5b87c"}, diff --git a/pyproject.toml b/pyproject.toml index cf6e8959..1f7b162c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,7 +38,7 @@ classifiers = [ [tool.poetry.dependencies] python = "^3.8" -magic-filter = "^1.0.3" +magic-filter = "^1.0.4" aiohttp = "^3.8.0" pydantic = "^1.8.2" aiofiles = "^0.7.0" diff --git a/tests/test_utils/test_magic_filter.py b/tests/test_utils/test_magic_filter.py new file mode 100644 index 00000000..4a1d05f3 --- /dev/null +++ b/tests/test_utils/test_magic_filter.py @@ -0,0 +1,21 @@ +from dataclasses import dataclass +from re import Match + +from aiogram import F +from aiogram.utils.magic_filter import MagicFilter + + +@dataclass +class MyObject: + text: str + + +class TestMagicFilter: + def test_operation_as(self): + magic: MagicFilter = F.text.regexp(r"^(\d+)$").as_("match") + + assert not magic.resolve(MyObject(text="test")) + + result = magic.resolve(MyObject(text="123")) + assert isinstance(result, dict) + assert isinstance(result["match"], Match)