Repository: aiogram/aiogram
Pull request #1661 (2025-03-31 20:45:02)

misc: simplify running non-async callback using asyncio.to_thread
# Description

Replaced `loop.run_in_executor` with `asyncio.to_thread` for improved readability and consistency.  
This change does not affect functionality but simplifies the code.  

No related issue.

## Type of change

- [x] Miscellaneous (code cleanup, refactoring, minor improvements)

# How Has This Been Tested?

Since this is a non-functional change, no new tests were required.  
Existing tests were run to ensure there were no regressions.

**Test Configuration**:
* Operating System:
* Python version:

# Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my own code
- [x] Existing unit tests pass locally with my changes


github-actions[bot]: # :x: Changelog is required!

You need to add a brief description of the changes to the `CHANGES` directory.

Changes file should be named like `<issue or PR number>.<category>.rst`,
example `1234.bugfix.rst` where `1234` is the PR or issue number and `bugfix` is the category.

The content of the file should be a brief description of the changes in
the PR in the format of a description of what has been done.

Possible categories are: `feature`, `bugfix`, `doc`, `removal` and `misc`.

Patch:
diff --git a/CHANGES/1657.feature.rst b/CHANGES/1657.feature.rst
new file mode 100644
index 00000000..cbb7c51b
--- /dev/null
+++ b/CHANGES/1657.feature.rst
@@ -0,0 +1,1 @@
+Add different shortcut methods for ``aiogram.utils.formatting.Text.as_kwargs()``
diff --git a/CHANGES/1661.misc.rst b/CHANGES/1661.misc.rst
new file mode 100644
index 00000000..b20cbc82
--- /dev/null
+++ b/CHANGES/1661.misc.rst
@@ -0,0 +1,1 @@
+Replaced ```loop.run_in_executor``` with ```asyncio.to_thread``` for improved readability and consistency.
diff --git a/aiogram/dispatcher/event/handler.py b/aiogram/dispatcher/event/handler.py
index 8c283dd2..b95cc962 100644
--- a/aiogram/dispatcher/event/handler.py
+++ b/aiogram/dispatcher/event/handler.py
@@ -41,11 +41,7 @@ class CallableObject:
         wrapped = partial(self.callback, *args, **self._prepare_kwargs(kwargs))
         if self.awaitable:
             return await wrapped()
-
-        loop = asyncio.get_event_loop()
-        context = contextvars.copy_context()
-        wrapped = partial(context.run, wrapped)
-        return await loop.run_in_executor(None, wrapped)
+        return await asyncio.to_thread(wrapped)
 
 
 @dataclass
diff --git a/aiogram/utils/formatting.py b/aiogram/utils/formatting.py
index 929afc07..88fa82b0 100644
--- a/aiogram/utils/formatting.py
+++ b/aiogram/utils/formatting.py
@@ -110,7 +110,7 @@ class Text(Iterable[NodeType]):
         parse_mode_key: str = "parse_mode",
     ) -> Dict[str, Any]:
         """
-        Render elements tree as keyword arguments for usage in the API call, for example:
+        Render element tree as keyword arguments for usage in an API call, for example:
 
         .. code-block:: python
 
@@ -132,6 +132,90 @@ class Text(Iterable[NodeType]):
             result[parse_mode_key] = None
         return result
 
+    def as_caption_kwargs(self, *, replace_parse_mode: bool = True) -> Dict[str, Any]:
+        """
+        Shortcut for :meth:`as_kwargs` for usage with API calls that take
+        ``caption`` as a parameter.
+
+        .. code-block:: python
+
+            entities = Text(...)
+            await message.answer_photo(**entities.as_caption_kwargs(), photo=phot)
+
+        :param replace_parse_mode: Will be passed to :meth:`as_kwargs`.
+        :return:
+        """
+        return self.as_kwargs(
+            text_key="caption",
+            entities_key="caption_entities",
+            replace_parse_mode=replace_parse_mode,
+        )
+
+    def as_poll_question_kwargs(self, *, replace_parse_mode: bool = True) -> Dict[str, Any]:
+        """
+        Shortcut for :meth:`as_kwargs` for usage with
+        method :class:`aiogram.methods.send_poll.SendPoll`.
+
+        .. code-block:: python
+
+            entities = Text(...)
+            await message.answer_poll(**entities.as_poll_question_kwargs(), options=options)
+
+        :param replace_parse_mode: Will be passed to :meth:`as_kwargs`.
+        :return:
+        """
+        return self.as_kwargs(
+            text_key="question",
+            entities_key="question_entities",
+            parse_mode_key="question_parse_mode",
+            replace_parse_mode=replace_parse_mode,
+        )
+
+    def as_poll_explanation_kwargs(self, *, replace_parse_mode: bool = True) -> Dict[str, Any]:
+        """
+        Shortcut for :meth:`as_kwargs` for usage with
+        method :class:`aiogram.methods.send_poll.SendPoll`.
+
+        .. code-block:: python
+
+            question_entities = Text(...)
+            explanation_entities = Text(...)
+            await message.answer_poll(
+                **question_entities.as_poll_question_kwargs(),
+                options=options,
+                **explanation_entities.as_poll_explanation_kwargs(),
+            )
+
+        :param replace_parse_mode: Will be passed to :meth:`as_kwargs`.
+        :return:
+        """
+        return self.as_kwargs(
+            text_key="explanation",
+            entities_key="explanation_entities",
+            parse_mode_key="explanation_parse_mode",
+            replace_parse_mode=replace_parse_mode,
+        )
+
+    def as_gift_text_kwargs(self, *, replace_parse_mode: bool = True) -> Dict[str, Any]:
+        """
+        Shortcut for :meth:`as_kwargs` for usage with
+        method :class:`aiogram.methods.send_gift.SendGift`.
+
+        .. code-block:: python
+
+            entities = Text(...)
+            await bot.send_gift(gift_id=gift_id, user_id=user_id, **entities.as_gift_text_kwargs())
+
+        :param replace_parse_mode: Will be passed to :meth:`as_kwargs`.
+        :return:
+        """
+        return self.as_kwargs(
+            text_key="text",
+            entities_key="text_entities",
+            parse_mode_key="text_parse_mode",
+            replace_parse_mode=replace_parse_mode,
+        )
+
     def as_html(self) -> str:
         """
         Render elements tree as HTML markup


Test patch:
diff --git a/tests/test_utils/test_formatting.py b/tests/test_utils/test_formatting.py
index dbe07d17..ffaef31e 100644
--- a/tests/test_utils/test_formatting.py
+++ b/tests/test_utils/test_formatting.py
@@ -172,6 +172,34 @@ class TestNode:
         assert "parse_mode" not in result
         assert "custom_parse_mode" in result
 
+    def test_as_caption_kwargs(self):
+        node = Text("Hello, ", Bold("World"), "!")
+        result = node.as_caption_kwargs()
+        assert "caption" in result
+        assert "caption_entities" in result
+        assert "parse_mode" in result
+
+    def test_as_poll_question_kwargs(self):
+        node = Text("Hello, ", Bold("World"), "!")
+        result = node.as_poll_question_kwargs()
+        assert "question" in result
+        assert "question_entities" in result
+        assert "question_parse_mode" in result
+
+    def test_as_poll_explanation_kwargs(self):
+        node = Text("Hello, ", Bold("World"), "!")
+        result = node.as_poll_explanation_kwargs()
+        assert "explanation" in result
+        assert "explanation_entities" in result
+        assert "explanation_parse_mode" in result
+
+    def test_as_as_gift_text_kwargs_kwargs(self):
+        node = Text("Hello, ", Bold("World"), "!")
+        result = node.as_gift_text_kwargs()
+        assert "text" in result
+        assert "text_entities" in result
+        assert "text_parse_mode" in result
+
     def test_as_html(self):
         node = Text("Hello, ", Bold("World"), "!")
         assert node.as_html() == "Hello, <b>World</b>!"