diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 305b4701..a4d0b90b 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -33,6 +33,7 @@ jobs: - "3.11" - "3.12" - "3.13" + - "3.14" defaults: # Windows sucks. Force use bash instead of PowerShell @@ -74,13 +75,13 @@ jobs: if: ${{ env.IS_WINDOWS == 'false' }} uses: shogo82148/actions-setup-redis@v1 with: - redis-version: 6 + redis-version: "8" - name: Setup mongodb if: ${{ env.IS_UBUNTU == 'true' }} - uses: supercharge/mongodb-github-action@1.10.0 + uses: supercharge/mongodb-github-action@1.12.0 with: - mongodb-version: "7.0" + mongodb-version: "8" mongodb-username: mongo mongodb-password: mongo mongodb-port: 27017 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6b91d3d1..97742762 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.6.0 + rev: v6.0.0 hooks: - id: "trailing-whitespace" - id: "check-case-conflict" @@ -20,6 +20,6 @@ repos: files: &files '^(aiogram|tests|examples)' - repo: https://github.com/astral-sh/ruff-pre-commit - rev: 'v0.13.3' + rev: 'v0.14.0' hooks: - id: ruff diff --git a/CHANGES/1730.feature.rst b/CHANGES/1730.feature.rst new file mode 100644 index 00000000..5c9b6d32 --- /dev/null +++ b/CHANGES/1730.feature.rst @@ -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` diff --git a/aiogram/__init__.py b/aiogram/__init__.py index b243ea2f..e38c03b1 100644 --- a/aiogram/__init__.py +++ b/aiogram/__init__.py @@ -1,6 +1,3 @@ -import asyncio as _asyncio -from contextlib import suppress - from aiogram.dispatcher.flags import FlagGenerator 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 markdown_decoration as md -with suppress(ImportError): - import uvloop as _uvloop - - _asyncio.set_event_loop_policy(_uvloop.EventLoopPolicy()) - - F = MagicFilter() flags = FlagGenerator() diff --git a/aiogram/dispatcher/dispatcher.py b/aiogram/dispatcher/dispatcher.py index a40355ab..807c0e21 100644 --- a/aiogram/dispatcher/dispatcher.py +++ b/aiogram/dispatcher/dispatcher.py @@ -3,6 +3,7 @@ from __future__ import annotations import asyncio import contextvars import signal +import sys import warnings from asyncio import CancelledError, Event, Future, Lock from collections.abc import AsyncGenerator, Awaitable @@ -656,16 +657,28 @@ class Dispatcher(Router): :return: """ with suppress(KeyboardInterrupt): - return asyncio.run( - self.start_polling( - *bots, - **kwargs, - polling_timeout=polling_timeout, - handle_as_tasks=handle_as_tasks, - backoff_config=backoff_config, - allowed_updates=allowed_updates, - handle_signals=handle_signals, - close_bot_session=close_bot_session, - tasks_concurrency_limit=tasks_concurrency_limit, - ), + coro = self.start_polling( + *bots, + **kwargs, + polling_timeout=polling_timeout, + handle_as_tasks=handle_as_tasks, + backoff_config=backoff_config, + allowed_updates=allowed_updates, + handle_signals=handle_signals, + close_bot_session=close_bot_session, + 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) diff --git a/pyproject.toml b/pyproject.toml index f4bfe873..61db4b89 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.10" +requires-python = ">=3.10,<3.15" license = "MIT" authors = [ { name = "Alex Root Junior", email = "jroot.junior@gmail.com" }, @@ -34,6 +34,7 @@ classifiers = [ "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Software Development :: Libraries :: Application Frameworks", "Topic :: Software Development :: Libraries :: Python Modules", @@ -41,7 +42,7 @@ classifiers = [ ] dependencies = [ "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", "aiofiles>=23.2.1,<24.2", "certifi>=2023.7.22", @@ -62,11 +63,11 @@ redis = [ "redis[hiredis]>=6.2.0,<7", ] mongo = [ - "motor>=3.3.2,<3.7.0", - "pymongo>4.5,<4.11", + "motor>=3.3.2,<3.8", + "pymongo>4.5,<4.16", ] proxy = [ - "aiohttp-socks~=0.8.3", + "aiohttp-socks~=0.10.1", ] i18n = [ "Babel>=2.13.0,<3", @@ -78,17 +79,15 @@ signature = [ "cryptography>=46.0.0", ] test = [ - "pytest~=7.4.2", - "pytest-html~=4.0.2", - "pytest-asyncio~=0.21.1", - "pytest-lazy-fixture~=0.6.3", - "pytest-mock~=3.12.0", - "pytest-mypy~=0.10.3", - "pytest-cov~=4.1.0", - "pytest-aiohttp~=1.0.5", - "aresponses~=2.1.6", - "pytz~=2025.2", - "pycryptodomex~=3.23.0", + "pytest==8.4.2", + "pytest-html==4.1.1", + "pytest-mock==3.15.1", + "pytest-mypy==1.0.1", + "pytest-cov==7.0.0", + "pytest-aiohttp==1.1.0", + "aresponses==3.0.0", + "pytz==2025.2", + "pycryptodomex==3.23.0", ] docs = [ "Sphinx~=8.0.2", @@ -104,14 +103,14 @@ docs = [ "sphinxcontrib-towncrier~=0.4.0a0", ] dev = [ - "black~=25.9.0", - "isort~=6.1.0", - "ruff~=0.13.3", - "mypy~=1.10.1", - "toml~=0.10.2", - "pre-commit~=4.3.0", - "packaging~=24.1", - "motor-types~=1.0.0b4", + "black==25.9.0", + "isort==6.1.0", + "ruff==0.14.0", + "mypy==1.10.1", + "toml==0.10.2", + "pre-commit==4.3.0", + "packaging==25.0", + "motor-types==1.0.0b4", ] [project.urls] diff --git a/tests/conftest.py b/tests/conftest.py index 6a0c37f4..5034cde5 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -39,7 +39,7 @@ def pytest_configure(config): config.addinivalue_line("markers", "redis: marked tests require redis 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()) else: asyncio.set_event_loop_policy(asyncio.DefaultEventLoopPolicy()) @@ -186,6 +186,16 @@ async def dispatcher(): 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") # def event_loop_policy(request): # if sys.platform == "win32": diff --git a/tests/test_fsm/storage/test_isolation.py b/tests/test_fsm/storage/test_isolation.py index 7257123d..2624ad26 100644 --- a/tests/test_fsm/storage/test_isolation.py +++ b/tests/test_fsm/storage/test_isolation.py @@ -8,11 +8,8 @@ from aiogram.fsm.storage.redis import RedisEventIsolation, RedisStorage @pytest.mark.parametrize( "isolation", - [ - pytest.lazy_fixture("redis_isolation"), - pytest.lazy_fixture("lock_isolation"), - pytest.lazy_fixture("disabled_isolation"), - ], + ["redis_isolation", "lock_isolation", "disabled_isolation"], + indirect=True, ) class TestIsolations: async def test_lock( diff --git a/tests/test_fsm/storage/test_storages.py b/tests/test_fsm/storage/test_storages.py index 884f6874..e374448b 100644 --- a/tests/test_fsm/storage/test_storages.py +++ b/tests/test_fsm/storage/test_storages.py @@ -8,12 +8,8 @@ from aiogram.fsm.storage.base import BaseStorage, StorageKey @pytest.mark.parametrize( "storage", - [ - pytest.lazy_fixture("redis_storage"), - pytest.lazy_fixture("mongo_storage"), - pytest.lazy_fixture("pymongo_storage"), - pytest.lazy_fixture("memory_storage"), - ], + ["memory_storage", "redis_storage", "mongo_storage", "pymongo_storage"], + indirect=True, ) class TestStorages: async def test_set_state(self, storage: BaseStorage, storage_key: StorageKey): diff --git a/tests/test_webhook/test_aiohtt_server.py b/tests/test_webhook/test_aiohttp_server.py similarity index 99% rename from tests/test_webhook/test_aiohtt_server.py rename to tests/test_webhook/test_aiohttp_server.py index c185b841..b6ee3b8b 100644 --- a/tests/test_webhook/test_aiohtt_server.py +++ b/tests/test_webhook/test_aiohttp_server.py @@ -185,8 +185,8 @@ class TestSimpleRequestHandler: handler_event.clear() resp = await self.make_reqest(client=client) assert resp.status == 200 - await asyncio.wait_for(handler_event.wait(), timeout=1) - await asyncio.wait_for(method_called_event.wait(), timeout=1) + await asyncio.wait_for(handler_event.wait(), timeout=3) + 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 # 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