Repository: aiogram/aiogram
Pull request #1162 (2023-04-08 15:44:15)

Fix compatibility with Python 3.8-3.9
# Description

Fixed compatibility with Python 3.8-3.9

## Type of change

Please delete options that are not relevant.

- [x] Bug fix (non-breaking change which fixes an issue)

# 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


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

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

For example, you can run `towncrier create <issue>.<type>` to create a file in the change directory and then write a description on that file.

Read more at [Towncrier docs](https://towncrier.readthedocs.io/en/latest/quickstart.html#creating-news-fragments)
github-actions[bot]: # :x: Changelog is required!

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

For example, you can run `towncrier create <issue>.<type>` to create a file in the change directory and then write a description on that file.

Read more at [Towncrier docs](https://towncrier.readthedocs.io/en/latest/quickstart.html#creating-news-fragments)
codecov[bot]: ## [Codecov](https://codecov.io/gh/aiogram/aiogram/pull/1162?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aiogram) Report
> Merging [#1162](https://codecov.io/gh/aiogram/aiogram/pull/1162?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aiogram) (42149c5) into [dev-3.x](https://codecov.io/gh/aiogram/aiogram/commit/2c756a1689a57d91fa1ebdbfa2f5c7bb3bf18a2c?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aiogram) (2c756a1) will **not change** coverage.
> The diff coverage is `100.00%`.

[![Impacted file tree graph](https://codecov.io/gh/aiogram/aiogram/pull/1162/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/1162?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     #1162   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files          354       354           
  Lines         8795      8803    +8     
=========================================
+ Hits          8795      8803    +8     
```

| Flag | Coverage Δ | |
|---|---|---|
| unittests | `100.00% <100.00%> (ø)` | |

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/1162?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aiogram) | Coverage Δ | |
|---|---|---|
| [aiogram/dispatcher/dispatcher.py](https://codecov.io/gh/aiogram/aiogram/pull/1162?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aiogram#diff-YWlvZ3JhbS9kaXNwYXRjaGVyL2Rpc3BhdGNoZXIucHk=) | `100.00% <100.00%> (ø)` | |


Patch:
diff --git a/CHANGES/1160.bugfix b/CHANGES/1160.bugfix
new file mode 100644
index 00000000..68e82835
--- /dev/null
+++ b/CHANGES/1160.bugfix
@@ -0,0 +1,1 @@
+Added missing FORUM_TOPIC_EDITED value to content_type property
diff --git a/CHANGES/1162.bugfix.rst b/CHANGES/1162.bugfix.rst
new file mode 100644
index 00000000..16d5be16
--- /dev/null
+++ b/CHANGES/1162.bugfix.rst
@@ -0,0 +1,1 @@
+Fixed compatibility with Python 3.8-3.9
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index ada02324..93f96099 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -1,1 +1,1 @@
-Please read the [Contributing](https://docs.aiogram.dev/en/dev-3.x/contributing/) guidelines in the documentation site.
+Please read the [Contributing](https://docs.aiogram.dev/en/dev-3.x/contributing.html) guidelines in the documentation site.
diff --git a/aiogram/dispatcher/dispatcher.py b/aiogram/dispatcher/dispatcher.py
index 8485d83c..9c61a447 100644
--- a/aiogram/dispatcher/dispatcher.py
+++ b/aiogram/dispatcher/dispatcher.py
@@ -92,8 +92,8 @@ class Dispatcher(Router):
 
         self.workflow_data: Dict[str, Any] = kwargs
         self._running_lock = Lock()
-        self._stop_signal = Event()
-        self._stopped_signal = Event()
+        self._stop_signal: Optional[Event] = None
+        self._stopped_signal: Optional[Event] = None
 
     def __getitem__(self, item: str) -> Any:
         return self.workflow_data[item]
@@ -430,6 +430,8 @@ class Dispatcher(Router):
         """
         if not self._running_lock.locked():
             raise RuntimeError("Polling is not started")
+        if not self._stop_signal or not self._stopped_signal:
+            return
         self._stop_signal.set()
         await self._stopped_signal.wait()
 
@@ -438,6 +440,8 @@ class Dispatcher(Router):
             return
 
         loggers.dispatcher.warning("Received %s signal", sig.name)
+        if not self._stop_signal:
+            return
         self._stop_signal.set()
 
     async def start_polling(
@@ -473,6 +477,11 @@ class Dispatcher(Router):
             )
 
         async with self._running_lock:  # Prevent to run this method twice at a once
+            if self._stop_signal is None:
+                self._stop_signal = Event()
+            if self._stopped_signal is None:
+                self._stopped_signal = Event()
+
             self._stop_signal.clear()
             self._stopped_signal.clear()
 
diff --git a/aiogram/types/message.py b/aiogram/types/message.py
index c74ccb97..a057ef97 100644
--- a/aiogram/types/message.py
+++ b/aiogram/types/message.py
@@ -317,6 +317,8 @@ class Message(TelegramObject):
             return ContentType.MESSAGE_AUTO_DELETE_TIMER_CHANGED
         if self.forum_topic_created:
             return ContentType.FORUM_TOPIC_CREATED
+        if self.forum_topic_edited:
+            return ContentType.FORUM_TOPIC_EDITED
         if self.forum_topic_closed:
             return ContentType.FORUM_TOPIC_CLOSED
         if self.forum_topic_reopened:
diff --git a/examples/echo_bot.py b/examples/echo_bot.py
index d51a0eeb..e18201af 100644
--- a/examples/echo_bot.py
+++ b/examples/echo_bot.py
@@ -5,7 +5,7 @@ from aiogram import Bot, Dispatcher, Router, types
 from aiogram.filters import Command
 from aiogram.types import Message
 
-# Bot token can be obtained via https://t.me/BotFahter
+# Bot token can be obtained via https://t.me/BotFather
 TOKEN = "42:TOKEN"
 
 # All handlers should be attached to the Router (or Dispatcher)
diff --git a/pyproject.toml b/pyproject.toml
index 5a203c92..cf0f81ac 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -148,7 +148,14 @@ features = [
     "test",
 ]
 extra-dependencies = [
-    "butcher @ git+https://github.com/aiogram/butcher.git@v0.1.13"
+    "butcher @ git+https://github.com/aiogram/butcher.git@v0.1.14"
+]
+
+[tool.hatch.envs.dev.scripts]
+update = [
+    "butcher parse",
+    "butcher refresh",
+    "butcher apply all",
 ]
 
 [tool.hatch.envs.test]


Test patch:
diff --git a/tests/test_api/test_types/test_message.py b/tests/test_api/test_types/test_message.py
index 7eec99c7..563141f1 100644
--- a/tests/test_api/test_types/test_message.py
+++ b/tests/test_api/test_types/test_message.py
@@ -43,6 +43,7 @@ from aiogram.types import (
     Document,
     EncryptedCredentials,
     ForumTopicClosed,
+    ForumTopicEdited,
     ForumTopicCreated,
     ForumTopicReopened,
     Game,
@@ -414,6 +415,16 @@ TEST_FORUM_TOPIC_CREATED = Message(
         icon_color=0xFFD67E,
     ),
 )
+TEST_FORUM_TOPIC_EDITED = Message(
+    message_id=42,
+    date=datetime.datetime.now(),
+    chat=Chat(id=42, type="private"),
+    from_user=User(id=42, is_bot=False, first_name="Test"),
+    forum_topic_edited=ForumTopicEdited(
+        name="test_edited",
+        icon_color=0xFFD67E,
+    ),
+)
 TEST_FORUM_TOPIC_CLOSED = Message(
     message_id=42,
     date=datetime.datetime.now(),
@@ -484,6 +495,7 @@ class TestMessage:
             [TEST_MESSAGE_DICE, ContentType.DICE],
             [TEST_MESSAGE_WEB_APP_DATA, ContentType.WEB_APP_DATA],
             [TEST_FORUM_TOPIC_CREATED, ContentType.FORUM_TOPIC_CREATED],
+            [TEST_FORUM_TOPIC_EDITED, ContentType.FORUM_TOPIC_EDITED],
             [TEST_FORUM_TOPIC_CLOSED, ContentType.FORUM_TOPIC_CLOSED],
             [TEST_FORUM_TOPIC_REOPENED, ContentType.FORUM_TOPIC_REOPENED],
             [TEST_MESSAGE_UNKNOWN, ContentType.UNKNOWN],
diff --git a/tests/test_dispatcher/test_dispatcher.py b/tests/test_dispatcher/test_dispatcher.py
index 4fb2b113..bcebfaa2 100644
--- a/tests/test_dispatcher/test_dispatcher.py
+++ b/tests/test_dispatcher/test_dispatcher.py
@@ -708,10 +708,15 @@ class TestDispatcher:
         with pytest.raises(RuntimeError):
             await dispatcher.stop_polling()
 
-        assert not dispatcher._stop_signal.is_set()
-        assert not dispatcher._stopped_signal.is_set()
+        assert not dispatcher._stop_signal
+        assert not dispatcher._stopped_signal
         with patch("asyncio.locks.Event.wait", new_callable=AsyncMock) as mocked_wait:
             async with dispatcher._running_lock:
+                await dispatcher.stop_polling()
+                assert not dispatcher._stop_signal
+
+                dispatcher._stop_signal = Event()
+                dispatcher._stopped_signal = Event()
                 await dispatcher.stop_polling()
                 assert dispatcher._stop_signal.is_set()
                 mocked_wait.assert_awaited()
@@ -723,6 +728,11 @@ class TestDispatcher:
             mocked_set.assert_not_called()
 
             async with dispatcher._running_lock:
+                dispatcher._signal_stop_polling(signal.SIGINT)
+                mocked_set.assert_not_called()
+
+                dispatcher._stop_signal = Event()
+                dispatcher._stopped_signal = Event()
                 dispatcher._signal_stop_polling(signal.SIGINT)
                 mocked_set.assert_called()
 
@@ -764,12 +774,29 @@ class TestDispatcher:
 
     def test_run_polling(self, bot: MockedBot):
         dispatcher = Dispatcher()
+
+        async def stop():
+            await asyncio.sleep(0.5)
+            await dispatcher.stop_polling()
+
+        start_called = False
+
+        @dispatcher.startup()
+        async def startup():
+            nonlocal start_called
+            start_called = True
+            asyncio.create_task(stop())
+
+        original_start_polling = dispatcher.start_polling
         with patch(
-            "aiogram.dispatcher.dispatcher.Dispatcher.start_polling"
+            "aiogram.dispatcher.dispatcher.Dispatcher.start_polling",
+            side_effect=original_start_polling,
         ) as patched_start_polling:
             dispatcher.run_polling(bot)
             patched_start_polling.assert_awaited_once()
 
+        assert start_called
+
     async def test_feed_webhook_update_fast_process(self, bot: MockedBot):
         dispatcher = Dispatcher()
         dispatcher.message.register(simple_message_handler)