├── async_timeout ├── py.typed └── __init__.py ├── setup.py ├── .codecov.yml ├── .github ├── dependabot.yml └── workflows │ ├── auto-merge.yml │ ├── codeql.yml │ └── ci.yml ├── MANIFEST.in ├── requirements.txt ├── pyproject.toml ├── Makefile ├── LICENSE ├── .mypy.ini ├── .pre-commit-config.yaml ├── .gitignore ├── setup.cfg ├── CHANGES └── README.rst ├── CHANGES.rst ├── README.rst └── tests └── test_timeout.py /async_timeout/py.typed: -------------------------------------------------------------------------------- 1 | Placeholder 2 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | 4 | setup() 5 | -------------------------------------------------------------------------------- /.codecov.yml: -------------------------------------------------------------------------------- 1 | codecov: 2 | notify: 3 | after_n_builds: 10 4 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: pip 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | open-pull-requests-limit: 10 8 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include CHANGES.rst 3 | include README.rst 4 | include requirements.txt 5 | graft async_timeout 6 | graft tests 7 | global-exclude *.pyc 8 | global-exclude *.cache 9 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | -e . 2 | build==1.3.0 3 | docutils==0.22.2 4 | mypy==1.18.2; implementation_name=="cpython" 5 | pre-commit==4.3.0 6 | pytest==8.4.2 7 | pytest-asyncio==1.2.0 8 | pytest-cov==7.0.0 9 | twine==6.2.0 10 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = [ 3 | "setuptools>=45", 4 | "wheel", 5 | ] 6 | build-backend = "setuptools.build_meta" 7 | 8 | [tool.towncrier] 9 | package = "async_timeout" 10 | filename = "CHANGES.rst" 11 | directory = "CHANGES/" 12 | title_format = "{version} ({project_date})" 13 | issue_format = "`#{issue} `_" 14 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | test: lint 2 | python -m pytest tests 3 | 4 | lint: fmt 5 | python -m mypy 6 | 7 | fmt: 8 | ifdef CI 9 | python -m pre_commit run --all-files --show-diff-on-failure 10 | else 11 | python -m pre_commit run --all-files 12 | endif 13 | 14 | 15 | check: 16 | python -m build 17 | python -m twine check dist/* 18 | 19 | install: 20 | python -m pip install --user -U pip 21 | python -m pip install --user -r requirements.txt 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2016-2020 aio-libs collaboration. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /.mypy.ini: -------------------------------------------------------------------------------- 1 | [mypy] 2 | files = async_timeout 3 | check_untyped_defs = True 4 | follow_imports_for_stubs = True 5 | disallow_any_decorated = True 6 | disallow_any_generics = True 7 | disallow_incomplete_defs = True 8 | disallow_subclassing_any = True 9 | disallow_untyped_calls = True 10 | disallow_untyped_decorators = True 11 | disallow_untyped_defs = True 12 | implicit_reexport = False 13 | no_implicit_optional = True 14 | show_error_codes = True 15 | strict_equality = True 16 | warn_incomplete_stub = True 17 | warn_redundant_casts = True 18 | warn_unreachable = True 19 | warn_unused_ignores = True 20 | disallow_any_unimported = True 21 | warn_return_any = True 22 | -------------------------------------------------------------------------------- /.github/workflows/auto-merge.yml: -------------------------------------------------------------------------------- 1 | name: Dependabot auto-merge 2 | on: pull_request_target 3 | 4 | permissions: 5 | pull-requests: write 6 | contents: write 7 | 8 | jobs: 9 | dependabot: 10 | runs-on: ubuntu-latest 11 | if: ${{ github.actor == 'dependabot[bot]' }} 12 | steps: 13 | - name: Dependabot metadata 14 | id: metadata 15 | uses: dependabot/fetch-metadata@v1.3.4 16 | with: 17 | github-token: "${{ secrets.GITHUB_TOKEN }}" 18 | - name: Enable auto-merge for Dependabot PRs 19 | run: gh pr merge --auto --squash "$PR_URL" 20 | env: 21 | PR_URL: ${{github.event.pull_request.html_url}} 22 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 23 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: 'v5.0.0' 4 | hooks: 5 | - id: check-merge-conflict 6 | exclude: "rst$" 7 | - repo: https://github.com/asottile/yesqa 8 | rev: v1.5.0 9 | hooks: 10 | - id: yesqa 11 | - repo: https://github.com/PyCQA/isort 12 | rev: '6.0.1' 13 | hooks: 14 | - id: isort 15 | - repo: https://github.com/psf/black 16 | rev: '25.1.0' 17 | hooks: 18 | - id: black 19 | language_version: python3 # Should be a command that runs python3.6+ 20 | - repo: https://github.com/pre-commit/pre-commit-hooks 21 | rev: 'v5.0.0' 22 | hooks: 23 | - id: check-case-conflict 24 | - id: check-json 25 | - id: check-xml 26 | - id: check-yaml 27 | - id: debug-statements 28 | - id: check-added-large-files 29 | - id: end-of-file-fixer 30 | exclude: "[.]md$" 31 | - id: requirements-txt-fixer 32 | - id: trailing-whitespace 33 | exclude: "[.]md$" 34 | - id: check-symlinks 35 | - id: debug-statements 36 | # Another entry is required to apply file-contents-sorter to another file 37 | - repo: https://github.com/pre-commit/pre-commit-hooks 38 | rev: 'v5.0.0' 39 | hooks: 40 | - id: file-contents-sorter 41 | files: | 42 | .gitignore 43 | - repo: https://github.com/asottile/pyupgrade 44 | rev: 'v3.20.0' 45 | hooks: 46 | - id: pyupgrade 47 | args: ['--py36-plus'] 48 | - repo: https://github.com/PyCQA/flake8 49 | rev: '7.3.0' 50 | hooks: 51 | - id: flake8 52 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | local_settings.py 55 | 56 | # Flask stuff: 57 | instance/ 58 | .webassets-cache 59 | 60 | # Scrapy stuff: 61 | .scrapy 62 | 63 | # Sphinx documentation 64 | docs/_build/ 65 | 66 | # PyBuilder 67 | target/ 68 | 69 | # IPython Notebook 70 | .ipynb_checkpoints 71 | 72 | # pyenv 73 | .python-version 74 | 75 | # celery beat schedule file 76 | celerybeat-schedule 77 | 78 | # dotenv 79 | .env 80 | 81 | # virtualenv 82 | venv/ 83 | ENV/ 84 | 85 | # Spyder project settings 86 | .spyderproject 87 | 88 | # Rope project settings 89 | .ropeproject 90 | 91 | .mypy_cache 92 | .pytest_cache 93 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = async-timeout 3 | version = attr: async_timeout.__version__ 4 | url = https://github.com/aio-libs/async-timeout 5 | project_urls = 6 | Chat: Gitter = https://gitter.im/aio-libs/Lobby 7 | CI: GitHub Actions = https://github.com/aio-libs/async-timeout/actions 8 | Coverage: codecov = https://codecov.io/github/aio-libs/async-timeout 9 | GitHub: issues = https://github.com/aio-libs/async-timeout/issues 10 | GitHub: repo = https://github.com/aio-libs/async-timeout 11 | description = Timeout context manager for asyncio programs 12 | long_description = file: README.rst 13 | long_description_content_type = text/x-rst 14 | author = Andrew Svetlov 15 | author_email = andrew.svetlov@gmail.com 16 | license = Apache 2 17 | license_files = LICENSE 18 | classifiers = 19 | Development Status :: 5 - Production/Stable 20 | 21 | Topic :: Software Development :: Libraries 22 | Framework :: AsyncIO 23 | 24 | Intended Audience :: Developers 25 | 26 | License :: OSI Approved :: Apache Software License 27 | 28 | Programming Language :: Python 29 | Programming Language :: Python :: 3 30 | Programming Language :: Python :: 3 :: Only 31 | 32 | [options] 33 | python_requires = >=3.8 34 | packages = 35 | async_timeout 36 | zip_safe = True 37 | include_package_data = True 38 | 39 | [flake8] 40 | exclude = .git,.env,__pycache__,.eggs 41 | max-line-length = 88 42 | ignore = N801,N802,N803,E252,W503,E133,E203 43 | 44 | [isort] 45 | line_length=88 46 | include_trailing_comma=True 47 | multi_line_output=3 48 | force_grid_wrap=0 49 | combine_as_imports=True 50 | lines_after_imports=2 51 | 52 | [tool:pytest] 53 | addopts= --cov=async_timeout --cov-report=term --cov-report=html --cov-branch 54 | asyncio_mode = strict 55 | 56 | [mypy-pytest] 57 | ignore_missing_imports = true 58 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ "master" ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ "master" ] 20 | schedule: 21 | - cron: '28 19 * * 2' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'python' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v3 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v2 46 | with: 47 | languages: ${{ matrix.language }} 48 | # If you wish to specify custom queries, you can do so here or in a config file. 49 | # By default, queries listed here will override any specified in a config file. 50 | # Prefix the list here with "+" to use these queries and those in the config file. 51 | 52 | # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 53 | queries: security-and-quality 54 | 55 | 56 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 57 | # If this step fails, then you should remove it and run the build manually (see below) 58 | - name: Autobuild 59 | uses: github/codeql-action/autobuild@v2 60 | 61 | # ℹ️ Command-line programs to run using the OS shell. 62 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 63 | 64 | # If the Autobuild fails above, remove it and uncomment the following three lines. 65 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. 66 | 67 | # - run: | 68 | # echo "Run, Build Application using script" 69 | # ./location_of_script_within_repo/buildscript.sh 70 | 71 | - name: Perform CodeQL Analysis 72 | uses: github/codeql-action/analyze@v2 73 | with: 74 | category: "/language:${{matrix.language}}" 75 | -------------------------------------------------------------------------------- /CHANGES/README.rst: -------------------------------------------------------------------------------- 1 | .. _Adding change notes with your PRs: 2 | 3 | Adding change notes with your PRs 4 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 5 | 6 | It is very important to maintain a log for news of how 7 | updating to the new version of the software will affect 8 | end-users. This is why we enforce collection of the change 9 | fragment files in pull requests as per `Towncrier philosophy`_. 10 | 11 | The idea is that when somebody makes a change, they must record 12 | the bits that would affect end-users only including information 13 | that would be useful to them. Then, when the maintainers publish 14 | a new release, they'll automatically use these records to compose 15 | a change log for the respective version. It is important to 16 | understand that including unnecessary low-level implementation 17 | related details generates noise that is not particularly useful 18 | to the end-users most of the time. And so such details should be 19 | recorded in the Git history rather than a changelog. 20 | 21 | Alright! So how to add a news fragment? 22 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 23 | 24 | ``async-timeout`` uses `towncrier `_ 25 | for changelog management. 26 | To submit a change note about your PR, add a text file into the 27 | ``CHANGES/`` folder. It should contain an 28 | explanation of what applying this PR will change in the way 29 | end-users interact with the project. One sentence is usually 30 | enough but feel free to add as many details as you feel necessary 31 | for the users to understand what it means. 32 | 33 | **Use the past tense** for the text in your fragment because, 34 | combined with others, it will be a part of the "news digest" 35 | telling the readers **what changed** in a specific version of 36 | the library *since the previous version*. You should also use 37 | reStructuredText syntax for highlighting code (inline or block), 38 | linking parts of the docs or external sites. 39 | 40 | Finally, name your file following the convention that Towncrier 41 | understands: it should start with the number of an issue or a 42 | PR followed by a dot, then add a patch type, like ``feature``, 43 | ``doc``, ``misc`` etc., and add ``.rst`` as a suffix. If you 44 | need to add more than one fragment, you may add an optional 45 | sequence number (delimited with another period) between the type 46 | and the suffix. 47 | 48 | In general the name will follow ``..rst`` pattern, 49 | where the categories are: 50 | 51 | - ``feature``: Any new feature 52 | - ``bugfix``: A bug fix 53 | - ``doc``: A change to the documentation 54 | - ``misc``: Changes internal to the repo like CI, test and build changes 55 | - ``removal``: For deprecations and removals of an existing feature or behavior 56 | 57 | A pull request may have more than one of these components, for example 58 | a code change may introduce a new feature that deprecates an old 59 | feature, in which case two fragments should be added. It is not 60 | necessary to make a separate documentation fragment for documentation 61 | changes accompanying the relevant code changes. 62 | 63 | Examples for adding changelog entries to your Pull Requests 64 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 65 | 66 | File :file:`CHANGES/603.removal.1.rst`: 67 | 68 | .. code-block:: rst 69 | 70 | Dropped Python 3.5 support; Python 3.6 is the minimal supported Python version. 71 | 72 | File :file:`CHANGES/550.bugfix.rst`: 73 | 74 | .. code-block:: rst 75 | 76 | Started shipping Windows wheels for the x86 architecture. 77 | 78 | File :file:`CHANGES/553.feature.rst`: 79 | 80 | .. code-block:: rst 81 | 82 | Added support for ``GenericAliases`` (``MultiDict[str]``) under Python 3.9 and higher. 83 | 84 | 85 | .. _Towncrier philosophy: 86 | https://towncrier.readthedocs.io/en/stable/#philosophy 87 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | - '[0-9].[0-9]+' # matches to backport branches, e.g. 3.6 8 | tags: [ 'v*' ] 9 | pull_request: 10 | branches: 11 | - master 12 | - '[0-9].[0-9]+' 13 | schedule: 14 | - cron: '0 6 * * *' # Daily 6AM UTC build 15 | 16 | 17 | jobs: 18 | 19 | lint: 20 | name: Linter 21 | runs-on: ubuntu-latest 22 | timeout-minutes: 30 # pre-commit env update can take time 23 | steps: 24 | - name: Checkout 25 | uses: actions/checkout@v4 26 | - name: Setup Python 27 | uses: actions/setup-python@v5 28 | with: 29 | python-version: 3.x 30 | - name: Cache PyPI 31 | uses: actions/cache@v4 32 | with: 33 | key: pip-lint-${{ hashFiles('requirements.txt') }} 34 | path: ~/.cache/pip 35 | restore-keys: | 36 | pip-lint- 37 | - name: Install dependencies 38 | run: | 39 | make install 40 | - name: Run linter 41 | run: | 42 | make lint 43 | - name: Run twine checker 44 | run: | 45 | make check 46 | 47 | test: 48 | name: Test 49 | needs: lint 50 | strategy: 51 | matrix: 52 | pyver: ["3.9", "3.10", "3.11", "3.12", "3.13"] 53 | os: [ubuntu, macos, windows] 54 | include: 55 | - pyver: pypy-3.10 56 | os: ubuntu 57 | runs-on: ${{ matrix.os }}-latest 58 | timeout-minutes: 15 59 | steps: 60 | - name: Checkout 61 | uses: actions/checkout@v4 62 | - name: Setup Python ${{ matrix.pyver }} 63 | uses: actions/setup-python@v5 64 | with: 65 | python-version: ${{ matrix.pyver }} 66 | - name: Get pip cache dir 67 | id: pip-cache 68 | run: | 69 | echo "dir=$(pip cache dir)" >> $GITHUB_OUTPUT # - name: Cache 70 | shell: bash 71 | - name: Cache PyPI 72 | uses: actions/cache@v4 73 | with: 74 | key: pip-ci-${{ runner.os }}-${{ matrix.pyver }}-${{ hashFiles('requirements/*.txt') }} 75 | path: ${{ steps.pip-cache.outputs.dir }} 76 | restore-keys: | 77 | pip-ci-${{ runner.os }}-${{ matrix.pyver }}- 78 | - name: Install dependencies 79 | run: | 80 | make install 81 | - name: Run unittests 82 | env: 83 | COLOR: 'yes' 84 | run: | 85 | python -m pytest tests 86 | python -m coverage xml 87 | - name: Upload coverage 88 | uses: codecov/codecov-action@v4 89 | with: 90 | file: ./coverage.xml 91 | flags: unit 92 | fail_ci_if_error: false 93 | 94 | test-summary: 95 | if: always() 96 | needs: [lint, test] 97 | runs-on: ubuntu-latest 98 | steps: 99 | - name: Test matrix status 100 | uses: re-actors/alls-green@release/v1 101 | with: 102 | jobs: ${{ toJSON(needs) }} 103 | 104 | deploy: 105 | name: Deploy 106 | runs-on: ubuntu-latest 107 | needs: test-summary 108 | # Run only on pushing a tag 109 | if: github.event_name == 'push' && contains(github.ref, 'refs/tags/') 110 | steps: 111 | - name: Checkout 112 | uses: actions/checkout@v4 113 | - name: Setup Python 114 | uses: actions/setup-python@v5 115 | with: 116 | python-version: 3.x 117 | - name: Install dependencies 118 | run: | 119 | make install 120 | - name: Make dists 121 | run: 122 | python -m build 123 | - name: Make Release 124 | uses: aio-libs/create-release@v1.6.6 125 | with: 126 | changes_file: CHANGES.rst 127 | name: async-timeout 128 | version_file: async_timeout/__init__.py 129 | github_token: ${{ secrets.GITHUB_TOKEN }} 130 | pypi_token: ${{ secrets.PYPI_TOKEN }} 131 | fix_issue_regex: '\(`#(\\d+) `_\)' 132 | fix_issue_repl: "(#\\1)" 133 | -------------------------------------------------------------------------------- /CHANGES.rst: -------------------------------------------------------------------------------- 1 | ======= 2 | CHANGES 3 | ======= 4 | 5 | .. towncrier release notes start 6 | 7 | 5.0.1 (2024-11-06) 8 | ================== 9 | 10 | Misc 11 | ---- 12 | 13 | - `#423 `_ 14 | 15 | 16 | 5.0.0 (2024-10-31) 17 | ================== 18 | 19 | Features 20 | -------- 21 | 22 | - Make ``asyncio_timeout`` fully compatible with the standard ``asyncio.Timeout`` but keep backward compatibility with existing ``asyncio_timeout.Timeout`` API. (`#422 `_) 23 | 24 | 25 | Improved Documentation 26 | ---------------------- 27 | 28 | - On the `CHANGES/README.rst `_ page, 29 | a link to the ``Towncrier philosophy`` has been fixed. (`#388 `_) 30 | 31 | 32 | Deprecations and Removals 33 | ------------------------- 34 | 35 | - Drop deprecated sync context manager support, use ``async with timeout(...): ...`` instead. (`#421 `_) 36 | 37 | 38 | 4.0.3 (2023-08-10) 39 | ================== 40 | 41 | * Fixed compatibility with asyncio.timeout() on Python 3.11+. 42 | * Added support for Python 3.11. 43 | * Dropped support for Python 3.6. 44 | 45 | 4.0.2 (2021-12-20) 46 | ================== 47 | 48 | Misc 49 | ---- 50 | 51 | - `#259 `_, `#274 `_ 52 | 53 | 54 | 4.0.1 (2121-11-10) 55 | ================== 56 | 57 | - Fix regression: 58 | 59 | 1. Don't raise TimeoutError from timeout object that doesn't enter into async context 60 | manager 61 | 62 | 2. Use call_soon() for raising TimeoutError if deadline is reached on entering into 63 | async context manager 64 | 65 | (#258) 66 | 67 | - Make ``Timeout`` class available in ``__all__``. 68 | 69 | 4.0.0 (2021-11-01) 70 | ================== 71 | 72 | * Implemented ``timeout_at(deadline)`` (#117) 73 | 74 | * Supported ``timeout.deadline`` and ``timeout.expired`` properties. 75 | 76 | * Dropped ``timeout.remaining`` property: it can be calculated as 77 | ``timeout.deadline - loop.time()`` 78 | 79 | * Dropped ``timeout.timeout`` property that returns a relative timeout based on the 80 | timeout object creation time; the absolute ``timeout.deadline`` should be used 81 | instead. 82 | 83 | * Added the deadline modification methods: ``timeout.reject()``, 84 | ``timeout.shift(delay)``, ``timeout.update(deadline)``. 85 | 86 | * Deprecated synchronous context manager usage 87 | 88 | 3.0.1 (2018-10-09) 89 | ================== 90 | 91 | * More aggressive typing (#48) 92 | 93 | 3.0.0 (2018-05-05) 94 | ================== 95 | 96 | * Drop Python 3.4, the minimal supported version is Python 3.5.3 97 | 98 | * Provide type annotations 99 | 100 | 2.0.1 (2018-03-13) 101 | ================== 102 | 103 | * Fix ``PendingDeprecationWarning`` on Python 3.7 (#33) 104 | 105 | 106 | 2.0.0 (2017-10-09) 107 | ================== 108 | 109 | * Changed ``timeout <= 0`` behaviour 110 | 111 | * Backward incompatibility change, prior this version ``0`` was 112 | shortcut for ``None`` 113 | * when timeout <= 0 ``TimeoutError`` raised faster 114 | 115 | 1.4.0 (2017-09-09) 116 | ================== 117 | 118 | * Implement ``remaining`` property (#20) 119 | 120 | * If timeout is not started yet or started unconstrained: 121 | ``remaining`` is ``None`` 122 | * If timeout is expired: ``remaining`` is ``0.0`` 123 | * All others: roughly amount of time before ``TimeoutError`` is triggered 124 | 125 | 1.3.0 (2017-08-23) 126 | ================== 127 | 128 | * Don't suppress nested exception on timeout. Exception context points 129 | on cancelled line with suspended ``await`` (#13) 130 | 131 | * Introduce ``.timeout`` property (#16) 132 | 133 | * Add methods for using as async context manager (#9) 134 | 135 | 1.2.1 (2017-05-02) 136 | ================== 137 | 138 | * Support unpublished event loop's "current_task" api. 139 | 140 | 141 | 1.2.0 (2017-03-11) 142 | ================== 143 | 144 | * Extra check on context manager exit 145 | 146 | * 0 is no-op timeout 147 | 148 | 149 | 1.1.0 (2016-10-20) 150 | ================== 151 | 152 | * Rename to ``async-timeout`` 153 | 154 | 1.0.0 (2016-09-09) 155 | ================== 156 | 157 | * The first release. 158 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | async-timeout 2 | ============= 3 | .. image:: https://github.com/aio-libs/async-timeout/actions/workflows/ci.yml/badge.svg 4 | :target: https://github.com/aio-libs/async-timeout/actions/workflows/ci.yml 5 | .. image:: https://codecov.io/gh/aio-libs/async-timeout/branch/master/graph/badge.svg 6 | :target: https://codecov.io/gh/aio-libs/async-timeout 7 | .. image:: https://img.shields.io/pypi/v/async-timeout.svg 8 | :target: https://pypi.python.org/pypi/async-timeout 9 | 10 | asyncio-compatible timeout context manager. 11 | 12 | 13 | 14 | DEPRECATED 15 | ---------- 16 | 17 | This library has effectively been upstreamed into Python 3.11+. 18 | 19 | Therefore this library is considered deprecated and no longer actively supported. 20 | 21 | Version 5.0+ provides dual-mode when executed on Python 3.11+: 22 | ``asyncio_timeout.Timeout`` is fully compatible with ``asyncio.Timeout`` *and* old 23 | versions of the library. 24 | 25 | Anyway, using upstream is highly recommended. ``asyncio_timeout`` exists only for the 26 | sake of backward compatibility, easy supporting both old and new Python by the same 27 | code, and easy misgration. 28 | 29 | If rescheduling API is not important and only ``async with timeout(...): ...`` functionality is required, 30 | a user could apply conditional import:: 31 | 32 | if sys.version_info >= (3, 11): 33 | from asyncio import timeout, timeout_at 34 | else: 35 | from async_timeout import timeout, timeout_at 36 | 37 | 38 | Usage example 39 | ------------- 40 | 41 | 42 | The context manager is useful in cases when you want to apply timeout 43 | logic around block of code or in cases when ``asyncio.wait_for()`` is 44 | not suitable. Also it's much faster than ``asyncio.wait_for()`` 45 | because ``timeout`` doesn't create a new task. 46 | 47 | The ``timeout(delay)`` call returns a context manager 48 | that cancels a block on *timeout* expiring:: 49 | 50 | from async_timeout import timeout 51 | async with timeout(1.5): 52 | await inner() 53 | 54 | 1. If ``inner()`` is executed faster than in ``1.5`` seconds nothing 55 | happens. 56 | 2. Otherwise ``inner()`` is cancelled internally by sending 57 | ``asyncio.CancelledError`` into but ``asyncio.TimeoutError`` is 58 | raised outside of context manager scope. 59 | 60 | *timeout* parameter could be ``None`` for skipping timeout functionality. 61 | 62 | 63 | Alternatively, ``timeout_at(when)`` can be used for scheduling 64 | at the absolute time:: 65 | 66 | loop = asyncio.get_event_loop() 67 | now = loop.time() 68 | 69 | async with timeout_at(now + 1.5): 70 | await inner() 71 | 72 | 73 | Please note: it is not POSIX time but a time with 74 | undefined starting base, e.g. the time of the system power on. 75 | 76 | 77 | Context manager has ``.expired()`` / ``.expired`` for check if timeout happens 78 | exactly in context manager:: 79 | 80 | async with timeout(1.5) as cm: 81 | await inner() 82 | print(cm.expired()) # recommended api 83 | print(cm.expired) # compatible api 84 | 85 | The property is ``True`` if ``inner()`` execution is cancelled by 86 | timeout context manager. 87 | 88 | If ``inner()`` call explicitly raises ``TimeoutError`` ``cm.expired`` 89 | is ``False``. 90 | 91 | The scheduled deadline time is available as ``.when()`` / ``.deadline``:: 92 | 93 | async with timeout(1.5) as cm: 94 | cm.when() # recommended api 95 | cm.deadline # compatible api 96 | 97 | Not finished yet timeout can be rescheduled by ``shift()`` 98 | or ``update()`` methods:: 99 | 100 | async with timeout(1.5) as cm: 101 | # recommended api 102 | cm.reschedule(cm.when() + 1) # add another second on waiting 103 | # compatible api 104 | cm.shift(1) # add another second on waiting 105 | cm.update(loop.time() + 5) # reschedule to now+5 seconds 106 | 107 | Rescheduling is forbidden if the timeout is expired or after exit from ``async with`` 108 | code block. 109 | 110 | 111 | Disable scheduled timeout:: 112 | 113 | async with timeout(1.5) as cm: 114 | cm.reschedule(None) # recommended api 115 | cm.reject() # compatible api 116 | 117 | 118 | 119 | Installation 120 | ------------ 121 | 122 | :: 123 | 124 | $ pip install async-timeout 125 | 126 | The library is Python 3 only! 127 | 128 | 129 | 130 | Authors and License 131 | ------------------- 132 | 133 | The module is written by Andrew Svetlov. 134 | 135 | It's *Apache 2* licensed and freely available. 136 | -------------------------------------------------------------------------------- /async_timeout/__init__.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import enum 3 | import sys 4 | from types import TracebackType 5 | from typing import Optional, Type, final 6 | 7 | 8 | __version__ = "5.0.1" 9 | 10 | 11 | __all__ = ("timeout", "timeout_at", "Timeout") 12 | 13 | 14 | def timeout(delay: Optional[float]) -> "Timeout": 15 | """timeout context manager. 16 | 17 | Useful in cases when you want to apply timeout logic around block 18 | of code or in cases when asyncio.wait_for is not suitable. For example: 19 | 20 | >>> async with timeout(0.001): 21 | ... async with aiohttp.get('https://github.com') as r: 22 | ... await r.text() 23 | 24 | 25 | delay - value in seconds or None to disable timeout logic 26 | """ 27 | loop = asyncio.get_running_loop() 28 | if delay is not None: 29 | deadline = loop.time() + delay # type: Optional[float] 30 | else: 31 | deadline = None 32 | return Timeout(deadline, loop) 33 | 34 | 35 | def timeout_at(deadline: Optional[float]) -> "Timeout": 36 | """Schedule the timeout at absolute time. 37 | 38 | deadline argument points on the time in the same clock system 39 | as loop.time(). 40 | 41 | Please note: it is not POSIX time but a time with 42 | undefined starting base, e.g. the time of the system power on. 43 | 44 | >>> async with timeout_at(loop.time() + 10): 45 | ... async with aiohttp.get('https://github.com') as r: 46 | ... await r.text() 47 | 48 | 49 | """ 50 | loop = asyncio.get_running_loop() 51 | return Timeout(deadline, loop) 52 | 53 | 54 | class _State(enum.Enum): 55 | INIT = "INIT" 56 | ENTER = "ENTER" 57 | TIMEOUT = "TIMEOUT" 58 | EXIT = "EXIT" 59 | 60 | 61 | if sys.version_info >= (3, 11): 62 | 63 | class _Expired: 64 | __slots__ = ("_val",) 65 | 66 | def __init__(self, val: bool) -> None: 67 | self._val = val 68 | 69 | def __call__(self) -> bool: 70 | return self._val 71 | 72 | def __bool__(self) -> bool: 73 | return self._val 74 | 75 | def __repr__(self) -> str: 76 | return repr(self._val) 77 | 78 | def __str__(self) -> str: 79 | return str(self._val) 80 | 81 | @final 82 | class Timeout(asyncio.Timeout): # type: ignore[misc] 83 | # Supports full asyncio.Timeout API. 84 | # Also provides several asyncio_timeout specific methods 85 | # for backward compatibility. 86 | def __init__( 87 | self, deadline: Optional[float], loop: asyncio.AbstractEventLoop 88 | ) -> None: 89 | super().__init__(deadline) 90 | 91 | @property 92 | def expired(self) -> _Expired: 93 | # a hacky property hat can provide both roles: 94 | # timeout.expired() from asyncio 95 | # timeout.expired from asyncio_timeout 96 | return _Expired(super().expired()) 97 | 98 | @property 99 | def deadline(self) -> Optional[float]: 100 | return self.when() 101 | 102 | def reject(self) -> None: 103 | """Reject scheduled timeout if any.""" 104 | # cancel is maybe better name but 105 | # task.cancel() raises CancelledError in asyncio world. 106 | self.reschedule(None) 107 | 108 | def shift(self, delay: float) -> None: 109 | """Advance timeout on delay seconds. 110 | 111 | The delay can be negative. 112 | 113 | Raise RuntimeError if shift is called when deadline is not scheduled 114 | """ 115 | deadline = self.when() 116 | if deadline is None: 117 | raise RuntimeError("cannot shift timeout if deadline is not scheduled") 118 | self.reschedule(deadline + delay) 119 | 120 | def update(self, deadline: float) -> None: 121 | """Set deadline to absolute value. 122 | 123 | deadline argument points on the time in the same clock system 124 | as loop.time(). 125 | 126 | If new deadline is in the past the timeout is raised immediately. 127 | 128 | Please note: it is not POSIX time but a time with 129 | undefined starting base, e.g. the time of the system power on. 130 | """ 131 | self.reschedule(deadline) 132 | 133 | else: 134 | 135 | @final 136 | class Timeout: 137 | # Internal class, please don't instantiate it directly 138 | # Use timeout() and timeout_at() public factories instead. 139 | # 140 | # Implementation note: `async with timeout()` is preferred 141 | # over `with timeout()`. 142 | # While technically the Timeout class implementation 143 | # doesn't need to be async at all, 144 | # the `async with` statement explicitly points that 145 | # the context manager should be used from async function context. 146 | # 147 | # This design allows to avoid many silly misusages. 148 | # 149 | # TimeoutError is raised immediately when scheduled 150 | # if the deadline is passed. 151 | # The purpose is to time out as soon as possible 152 | # without waiting for the next await expression. 153 | 154 | __slots__ = ("_deadline", "_loop", "_state", "_timeout_handler", "_task") 155 | 156 | def __init__( 157 | self, deadline: Optional[float], loop: asyncio.AbstractEventLoop 158 | ) -> None: 159 | self._loop = loop 160 | self._state = _State.INIT 161 | 162 | self._task: Optional["asyncio.Task[object]"] = None 163 | self._timeout_handler = None # type: Optional[asyncio.Handle] 164 | if deadline is None: 165 | self._deadline = None # type: Optional[float] 166 | else: 167 | self.update(deadline) 168 | 169 | async def __aenter__(self) -> "Timeout": 170 | self._do_enter() 171 | return self 172 | 173 | async def __aexit__( 174 | self, 175 | exc_type: Optional[Type[BaseException]], 176 | exc_val: Optional[BaseException], 177 | exc_tb: Optional[TracebackType], 178 | ) -> Optional[bool]: 179 | self._do_exit(exc_type) 180 | return None 181 | 182 | @property 183 | def expired(self) -> bool: 184 | """Is timeout expired during execution?""" 185 | return self._state == _State.TIMEOUT 186 | 187 | @property 188 | def deadline(self) -> Optional[float]: 189 | return self._deadline 190 | 191 | def reject(self) -> None: 192 | """Reject scheduled timeout if any.""" 193 | # cancel is maybe better name but 194 | # task.cancel() raises CancelledError in asyncio world. 195 | if self._state not in (_State.INIT, _State.ENTER): 196 | raise RuntimeError(f"invalid state {self._state.value}") 197 | self._reject() 198 | 199 | def _reject(self) -> None: 200 | self._task = None 201 | if self._timeout_handler is not None: 202 | self._timeout_handler.cancel() 203 | self._timeout_handler = None 204 | 205 | def shift(self, delay: float) -> None: 206 | """Advance timeout on delay seconds. 207 | 208 | The delay can be negative. 209 | 210 | Raise RuntimeError if shift is called when deadline is not scheduled 211 | """ 212 | deadline = self._deadline 213 | if deadline is None: 214 | raise RuntimeError("cannot shift timeout if deadline is not scheduled") 215 | self.update(deadline + delay) 216 | 217 | def update(self, deadline: float) -> None: 218 | """Set deadline to absolute value. 219 | 220 | deadline argument points on the time in the same clock system 221 | as loop.time(). 222 | 223 | If new deadline is in the past the timeout is raised immediately. 224 | 225 | Please note: it is not POSIX time but a time with 226 | undefined starting base, e.g. the time of the system power on. 227 | """ 228 | if self._state == _State.EXIT: 229 | raise RuntimeError("cannot reschedule after exit from context manager") 230 | if self._state == _State.TIMEOUT: 231 | raise RuntimeError("cannot reschedule expired timeout") 232 | if self._timeout_handler is not None: 233 | self._timeout_handler.cancel() 234 | self._deadline = deadline 235 | if self._state != _State.INIT: 236 | self._reschedule() 237 | 238 | def _reschedule(self) -> None: 239 | assert self._state == _State.ENTER 240 | deadline = self._deadline 241 | if deadline is None: 242 | return 243 | 244 | now = self._loop.time() 245 | if self._timeout_handler is not None: 246 | self._timeout_handler.cancel() 247 | 248 | self._task = asyncio.current_task() 249 | if deadline <= now: 250 | self._timeout_handler = self._loop.call_soon(self._on_timeout) 251 | else: 252 | self._timeout_handler = self._loop.call_at(deadline, self._on_timeout) 253 | 254 | def _do_enter(self) -> None: 255 | if self._state != _State.INIT: 256 | raise RuntimeError(f"invalid state {self._state.value}") 257 | self._state = _State.ENTER 258 | self._reschedule() 259 | 260 | def _do_exit(self, exc_type: Optional[Type[BaseException]]) -> None: 261 | if exc_type is asyncio.CancelledError and self._state == _State.TIMEOUT: 262 | assert self._task is not None 263 | self._timeout_handler = None 264 | self._task = None 265 | raise asyncio.TimeoutError 266 | # timeout has not expired 267 | self._state = _State.EXIT 268 | self._reject() 269 | return None 270 | 271 | def _on_timeout(self) -> None: 272 | assert self._task is not None 273 | self._task.cancel() 274 | self._state = _State.TIMEOUT 275 | # drop the reference early 276 | self._timeout_handler = None 277 | -------------------------------------------------------------------------------- /tests/test_timeout.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import sys 3 | import time 4 | from functools import wraps 5 | from typing import Any, Callable, List, TypeVar 6 | 7 | import pytest 8 | 9 | from async_timeout import timeout, timeout_at 10 | 11 | 12 | _Func = TypeVar("_Func", bound=Callable[..., Any]) 13 | 14 | 15 | def log_func(func: _Func, msg: str, call_order: List[str]) -> _Func: 16 | """Simple wrapper to add a log to call_order when the function is called.""" 17 | 18 | @wraps(func) 19 | def wrapper(*args: Any, **kwargs: Any) -> Any: # type: ignore[misc] 20 | call_order.append(msg) 21 | return func(*args, **kwargs) 22 | 23 | return wrapper # type: ignore[return-value] 24 | 25 | 26 | @pytest.mark.asyncio 27 | async def test_timeout() -> None: 28 | canceled_raised = False 29 | 30 | async def long_running_task() -> None: 31 | try: 32 | await asyncio.sleep(10) 33 | except asyncio.CancelledError: 34 | nonlocal canceled_raised 35 | canceled_raised = True 36 | raise 37 | 38 | with pytest.raises(asyncio.TimeoutError): 39 | async with timeout(0.01) as t: 40 | await long_running_task() 41 | assert t._loop is asyncio.get_event_loop() 42 | assert canceled_raised, "CancelledError was not raised" 43 | if sys.version_info >= (3, 11): 44 | task = asyncio.current_task() 45 | assert task is not None 46 | assert not task.cancelling() 47 | 48 | 49 | @pytest.mark.asyncio 50 | async def test_timeout_finish_in_time() -> None: 51 | async def long_running_task() -> str: 52 | await asyncio.sleep(0.01) 53 | return "done" 54 | 55 | # timeout should be long enough to work even on slow bisy test boxes 56 | async with timeout(0.5): 57 | resp = await long_running_task() 58 | assert resp == "done" 59 | 60 | 61 | @pytest.mark.asyncio 62 | async def test_timeout_disable() -> None: 63 | async def long_running_task() -> str: 64 | await asyncio.sleep(0.1) 65 | return "done" 66 | 67 | loop = asyncio.get_event_loop() 68 | t0 = loop.time() 69 | async with timeout(None): 70 | resp = await long_running_task() 71 | assert resp == "done" 72 | dt = loop.time() - t0 73 | assert 0.09 < dt < 0.3, dt 74 | 75 | 76 | @pytest.mark.asyncio 77 | async def test_timeout_is_none_no_schedule() -> None: 78 | async with timeout(None) as cm: 79 | assert cm._timeout_handler is None 80 | assert cm.deadline is None 81 | 82 | 83 | def test_timeout_no_loop() -> None: 84 | with pytest.raises(RuntimeError, match="no running event loop"): 85 | timeout(None) 86 | 87 | 88 | @pytest.mark.asyncio 89 | async def test_timeout_zero() -> None: 90 | with pytest.raises(asyncio.TimeoutError): 91 | async with timeout(0): 92 | await asyncio.sleep(10) 93 | 94 | 95 | @pytest.mark.asyncio 96 | async def test_timeout_not_relevant_exception() -> None: 97 | await asyncio.sleep(0) 98 | with pytest.raises(KeyError): 99 | async with timeout(0.1): 100 | raise KeyError 101 | 102 | 103 | @pytest.mark.asyncio 104 | async def test_timeout_canceled_error_is_not_converted_to_timeout() -> None: 105 | await asyncio.sleep(0) 106 | with pytest.raises(asyncio.CancelledError): 107 | async with timeout(0.001): 108 | raise asyncio.CancelledError 109 | 110 | 111 | @pytest.mark.asyncio 112 | async def test_timeout_blocking_loop() -> None: 113 | async def long_running_task() -> str: 114 | time.sleep(0.1) 115 | return "done" 116 | 117 | async with timeout(0.01): 118 | result = await long_running_task() 119 | assert result == "done" 120 | 121 | 122 | @pytest.mark.asyncio 123 | async def test_for_race_conditions() -> None: 124 | loop = asyncio.get_event_loop() 125 | fut = loop.create_future() 126 | loop.call_later(0.1, fut.set_result, "done") 127 | async with timeout(0.5): 128 | resp = await fut 129 | assert resp == "done" 130 | 131 | 132 | @pytest.mark.asyncio 133 | async def test_timeout_time() -> None: 134 | foo_running = None 135 | loop = asyncio.get_event_loop() 136 | start = loop.time() 137 | with pytest.raises(asyncio.TimeoutError): 138 | async with timeout(0.1): 139 | foo_running = True 140 | try: 141 | await asyncio.sleep(0.2) 142 | finally: 143 | foo_running = False 144 | 145 | dt = loop.time() - start 146 | assert 0.09 < dt < 0.3 147 | assert not foo_running 148 | 149 | 150 | @pytest.mark.asyncio 151 | async def test_outer_coro_is_not_cancelled() -> None: 152 | has_timeout = False 153 | 154 | async def outer() -> None: 155 | nonlocal has_timeout 156 | try: 157 | async with timeout(0.001): 158 | await asyncio.sleep(1) 159 | except asyncio.TimeoutError: 160 | has_timeout = True 161 | 162 | task = asyncio.ensure_future(outer()) 163 | await task 164 | assert has_timeout 165 | assert not task.cancelled() 166 | if sys.version_info >= (3, 11): 167 | assert not task.cancelling() 168 | assert task.done() 169 | 170 | 171 | @pytest.mark.asyncio 172 | async def test_cancel_outer_coro() -> None: 173 | loop = asyncio.get_event_loop() 174 | fut = loop.create_future() 175 | 176 | async def outer() -> None: 177 | fut.set_result(None) 178 | await asyncio.sleep(1) 179 | 180 | task = asyncio.ensure_future(outer()) 181 | await fut 182 | task.cancel() 183 | with pytest.raises(asyncio.CancelledError): 184 | await task 185 | assert task.cancelled() 186 | assert task.done() 187 | 188 | 189 | @pytest.mark.skipif( 190 | sys.version_info >= (3, 11), reason="3.11+ has a different implementation" 191 | ) 192 | @pytest.mark.asyncio 193 | async def test_timeout_suppress_exception_chain() -> None: 194 | with pytest.raises(asyncio.TimeoutError) as ctx: 195 | async with timeout(0.01): 196 | await asyncio.sleep(10) 197 | assert not ctx.value.__suppress_context__ 198 | 199 | 200 | @pytest.mark.asyncio 201 | async def test_timeout_expired() -> None: 202 | with pytest.raises(asyncio.TimeoutError): 203 | async with timeout(0.01) as cm: 204 | await asyncio.sleep(10) 205 | assert cm.expired 206 | 207 | 208 | @pytest.mark.skipif( 209 | sys.version_info < (3, 11), reason="Old versions don't support expired method" 210 | ) 211 | @pytest.mark.asyncio 212 | async def test_timeout_expired_as_function() -> None: 213 | with pytest.raises(asyncio.TimeoutError): 214 | async with timeout(0.01) as cm: 215 | await asyncio.sleep(10) 216 | assert cm.expired() 217 | 218 | 219 | @pytest.mark.skipif( 220 | sys.version_info < (3, 11), reason="Old versions don't support expired method" 221 | ) 222 | @pytest.mark.asyncio 223 | async def test_timeout_expired_methods() -> None: 224 | async with timeout(0.01) as cm: 225 | exp = cm.expired() 226 | assert not exp 227 | assert bool(exp) is False 228 | assert str(exp) == "False" 229 | assert repr(exp) == "False" 230 | 231 | 232 | @pytest.mark.asyncio 233 | async def test_timeout_inner_timeout_error() -> None: 234 | with pytest.raises(asyncio.TimeoutError): 235 | async with timeout(0.01) as cm: 236 | raise asyncio.TimeoutError 237 | assert not cm.expired 238 | 239 | 240 | @pytest.mark.asyncio 241 | async def test_timeout_inner_other_error() -> None: 242 | class MyError(RuntimeError): 243 | pass 244 | 245 | with pytest.raises(MyError): 246 | async with timeout(0.01) as cm: 247 | raise MyError 248 | assert not cm.expired 249 | 250 | 251 | @pytest.mark.asyncio 252 | async def test_timeout_at() -> None: 253 | loop = asyncio.get_event_loop() 254 | with pytest.raises(asyncio.TimeoutError): 255 | now = loop.time() 256 | async with timeout_at(now + 0.01) as cm: 257 | await asyncio.sleep(10) 258 | assert cm.expired 259 | 260 | 261 | @pytest.mark.asyncio 262 | async def test_timeout_at_not_fired() -> None: 263 | loop = asyncio.get_event_loop() 264 | now = loop.time() 265 | async with timeout_at(now + 1) as cm: 266 | await asyncio.sleep(0) 267 | assert not cm.expired 268 | 269 | 270 | @pytest.mark.asyncio 271 | async def test_expired_after_rejecting() -> None: 272 | async with timeout(10) as t: 273 | assert not t.expired 274 | t.reject() 275 | assert not t.expired 276 | 277 | 278 | @pytest.mark.asyncio 279 | async def test_reject_finished() -> None: 280 | async with timeout(10) as t: 281 | await asyncio.sleep(0) 282 | 283 | assert not t.expired 284 | with pytest.raises( 285 | RuntimeError, 286 | match="(invalid state EXIT)|(Cannot change state of finished Timeout)", 287 | ): 288 | t.reject() 289 | 290 | 291 | @pytest.mark.asyncio 292 | async def test_expired_after_timeout() -> None: 293 | with pytest.raises(asyncio.TimeoutError): 294 | async with timeout(0.01) as t: 295 | assert not t.expired 296 | await asyncio.sleep(10) 297 | assert t.expired 298 | 299 | 300 | @pytest.mark.asyncio 301 | async def test_deadline() -> None: 302 | loop = asyncio.get_event_loop() 303 | t0 = loop.time() 304 | async with timeout(1) as cm: 305 | t1 = loop.time() 306 | assert cm.deadline is not None 307 | assert t0 + 1 <= cm.deadline <= t1 + 1 308 | 309 | 310 | @pytest.mark.asyncio 311 | async def test_async_timeout() -> None: 312 | with pytest.raises(asyncio.TimeoutError): 313 | async with timeout(0.01) as cm: 314 | await asyncio.sleep(10) 315 | assert cm.expired 316 | 317 | 318 | @pytest.mark.asyncio 319 | async def test_async_no_timeout() -> None: 320 | async with timeout(1) as cm: 321 | await asyncio.sleep(0) 322 | assert not cm.expired 323 | 324 | 325 | @pytest.mark.asyncio 326 | async def test_shift() -> None: 327 | loop = asyncio.get_event_loop() 328 | t0 = loop.time() 329 | async with timeout(1) as cm: 330 | t1 = loop.time() 331 | assert cm.deadline is not None 332 | assert t0 + 1 <= cm.deadline <= t1 + 1 333 | cm.shift(1) 334 | assert t0 + 2 <= cm.deadline <= t0 + 2.1 335 | 336 | 337 | @pytest.mark.asyncio 338 | async def test_shift_nonscheduled() -> None: 339 | async with timeout(None) as cm: 340 | with pytest.raises( 341 | RuntimeError, 342 | match="cannot shift timeout if deadline is not scheduled", 343 | ): 344 | cm.shift(1) 345 | 346 | 347 | @pytest.mark.asyncio 348 | async def test_shift_negative_expired() -> None: 349 | async with timeout(1) as cm: 350 | with pytest.raises(asyncio.CancelledError): 351 | cm.shift(-1) 352 | await asyncio.sleep(10) 353 | 354 | 355 | @pytest.mark.asyncio 356 | async def test_shift_by_expired() -> None: 357 | async with timeout(0.001) as cm: 358 | with pytest.raises(asyncio.CancelledError): 359 | await asyncio.sleep(10) 360 | with pytest.raises( 361 | RuntimeError, 362 | match=( 363 | "(cannot reschedule expired timeout)|" 364 | "(Cannot change state of expiring Timeout)" 365 | ), 366 | ): 367 | cm.shift(10) 368 | 369 | 370 | @pytest.mark.asyncio 371 | async def test_shift_to_expired() -> None: 372 | loop = asyncio.get_event_loop() 373 | t0 = loop.time() 374 | async with timeout_at(t0 + 0.001) as cm: 375 | with pytest.raises(asyncio.CancelledError): 376 | await asyncio.sleep(10) 377 | with pytest.raises( 378 | RuntimeError, 379 | match=( 380 | "(cannot reschedule expired timeout)|" 381 | "(Cannot change state of expiring Timeout)" 382 | ), 383 | ): 384 | cm.update(t0 + 10) 385 | 386 | 387 | @pytest.mark.asyncio 388 | async def test_shift_by_after_cm_exit() -> None: 389 | async with timeout(1) as cm: 390 | await asyncio.sleep(0) 391 | with pytest.raises( 392 | RuntimeError, 393 | match=( 394 | "(cannot reschedule after exit from context manager)|" 395 | "(Cannot change state of finished Timeout)" 396 | ), 397 | ): 398 | cm.shift(1) 399 | 400 | 401 | @pytest.mark.asyncio 402 | async def test_enter_twice() -> None: 403 | async with timeout(10) as t: 404 | await asyncio.sleep(0) 405 | 406 | with pytest.raises( 407 | RuntimeError, match="(invalid state EXIT)|(Timeout has already been entered)" 408 | ): 409 | async with t: 410 | await asyncio.sleep(0) 411 | --------------------------------------------------------------------------------