diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index d238a558..030da513 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -20,7 +20,7 @@ on: - "pyproject.toml" jobs: - build: + tests: strategy: fail-fast: false matrix: @@ -33,8 +33,7 @@ jobs: - '3.9' - '3.10' - '3.11' - - 'pypy3.8' - - 'pypy3.9' + - '3.12' defaults: # Windows sucks. Force use bash instead of PowerShell @@ -44,21 +43,13 @@ jobs: runs-on: ${{ matrix.os }} env: - # We disable some features for PyPy by this environment variable such as: - # – Installation of `fast` extras: `uvloop` on PyPy is useless and may be even slower - # than the default loop; - # – Coverage reports: code introspection disables any optimizations, so tests with - # coverage enabled are very slow on PyPy. - # More: https://www.pypy.org/performance.html - IS_PYPY: ${{ startswith(matrix.python-version, 'pypy') }} - # Windows has also some limitations: + # Windows has some limitations: # – Redis is not supported on GitHub Windows runners; - # – Poetry installer doesn't work on Windows with PyPy. IS_WINDOWS: ${{ startswith(matrix.os, 'windows') }} steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} on ${{ matrix.os }} uses: actions/setup-python@v4 @@ -72,7 +63,6 @@ jobs: pip install -e .[dev,test,redis,proxy,i18n,fast] - name: Lint code - if: "env.IS_PYPY == 'false'" run: | ruff --output-format=github aiogram examples mypy aiogram @@ -86,13 +76,11 @@ jobs: - name: Run tests run: | - flags="" - [[ "$IS_PYPY" == "false" ]] && flags="$flags --cov=aiogram --cov-config .coveragerc --cov-report=xml" + flags="$flags --cov=aiogram --cov-config .coveragerc --cov-report=xml" [[ "$IS_WINDOWS" == "false" ]] && flags="$flags --redis redis://localhost:6379/0" pytest $flags - name: Upload coverage data - if: "env.IS_PYPY == 'false'" uses: codecov/codecov-action@v3 with: token: ${{ secrets.CODECOV_TOKEN }} @@ -100,3 +88,47 @@ jobs: flags: unittests name: py-${{ matrix.python-version }}-${{ matrix.os }} fail_ci_if_error: true + + pypy-tests: + strategy: + fail-fast: false + matrix: + os: + - ubuntu-latest + - macos-latest + # - windows-latest + python-version: + - 'pypy3.8' + - 'pypy3.9' + + defaults: + # Windows sucks. Force use bash instead of PowerShell + run: + shell: bash + + runs-on: ${{ matrix.os }} + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} on ${{ matrix.os }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + cache: "pip" + cache-dependency-path: pyproject.toml + + - name: Install project dependencies + run: | + pip install -e .[dev,test,redis,proxy,i18n,fast] + + - name: Setup redis + uses: shogo82148/actions-setup-redis@v1 + with: + redis-version: 6 + + - name: Run tests + run: | + flags="" + pytest $flags diff --git a/CHANGES/1354.misc.rst b/CHANGES/1354.misc.rst new file mode 100644 index 00000000..42b4cfa5 --- /dev/null +++ b/CHANGES/1354.misc.rst @@ -0,0 +1 @@ +Introduce Python 3.12 support diff --git a/aiogram/__init__.py b/aiogram/__init__.py index 31d1b16b..b945226f 100644 --- a/aiogram/__init__.py +++ b/aiogram/__init__.py @@ -15,8 +15,10 @@ from .utils.text_decorations import markdown_decoration as md with suppress(ImportError): import uvloop as _uvloop + import asyncio + + asyncio.set_event_loop_policy(_uvloop.EventLoopPolicy()) - _uvloop.install() # type: ignore[attr-defined,unused-ignore] F = MagicFilter() flags = FlagGenerator() diff --git a/pyproject.toml b/pyproject.toml index 0557223b..2d17be3b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,6 +34,7 @@ classifiers = [ "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", "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.8.5", + "aiohttp~=3.9.0", "pydantic>=2.4.1,<2.6", "aiofiles~=23.2.1", "certifi>=2023.7.22", @@ -144,7 +145,7 @@ features = [ serve = "sphinx-autobuild --watch aiogram/ --watch CHANGELOG.rst --watch README.rst docs/ docs/_build/ {args}" [tool.hatch.envs.dev] -python = "3.11" +python = "3.12" features = [ "dev", "fast", @@ -185,7 +186,7 @@ view-cov = "google-chrome-stable reports/py{matrix:python}/coverage/index.html" [[tool.hatch.envs.test.matrix]] -python = ["38", "39", "310", "311"] +python = ["38", "39", "310", "311", "312"] [tool.ruff] line-length = 99 diff --git a/tests/test_fsm/storage/test_isolation.py b/tests/test_fsm/storage/test_isolation.py index 8be303c4..1fb21d55 100644 --- a/tests/test_fsm/storage/test_isolation.py +++ b/tests/test_fsm/storage/test_isolation.py @@ -53,7 +53,7 @@ class TestRedisEventIsolation: assert isolation.redis is not None assert isolation.key_builder is not None - assert pool.called_once_with("redis://localhost:6379/0") + pool.assert_called_once_with("redis://localhost:6379/0") async def test_close(self): isolation = RedisEventIsolation(redis=AsyncMock()) diff --git a/tests/test_fsm/test_state.py b/tests/test_fsm/test_state.py index 159b20ca..dd240946 100644 --- a/tests/test_fsm/test_state.py +++ b/tests/test_fsm/test_state.py @@ -1,7 +1,10 @@ import pytest +import sys from aiogram.fsm.state import State, StatesGroup, any_state +PY312_OR_GREATER = sys.version_info >= (3, 12) + class TestState: def test_empty(self): @@ -72,10 +75,20 @@ class TestState: assert state(None, check) is result def test_state_in_unknown_class(self): - with pytest.raises(RuntimeError): + if PY312_OR_GREATER: + # Python 3.12+ does not wrap __set_name__ exceptions with RuntimeError anymore as part + # of PEP 678. See "Other Language Changes" in the changelogs: + # https://docs.python.org/3/whatsnew/3.12.html + with pytest.raises(ValueError): - class MyClass: - state1 = State() + class MyClass: + state1 = State() + + else: + with pytest.raises(RuntimeError): + + class MyClass: + state1 = State() class TestStatesGroup: diff --git a/tests/test_webhook/test_aiohtt_server.py b/tests/test_webhook/test_aiohtt_server.py index 4e3f6658..f29895ba 100644 --- a/tests/test_webhook/test_aiohtt_server.py +++ b/tests/test_webhook/test_aiohtt_server.py @@ -181,10 +181,20 @@ class TestSimpleRequestHandler: "aiogram.dispatcher.dispatcher.Dispatcher.silent_call_request", new_callable=AsyncMock, ) as mocked_silent_call_request: + method_called_event = asyncio.Event() + mocked_silent_call_request.side_effect = ( + lambda *args, **kwargs: method_called_event.set() + ) + 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) + # 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 + # before asserting if it has been awaited. mocked_silent_call_request.assert_awaited() result = await resp.json() assert not result