Support of Py3.14 (#1730)
Some checks failed
Tests / tests (macos-latest, 3.10) (push) Has been cancelled
Tests / tests (macos-latest, 3.11) (push) Has been cancelled
Tests / tests (macos-latest, 3.12) (push) Has been cancelled
Tests / tests (macos-latest, 3.13) (push) Has been cancelled
Tests / tests (macos-latest, 3.14) (push) Has been cancelled
Tests / tests (ubuntu-latest, 3.10) (push) Has been cancelled
Tests / tests (ubuntu-latest, 3.11) (push) Has been cancelled
Tests / tests (ubuntu-latest, 3.12) (push) Has been cancelled
Tests / tests (ubuntu-latest, 3.13) (push) Has been cancelled
Tests / tests (ubuntu-latest, 3.14) (push) Has been cancelled
Tests / tests (windows-latest, 3.10) (push) Has been cancelled
Tests / tests (windows-latest, 3.11) (push) Has been cancelled
Tests / tests (windows-latest, 3.12) (push) Has been cancelled
Tests / tests (windows-latest, 3.13) (push) Has been cancelled
Tests / tests (windows-latest, 3.14) (push) Has been cancelled
Tests / pypy-tests (macos-latest, pypy3.10) (push) Has been cancelled
Tests / pypy-tests (macos-latest, pypy3.11) (push) Has been cancelled
Tests / pypy-tests (ubuntu-latest, pypy3.10) (push) Has been cancelled
Tests / pypy-tests (ubuntu-latest, pypy3.11) (push) Has been cancelled

* Py3.14 support
Bump .pre-commit-config.yaml
Bump `mongo` feature deps
Bump `proxy` feature dep
Bump `test` feature deps
Bump `dev` feature deps

Set `aiohttp` max version `<3.14`

Fix `test_isolation.py` tests
Fix `test_storages.py` tests

Add Py version limit `<3.15` (breaking changes possible)
Add new `uvloop` starter to `Dispatcher.run_polling`

Remove old `uvloop` `set_event_loop_policy`
Remove `pytest-lazy-fixture`

* Make `test` and `dev` features deps strong fixed

* Add 1730.feature.rst

* Remove unneeded `None` from `Dispatcher.run_polling`

* Fix `macos-latest-pypy3.11` test `test_aiohtt_server.py`

* Update `tests.yml`

* Update `tests.yml`
This commit is contained in:
Andrew 2025-10-11 00:16:50 +03:00 committed by GitHub
parent 0c6a705310
commit 4caf56814e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 77 additions and 64 deletions

View file

@ -33,6 +33,7 @@ jobs:
- "3.11" - "3.11"
- "3.12" - "3.12"
- "3.13" - "3.13"
- "3.14"
defaults: defaults:
# Windows sucks. Force use bash instead of PowerShell # Windows sucks. Force use bash instead of PowerShell
@ -74,13 +75,13 @@ jobs:
if: ${{ env.IS_WINDOWS == 'false' }} if: ${{ env.IS_WINDOWS == 'false' }}
uses: shogo82148/actions-setup-redis@v1 uses: shogo82148/actions-setup-redis@v1
with: with:
redis-version: 6 redis-version: "8"
- name: Setup mongodb - name: Setup mongodb
if: ${{ env.IS_UBUNTU == 'true' }} if: ${{ env.IS_UBUNTU == 'true' }}
uses: supercharge/mongodb-github-action@1.10.0 uses: supercharge/mongodb-github-action@1.12.0
with: with:
mongodb-version: "7.0" mongodb-version: "8"
mongodb-username: mongo mongodb-username: mongo
mongodb-password: mongo mongodb-password: mongo
mongodb-port: 27017 mongodb-port: 27017

View file

@ -1,6 +1,6 @@
repos: repos:
- repo: https://github.com/pre-commit/pre-commit-hooks - repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.6.0 rev: v6.0.0
hooks: hooks:
- id: "trailing-whitespace" - id: "trailing-whitespace"
- id: "check-case-conflict" - id: "check-case-conflict"
@ -20,6 +20,6 @@ repos:
files: &files '^(aiogram|tests|examples)' files: &files '^(aiogram|tests|examples)'
- repo: https://github.com/astral-sh/ruff-pre-commit - repo: https://github.com/astral-sh/ruff-pre-commit
rev: 'v0.13.3' rev: 'v0.14.0'
hooks: hooks:
- id: ruff - id: ruff

6
CHANGES/1730.feature.rst Normal file
View file

@ -0,0 +1,6 @@
This PR updates the codebase to support Python 3.14.
- Updated project dep `aiohttp`
- Updated development deps
- Fixed tests to support Py3.14
- Refactored `uvloop` using due to deprecation of `asyncio.set_event_loop_police`

View file

@ -1,6 +1,3 @@
import asyncio as _asyncio
from contextlib import suppress
from aiogram.dispatcher.flags import FlagGenerator from aiogram.dispatcher.flags import FlagGenerator
from . import enums, methods, types from . import enums, methods, types
@ -14,12 +11,6 @@ from .utils.magic_filter import MagicFilter
from .utils.text_decorations import html_decoration as html from .utils.text_decorations import html_decoration as html
from .utils.text_decorations import markdown_decoration as md from .utils.text_decorations import markdown_decoration as md
with suppress(ImportError):
import uvloop as _uvloop
_asyncio.set_event_loop_policy(_uvloop.EventLoopPolicy())
F = MagicFilter() F = MagicFilter()
flags = FlagGenerator() flags = FlagGenerator()

View file

@ -3,6 +3,7 @@ from __future__ import annotations
import asyncio import asyncio
import contextvars import contextvars
import signal import signal
import sys
import warnings import warnings
from asyncio import CancelledError, Event, Future, Lock from asyncio import CancelledError, Event, Future, Lock
from collections.abc import AsyncGenerator, Awaitable from collections.abc import AsyncGenerator, Awaitable
@ -656,16 +657,28 @@ class Dispatcher(Router):
:return: :return:
""" """
with suppress(KeyboardInterrupt): with suppress(KeyboardInterrupt):
return asyncio.run( coro = self.start_polling(
self.start_polling( *bots,
*bots, **kwargs,
**kwargs, polling_timeout=polling_timeout,
polling_timeout=polling_timeout, handle_as_tasks=handle_as_tasks,
handle_as_tasks=handle_as_tasks, backoff_config=backoff_config,
backoff_config=backoff_config, allowed_updates=allowed_updates,
allowed_updates=allowed_updates, handle_signals=handle_signals,
handle_signals=handle_signals, close_bot_session=close_bot_session,
close_bot_session=close_bot_session, tasks_concurrency_limit=tasks_concurrency_limit,
tasks_concurrency_limit=tasks_concurrency_limit,
),
) )
try:
import uvloop
except ImportError:
return asyncio.run(coro)
else:
if sys.version_info >= (3, 11):
with asyncio.Runner(loop_factory=uvloop.new_event_loop) as runner:
return runner.run(coro)
else:
uvloop.install()
return asyncio.run(coro)

View file

@ -6,7 +6,7 @@ build-backend = "hatchling.build"
name = "aiogram" name = "aiogram"
description = 'Modern and fully asynchronous framework for Telegram Bot API' description = 'Modern and fully asynchronous framework for Telegram Bot API'
readme = "README.rst" readme = "README.rst"
requires-python = ">=3.10" requires-python = ">=3.10,<3.15"
license = "MIT" license = "MIT"
authors = [ authors = [
{ name = "Alex Root Junior", email = "jroot.junior@gmail.com" }, { name = "Alex Root Junior", email = "jroot.junior@gmail.com" },
@ -34,6 +34,7 @@ classifiers = [
"Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
"Programming Language :: Python :: Implementation :: PyPy", "Programming Language :: Python :: Implementation :: PyPy",
"Topic :: Software Development :: Libraries :: Application Frameworks", "Topic :: Software Development :: Libraries :: Application Frameworks",
"Topic :: Software Development :: Libraries :: Python Modules", "Topic :: Software Development :: Libraries :: Python Modules",
@ -41,7 +42,7 @@ classifiers = [
] ]
dependencies = [ dependencies = [
"magic-filter>=1.0.12,<1.1", "magic-filter>=1.0.12,<1.1",
"aiohttp>=3.9.0,<3.13", "aiohttp>=3.9.0,<3.14",
"pydantic>=2.4.1,<2.13", "pydantic>=2.4.1,<2.13",
"aiofiles>=23.2.1,<24.2", "aiofiles>=23.2.1,<24.2",
"certifi>=2023.7.22", "certifi>=2023.7.22",
@ -62,11 +63,11 @@ redis = [
"redis[hiredis]>=6.2.0,<7", "redis[hiredis]>=6.2.0,<7",
] ]
mongo = [ mongo = [
"motor>=3.3.2,<3.7.0", "motor>=3.3.2,<3.8",
"pymongo>4.5,<4.11", "pymongo>4.5,<4.16",
] ]
proxy = [ proxy = [
"aiohttp-socks~=0.8.3", "aiohttp-socks~=0.10.1",
] ]
i18n = [ i18n = [
"Babel>=2.13.0,<3", "Babel>=2.13.0,<3",
@ -78,17 +79,15 @@ signature = [
"cryptography>=46.0.0", "cryptography>=46.0.0",
] ]
test = [ test = [
"pytest~=7.4.2", "pytest==8.4.2",
"pytest-html~=4.0.2", "pytest-html==4.1.1",
"pytest-asyncio~=0.21.1", "pytest-mock==3.15.1",
"pytest-lazy-fixture~=0.6.3", "pytest-mypy==1.0.1",
"pytest-mock~=3.12.0", "pytest-cov==7.0.0",
"pytest-mypy~=0.10.3", "pytest-aiohttp==1.1.0",
"pytest-cov~=4.1.0", "aresponses==3.0.0",
"pytest-aiohttp~=1.0.5", "pytz==2025.2",
"aresponses~=2.1.6", "pycryptodomex==3.23.0",
"pytz~=2025.2",
"pycryptodomex~=3.23.0",
] ]
docs = [ docs = [
"Sphinx~=8.0.2", "Sphinx~=8.0.2",
@ -104,14 +103,14 @@ docs = [
"sphinxcontrib-towncrier~=0.4.0a0", "sphinxcontrib-towncrier~=0.4.0a0",
] ]
dev = [ dev = [
"black~=25.9.0", "black==25.9.0",
"isort~=6.1.0", "isort==6.1.0",
"ruff~=0.13.3", "ruff==0.14.0",
"mypy~=1.10.1", "mypy==1.10.1",
"toml~=0.10.2", "toml==0.10.2",
"pre-commit~=4.3.0", "pre-commit==4.3.0",
"packaging~=24.1", "packaging==25.0",
"motor-types~=1.0.0b4", "motor-types==1.0.0b4",
] ]
[project.urls] [project.urls]

View file

@ -39,7 +39,7 @@ def pytest_configure(config):
config.addinivalue_line("markers", "redis: marked tests require redis connection to run") config.addinivalue_line("markers", "redis: marked tests require redis connection to run")
config.addinivalue_line("markers", "mongo: marked tests require mongo connection to run") config.addinivalue_line("markers", "mongo: marked tests require mongo connection to run")
if sys.platform == "win32": if sys.platform == "win32" and sys.version_info < (3, 14):
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
else: else:
asyncio.set_event_loop_policy(asyncio.DefaultEventLoopPolicy()) asyncio.set_event_loop_policy(asyncio.DefaultEventLoopPolicy())
@ -186,6 +186,16 @@ async def dispatcher():
await dp.emit_shutdown() await dp.emit_shutdown()
@pytest.fixture()
def storage(request):
return request.getfixturevalue(request.param)
@pytest.fixture()
def isolation(request):
return request.getfixturevalue(request.param)
# @pytest.fixture(scope="session") # @pytest.fixture(scope="session")
# def event_loop_policy(request): # def event_loop_policy(request):
# if sys.platform == "win32": # if sys.platform == "win32":

View file

@ -8,11 +8,8 @@ from aiogram.fsm.storage.redis import RedisEventIsolation, RedisStorage
@pytest.mark.parametrize( @pytest.mark.parametrize(
"isolation", "isolation",
[ ["redis_isolation", "lock_isolation", "disabled_isolation"],
pytest.lazy_fixture("redis_isolation"), indirect=True,
pytest.lazy_fixture("lock_isolation"),
pytest.lazy_fixture("disabled_isolation"),
],
) )
class TestIsolations: class TestIsolations:
async def test_lock( async def test_lock(

View file

@ -8,12 +8,8 @@ from aiogram.fsm.storage.base import BaseStorage, StorageKey
@pytest.mark.parametrize( @pytest.mark.parametrize(
"storage", "storage",
[ ["memory_storage", "redis_storage", "mongo_storage", "pymongo_storage"],
pytest.lazy_fixture("redis_storage"), indirect=True,
pytest.lazy_fixture("mongo_storage"),
pytest.lazy_fixture("pymongo_storage"),
pytest.lazy_fixture("memory_storage"),
],
) )
class TestStorages: class TestStorages:
async def test_set_state(self, storage: BaseStorage, storage_key: StorageKey): async def test_set_state(self, storage: BaseStorage, storage_key: StorageKey):

View file

@ -185,8 +185,8 @@ class TestSimpleRequestHandler:
handler_event.clear() handler_event.clear()
resp = await self.make_reqest(client=client) resp = await self.make_reqest(client=client)
assert resp.status == 200 assert resp.status == 200
await asyncio.wait_for(handler_event.wait(), timeout=1) await asyncio.wait_for(handler_event.wait(), timeout=3)
await asyncio.wait_for(method_called_event.wait(), timeout=1) await asyncio.wait_for(method_called_event.wait(), timeout=3)
# Python 3.12 had some changes to asyncio which make it quite a bit faster. But # Python 3.12 had some changes to asyncio which make it quite a bit faster. But
# probably because of that the assert_awaited call is consistently scheduled before the # probably because of that the assert_awaited call is consistently scheduled before the
# silent_call_request call - failing the test. So we wait for the method to be called # silent_call_request call - failing the test. So we wait for the method to be called