diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 2dfebe63..3ababc4c 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -32,7 +32,6 @@ jobs: build: strategy: fail-fast: false - max-parallel: 9 matrix: os: - ubuntu-latest @@ -42,6 +41,8 @@ jobs: - '3.8' - '3.9' - '3.10' + - 'pypy3.8' + - 'pypy3.9' defaults: # Windows sucks. Force use bash instead of PowerShell @@ -50,8 +51,22 @@ 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: + # – Redis is not supported on GitHub Windows runners; + # – Poetry installer doesn't work on Windows with PyPy. + IS_WINDOWS: ${{ startswith(matrix.os, 'windows') }} + steps: - - uses: actions/checkout@master + - name: Checkout code + uses: actions/checkout@master - name: Set up Python ${{ matrix.python-version }} on ${{ matrix.os }} uses: actions/setup-python@v4 @@ -60,14 +75,24 @@ jobs: - name: Install and configure Poetry uses: snok/install-poetry@v1 + if: "env.IS_PYPY == 'false' || env.IS_WINDOWS == 'false'" with: version: 1.1.11 virtualenvs-create: true virtualenvs-in-project: true installer-parallel: true + - name: Install and configure Poetry (PyPy on Windows) + if: "env.IS_PYPY == 'true' && env.IS_WINDOWS == 'true'" + run: | + set -eu + pip install "poetry==1.1.11" + poetry config virtualenvs.create true + poetry config virtualenvs.in-project true + poetry config installer.parallel true + - name: Setup redis - if: ${{ matrix.os != 'windows-latest' }} + if: ${{ env.IS_WINDOWS == 'false' }} uses: shogo82148/actions-setup-redis@v1 with: redis-version: 6 @@ -79,32 +104,29 @@ jobs: path: .venv key: venv-${{ matrix.os }}-${{ matrix.python-version }}-${{ hashFiles('**/poetry.lock') }}-${{ secrets.CACHE_VERSION }} - - name: Project dependencies + - name: Install project dependencies if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true' run: | - poetry install --no-interaction -E fast -E redis -E proxy -E i18n -E docs + flags="" + [[ "$IS_PYPY" == "false" ]] && flags="$flags -E fast" + poetry install --no-interaction -E redis -E proxy -E i18n -E docs $flags - name: Lint code run: | poetry run flake8 aiogram poetry run mypy aiogram - - - name: Check code-style (Black) - run: | poetry run black --check --diff aiogram tests - - name: Run tests (with Redis) - if: ${{ matrix.os != 'windows-latest' }} + - name: Run tests run: | - poetry run pytest --cov=aiogram --cov-config .coveragerc --cov-report=xml --redis redis://localhost:6379/0 + flags="" + [[ "$IS_PYPY" == "false" ]] && flags="$flags --cov=aiogram --cov-config .coveragerc --cov-report=xml" + [[ "$IS_WINDOWS" == "false" ]] && flags="$flags --redis redis://localhost:6379/0" + poetry run pytest $flags - - name: Run tests (without Redis) - # Redis can't be used on GitHub Windows Runners - if: ${{ matrix.os == 'windows-latest' }} - run: | - poetry run pytest --cov=aiogram --cov-config .coveragerc --cov-report=xml - - - uses: codecov/codecov-action@v1 + - name: Upload coverage data + if: "env.IS_PYPY == 'false'" + uses: codecov/codecov-action@v1 with: token: ${{ secrets.CODECOV_TOKEN }} file: coverage.xml diff --git a/CHANGES/985.feature b/CHANGES/985.feature new file mode 100644 index 00000000..d7f8a052 --- /dev/null +++ b/CHANGES/985.feature @@ -0,0 +1 @@ +Add PyPy support and run tests under PyPy diff --git a/CHANGES/995.misc.rst b/CHANGES/995.misc.rst new file mode 100644 index 00000000..e47bd739 --- /dev/null +++ b/CHANGES/995.misc.rst @@ -0,0 +1 @@ +Fixed polling crash when Telegram Bot API raises HTTP 429 status-code. diff --git a/README.rst b/README.rst index 312cc63f..3be42b83 100644 --- a/README.rst +++ b/README.rst @@ -61,6 +61,7 @@ Features - Asynchronous (`asyncio docs `_, :pep:`492`) - Has type hints (:pep:`484`) and can be used with `mypy `_ +- Supports `PyPy `_ - Supports `Telegram Bot API 5.3 `_ - Telegram Bot API integration code was `autogenerated `_ and can be easily re-generated when API gets updated - Updates router (Blueprints) diff --git a/aiogram/dispatcher/dispatcher.py b/aiogram/dispatcher/dispatcher.py index 897eea59..52acda8e 100644 --- a/aiogram/dispatcher/dispatcher.py +++ b/aiogram/dispatcher/dispatcher.py @@ -8,7 +8,7 @@ from typing import Any, AsyncGenerator, Dict, List, Optional, Union from .. import loggers from ..client.bot import Bot -from ..exceptions import TelegramAPIError, TelegramNetworkError, TelegramServerError +from ..exceptions import TelegramAPIError from ..fsm.middleware import FSMContextMiddleware from ..fsm.storage.base import BaseEventIsolation, BaseStorage from ..fsm.storage.memory import DisabledEventIsolation, MemoryStorage @@ -183,15 +183,15 @@ class Dispatcher(Router): get_updates = GetUpdates(timeout=polling_timeout, allowed_updates=allowed_updates) kwargs = {} if bot.session.timeout: - # Request timeout can be lower than session timeout ant that's OK. + # Request timeout can be lower than session timeout and that's OK. # To prevent false-positive TimeoutError we should wait longer than polling timeout kwargs["request_timeout"] = int(bot.session.timeout + polling_timeout) while True: try: updates = await bot(get_updates, **kwargs) - except (TelegramNetworkError, TelegramServerError) as e: - # In cases when Telegram Bot API was inaccessible don't need to stop polling process - # because some of developers can't make auto-restarting of the script + except Exception as e: + # In cases when Telegram Bot API was inaccessible don't need to stop polling + # process because some developers can't make auto-restarting of the script loggers.dispatcher.error("Failed to fetch updates - %s: %s", type(e).__name__, e) # And also backoff timeout is best practice to retry any network activity loggers.dispatcher.warning( @@ -211,8 +211,8 @@ class Dispatcher(Router): yield update # The getUpdates method returns the earliest 100 unconfirmed updates. # To confirm an update, use the offset parameter when calling getUpdates - # All updates with update_id less than or equal to offset will be marked as confirmed on the server - # and will no longer be returned. + # All updates with update_id less than or equal to offset will be marked + # as confirmed on the server and will no longer be returned. get_updates.offset = update.update_id + 1 async def _listen_update(self, update: Update, **kwargs: Any) -> Any: @@ -417,7 +417,7 @@ class Dispatcher(Router): workflow_data = {"dispatcher": self, "bots": bots, "bot": bots[-1]} workflow_data.update(kwargs) await self.emit_startup(**workflow_data) - loggers.dispatcher.info("Start poling") + loggers.dispatcher.info("Start polling") try: coro_list = [] for bot in bots: