diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 0d95be48..7c5292ef 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -29,11 +29,11 @@ jobs:
- macos-latest
- windows-latest
python-version:
- - '3.8'
- '3.9'
- '3.10'
- '3.11'
- '3.12'
+ - '3.13'
defaults:
# Windows sucks. Force use bash instead of PowerShell
@@ -111,7 +111,6 @@ jobs:
- macos-latest
# - windows-latest
python-version:
- - 'pypy3.8'
- 'pypy3.9'
- 'pypy3.10'
diff --git a/CHANGES/1589.misc.rst b/CHANGES/1589.misc.rst
new file mode 100644
index 00000000..80c531c8
--- /dev/null
+++ b/CHANGES/1589.misc.rst
@@ -0,0 +1,16 @@
+Checked compatibility with Python 3.13 (added to the CI/CD processes),
+so now aiogram is totally compatible with it.
+
+Dropped compatibility with Python 3.8 due to this version being `EOL `_.
+
+.. warning::
+
+ In some cases you will need to have the installed compiler (Rust or C++)
+ to install some of the dependencies to compile packages from source on `pip install` command.
+
+ - If you are using Windows, you will need to have the `Visual Studio `_ installed.
+ - If you are using Linux, you will need to have the `build-essential` package installed.
+ - If you are using macOS, you will need to have the `Xcode `_ installed.
+
+ When developers of this dependencies will release new versions with precompiled wheels for Windows, Linux and macOS,
+ this action will not be necessary anymore until the next version of the Python interpreter.
diff --git a/pyproject.toml b/pyproject.toml
index 03b3be31..d1d59a40 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -6,7 +6,7 @@ build-backend = "hatchling.build"
name = "aiogram"
description = 'Modern and fully asynchronous framework for Telegram Bot API'
readme = "README.rst"
-requires-python = ">=3.8"
+requires-python = ">=3.9"
license = "MIT"
authors = [
{ name = "Alex Root Junior", email = "jroot.junior@gmail.com" },
@@ -30,11 +30,11 @@ classifiers = [
"Typing :: Typed",
"Intended Audience :: Developers",
"Intended Audience :: System Administrators",
- "Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
+ "Programming Language :: Python :: 3.13",
"Programming Language :: Python :: Implementation :: PyPy",
"Topic :: Software Development :: Libraries :: Application Frameworks",
"Topic :: Software Development :: Libraries :: Python Modules",
@@ -43,8 +43,7 @@ classifiers = [
dependencies = [
"magic-filter>=1.0.12,<1.1",
"aiohttp>=3.9.0,<3.11",
- "pydantic>=2.4.1,<2.9; python_version < '3.9'", # v2.9 breaks compatibility with Python 3.8 without any reason
- "pydantic>=2.4.1,<2.10; python_version >= '3.9'",
+ "pydantic>=2.4.1,<2.10",
"aiofiles>=23.2.1,<24.2",
"certifi>=2023.7.22",
"typing-extensions>=4.7.0,<=5.0",
@@ -56,7 +55,8 @@ path = "aiogram/__meta__.py"
[project.optional-dependencies]
fast = [
- "uvloop>=0.17.0; (sys_platform == 'darwin' or sys_platform == 'linux') and platform_python_implementation != 'PyPy'",
+ "uvloop>=0.17.0; (sys_platform == 'darwin' or sys_platform == 'linux') and platform_python_implementation != 'PyPy' and python_version < '3.13'",
+ "uvloop>=0.21.0; (sys_platform == 'darwin' or sys_platform == 'linux') and platform_python_implementation != 'PyPy' and python_version >= '3.13'",
"aiodns>=3.0.0",
]
redis = [
@@ -198,7 +198,7 @@ view-cov = "google-chrome-stable reports/py{matrix:python}/coverage/index.html"
[[tool.hatch.envs.test.matrix]]
-python = ["38", "39", "310", "311", "312"]
+python = ["39", "310", "311", "312", "313"]
[tool.ruff]
line-length = 99
@@ -215,7 +215,7 @@ exclude = [
"scripts",
"*.egg-info",
]
-target-version = "py310"
+target-version = "py39"
[tool.ruff.lint]
select = [
@@ -275,7 +275,7 @@ exclude_lines = [
[tool.mypy]
plugins = "pydantic.mypy"
-python_version = "3.8"
+python_version = "3.9"
show_error_codes = true
show_error_context = true
pretty = true
@@ -309,7 +309,7 @@ disallow_untyped_defs = true
[tool.black]
line-length = 99
-target-version = ['py38', 'py39', 'py310', 'py311']
+target-version = ['py39', 'py310', 'py311', 'py312', 'py313']
exclude = '''
(
\.eggs
diff --git a/tests/test_api/test_client/test_session/test_aiohttp_session.py b/tests/test_api/test_client/test_session/test_aiohttp_session.py
index 4d0e7153..e23abc13 100644
--- a/tests/test_api/test_client/test_session/test_aiohttp_session.py
+++ b/tests/test_api/test_client/test_session/test_aiohttp_session.py
@@ -248,13 +248,16 @@ class TestAiohttpSession:
async with AiohttpSession() as session:
assert isinstance(session, AsyncContextManager)
- with patch(
- "aiogram.client.session.aiohttp.AiohttpSession.create_session",
- new_callable=AsyncMock,
- ) as mocked_create_session, patch(
- "aiogram.client.session.aiohttp.AiohttpSession.close",
- new_callable=AsyncMock,
- ) as mocked_close:
+ with (
+ patch(
+ "aiogram.client.session.aiohttp.AiohttpSession.create_session",
+ new_callable=AsyncMock,
+ ) as mocked_create_session,
+ patch(
+ "aiogram.client.session.aiohttp.AiohttpSession.close",
+ new_callable=AsyncMock,
+ ) as mocked_close,
+ ):
async with session as ctx:
assert session == ctx
mocked_close.assert_awaited_once()
diff --git a/tests/test_dispatcher/test_dispatcher.py b/tests/test_dispatcher/test_dispatcher.py
index 2e324a86..81bb6c2f 100644
--- a/tests/test_dispatcher/test_dispatcher.py
+++ b/tests/test_dispatcher/test_dispatcher.py
@@ -778,11 +778,14 @@ class TestDispatcher:
async def _mock_updates(*_):
yield Update(update_id=42)
- with patch(
- "aiogram.dispatcher.dispatcher.Dispatcher._process_update", new_callable=AsyncMock
- ) as mocked_process_update, patch(
- "aiogram.dispatcher.dispatcher.Dispatcher._listen_updates"
- ) as patched_listen_updates:
+ with (
+ patch(
+ "aiogram.dispatcher.dispatcher.Dispatcher._process_update", new_callable=AsyncMock
+ ) as mocked_process_update,
+ patch(
+ "aiogram.dispatcher.dispatcher.Dispatcher._listen_updates"
+ ) as patched_listen_updates,
+ ):
patched_listen_updates.return_value = _mock_updates()
await dispatcher._polling(bot=bot, handle_as_tasks=as_task)
if as_task:
@@ -852,15 +855,20 @@ class TestDispatcher:
async def _mock_updates(*_):
yield Update(update_id=42)
- with patch(
- "aiogram.dispatcher.dispatcher.Dispatcher._process_update", new_callable=AsyncMock
- ) as mocked_process_update, patch(
- "aiogram.dispatcher.router.Router.emit_startup", new_callable=AsyncMock
- ) as mocked_emit_startup, patch(
- "aiogram.dispatcher.router.Router.emit_shutdown", new_callable=AsyncMock
- ) as mocked_emit_shutdown, patch(
- "aiogram.dispatcher.dispatcher.Dispatcher._listen_updates"
- ) as patched_listen_updates:
+ with (
+ patch(
+ "aiogram.dispatcher.dispatcher.Dispatcher._process_update", new_callable=AsyncMock
+ ) as mocked_process_update,
+ patch(
+ "aiogram.dispatcher.router.Router.emit_startup", new_callable=AsyncMock
+ ) as mocked_emit_startup,
+ patch(
+ "aiogram.dispatcher.router.Router.emit_shutdown", new_callable=AsyncMock
+ ) as mocked_emit_shutdown,
+ patch(
+ "aiogram.dispatcher.dispatcher.Dispatcher._listen_updates"
+ ) as patched_listen_updates,
+ ):
patched_listen_updates.return_value = _mock_updates()
await dispatcher.start_polling(bot)
@@ -916,11 +924,14 @@ class TestDispatcher:
yield Update(update_id=42)
await asyncio.sleep(1)
- with patch(
- "aiogram.dispatcher.dispatcher.Dispatcher._process_update", new_callable=AsyncMock
- ) as mocked_process_update, patch(
- "aiogram.dispatcher.dispatcher.Dispatcher._listen_updates",
- return_value=_mock_updates(),
+ with (
+ patch(
+ "aiogram.dispatcher.dispatcher.Dispatcher._process_update", new_callable=AsyncMock
+ ) as mocked_process_update,
+ patch(
+ "aiogram.dispatcher.dispatcher.Dispatcher._listen_updates",
+ return_value=_mock_updates(),
+ ),
):
task = asyncio.ensure_future(dispatcher.start_polling(bot))
await running.wait()
diff --git a/tests/test_utils/test_callback_answer.py b/tests/test_utils/test_callback_answer.py
index cf641f46..8c0d76c2 100644
--- a/tests/test_utils/test_callback_answer.py
+++ b/tests/test_utils/test_callback_answer.py
@@ -198,13 +198,16 @@ class TestCallbackAnswerMiddleware:
stack.append("answer")
middleware = CallbackAnswerMiddleware()
- with patch(
- "aiogram.utils.callback_answer.CallbackAnswerMiddleware.construct_callback_answer",
- new_callable=MagicMock,
- side_effect=lambda **kwargs: CallbackAnswer(**{"answered": False, **properties}),
- ), patch(
- "aiogram.utils.callback_answer.CallbackAnswerMiddleware.answer",
- new=answer,
+ with (
+ patch(
+ "aiogram.utils.callback_answer.CallbackAnswerMiddleware.construct_callback_answer",
+ new_callable=MagicMock,
+ side_effect=lambda **kwargs: CallbackAnswer(**{"answered": False, **properties}),
+ ),
+ patch(
+ "aiogram.utils.callback_answer.CallbackAnswerMiddleware.answer",
+ new=answer,
+ ),
):
await middleware(handler, event, {})
diff --git a/tests/test_utils/test_chat_action.py b/tests/test_utils/test_chat_action.py
index 14048e00..d893b074 100644
--- a/tests/test_utils/test_chat_action.py
+++ b/tests/test_utils/test_chat_action.py
@@ -96,13 +96,16 @@ class TestChatActionMiddleware:
handler1 = flags.chat_action(value)(handler)
middleware = ChatActionMiddleware()
- with patch(
- "aiogram.utils.chat_action.ChatActionSender._run",
- new_callable=AsyncMock,
- ) as mocked_run, patch(
- "aiogram.utils.chat_action.ChatActionSender._stop",
- new_callable=AsyncMock,
- ) as mocked_stop:
+ with (
+ patch(
+ "aiogram.utils.chat_action.ChatActionSender._run",
+ new_callable=AsyncMock,
+ ) as mocked_run,
+ patch(
+ "aiogram.utils.chat_action.ChatActionSender._stop",
+ new_callable=AsyncMock,
+ ) as mocked_stop,
+ ):
data = {"handler": HandlerObject(callback=handler1), "bot": bot}
message = Message(
chat=Chat(id=42, type="private", title="Test"),