Repository: aiogram/aiogram
Pull request #759 (2021-11-24 02:35:03)

Extended MagicFilter with aiogram-specific operation
# Description

Added possibility to get MagicFilter result as handler argument


## Example

```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)))
```

## Type of change

Please delete options that are not relevant.

- [x] Documentation (typos, code examples or any documentation update)
- [x] New feature (non-breaking change which adds functionality)
- [x] This change requires a documentation update

# Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my own code
- [x] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my feature works
- [x] New and existing unit tests pass locally with my changes


codecov[bot]: # [Codecov](https://codecov.io/gh/aiogram/aiogram/pull/759?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aiogram) Report
> Merging [#759](https://codecov.io/gh/aiogram/aiogram/pull/759?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aiogram) (c3fbc26) into [dev-3.x](https://codecov.io/gh/aiogram/aiogram/commit/092b3f06f20c813d09b9ffe08316e53d58b6c8df?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aiogram) (092b3f0) will **decrease** coverage by `0.19%`.
> The diff coverage is `0.00%`.

[![Impacted file tree graph](https://codecov.io/gh/aiogram/aiogram/pull/759/graphs/tree.svg?width=650&height=150&src=pr&token=cFqEr3p9MX&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aiogram)](https://codecov.io/gh/aiogram/aiogram/pull/759?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aiogram)

```diff
@@             Coverage Diff             @@
##           dev-3.x     #759      +/-   ##
===========================================
- Coverage   100.00%   99.80%   -0.20%     
===========================================
  Files          277      278       +1     
  Lines         7302     7316      +14     
===========================================
  Hits          7302     7302              
- Misses           0       14      +14     
```

| Flag | Coverage Δ | |
|---|---|---|
| unittests | `99.80% <0.00%> (-0.20%)` | :arrow_down: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aiogram#carryforward-flags-in-the-pull-request-comment) to find out more.

| [Impacted Files](https://codecov.io/gh/aiogram/aiogram/pull/759?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aiogram) | Coverage Δ | |
|---|---|---|
| [aiogram/utils/magic\_filter.py](https://codecov.io/gh/aiogram/aiogram/pull/759/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aiogram#diff-YWlvZ3JhbS91dGlscy9tYWdpY19maWx0ZXIucHk=) | `0.00% <0.00%> (ø)` | |


Patch:
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 <magic-filter-possible-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"


Test patch:
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)