├── .github ├── CODEOWNERS ├── actionlint-matcher.json ├── dependabot.yml └── workflows │ └── main.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .readthedocs.yaml ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.rst ├── SECURITY.rst ├── changelog.d └── .gitkeep ├── dependencies ├── default │ ├── constraints.txt │ └── requirements.txt ├── docs │ ├── constraints.txt │ └── requirements.txt └── pytest-min │ ├── constraints.txt │ └── requirements.txt ├── docs ├── concepts.rst ├── concepts_function_scope_example.py ├── concepts_module_scope_example.py ├── conf.py ├── how-to-guides │ ├── change_default_fixture_loop.rst │ ├── change_default_test_loop.rst │ ├── change_fixture_loop.rst │ ├── change_fixture_loop_example.py │ ├── class_scoped_loop_example.py │ ├── index.rst │ ├── migrate_from_0_21.rst │ ├── migrate_from_0_23.rst │ ├── module_scoped_loop_example.py │ ├── multiple_loops.rst │ ├── multiple_loops_example.py │ ├── package_scoped_loop_example.py │ ├── run_class_tests_in_same_loop.rst │ ├── run_module_tests_in_same_loop.rst │ ├── run_package_tests_in_same_loop.rst │ ├── test_item_is_async.rst │ ├── test_item_is_async_example.py │ └── uvloop.rst ├── index.rst ├── reference │ ├── changelog.rst │ ├── configuration.rst │ ├── decorators │ │ ├── index.rst │ │ └── pytest_asyncio_fixture_example.py │ ├── fixtures │ │ ├── event_loop_policy_example.py │ │ ├── event_loop_policy_parametrized_example.py │ │ └── index.rst │ ├── functions.rst │ ├── index.rst │ └── markers │ │ ├── class_scoped_loop_custom_policies_strict_mode_example.py │ │ ├── class_scoped_loop_strict_mode_example.py │ │ ├── class_scoped_loop_with_fixture_strict_mode_example.py │ │ ├── function_scoped_loop_pytestmark_strict_mode_example.py │ │ ├── function_scoped_loop_strict_mode_example.py │ │ ├── index.rst │ │ └── module_scoped_loop_strict_mode_example.py └── support.rst ├── pyproject.toml ├── pytest_asyncio ├── __init__.py ├── plugin.py └── py.typed ├── setup.cfg ├── tests ├── __init__.py ├── async_fixtures │ ├── __init__.py │ ├── test_async_fixtures.py │ ├── test_async_fixtures_contextvars.py │ ├── test_nested.py │ └── test_shared_module_fixture.py ├── conftest.py ├── hypothesis │ ├── __init__.py │ └── test_base.py ├── markers │ ├── __init__.py │ ├── test_class_scope.py │ ├── test_function_scope.py │ ├── test_invalid_arguments.py │ ├── test_mixed_scope.py │ ├── test_module_scope.py │ ├── test_package_scope.py │ └── test_session_scope.py ├── modes │ ├── __init__.py │ ├── test_auto_mode.py │ └── test_strict_mode.py ├── test_asyncio_fixture.py ├── test_asyncio_mark.py ├── test_doctest.py ├── test_event_loop_fixture.py ├── test_fixture_loop_scopes.py ├── test_import.py ├── test_is_async_test.py ├── test_port_factories.py ├── test_simple.py ├── test_skips.py └── test_subprocess.py ├── tools └── get-version.py └── tox.ini /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @asvetlov @seifertm @Tinche 2 | -------------------------------------------------------------------------------- /.github/actionlint-matcher.json: -------------------------------------------------------------------------------- 1 | { 2 | "problemMatcher": [ 3 | { 4 | "owner": "actionlint", 5 | "pattern": [ 6 | { 7 | "code": 5, 8 | "column": 3, 9 | "file": 1, 10 | "line": 2, 11 | "message": 4, 12 | "regexp": "^(?:\\x1b\\[\\d+m)?(.+?)(?:\\x1b\\[\\d+m)*:(?:\\x1b\\[\\d+m)*(\\d+)(?:\\x1b\\[\\d+m)*:(?:\\x1b\\[\\d+m)*(\\d+)(?:\\x1b\\[\\d+m)*: (?:\\x1b\\[\\d+m)*(.+?)(?:\\x1b\\[\\d+m)* \\[(.+?)\\]$" 13 | } 14 | ] 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2 3 | updates: 4 | - package-ecosystem: pip 5 | directory: /dependencies/default 6 | schedule: 7 | interval: weekly 8 | open-pull-requests-limit: 10 9 | target-branch: main 10 | - package-ecosystem: pip 11 | directory: /dependencies/docs 12 | schedule: 13 | interval: weekly 14 | open-pull-requests-limit: 10 15 | target-branch: main 16 | - package-ecosystem: github-actions 17 | directory: / 18 | schedule: 19 | interval: daily 20 | open-pull-requests-limit: 10 21 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: CI 3 | 4 | on: 5 | push: 6 | branches: [main] 7 | tags: [v*] 8 | pull_request: 9 | branches: [main] 10 | merge_group: 11 | workflow_dispatch: 12 | 13 | env: 14 | PYTHON_LATEST: 3.13 15 | 16 | jobs: 17 | lint: 18 | name: Run linters 19 | runs-on: ubuntu-latest 20 | outputs: 21 | version: ${{ steps.version.outputs.version }} 22 | prerelease: ${{ steps.version.outputs.prerelease }} 23 | steps: 24 | - uses: actions/checkout@v4 25 | with: 26 | fetch-depth: 0 27 | - uses: actions/setup-python@v5 28 | with: 29 | python-version: ${{ env.PYTHON_LATEST }} 30 | - name: Install GitHub matcher for ActionLint checker 31 | run: | 32 | echo "::add-matcher::.github/actionlint-matcher.json" 33 | - name: Install pre-commit 34 | run: python -m pip install pre-commit 35 | - name: Run pre-commit checks 36 | run: pre-commit run --all-files --show-diff-on-failure 37 | - name: Install check-wheel-content, and twine 38 | run: python -m pip install build check-wheel-contents twine 39 | - name: Build package 40 | run: python -m build 41 | - name: List result 42 | run: ls -l dist 43 | - name: Check wheel contents 44 | run: check-wheel-contents dist/*.whl 45 | - name: Check long_description 46 | run: python -m twine check dist/* 47 | - name: Install pytest-asyncio 48 | run: pip install . 49 | - name: Get version info 50 | id: version 51 | run: python ./tools/get-version.py >> $GITHUB_OUTPUT 52 | - name: Upload artifacts 53 | uses: actions/upload-artifact@v4 54 | with: 55 | name: dist 56 | path: dist 57 | 58 | test: 59 | name: ${{ matrix.os }} - Python ${{ matrix.python-version }} 60 | runs-on: ${{ matrix.os }}-latest 61 | continue-on-error: ${{ !matrix.required }} 62 | strategy: 63 | matrix: 64 | os: [ubuntu, windows] 65 | python-version: ['3.9', '3.10', '3.11', '3.12', '3.13'] 66 | required: [true] 67 | include: 68 | - os: ubuntu 69 | python-version: 3.14-dev 70 | required: false 71 | - os: windows 72 | python-version: 3.14-dev 73 | required: false 74 | 75 | 76 | steps: 77 | - uses: actions/checkout@v4 78 | - uses: actions/setup-python@v5 79 | with: 80 | python-version: ${{ matrix.python-version }} 81 | - name: Install dependencies 82 | run: | 83 | python -VV 84 | python -m site 85 | python -m pip install --upgrade pip 86 | python -m pip install --upgrade coverage[toml] virtualenv tox tox-gh-actions 87 | - name: Run tox targets for ${{ matrix.python-version }} 88 | run: python -m tox 89 | 90 | - name: Store coverage data 91 | uses: actions/upload-artifact@v4 92 | if: "!endsWith(matrix.os, 'windows')" 93 | with: 94 | name: coverage-python-${{ matrix.python-version }} 95 | path: coverage/coverage.* 96 | if-no-files-found: error 97 | 98 | check: 99 | name: Check 100 | if: always() 101 | needs: [lint, test] 102 | runs-on: ubuntu-latest 103 | steps: 104 | - name: Decide whether the needed jobs succeeded or failed 105 | uses: re-actors/alls-green@release/v1 106 | with: 107 | jobs: ${{ toJSON(needs) }} 108 | - uses: actions/checkout@v4 109 | - uses: actions/setup-python@v5 110 | with: 111 | python-version: ${{ env.PYTHON_LATEST }} 112 | - name: Install Coverage.py 113 | run: | 114 | set -xe 115 | python -m pip install --upgrade coverage[toml] 116 | - name: Download coverage data for all test runs 117 | uses: actions/download-artifact@v4 118 | with: 119 | pattern: coverage-* 120 | path: coverage 121 | merge-multiple: true 122 | - name: Combine coverage data and create report 123 | run: | 124 | coverage combine 125 | coverage xml 126 | - name: Upload coverage report 127 | uses: codecov/codecov-action@v5 128 | with: 129 | files: coverage.xml 130 | fail_ci_if_error: true 131 | token: ${{ secrets.CODECOV_TOKEN }} 132 | 133 | prepare-release-notes: 134 | name: Prepare Release Notes 135 | needs: [lint] 136 | runs-on: ubuntu-latest 137 | steps: 138 | - name: Checkout 139 | uses: actions/checkout@v4 140 | with: 141 | fetch-depth: 0 142 | - name: Install Python 143 | uses: actions/setup-python@v5 144 | - name: Install towncrier 145 | run: pip install towncrier==24.8.0 146 | - name: Install pandoc 147 | run: | 148 | sudo apt-get install -y pandoc 149 | - name: Install pytest-asyncio 150 | run: pip install . 151 | - name: Compile Release Notes Draft 152 | if: ${{ !contains(github.ref, 'refs/tags/') }} 153 | run: towncrier build --draft --version "${{ needs.lint.outputs.version }}" > release-notes.rst 154 | - name: Extract release notes from Git tag 155 | if: github.event_name == 'push' && contains(github.ref, 'refs/tags/') 156 | run: | 157 | set -e 158 | git for-each-ref "${GITHUB_REF}" --format='%(contents)' > release-notes.rst 159 | # Strip PGP signature from signed tags 160 | sed -i "/-----BEGIN PGP SIGNATURE-----/,/-----END PGP SIGNATURE-----\n/d" release-notes.rst 161 | - name: Convert Release Notes to Markdown 162 | run: | 163 | pandoc -s -o release-notes.md release-notes.rst 164 | - name: Upload artifacts 165 | uses: actions/upload-artifact@v4 166 | with: 167 | name: release-notes.md 168 | path: release-notes.md 169 | 170 | deploy: 171 | name: Deploy 172 | environment: release 173 | # Run only on pushing a tag 174 | if: github.event_name == 'push' && contains(github.ref, 'refs/tags/') 175 | needs: [lint, check, prepare-release-notes] 176 | runs-on: ubuntu-latest 177 | steps: 178 | - name: Download distributions 179 | uses: actions/download-artifact@v4 180 | with: 181 | name: dist 182 | path: dist 183 | - name: Collected dists 184 | run: | 185 | tree dist 186 | - name: PyPI upload 187 | uses: pypa/gh-action-pypi-publish@v1.12.4 188 | with: 189 | attestations: true 190 | packages-dir: dist 191 | password: ${{ secrets.PYPI_API_TOKEN }} 192 | - name: Download Release Notes 193 | uses: actions/download-artifact@v4 194 | with: 195 | name: release-notes.md 196 | path: release-notes.md 197 | - name: GitHub Release 198 | uses: ncipollo/release-action@v1 199 | with: 200 | name: pytest-asyncio ${{ needs.lint.outputs.version }} 201 | artifacts: dist/* 202 | bodyFile: release-notes.md 203 | prerelease: ${{ needs.lint.outputs.prerelease }} 204 | token: ${{ secrets.GITHUB_TOKEN }} 205 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | *.egg-info/ 23 | .installed.cfg 24 | *.egg 25 | .hypothesis/ 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 | .pytest_cache 42 | nosetests.xml 43 | coverage.xml 44 | *,cover 45 | 46 | # Translations 47 | *.mo 48 | *.pot 49 | 50 | # Django stuff: 51 | *.log 52 | 53 | # Sphinx documentation 54 | docs/_build/ 55 | 56 | # PyBuilder 57 | target/ 58 | 59 | .venv* 60 | .idea 61 | .vscode 62 | 63 | # pyenv 64 | .python-version 65 | 66 | 67 | # generated by setuptools_scm 68 | pytest_asyncio/_version.py 69 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | repos: 3 | - repo: https://github.com/pre-commit/pre-commit-hooks 4 | rev: v5.0.0 5 | hooks: 6 | - id: check-merge-conflict 7 | exclude: rst$ 8 | - repo: https://github.com/astral-sh/ruff-pre-commit 9 | rev: v0.11.11 10 | hooks: 11 | - id: ruff 12 | args: [--fix] 13 | - repo: https://github.com/asottile/yesqa 14 | rev: v1.5.0 15 | hooks: 16 | - id: yesqa 17 | - repo: https://github.com/Zac-HD/shed 18 | rev: 2024.10.1 19 | hooks: 20 | - id: shed 21 | args: 22 | - --refactor 23 | types_or: 24 | - python 25 | - markdown 26 | - rst 27 | - repo: https://github.com/jumanjihouse/pre-commit-hook-yamlfmt 28 | rev: 0.2.3 29 | hooks: 30 | - id: yamlfmt 31 | args: [--mapping, '2', --sequence, '2', --offset, '0'] 32 | - repo: https://github.com/pre-commit/pre-commit-hooks 33 | rev: v5.0.0 34 | hooks: 35 | - id: trailing-whitespace 36 | - id: end-of-file-fixer 37 | - id: fix-encoding-pragma 38 | args: [--remove] 39 | - id: check-case-conflict 40 | - id: check-json 41 | - id: check-xml 42 | - id: check-yaml 43 | - id: debug-statements 44 | - repo: https://github.com/pre-commit/mirrors-mypy 45 | rev: v1.15.0 46 | hooks: 47 | - id: mypy 48 | exclude: ^(docs|tests)/.* 49 | additional_dependencies: 50 | - pytest 51 | - repo: https://github.com/pre-commit/pygrep-hooks 52 | rev: v1.10.0 53 | hooks: 54 | - id: python-use-type-annotations 55 | - repo: https://github.com/rhysd/actionlint 56 | rev: v1.7.7 57 | hooks: 58 | - id: actionlint-docker 59 | args: 60 | - -ignore 61 | - 'SC2155:' 62 | - -ignore 63 | - 'SC2086:' 64 | - -ignore 65 | - 'SC1004:' 66 | stages: [manual] 67 | - repo: https://github.com/sirosen/check-jsonschema 68 | rev: 0.33.0 69 | hooks: 70 | - id: check-github-actions 71 | - repo: https://github.com/tox-dev/pyproject-fmt 72 | rev: v2.6.0 73 | hooks: 74 | - id: pyproject-fmt 75 | # https://pyproject-fmt.readthedocs.io/en/latest/#calculating-max-supported-python-version 76 | additional_dependencies: [tox>=4.9] 77 | ci: 78 | skip: 79 | - actionlint-docker 80 | - check-github-actions 81 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | version: 2 4 | 5 | build: 6 | os: ubuntu-24.04 7 | tools: 8 | python: >- 9 | 3.12 10 | commands: 11 | - >- 12 | PYTHONWARNINGS=error 13 | python3 -Im venv "${READTHEDOCS_VIRTUALENV_PATH}" 14 | - >- 15 | PYTHONWARNINGS=error 16 | "${READTHEDOCS_VIRTUALENV_PATH}"/bin/python -Im 17 | pip install tox 18 | - >- 19 | PYTHONWARNINGS=error 20 | "${READTHEDOCS_VIRTUALENV_PATH}"/bin/python -Im 21 | tox -e docs --notest -vvvvv 22 | - >- 23 | PYTHONWARNINGS=error 24 | "${READTHEDOCS_VIRTUALENV_PATH}"/bin/python -Im 25 | tox -e docs --skip-pkg-install -q 26 | -- 27 | "${READTHEDOCS_OUTPUT}"/html 28 | -b html 29 | -D language=en 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-exclude .github * 2 | exclude .gitignore 3 | exclude .pre-commit-config.yaml 4 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: clean clean-build clean-pyc clean-test lint test 2 | 3 | clean: clean-build clean-pyc clean-test ## remove all build, test, coverage and Python artifacts 4 | 5 | clean-build: ## remove build artifacts 6 | rm -fr build/ 7 | rm -fr dist/ 8 | rm -fr .eggs/ 9 | find . -name '*.egg-info' -exec rm -fr {} + 10 | find . -name '*.egg' -exec rm -f {} + 11 | 12 | clean-pyc: ## remove Python file artifacts 13 | find . -name '*.pyc' -exec rm -f {} + 14 | find . -name '*.pyo' -exec rm -f {} + 15 | find . -name '*~' -exec rm -f {} + 16 | find . -name '__pycache__' -exec rm -fr {} + 17 | 18 | clean-test: ## remove test and coverage artifacts 19 | rm -fr .tox/ 20 | rm -fr coverage/ 21 | rm -fr htmlcov/ 22 | 23 | test: 24 | coverage run -m pytest 25 | 26 | install: 27 | pip install -U pre-commit 28 | pre-commit install 29 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | pytest-asyncio 2 | ============== 3 | 4 | .. image:: https://img.shields.io/pypi/v/pytest-asyncio.svg 5 | :target: https://pypi.python.org/pypi/pytest-asyncio 6 | .. image:: https://github.com/pytest-dev/pytest-asyncio/workflows/CI/badge.svg 7 | :target: https://github.com/pytest-dev/pytest-asyncio/actions?workflow=CI 8 | .. image:: https://codecov.io/gh/pytest-dev/pytest-asyncio/branch/main/graph/badge.svg 9 | :target: https://codecov.io/gh/pytest-dev/pytest-asyncio 10 | .. image:: https://img.shields.io/pypi/pyversions/pytest-asyncio.svg 11 | :target: https://github.com/pytest-dev/pytest-asyncio 12 | :alt: Supported Python versions 13 | .. image:: https://img.shields.io/badge/Matrix-%23pytest--asyncio-brightgreen 14 | :alt: Matrix chat room: #pytest-asyncio 15 | :target: https://matrix.to/#/#pytest-asyncio:matrix.org 16 | 17 | `pytest-asyncio `_ is a `pytest `_ plugin. It facilitates testing of code that uses the `asyncio `_ library. 18 | 19 | Specifically, pytest-asyncio provides support for coroutines as test functions. This allows users to *await* code inside their tests. For example, the following code is executed as a test item by pytest: 20 | 21 | .. code-block:: python 22 | 23 | @pytest.mark.asyncio 24 | async def test_some_asyncio_code(): 25 | res = await library.do_something() 26 | assert b"expected result" == res 27 | 28 | More details can be found in the `documentation `_. 29 | 30 | Note that test classes subclassing the standard `unittest `__ library are not supported. Users 31 | are advised to use `unittest.IsolatedAsyncioTestCase `__ 32 | or an async framework such as `asynctest `__. 33 | 34 | 35 | pytest-asyncio is available under the `Apache License 2.0 `_. 36 | 37 | 38 | Installation 39 | ------------ 40 | 41 | To install pytest-asyncio, simply: 42 | 43 | .. code-block:: bash 44 | 45 | $ pip install pytest-asyncio 46 | 47 | This is enough for pytest to pick up pytest-asyncio. 48 | 49 | 50 | Contributing 51 | ------------ 52 | Contributions are very welcome. Tests can be run with ``tox``, please ensure 53 | the coverage at least stays the same before you submit a pull request. 54 | -------------------------------------------------------------------------------- /SECURITY.rst: -------------------------------------------------------------------------------- 1 | Security contact information 2 | ============================ 3 | 4 | To report a security vulnerability, please use the `Tidelift security contact. `__ Tidelift will coordinate the fix and disclosure. 5 | -------------------------------------------------------------------------------- /changelog.d/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pytest-dev/pytest-asyncio/bd741b82e22030732e42a92eedebdb133cc3be36/changelog.d/.gitkeep -------------------------------------------------------------------------------- /dependencies/default/constraints.txt: -------------------------------------------------------------------------------- 1 | attrs==25.3.0 2 | coverage==7.8.2 3 | exceptiongroup==1.3.0 4 | hypothesis==6.131.28 5 | iniconfig==2.1.0 6 | packaging==25.0 7 | pluggy==1.6.0 8 | pytest==8.3.5 9 | sortedcontainers==2.4.0 10 | tomli==2.2.1 11 | typing_extensions==4.13.2 12 | -------------------------------------------------------------------------------- /dependencies/default/requirements.txt: -------------------------------------------------------------------------------- 1 | # Always adjust install_requires in setup.cfg and pytest-min-requirements.txt 2 | # when changing runtime dependencies 3 | pytest >= 8.2,<9 4 | -------------------------------------------------------------------------------- /dependencies/docs/constraints.txt: -------------------------------------------------------------------------------- 1 | alabaster==0.7.16 2 | Babel==2.17.0 3 | certifi==2025.4.26 4 | charset-normalizer==3.4.2 5 | docutils==0.21.2 6 | idna==3.10 7 | imagesize==1.4.1 8 | Jinja2==3.1.6 9 | MarkupSafe==3.0.2 10 | packaging==25.0 11 | Pygments==2.19.1 12 | requests==2.32.3 13 | snowballstemmer==3.0.1 14 | Sphinx==8.0.2 15 | sphinx-rtd-theme==3.0.2 16 | sphinxcontrib-applehelp==2.0.0 17 | sphinxcontrib-devhelp==2.0.0 18 | sphinxcontrib-htmlhelp==2.1.0 19 | sphinxcontrib-jquery==4.1 20 | sphinxcontrib-jsmath==1.0.1 21 | sphinxcontrib-qthelp==2.0.0 22 | sphinxcontrib-serializinghtml==2.0.0 23 | urllib3==2.4.0 24 | -------------------------------------------------------------------------------- /dependencies/docs/requirements.txt: -------------------------------------------------------------------------------- 1 | sphinx >= 5.3 2 | sphinx-rtd-theme >= 1.0 3 | -------------------------------------------------------------------------------- /dependencies/pytest-min/constraints.txt: -------------------------------------------------------------------------------- 1 | argcomplete==3.1.2 2 | attrs==23.1.0 3 | certifi==2023.7.22 4 | charset-normalizer==3.3.1 5 | coverage==7.3.2 6 | elementpath==4.1.5 7 | exceptiongroup==1.1.3 8 | hypothesis==6.88.3 9 | idna==3.4 10 | iniconfig==2.0.0 11 | mock==5.1.0 12 | nose==1.3.7 13 | packaging==23.2 14 | pluggy==1.5.0 15 | py==1.11.0 16 | Pygments==2.16.1 17 | pytest==8.2.0 18 | requests==2.31.0 19 | sortedcontainers==2.4.0 20 | tomli==2.0.1 21 | urllib3==2.0.7 22 | xmlschema==2.5.0 23 | -------------------------------------------------------------------------------- /dependencies/pytest-min/requirements.txt: -------------------------------------------------------------------------------- 1 | # Always adjust install_requires in setup.cfg and requirements.txt 2 | # when changing minimum version dependencies 3 | pytest[testing] == 8.2.0 4 | -------------------------------------------------------------------------------- /docs/concepts.rst: -------------------------------------------------------------------------------- 1 | ======== 2 | Concepts 3 | ======== 4 | 5 | .. _concepts/event_loops: 6 | 7 | asyncio event loops 8 | =================== 9 | In order to understand how pytest-asyncio works, it helps to understand how pytest collectors work. 10 | If you already know about pytest collectors, please :ref:`skip ahead `. 11 | Otherwise, continue reading. 12 | Let's assume we have a test suite with a file named *test_all_the_things.py* holding a single test, async or not: 13 | 14 | .. include:: concepts_function_scope_example.py 15 | :code: python 16 | 17 | The file *test_all_the_things.py* is a Python module with a Python test function. 18 | When we run pytest, the test runner descends into Python packages, modules, and classes, in order to find all tests, regardless whether the tests will run or not. 19 | This process is referred to as *test collection* by pytest. 20 | In our particular example, pytest will find our test module and the test function. 21 | We can visualize the collection result by running ``pytest --collect-only``:: 22 | 23 | 24 | 25 | 26 | The example illustrates that the code of our test suite is hierarchical. 27 | Pytest uses so called *collectors* for each level of the hierarchy. 28 | Our contrived example test suite uses the *Module* and *Function* collectors, but real world test code may contain additional hierarchy levels via the *Package* or *Class* collectors. 29 | There's also a special *Session* collector at the root of the hierarchy. 30 | You may notice that the individual levels resemble the possible `scopes of a pytest fixture. `__ 31 | 32 | .. _pytest-asyncio-event-loops: 33 | 34 | Pytest-asyncio provides one asyncio event loop for each pytest collector. 35 | By default, each test runs in the event loop provided by the *Function* collector, i.e. tests use the loop with the narrowest scope. 36 | This gives the highest level of isolation between tests. 37 | If two or more tests share a common ancestor collector, the tests can be configured to run in their ancestor's loop by passing the appropriate *loop_scope* keyword argument to the *asyncio* mark. 38 | For example, the following two tests use the asyncio event loop provided by the *Module* collector: 39 | 40 | .. include:: concepts_module_scope_example.py 41 | :code: python 42 | 43 | It's highly recommended for neighboring tests to use the same event loop scope. 44 | For example, all tests in a class or module should use the same scope. 45 | Assigning neighboring tests to different event loop scopes is discouraged as it can make test code hard to follow. 46 | 47 | Test discovery modes 48 | ==================== 49 | 50 | Pytest-asyncio provides two modes for test discovery, *strict* and *auto*. This can be set through Pytest's ``--asyncio-mode`` command line flag, or through the configuration file: 51 | 52 | .. code-block:: toml 53 | 54 | [tool.pytest.ini_options] 55 | asyncio_mode = "auto" # or "strict" 56 | 57 | Strict mode 58 | ----------- 59 | 60 | In strict mode pytest-asyncio will only run tests that have the *asyncio* marker and will only evaluate async fixtures decorated with ``@pytest_asyncio.fixture``. Test functions and fixtures without these markers and decorators will not be handled by pytest-asyncio. 61 | 62 | This mode is intended for projects that want so support multiple asynchronous programming libraries as it allows pytest-asyncio to coexist with other async testing plugins in the same codebase. 63 | 64 | Pytest automatically enables installed plugins. As a result pytest plugins need to coexist peacefully in their default configuration. This is why strict mode is the default mode. 65 | 66 | Auto mode 67 | --------- 68 | 69 | In *auto* mode pytest-asyncio automatically adds the *asyncio* marker to all asynchronous test functions. It will also take ownership of all async fixtures, regardless of whether they are decorated with ``@pytest.fixture`` or ``@pytest_asyncio.fixture``. 70 | 71 | This mode is intended for projects that use *asyncio* as their only asynchronous programming library. Auto mode makes for the simplest test and fixture configuration and is the recommended default. 72 | 73 | If you intend to support multiple asynchronous programming libraries, e.g. *asyncio* and *trio*, strict mode will be the preferred option. 74 | -------------------------------------------------------------------------------- /docs/concepts_function_scope_example.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | import pytest 4 | 5 | 6 | @pytest.mark.asyncio 7 | async def test_runs_in_a_loop(): 8 | assert asyncio.get_running_loop() 9 | -------------------------------------------------------------------------------- /docs/concepts_module_scope_example.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | import pytest 4 | 5 | loop: asyncio.AbstractEventLoop 6 | 7 | 8 | @pytest.mark.asyncio(loop_scope="module") 9 | async def test_remember_loop(): 10 | global loop 11 | loop = asyncio.get_running_loop() 12 | 13 | 14 | @pytest.mark.asyncio(loop_scope="module") 15 | async def test_runs_in_a_loop(): 16 | global loop 17 | assert asyncio.get_running_loop() is loop 18 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # For the full list of built-in configuration values, see the documentation: 4 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 5 | 6 | # -- Project information ----------------------------------------------------- 7 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information 8 | 9 | import importlib.metadata 10 | 11 | project = "pytest-asyncio" 12 | copyright = "2023, pytest-asyncio contributors" 13 | author = "Tin Tvrtković" 14 | release = importlib.metadata.version(project) 15 | 16 | # -- General configuration --------------------------------------------------- 17 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration 18 | 19 | extensions = [] 20 | 21 | templates_path = ["_templates"] 22 | exclude_patterns = [] 23 | 24 | 25 | # -- Options for HTML output ------------------------------------------------- 26 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output 27 | 28 | html_theme = "sphinx_rtd_theme" 29 | html_static_path = [] 30 | -------------------------------------------------------------------------------- /docs/how-to-guides/change_default_fixture_loop.rst: -------------------------------------------------------------------------------- 1 | ========================================================== 2 | How to change the default event loop scope of all fixtures 3 | ========================================================== 4 | The :ref:`configuration/asyncio_default_fixture_loop_scope` configuration option sets the default event loop scope for asynchronous fixtures. The following code snippets configure all fixtures to run in a session-scoped loop by default: 5 | 6 | .. code-block:: ini 7 | :caption: pytest.ini 8 | 9 | [pytest] 10 | asyncio_default_fixture_loop_scope = session 11 | 12 | .. code-block:: toml 13 | :caption: pyproject.toml 14 | 15 | [tool.pytest.ini_options] 16 | asyncio_default_fixture_loop_scope = "session" 17 | 18 | .. code-block:: ini 19 | :caption: setup.cfg 20 | 21 | [tool:pytest] 22 | asyncio_default_fixture_loop_scope = session 23 | 24 | Please refer to :ref:`configuration/asyncio_default_fixture_loop_scope` for other valid scopes. 25 | -------------------------------------------------------------------------------- /docs/how-to-guides/change_default_test_loop.rst: -------------------------------------------------------------------------------- 1 | ======================================================= 2 | How to change the default event loop scope of all tests 3 | ======================================================= 4 | The :ref:`configuration/asyncio_default_test_loop_scope` configuration option sets the default event loop scope for asynchronous tests. The following code snippets configure all tests to run in a session-scoped loop by default: 5 | 6 | .. code-block:: ini 7 | :caption: pytest.ini 8 | 9 | [pytest] 10 | asyncio_default_test_loop_scope = session 11 | 12 | .. code-block:: toml 13 | :caption: pyproject.toml 14 | 15 | [tool.pytest.ini_options] 16 | asyncio_default_test_loop_scope = "session" 17 | 18 | .. code-block:: ini 19 | :caption: setup.cfg 20 | 21 | [tool:pytest] 22 | asyncio_default_test_loop_scope = session 23 | 24 | Please refer to :ref:`configuration/asyncio_default_test_loop_scope` for other valid scopes. 25 | -------------------------------------------------------------------------------- /docs/how-to-guides/change_fixture_loop.rst: -------------------------------------------------------------------------------- 1 | =============================================== 2 | How to change the event loop scope of a fixture 3 | =============================================== 4 | The event loop scope of an asynchronous fixture is specified via the *loop_scope* keyword argument to :ref:`pytest_asyncio.fixture `. The following fixture runs in the module-scoped event loop: 5 | 6 | .. include:: change_fixture_loop_example.py 7 | :code: python 8 | -------------------------------------------------------------------------------- /docs/how-to-guides/change_fixture_loop_example.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | import pytest 4 | 5 | import pytest_asyncio 6 | 7 | 8 | @pytest_asyncio.fixture(loop_scope="module") 9 | async def current_loop(): 10 | return asyncio.get_running_loop() 11 | 12 | 13 | @pytest.mark.asyncio(loop_scope="module") 14 | async def test_runs_in_module_loop(current_loop): 15 | assert current_loop is asyncio.get_running_loop() 16 | -------------------------------------------------------------------------------- /docs/how-to-guides/class_scoped_loop_example.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | import pytest 4 | 5 | 6 | @pytest.mark.asyncio(loop_scope="class") 7 | class TestInOneEventLoopPerClass: 8 | loop: asyncio.AbstractEventLoop 9 | 10 | async def test_remember_loop(self): 11 | TestInOneEventLoopPerClass.loop = asyncio.get_running_loop() 12 | 13 | async def test_assert_same_loop(self): 14 | assert asyncio.get_running_loop() is TestInOneEventLoopPerClass.loop 15 | -------------------------------------------------------------------------------- /docs/how-to-guides/index.rst: -------------------------------------------------------------------------------- 1 | ============= 2 | How-To Guides 3 | ============= 4 | 5 | .. toctree:: 6 | :hidden: 7 | 8 | migrate_from_0_21 9 | migrate_from_0_23 10 | change_fixture_loop 11 | change_default_fixture_loop 12 | change_default_test_loop 13 | run_class_tests_in_same_loop 14 | run_module_tests_in_same_loop 15 | run_package_tests_in_same_loop 16 | multiple_loops 17 | uvloop 18 | test_item_is_async 19 | 20 | This section of the documentation provides code snippets and recipes to accomplish specific tasks with pytest-asyncio. 21 | -------------------------------------------------------------------------------- /docs/how-to-guides/migrate_from_0_21.rst: -------------------------------------------------------------------------------- 1 | .. _how_to_guides/migrate_from_0_21: 2 | 3 | ======================================== 4 | How to migrate from pytest-asyncio v0.21 5 | ======================================== 6 | 1. If your test suite re-implements the *event_loop* fixture, make sure the fixture implementations don't do anything besides creating a new asyncio event loop, yielding it, and closing it. 7 | 2. Convert all synchronous test cases requesting the *event_loop* fixture to asynchronous test cases. 8 | 3. Convert all synchronous fixtures requesting the *event_loop* fixture to asynchronous fixtures. 9 | 4. Remove the *event_loop* argument from all asynchronous test cases in favor of ``event_loop = asyncio.get_running_loop()``. 10 | 5. Remove the *event_loop* argument from all asynchronous fixtures in favor of ``event_loop = asyncio.get_running_loop()``. 11 | 12 | Go through all re-implemented *event_loop* fixtures in your test suite one by one, starting with the the fixture with the deepest nesting level and take note of the fixture scope: 13 | 14 | 1. For all tests and fixtures affected by the re-implemented *event_loop* fixture, configure the *loop_scope* for async tests and fixtures to match the *event_loop* fixture scope. This can be done for each test and fixture individually using either the ``pytest.mark.asyncio(loop_scope="…")`` marker for async tests or ``@pytest_asyncio.fixture(loop_scope="…")`` for async fixtures. Alternatively, you can set the default loop scope for fixtures using the :ref:`asyncio_default_fixture_loop_scope ` configuration option. Snippets to mark all tests with the same *asyncio* marker, thus sharing the same loop scope, are present in the how-to section of the documentation. Depending on the homogeneity of your test suite, you may want a mixture of explicit decorators and default settings. 15 | 2. Remove the re-implemented *event_loop* fixture. 16 | 17 | If you haven't set the *asyncio_default_fixture_loop_scope* configuration option, yet, set it to *function* to silence the deprecation warning. 18 | -------------------------------------------------------------------------------- /docs/how-to-guides/migrate_from_0_23.rst: -------------------------------------------------------------------------------- 1 | ======================================== 2 | How to migrate from pytest-asyncio v0.23 3 | ======================================== 4 | The following steps assume that your test suite has no re-implementations of the *event_loop* fixture, nor explicit fixtures requests for it. If this isn't the case, please follow the :ref:`migration guide for pytest-asyncio v0.21. ` 5 | 6 | 1. Explicitly set the *loop_scope* of async fixtures by replacing occurrences of ``@pytest.fixture(scope="…")`` and ``@pytest_asyncio.fixture(scope="…")`` with ``@pytest_asyncio.fixture(loop_scope="…", scope="…")`` such that *loop_scope* and *scope* are the same. If you use auto mode, resolve all import errors from missing imports of *pytest_asyncio*. If your async fixtures all use the same *loop_scope*, you may choose to set the *asyncio_default_fixture_loop_scope* configuration option to that loop scope, instead. 7 | 2. If you haven't set *asyncio_default_fixture_loop_scope*, set it to *function* to address the deprecation warning about the unset configuration option. 8 | 3. Change all occurrences of ``pytest.mark.asyncio(scope="…")`` to ``pytest.mark.asyncio(loop_scope="…")`` to address the deprecation warning about the *scope* argument to the *asyncio* marker. 9 | -------------------------------------------------------------------------------- /docs/how-to-guides/module_scoped_loop_example.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | import pytest 4 | 5 | pytestmark = pytest.mark.asyncio(loop_scope="module") 6 | 7 | loop: asyncio.AbstractEventLoop 8 | 9 | 10 | async def test_remember_loop(): 11 | global loop 12 | loop = asyncio.get_running_loop() 13 | 14 | 15 | async def test_assert_same_loop(): 16 | global loop 17 | assert asyncio.get_running_loop() is loop 18 | -------------------------------------------------------------------------------- /docs/how-to-guides/multiple_loops.rst: -------------------------------------------------------------------------------- 1 | ====================================== 2 | How to test with different event loops 3 | ====================================== 4 | 5 | Parametrizing the *event_loop_policy* fixture parametrizes all async tests. The following example causes all async tests to run multiple times, once for each event loop in the fixture parameters: 6 | 7 | .. include:: multiple_loops_example.py 8 | :code: python 9 | 10 | You may choose to limit the scope of the fixture to *package,* *module,* or *class,* if you only want a subset of your tests to run with different event loops. 11 | -------------------------------------------------------------------------------- /docs/how-to-guides/multiple_loops_example.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import warnings 3 | 4 | with warnings.catch_warnings(): 5 | warnings.simplefilter("ignore", DeprecationWarning) 6 | from asyncio import DefaultEventLoopPolicy 7 | 8 | import pytest 9 | 10 | 11 | class CustomEventLoopPolicy(DefaultEventLoopPolicy): 12 | pass 13 | 14 | 15 | @pytest.fixture( 16 | scope="session", 17 | params=( 18 | CustomEventLoopPolicy(), 19 | CustomEventLoopPolicy(), 20 | ), 21 | ) 22 | def event_loop_policy(request): 23 | return request.param 24 | 25 | 26 | @pytest.mark.asyncio 27 | @pytest.mark.filterwarnings("ignore::DeprecationWarning") 28 | async def test_uses_custom_event_loop_policy(): 29 | assert isinstance(asyncio.get_event_loop_policy(), CustomEventLoopPolicy) 30 | -------------------------------------------------------------------------------- /docs/how-to-guides/package_scoped_loop_example.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | pytestmark = pytest.mark.asyncio(loop_scope="package") 4 | -------------------------------------------------------------------------------- /docs/how-to-guides/run_class_tests_in_same_loop.rst: -------------------------------------------------------------------------------- 1 | ====================================================== 2 | How to run all tests in a class in the same event loop 3 | ====================================================== 4 | All tests can be run inside the same event loop by marking them with ``pytest.mark.asyncio(loop_scope="class")``. 5 | This is easily achieved by using the *asyncio* marker as a class decorator. 6 | 7 | .. include:: class_scoped_loop_example.py 8 | :code: python 9 | -------------------------------------------------------------------------------- /docs/how-to-guides/run_module_tests_in_same_loop.rst: -------------------------------------------------------------------------------- 1 | ======================================================= 2 | How to run all tests in a module in the same event loop 3 | ======================================================= 4 | All tests can be run inside the same event loop by marking them with ``pytest.mark.asyncio(loop_scope="module")``. 5 | This is easily achieved by adding a `pytestmark` statement to your module. 6 | 7 | .. include:: module_scoped_loop_example.py 8 | :code: python 9 | -------------------------------------------------------------------------------- /docs/how-to-guides/run_package_tests_in_same_loop.rst: -------------------------------------------------------------------------------- 1 | ======================================================== 2 | How to run all tests in a package in the same event loop 3 | ======================================================== 4 | All tests can be run inside the same event loop by marking them with ``pytest.mark.asyncio(loop_scope="package")``. 5 | Add the following code to the ``__init__.py`` of the test package: 6 | 7 | .. include:: package_scoped_loop_example.py 8 | :code: python 9 | 10 | Note that this marker is not passed down to tests in subpackages. 11 | Subpackages constitute their own, separate package. 12 | -------------------------------------------------------------------------------- /docs/how-to-guides/test_item_is_async.rst: -------------------------------------------------------------------------------- 1 | ======================================= 2 | How to tell if a test function is async 3 | ======================================= 4 | Use ``pytest_asyncio.is_async_item`` to determine if a test item is asynchronous and managed by pytest-asyncio. 5 | 6 | .. include:: test_item_is_async_example.py 7 | :code: python 8 | -------------------------------------------------------------------------------- /docs/how-to-guides/test_item_is_async_example.py: -------------------------------------------------------------------------------- 1 | from pytest_asyncio import is_async_test 2 | 3 | 4 | def pytest_collection_modifyitems(items): 5 | for item in items: 6 | if is_async_test(item): 7 | pass 8 | -------------------------------------------------------------------------------- /docs/how-to-guides/uvloop.rst: -------------------------------------------------------------------------------- 1 | ======================= 2 | How to test with uvloop 3 | ======================= 4 | 5 | Redefining the *event_loop_policy* fixture will parametrize all async tests. The following example causes all async tests to run multiple times, once for each event loop in the fixture parameters: 6 | Replace the default event loop policy in your *conftest.py:* 7 | 8 | .. code-block:: python 9 | 10 | import pytest 11 | import uvloop 12 | 13 | 14 | @pytest.fixture(scope="session") 15 | def event_loop_policy(): 16 | return uvloop.EventLoopPolicy() 17 | 18 | You may choose to limit the scope of the fixture to *package,* *module,* or *class,* if you only want a subset of your tests to run with uvloop. 19 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | ========================== 2 | Welcome to pytest-asyncio! 3 | ========================== 4 | 5 | .. toctree:: 6 | :maxdepth: 1 7 | :hidden: 8 | 9 | concepts 10 | how-to-guides/index 11 | reference/index 12 | support 13 | 14 | pytest-asyncio is a `pytest `_ plugin. It facilitates testing of code that uses the `asyncio `_ library. 15 | 16 | Specifically, pytest-asyncio provides support for coroutines as test functions. This allows users to *await* code inside their tests. For example, the following code is executed as a test item by pytest: 17 | 18 | .. code-block:: python 19 | 20 | @pytest.mark.asyncio 21 | async def test_some_asyncio_code(): 22 | res = await library.do_something() 23 | assert b"expected result" == res 24 | 25 | 26 | Note that test classes subclassing the standard `unittest `__ library are not supported. Users 27 | are advised to use `unittest.IsolatedAsyncioTestCase `__ 28 | or an async framework such as `asynctest `__. 29 | 30 | 31 | pytest-asyncio is available under the `Apache License 2.0 `_. 32 | -------------------------------------------------------------------------------- /docs/reference/configuration.rst: -------------------------------------------------------------------------------- 1 | ============= 2 | Configuration 3 | ============= 4 | 5 | .. _configuration/asyncio_default_fixture_loop_scope: 6 | 7 | asyncio_default_fixture_loop_scope 8 | ================================== 9 | Determines the default event loop scope of asynchronous fixtures. When this configuration option is unset, it defaults to the fixture scope. In future versions of pytest-asyncio, the value will default to ``function`` when unset. Possible values are: ``function``, ``class``, ``module``, ``package``, ``session`` 10 | 11 | .. _configuration/asyncio_default_test_loop_scope: 12 | 13 | asyncio_default_test_loop_scope 14 | =============================== 15 | Determines the default event loop scope of asynchronous tests. When this configuration option is unset, it default to function scope. Possible values are: ``function``, ``class``, ``module``, ``package``, ``session`` 16 | 17 | asyncio_mode 18 | ============ 19 | The pytest-asyncio mode can be set by the ``asyncio_mode`` configuration option in the `configuration file 20 | `_: 21 | 22 | .. code-block:: ini 23 | 24 | # pytest.ini 25 | [pytest] 26 | asyncio_mode = auto 27 | 28 | The value can also be set via the ``--asyncio-mode`` command-line option: 29 | 30 | .. code-block:: bash 31 | 32 | $ pytest tests --asyncio-mode=strict 33 | 34 | 35 | If the asyncio mode is set in both the pytest configuration file and the command-line option, the command-line option takes precedence. If no asyncio mode is specified, the mode defaults to `strict`. 36 | -------------------------------------------------------------------------------- /docs/reference/decorators/index.rst: -------------------------------------------------------------------------------- 1 | .. _decorators/pytest_asyncio_fixture: 2 | 3 | ========== 4 | Decorators 5 | ========== 6 | The ``@pytest_asyncio.fixture`` decorator allows coroutines and async generator functions to be used as pytest fixtures. 7 | 8 | The decorator takes all arguments supported by `@pytest.fixture`. 9 | Additionally, ``@pytest_asyncio.fixture`` supports the *loop_scope* keyword argument, which selects the event loop in which the fixture is run (see :ref:`concepts/event_loops`). 10 | The default event loop scope is *function* scope. 11 | Possible loop scopes are *session,* *package,* *module,* *class,* and *function*. 12 | 13 | The *loop_scope* of a fixture can be chosen independently from its caching *scope*. 14 | However, the event loop scope must be larger or the same as the fixture's caching scope. 15 | In other words, it's possible to reevaluate an async fixture multiple times within the same event loop, but it's not possible to switch out the running event loop in an async fixture. 16 | 17 | Examples: 18 | 19 | .. include:: pytest_asyncio_fixture_example.py 20 | :code: python 21 | 22 | *auto* mode automatically converts coroutines and async generator functions declared with the standard ``@pytest.fixture`` decorator to pytest-asyncio fixtures. 23 | -------------------------------------------------------------------------------- /docs/reference/decorators/pytest_asyncio_fixture_example.py: -------------------------------------------------------------------------------- 1 | import pytest_asyncio 2 | 3 | 4 | @pytest_asyncio.fixture 5 | async def fixture_runs_in_fresh_loop_for_every_function(): ... 6 | 7 | 8 | @pytest_asyncio.fixture(loop_scope="session", scope="module") 9 | async def fixture_runs_in_session_loop_once_per_module(): ... 10 | 11 | 12 | @pytest_asyncio.fixture(loop_scope="module", scope="module") 13 | async def fixture_runs_in_module_loop_once_per_module(): ... 14 | 15 | 16 | @pytest_asyncio.fixture(loop_scope="module") 17 | async def fixture_runs_in_module_loop_once_per_function(): ... 18 | -------------------------------------------------------------------------------- /docs/reference/fixtures/event_loop_policy_example.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import warnings 3 | 4 | with warnings.catch_warnings(): 5 | warnings.simplefilter("ignore", DeprecationWarning) 6 | from asyncio import DefaultEventLoopPolicy 7 | 8 | import pytest 9 | 10 | 11 | class CustomEventLoopPolicy(DefaultEventLoopPolicy): 12 | pass 13 | 14 | 15 | @pytest.fixture(scope="module") 16 | def event_loop_policy(request): 17 | return CustomEventLoopPolicy() 18 | 19 | 20 | @pytest.mark.asyncio(loop_scope="module") 21 | @pytest.mark.filterwarnings("ignore::DeprecationWarning") 22 | async def test_uses_custom_event_loop_policy(): 23 | assert isinstance(asyncio.get_event_loop_policy(), CustomEventLoopPolicy) 24 | -------------------------------------------------------------------------------- /docs/reference/fixtures/event_loop_policy_parametrized_example.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import warnings 3 | 4 | with warnings.catch_warnings(): 5 | warnings.simplefilter("ignore", DeprecationWarning) 6 | from asyncio import DefaultEventLoopPolicy 7 | 8 | import pytest 9 | 10 | 11 | class CustomEventLoopPolicy(DefaultEventLoopPolicy): 12 | pass 13 | 14 | 15 | @pytest.fixture( 16 | params=( 17 | DefaultEventLoopPolicy(), 18 | CustomEventLoopPolicy(), 19 | ), 20 | ) 21 | def event_loop_policy(request): 22 | return request.param 23 | 24 | 25 | @pytest.mark.asyncio 26 | @pytest.mark.filterwarnings("ignore::DeprecationWarning") 27 | async def test_uses_custom_event_loop_policy(): 28 | assert isinstance(asyncio.get_event_loop_policy(), DefaultEventLoopPolicy) 29 | -------------------------------------------------------------------------------- /docs/reference/fixtures/index.rst: -------------------------------------------------------------------------------- 1 | ======== 2 | Fixtures 3 | ======== 4 | 5 | event_loop_policy 6 | ================= 7 | Returns the event loop policy used to create asyncio event loops. 8 | The default return value is *asyncio.get_event_loop_policy().* 9 | 10 | This fixture can be overridden when a different event loop policy should be used. 11 | 12 | .. include:: event_loop_policy_example.py 13 | :code: python 14 | 15 | Multiple policies can be provided via fixture parameters. 16 | The fixture is automatically applied to all pytest-asyncio tests. 17 | Therefore, all tests managed by pytest-asyncio are run once for each fixture parameter. 18 | The following example runs the test with different event loop policies. 19 | 20 | .. include:: event_loop_policy_parametrized_example.py 21 | :code: python 22 | 23 | unused_tcp_port 24 | =============== 25 | Finds and yields a single unused TCP port on the localhost interface. Useful for 26 | binding temporary test servers. 27 | 28 | unused_tcp_port_factory 29 | ======================= 30 | A callable which returns a different unused TCP port each invocation. Useful 31 | when several unused TCP ports are required in a test. 32 | 33 | .. code-block:: python 34 | 35 | def a_test(unused_tcp_port_factory): 36 | _port1, _port2 = unused_tcp_port_factory(), unused_tcp_port_factory() 37 | 38 | unused_udp_port and unused_udp_port_factory 39 | =========================================== 40 | Works just like their TCP counterparts but returns unused UDP ports. 41 | -------------------------------------------------------------------------------- /docs/reference/functions.rst: -------------------------------------------------------------------------------- 1 | ========= 2 | Functions 3 | ========= 4 | 5 | is_async_test 6 | ============= 7 | Returns whether a specific pytest Item is an asynchronous test managed by pytest-asyncio. 8 | 9 | This function is intended to be used in pytest hooks or by plugins that depend on pytest-asyncio. 10 | -------------------------------------------------------------------------------- /docs/reference/index.rst: -------------------------------------------------------------------------------- 1 | ========= 2 | Reference 3 | ========= 4 | 5 | .. toctree:: 6 | :hidden: 7 | 8 | configuration 9 | fixtures/index 10 | functions 11 | markers/index 12 | decorators/index 13 | changelog 14 | 15 | This section of the documentation provides descriptions of the individual parts provided by pytest-asyncio. 16 | The reference section also provides a chronological list of changes for each release. 17 | -------------------------------------------------------------------------------- /docs/reference/markers/class_scoped_loop_custom_policies_strict_mode_example.py: -------------------------------------------------------------------------------- 1 | import warnings 2 | 3 | with warnings.catch_warnings(): 4 | warnings.simplefilter("ignore", DeprecationWarning) 5 | from asyncio import DefaultEventLoopPolicy 6 | 7 | import pytest 8 | 9 | 10 | @pytest.fixture( 11 | params=[ 12 | DefaultEventLoopPolicy(), 13 | DefaultEventLoopPolicy(), 14 | ] 15 | ) 16 | def event_loop_policy(request): 17 | return request.param 18 | 19 | 20 | class TestWithDifferentLoopPolicies: 21 | @pytest.mark.asyncio 22 | async def test_parametrized_loop(self): 23 | pass 24 | -------------------------------------------------------------------------------- /docs/reference/markers/class_scoped_loop_strict_mode_example.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | import pytest 4 | 5 | 6 | @pytest.mark.asyncio(loop_scope="class") 7 | class TestClassScopedLoop: 8 | loop: asyncio.AbstractEventLoop 9 | 10 | async def test_remember_loop(self): 11 | TestClassScopedLoop.loop = asyncio.get_running_loop() 12 | 13 | async def test_this_runs_in_same_loop(self): 14 | assert asyncio.get_running_loop() is TestClassScopedLoop.loop 15 | -------------------------------------------------------------------------------- /docs/reference/markers/class_scoped_loop_with_fixture_strict_mode_example.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | import pytest 4 | 5 | import pytest_asyncio 6 | 7 | 8 | @pytest.mark.asyncio(loop_scope="class") 9 | class TestClassScopedLoop: 10 | loop: asyncio.AbstractEventLoop 11 | 12 | @pytest_asyncio.fixture(loop_scope="class") 13 | async def my_fixture(self): 14 | TestClassScopedLoop.loop = asyncio.get_running_loop() 15 | 16 | async def test_runs_is_same_loop_as_fixture(self, my_fixture): 17 | assert asyncio.get_running_loop() is TestClassScopedLoop.loop 18 | -------------------------------------------------------------------------------- /docs/reference/markers/function_scoped_loop_pytestmark_strict_mode_example.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | import pytest 4 | 5 | # Marks all test coroutines in this module 6 | pytestmark = pytest.mark.asyncio 7 | 8 | 9 | async def test_runs_in_asyncio_event_loop(): 10 | assert asyncio.get_running_loop() 11 | -------------------------------------------------------------------------------- /docs/reference/markers/function_scoped_loop_strict_mode_example.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | import pytest 4 | 5 | 6 | @pytest.mark.asyncio 7 | async def test_runs_in_asyncio_event_loop(): 8 | assert asyncio.get_running_loop() 9 | -------------------------------------------------------------------------------- /docs/reference/markers/index.rst: -------------------------------------------------------------------------------- 1 | ======= 2 | Markers 3 | ======= 4 | 5 | .. _reference/markers/asyncio: 6 | 7 | ``pytest.mark.asyncio`` 8 | ======================= 9 | A coroutine or async generator with this marker is treated as a test function by pytest. 10 | The marked function is executed as an asyncio task in the event loop provided by pytest-asyncio. 11 | 12 | .. include:: function_scoped_loop_strict_mode_example.py 13 | :code: python 14 | 15 | Multiple async tests in a single class or module can be marked using |pytestmark|_. 16 | 17 | .. include:: function_scoped_loop_pytestmark_strict_mode_example.py 18 | :code: python 19 | 20 | The ``pytest.mark.asyncio`` marker can be omitted entirely in |auto mode|_ where the *asyncio* marker is added automatically to *async* test functions. 21 | 22 | By default, each test runs in it's own asyncio event loop. 23 | Multiple tests can share the same event loop by providing a *loop_scope* keyword argument to the *asyncio* mark. 24 | The supported scopes are *function,* *class,* and *module,* *package,* and *session*. 25 | The following code example provides a shared event loop for all tests in `TestClassScopedLoop`: 26 | 27 | .. include:: class_scoped_loop_strict_mode_example.py 28 | :code: python 29 | 30 | Similar to class-scoped event loops, a module-scoped loop is provided when setting mark's scope to *module:* 31 | 32 | .. include:: module_scoped_loop_strict_mode_example.py 33 | :code: python 34 | 35 | Subpackages do not share the loop with their parent package. 36 | 37 | Tests marked with *session* scope share the same event loop, even if the tests exist in different packages. 38 | 39 | .. |auto mode| replace:: *auto mode* 40 | .. _auto mode: ../../concepts.html#auto-mode 41 | .. |pytestmark| replace:: ``pytestmark`` 42 | .. _pytestmark: http://doc.pytest.org/en/latest/example/markers.html#marking-whole-classes-or-modules 43 | -------------------------------------------------------------------------------- /docs/reference/markers/module_scoped_loop_strict_mode_example.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | import pytest 4 | 5 | pytestmark = pytest.mark.asyncio(loop_scope="module") 6 | 7 | loop: asyncio.AbstractEventLoop 8 | 9 | 10 | async def test_remember_loop(): 11 | global loop 12 | loop = asyncio.get_running_loop() 13 | 14 | 15 | async def test_this_runs_in_same_loop(): 16 | global loop 17 | assert asyncio.get_running_loop() is loop 18 | 19 | 20 | class TestClassA: 21 | async def test_this_runs_in_same_loop(self): 22 | global loop 23 | assert asyncio.get_running_loop() is loop 24 | -------------------------------------------------------------------------------- /docs/support.rst: -------------------------------------------------------------------------------- 1 | =============== 2 | Getting support 3 | =============== 4 | 5 | Enterprise support 6 | ================== 7 | `Tidelift `_ works with maintainers of numerous open source projects to ensure enterprise-grade support for your software supply chain. 8 | 9 | The Tidelift subscription includes security updates, verified license compliance, continuous software maintenance, and more. As a result, you get the guarantees provided by commercial software for the open source packages you use. 10 | 11 | Consider `signing up for the Tidelift subscription `__. 12 | 13 | 14 | Direct maintainer support 15 | ========================= 16 | If you require commercial support outside of the Tidelift subscription, reach out to `Michael Seifert, `__ one of the project's maintainers. 17 | 18 | 19 | Community support 20 | ================= 21 | The GitHub page of pytest-asyncio offers free community support on a best-effort basis. Please use the `issue tracker `__ to report bugs and the Matrix chat room `#pytest-asyncio:matrix.org `__ or `GitHub discussions `__ to ask questions. 22 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | build-backend = "setuptools.build_meta" 3 | requires = [ 4 | "setuptools>=77", 5 | "setuptools-scm[toml]>=6.2", 6 | ] 7 | 8 | [project] 9 | name = "pytest-asyncio" 10 | description = "Pytest support for asyncio" 11 | readme.content-type = "text/x-rst" 12 | readme.file = "README.rst" 13 | license = "Apache-2.0" 14 | license-files = [ 15 | "LICENSE", 16 | ] 17 | authors = [ 18 | { name = "Tin Tvrtković ", email = "tinchester@gmail.com" }, 19 | ] 20 | requires-python = ">=3.9" 21 | classifiers = [ 22 | "Development Status :: 4 - Beta", 23 | "Framework :: AsyncIO", 24 | "Framework :: Pytest", 25 | "Intended Audience :: Developers", 26 | "Programming Language :: Python :: 3 :: Only", 27 | "Programming Language :: Python :: 3.9", 28 | "Programming Language :: Python :: 3.10", 29 | "Programming Language :: Python :: 3.11", 30 | "Programming Language :: Python :: 3.12", 31 | "Programming Language :: Python :: 3.13", 32 | "Topic :: Software Development :: Testing", 33 | "Typing :: Typed", 34 | ] 35 | dynamic = [ 36 | "version", 37 | ] 38 | 39 | dependencies = [ 40 | "pytest>=8.2,<9", 41 | "typing-extensions>=4.12; python_version<'3.10'", 42 | ] 43 | optional-dependencies.docs = [ 44 | "sphinx>=5.3", 45 | "sphinx-rtd-theme>=1", 46 | ] 47 | optional-dependencies.testing = [ 48 | "coverage>=6.2", 49 | "hypothesis>=5.7.1", 50 | ] 51 | urls."Bug Tracker" = "https://github.com/pytest-dev/pytest-asyncio/issues" 52 | urls.Changelog = "https://pytest-asyncio.readthedocs.io/en/latest/reference/changelog.html" 53 | urls.Documentation = "https://pytest-asyncio.readthedocs.io" 54 | urls.Homepage = "https://github.com/pytest-dev/pytest-asyncio" 55 | urls."Source Code" = "https://github.com/pytest-dev/pytest-asyncio" 56 | entry-points.pytest11.asyncio = "pytest_asyncio.plugin" 57 | 58 | [tool.setuptools] 59 | packages = [ 60 | "pytest_asyncio", 61 | ] 62 | include-package-data = true 63 | 64 | [tool.setuptools_scm] 65 | write_to = "pytest_asyncio/_version.py" 66 | 67 | [tool.ruff] 68 | line-length = 88 69 | format.docstring-code-format = true 70 | lint.select = [ 71 | "B", # bugbear 72 | "D", # pydocstyle 73 | "E", # pycodestyle 74 | "F", # pyflakes 75 | "FA100", # add future annotations 76 | "PGH004", # pygrep-hooks - Use specific rule codes when using noqa 77 | "PIE", # flake8-pie 78 | "PLE", # pylint error 79 | "PYI", # flake8-pyi 80 | "RUF", # ruff 81 | "T100", # flake8-debugger 82 | "UP", # pyupgrade 83 | "W", # pycodestyle 84 | ] 85 | 86 | lint.ignore = [ 87 | # bugbear ignore 88 | "B028", # No explicit `stacklevel` keyword argument found 89 | # pydocstyle ignore 90 | "D100", # Missing docstring in public module 91 | "D101", # Missing docstring in public class 92 | "D102", # Missing docstring in public method 93 | "D103", # Missing docstring in public function 94 | "D104", # Missing docstring in public package 95 | "D105", # Missing docstring in magic method 96 | "D106", # Missing docstring in public nested class 97 | "D107", # Missing docstring in `__init__` 98 | "D203", # `one-blank-line-before-class` (D203) and `no-blank-line-before-class` (D211) are incompatible 99 | "D205", # 1 blank line required between summary line and description 100 | "D209", # [*] Multi-line docstring closing quotes should be on a separate line 101 | "D212", # `multi-line-summary-first-line` (D212) and `multi-line-summary-second-line` (D213) are incompatible. 102 | "D400", # First line should end with a period 103 | "D401", # First line of docstring should be in imperative mood 104 | "D402", # First line should not be the function's signature 105 | "D404", # First word of the docstring should not be "This" 106 | "D415", # First line should end with a period, question mark, or exclamation point 107 | ] 108 | 109 | [tool.pytest.ini_options] 110 | python_files = [ 111 | "test_*.py", 112 | "*_example.py", 113 | ] 114 | addopts = "-rsx --tb=short" 115 | testpaths = [ 116 | "docs", 117 | "tests", 118 | ] 119 | asyncio_mode = "auto" 120 | asyncio_default_fixture_loop_scope = "function" 121 | junit_family = "xunit2" 122 | filterwarnings = [ 123 | "error", 124 | ] 125 | 126 | [tool.coverage.run] 127 | source = [ 128 | "pytest_asyncio", 129 | ] 130 | branch = true 131 | data_file = "coverage/coverage" 132 | omit = [ 133 | "*/_version.py", 134 | ] 135 | parallel = true 136 | 137 | [tool.coverage.report] 138 | show_missing = true 139 | 140 | [tool.towncrier] 141 | directory = "changelog.d" 142 | filename = "docs/reference/changelog.rst" 143 | title_format = "`{version} `_ - {project_date}" 144 | issue_format = "`#{issue} `_" 145 | 146 | [[tool.towncrier.type]] 147 | directory = "security" 148 | name = "Security" 149 | showcontent = true 150 | 151 | [[tool.towncrier.type]] 152 | directory = "removed" 153 | name = "Removed" 154 | showcontent = true 155 | 156 | [[tool.towncrier.type]] 157 | directory = "deprecated" 158 | name = "Deprecated" 159 | showcontent = true 160 | 161 | [[tool.towncrier.type]] 162 | directory = "added" 163 | name = "Added" 164 | showcontent = true 165 | 166 | [[tool.towncrier.type]] 167 | directory = "changed" 168 | name = "Changed" 169 | showcontent = true 170 | 171 | [[tool.towncrier.type]] 172 | directory = "fixed" 173 | name = "Fixed" 174 | showcontent = true 175 | 176 | [[tool.towncrier.type]] 177 | directory = "downstream" 178 | name = "Notes for Downstream Packagers" 179 | showcontent = true 180 | -------------------------------------------------------------------------------- /pytest_asyncio/__init__.py: -------------------------------------------------------------------------------- 1 | """The main point for importing pytest-asyncio items.""" 2 | 3 | from __future__ import annotations 4 | 5 | from ._version import version as __version__ # noqa: F401 6 | from .plugin import fixture, is_async_test 7 | 8 | __all__ = ("fixture", "is_async_test") 9 | -------------------------------------------------------------------------------- /pytest_asyncio/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pytest-dev/pytest-asyncio/bd741b82e22030732e42a92eedebdb133cc3be36/pytest_asyncio/py.typed -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | # Not everything is in in pyproject.toml because of this issue: 3 | ; Traceback (most recent call last): 4 | ; File "/tmp/build-env-rud8b5r6/lib/python3.12/site-packages/setuptools/config/expand.py", line 69, in __getattr__ 5 | ; return next( 6 | ; ^^^^^ 7 | ;StopIteration 8 | ; 9 | ;The above exception was the direct cause of the following exception: 10 | ; 11 | ;Traceback (most recent call last): 12 | ; File "/tmp/build-env-rud8b5r6/lib/python3.12/site-packages/setuptools/config/expand.py", line 183, in read_attr 13 | ; return getattr(StaticModule(module_name, spec), attr_name) 14 | ; ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 15 | ; File "/tmp/build-env-rud8b5r6/lib/python3.12/site-packages/setuptools/config/expand.py", line 75, in __getattr__ 16 | ; raise AttributeError(f"{self.name} has no attribute {attr}") from e 17 | ;AttributeError: pytest_asyncio has no attribute __version__ 18 | version = attr: pytest_asyncio.__version__ 19 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pytest-dev/pytest-asyncio/bd741b82e22030732e42a92eedebdb133cc3be36/tests/__init__.py -------------------------------------------------------------------------------- /tests/async_fixtures/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pytest-dev/pytest-asyncio/bd741b82e22030732e42a92eedebdb133cc3be36/tests/async_fixtures/__init__.py -------------------------------------------------------------------------------- /tests/async_fixtures/test_async_fixtures.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import asyncio 4 | import unittest.mock 5 | 6 | import pytest 7 | 8 | START = object() 9 | END = object() 10 | RETVAL = object() 11 | 12 | 13 | @pytest.fixture 14 | def mock(): 15 | return unittest.mock.Mock(return_value=RETVAL) 16 | 17 | 18 | @pytest.fixture 19 | async def async_fixture(mock): 20 | return await asyncio.sleep(0.1, result=mock(START)) 21 | 22 | 23 | @pytest.mark.asyncio 24 | async def test_async_fixture(async_fixture, mock): 25 | assert mock.call_count == 1 26 | assert mock.call_args_list[-1] == unittest.mock.call(START) 27 | assert async_fixture is RETVAL 28 | 29 | 30 | class TestAsyncFixtureMethod: 31 | is_same_instance = False 32 | 33 | @pytest.fixture(autouse=True) 34 | async def async_fixture_method(self): 35 | self.is_same_instance = True 36 | 37 | @pytest.mark.asyncio 38 | async def test_async_fixture_method(self): 39 | assert self.is_same_instance 40 | -------------------------------------------------------------------------------- /tests/async_fixtures/test_async_fixtures_contextvars.py: -------------------------------------------------------------------------------- 1 | """ 2 | Regression test for https://github.com/pytest-dev/pytest-asyncio/issues/127: 3 | contextvars were not properly maintained among fixtures and tests. 4 | """ 5 | 6 | from __future__ import annotations 7 | 8 | import sys 9 | from textwrap import dedent 10 | 11 | import pytest 12 | from pytest import Pytester 13 | 14 | _prelude = dedent( 15 | """ 16 | import pytest 17 | import pytest_asyncio 18 | from contextlib import contextmanager 19 | from contextvars import ContextVar 20 | 21 | _context_var = ContextVar("context_var") 22 | 23 | @contextmanager 24 | def context_var_manager(value): 25 | token = _context_var.set(value) 26 | try: 27 | yield 28 | finally: 29 | _context_var.reset(token) 30 | """ 31 | ) 32 | 33 | 34 | def test_var_from_sync_generator_propagates_to_async(pytester: Pytester): 35 | pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") 36 | pytester.makepyfile( 37 | _prelude 38 | + dedent( 39 | """ 40 | @pytest.fixture 41 | def var_fixture(): 42 | with context_var_manager("value"): 43 | yield 44 | 45 | @pytest_asyncio.fixture 46 | async def check_var_fixture(var_fixture): 47 | assert _context_var.get() == "value" 48 | 49 | @pytest.mark.asyncio 50 | async def test(check_var_fixture): 51 | assert _context_var.get() == "value" 52 | """ 53 | ) 54 | ) 55 | result = pytester.runpytest("--asyncio-mode=strict") 56 | result.assert_outcomes(passed=1) 57 | 58 | 59 | @pytest.mark.xfail( 60 | sys.version_info < (3, 11), 61 | reason="requires asyncio Task context support", 62 | strict=True, 63 | ) 64 | def test_var_from_async_generator_propagates_to_sync(pytester: Pytester): 65 | pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") 66 | pytester.makepyfile( 67 | _prelude 68 | + dedent( 69 | """ 70 | @pytest_asyncio.fixture 71 | async def var_fixture(): 72 | with context_var_manager("value"): 73 | yield 74 | 75 | @pytest.fixture 76 | def check_var_fixture(var_fixture): 77 | assert _context_var.get() == "value" 78 | 79 | @pytest.mark.asyncio 80 | async def test(check_var_fixture): 81 | assert _context_var.get() == "value" 82 | """ 83 | ) 84 | ) 85 | result = pytester.runpytest("--asyncio-mode=strict") 86 | result.assert_outcomes(passed=1) 87 | 88 | 89 | @pytest.mark.xfail( 90 | sys.version_info < (3, 11), 91 | reason="requires asyncio Task context support", 92 | strict=True, 93 | ) 94 | def test_var_from_async_fixture_propagates_to_sync(pytester: Pytester): 95 | pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") 96 | pytester.makepyfile( 97 | _prelude 98 | + dedent( 99 | """ 100 | @pytest_asyncio.fixture 101 | async def var_fixture(): 102 | _context_var.set("value") 103 | # Rely on async fixture teardown to reset the context var. 104 | 105 | @pytest.fixture 106 | def check_var_fixture(var_fixture): 107 | assert _context_var.get() == "value" 108 | 109 | def test(check_var_fixture): 110 | assert _context_var.get() == "value" 111 | """ 112 | ) 113 | ) 114 | result = pytester.runpytest("--asyncio-mode=strict") 115 | result.assert_outcomes(passed=1) 116 | 117 | 118 | @pytest.mark.xfail( 119 | sys.version_info < (3, 11), 120 | reason="requires asyncio Task context support", 121 | strict=True, 122 | ) 123 | def test_var_from_generator_reset_before_previous_fixture_cleanup(pytester: Pytester): 124 | pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") 125 | pytester.makepyfile( 126 | _prelude 127 | + dedent( 128 | """ 129 | @pytest_asyncio.fixture 130 | async def no_var_fixture(): 131 | with pytest.raises(LookupError): 132 | _context_var.get() 133 | yield 134 | with pytest.raises(LookupError): 135 | _context_var.get() 136 | 137 | @pytest_asyncio.fixture 138 | async def var_fixture(no_var_fixture): 139 | with context_var_manager("value"): 140 | yield 141 | 142 | @pytest.mark.asyncio 143 | async def test(var_fixture): 144 | assert _context_var.get() == "value" 145 | """ 146 | ) 147 | ) 148 | result = pytester.runpytest("--asyncio-mode=strict") 149 | result.assert_outcomes(passed=1) 150 | 151 | 152 | @pytest.mark.xfail( 153 | sys.version_info < (3, 11), 154 | reason="requires asyncio Task context support", 155 | strict=True, 156 | ) 157 | def test_var_from_fixture_reset_before_previous_fixture_cleanup(pytester: Pytester): 158 | pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") 159 | pytester.makepyfile( 160 | _prelude 161 | + dedent( 162 | """ 163 | @pytest_asyncio.fixture 164 | async def no_var_fixture(): 165 | with pytest.raises(LookupError): 166 | _context_var.get() 167 | yield 168 | with pytest.raises(LookupError): 169 | _context_var.get() 170 | 171 | @pytest_asyncio.fixture 172 | async def var_fixture(no_var_fixture): 173 | _context_var.set("value") 174 | # Rely on async fixture teardown to reset the context var. 175 | 176 | @pytest.mark.asyncio 177 | async def test(var_fixture): 178 | assert _context_var.get() == "value" 179 | """ 180 | ) 181 | ) 182 | result = pytester.runpytest("--asyncio-mode=strict") 183 | result.assert_outcomes(passed=1) 184 | 185 | 186 | @pytest.mark.xfail( 187 | sys.version_info < (3, 11), 188 | reason="requires asyncio Task context support", 189 | strict=True, 190 | ) 191 | def test_var_previous_value_restored_after_fixture(pytester: Pytester): 192 | pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") 193 | pytester.makepyfile( 194 | _prelude 195 | + dedent( 196 | """ 197 | @pytest_asyncio.fixture 198 | async def var_fixture_1(): 199 | with context_var_manager("value1"): 200 | yield 201 | assert _context_var.get() == "value1" 202 | 203 | @pytest_asyncio.fixture 204 | async def var_fixture_2(var_fixture_1): 205 | with context_var_manager("value2"): 206 | yield 207 | assert _context_var.get() == "value2" 208 | 209 | @pytest.mark.asyncio 210 | async def test(var_fixture_2): 211 | assert _context_var.get() == "value2" 212 | """ 213 | ) 214 | ) 215 | result = pytester.runpytest("--asyncio-mode=strict") 216 | result.assert_outcomes(passed=1) 217 | 218 | 219 | @pytest.mark.xfail( 220 | sys.version_info < (3, 11), 221 | reason="requires asyncio Task context support", 222 | strict=True, 223 | ) 224 | def test_var_set_to_existing_value_ok(pytester: Pytester): 225 | pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") 226 | pytester.makepyfile( 227 | _prelude 228 | + dedent( 229 | """ 230 | @pytest_asyncio.fixture 231 | async def var_fixture(): 232 | with context_var_manager("value"): 233 | yield 234 | 235 | @pytest_asyncio.fixture 236 | async def same_var_fixture(var_fixture): 237 | with context_var_manager(_context_var.get()): 238 | yield 239 | 240 | @pytest.mark.asyncio 241 | async def test(same_var_fixture): 242 | assert _context_var.get() == "value" 243 | """ 244 | ) 245 | ) 246 | result = pytester.runpytest("--asyncio-mode=strict") 247 | result.assert_outcomes(passed=1) 248 | -------------------------------------------------------------------------------- /tests/async_fixtures/test_nested.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import asyncio 4 | 5 | import pytest 6 | 7 | 8 | @pytest.fixture() 9 | async def async_inner_fixture(): 10 | await asyncio.sleep(0.01) 11 | print("inner start") 12 | yield True 13 | print("inner stop") 14 | 15 | 16 | @pytest.fixture() 17 | async def async_fixture_outer(async_inner_fixture): 18 | await asyncio.sleep(0.01) 19 | print("outer start") 20 | assert async_inner_fixture is True 21 | yield True 22 | print("outer stop") 23 | 24 | 25 | @pytest.mark.asyncio 26 | async def test_async_fixture(async_fixture_outer): 27 | assert async_fixture_outer is True 28 | print("test_async_fixture") 29 | -------------------------------------------------------------------------------- /tests/async_fixtures/test_shared_module_fixture.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from textwrap import dedent 4 | 5 | from pytest import Pytester 6 | 7 | 8 | def test_asyncio_mark_provides_package_scoped_loop_strict_mode(pytester: Pytester): 9 | pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") 10 | pytester.makepyfile( 11 | __init__="", 12 | conftest=dedent( 13 | """\ 14 | import pytest_asyncio 15 | @pytest_asyncio.fixture(loop_scope="module", scope="module") 16 | async def async_shared_module_fixture(): 17 | return True 18 | """ 19 | ), 20 | test_module_one=dedent( 21 | """\ 22 | import pytest 23 | @pytest.mark.asyncio 24 | async def test_shared_module_fixture_use_a(async_shared_module_fixture): 25 | assert async_shared_module_fixture is True 26 | """ 27 | ), 28 | test_module_two=dedent( 29 | """\ 30 | import pytest 31 | @pytest.mark.asyncio 32 | async def test_shared_module_fixture_use_b(async_shared_module_fixture): 33 | assert async_shared_module_fixture is True 34 | """ 35 | ), 36 | ) 37 | result = pytester.runpytest("--asyncio-mode=strict") 38 | result.assert_outcomes(passed=2) 39 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | pytest_plugins = "pytester" 4 | -------------------------------------------------------------------------------- /tests/hypothesis/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pytest-dev/pytest-asyncio/bd741b82e22030732e42a92eedebdb133cc3be36/tests/hypothesis/__init__.py -------------------------------------------------------------------------------- /tests/hypothesis/test_base.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tests for the Hypothesis integration, which wraps async functions in a 3 | sync shim for Hypothesis. 4 | """ 5 | 6 | from __future__ import annotations 7 | 8 | from textwrap import dedent 9 | 10 | import pytest 11 | from hypothesis import given, strategies as st 12 | from pytest import Pytester 13 | 14 | 15 | def test_hypothesis_given_decorator_before_asyncio_mark(pytester: Pytester): 16 | pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") 17 | pytester.makepyfile( 18 | dedent( 19 | """\ 20 | import pytest 21 | from hypothesis import given, strategies as st 22 | 23 | @given(st.integers()) 24 | @pytest.mark.asyncio 25 | async def test_mark_inner(n): 26 | assert isinstance(n, int) 27 | """ 28 | ) 29 | ) 30 | result = pytester.runpytest_subprocess("--asyncio-mode=strict", "-W default") 31 | result.assert_outcomes(passed=1) 32 | 33 | 34 | @pytest.mark.asyncio 35 | @given(st.integers()) 36 | async def test_mark_outer(n): 37 | assert isinstance(n, int) 38 | 39 | 40 | @pytest.mark.parametrize("y", [1, 2]) 41 | @given(x=st.none()) 42 | @pytest.mark.asyncio 43 | async def test_mark_and_parametrize(x, y): 44 | assert x is None 45 | assert y in (1, 2) 46 | 47 | 48 | def test_async_auto_marked(pytester: Pytester): 49 | pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") 50 | pytester.makepyfile( 51 | dedent( 52 | """\ 53 | import asyncio 54 | import pytest 55 | from hypothesis import given 56 | import hypothesis.strategies as st 57 | 58 | pytest_plugins = 'pytest_asyncio' 59 | 60 | @given(n=st.integers()) 61 | async def test_hypothesis(n: int): 62 | assert isinstance(n, int) 63 | """ 64 | ) 65 | ) 66 | result = pytester.runpytest("--asyncio-mode=auto") 67 | result.assert_outcomes(passed=1) 68 | 69 | 70 | def test_sync_not_auto_marked(pytester: Pytester): 71 | """Assert that synchronous Hypothesis functions are not marked with asyncio""" 72 | pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") 73 | pytester.makepyfile( 74 | dedent( 75 | """\ 76 | import asyncio 77 | import pytest 78 | from hypothesis import given 79 | import hypothesis.strategies as st 80 | 81 | pytest_plugins = 'pytest_asyncio' 82 | 83 | @given(n=st.integers()) 84 | def test_hypothesis(request, n: int): 85 | markers = [marker.name for marker in request.node.own_markers] 86 | assert "asyncio" not in markers 87 | assert isinstance(n, int) 88 | """ 89 | ) 90 | ) 91 | result = pytester.runpytest("--asyncio-mode=auto") 92 | result.assert_outcomes(passed=1) 93 | -------------------------------------------------------------------------------- /tests/markers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pytest-dev/pytest-asyncio/bd741b82e22030732e42a92eedebdb133cc3be36/tests/markers/__init__.py -------------------------------------------------------------------------------- /tests/markers/test_class_scope.py: -------------------------------------------------------------------------------- 1 | """Test if pytestmark works when defined on a class.""" 2 | 3 | from __future__ import annotations 4 | 5 | import asyncio 6 | from textwrap import dedent 7 | 8 | import pytest 9 | 10 | 11 | class TestPyTestMark: 12 | pytestmark = pytest.mark.asyncio 13 | 14 | async def test_is_asyncio(self, sample_fixture): 15 | assert asyncio.get_event_loop() 16 | counter = 1 17 | 18 | async def inc(): 19 | nonlocal counter 20 | counter += 1 21 | await asyncio.sleep(0) 22 | 23 | await asyncio.ensure_future(inc()) 24 | assert counter == 2 25 | 26 | 27 | @pytest.fixture 28 | def sample_fixture(): 29 | return None 30 | 31 | 32 | def test_asyncio_mark_provides_class_scoped_loop_when_applied_to_functions( 33 | pytester: pytest.Pytester, 34 | ): 35 | pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") 36 | pytester.makepyfile( 37 | dedent( 38 | """\ 39 | import asyncio 40 | import pytest 41 | 42 | class TestClassScopedLoop: 43 | loop: asyncio.AbstractEventLoop 44 | 45 | @pytest.mark.asyncio(loop_scope="class") 46 | async def test_remember_loop(self): 47 | TestClassScopedLoop.loop = asyncio.get_running_loop() 48 | 49 | @pytest.mark.asyncio(loop_scope="class") 50 | async def test_this_runs_in_same_loop(self): 51 | assert asyncio.get_running_loop() is TestClassScopedLoop.loop 52 | """ 53 | ) 54 | ) 55 | result = pytester.runpytest("--asyncio-mode=strict") 56 | result.assert_outcomes(passed=2) 57 | 58 | 59 | def test_asyncio_mark_provides_class_scoped_loop_when_applied_to_class( 60 | pytester: pytest.Pytester, 61 | ): 62 | pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") 63 | pytester.makepyfile( 64 | dedent( 65 | """\ 66 | import asyncio 67 | import pytest 68 | 69 | @pytest.mark.asyncio(loop_scope="class") 70 | class TestClassScopedLoop: 71 | loop: asyncio.AbstractEventLoop 72 | 73 | async def test_remember_loop(self): 74 | TestClassScopedLoop.loop = asyncio.get_running_loop() 75 | 76 | async def test_this_runs_in_same_loop(self): 77 | assert asyncio.get_running_loop() is TestClassScopedLoop.loop 78 | """ 79 | ) 80 | ) 81 | result = pytester.runpytest("--asyncio-mode=strict") 82 | result.assert_outcomes(passed=2) 83 | 84 | 85 | def test_asyncio_mark_is_inherited_to_subclasses(pytester: pytest.Pytester): 86 | pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") 87 | pytester.makepyfile( 88 | dedent( 89 | """\ 90 | import asyncio 91 | import pytest 92 | 93 | @pytest.mark.asyncio(loop_scope="class") 94 | class TestSuperClassWithMark: 95 | pass 96 | 97 | class TestWithoutMark(TestSuperClassWithMark): 98 | loop: asyncio.AbstractEventLoop 99 | 100 | async def test_remember_loop(self): 101 | TestWithoutMark.loop = asyncio.get_running_loop() 102 | 103 | async def test_this_runs_in_same_loop(self): 104 | assert asyncio.get_running_loop() is TestWithoutMark.loop 105 | """ 106 | ) 107 | ) 108 | result = pytester.runpytest("--asyncio-mode=strict") 109 | result.assert_outcomes(passed=2) 110 | 111 | 112 | def test_asyncio_mark_respects_the_loop_policy( 113 | pytester: pytest.Pytester, 114 | ): 115 | pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") 116 | pytester.makepyfile( 117 | dedent( 118 | """\ 119 | import asyncio 120 | import pytest 121 | 122 | class CustomEventLoopPolicy(asyncio.DefaultEventLoopPolicy): 123 | pass 124 | 125 | class TestUsesCustomEventLoop: 126 | @pytest.fixture(scope="class") 127 | def event_loop_policy(self): 128 | return CustomEventLoopPolicy() 129 | 130 | @pytest.mark.asyncio 131 | async def test_uses_custom_event_loop_policy(self): 132 | assert isinstance( 133 | asyncio.get_event_loop_policy(), 134 | CustomEventLoopPolicy, 135 | ) 136 | 137 | @pytest.mark.asyncio 138 | async def test_does_not_use_custom_event_loop_policy(): 139 | assert not isinstance( 140 | asyncio.get_event_loop_policy(), 141 | CustomEventLoopPolicy, 142 | ) 143 | """ 144 | ) 145 | ) 146 | result = pytester.runpytest("--asyncio-mode=strict") 147 | result.assert_outcomes(passed=2) 148 | 149 | 150 | def test_asyncio_mark_respects_parametrized_loop_policies( 151 | pytester: pytest.Pytester, 152 | ): 153 | pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") 154 | pytester.makepyfile( 155 | dedent( 156 | """\ 157 | import asyncio 158 | 159 | import pytest 160 | 161 | @pytest.fixture( 162 | scope="class", 163 | params=[ 164 | asyncio.DefaultEventLoopPolicy(), 165 | asyncio.DefaultEventLoopPolicy(), 166 | ] 167 | ) 168 | def event_loop_policy(request): 169 | return request.param 170 | 171 | @pytest.mark.asyncio(loop_scope="class") 172 | class TestWithDifferentLoopPolicies: 173 | async def test_parametrized_loop(self, request): 174 | pass 175 | """ 176 | ) 177 | ) 178 | result = pytester.runpytest_subprocess("--asyncio-mode=strict") 179 | result.assert_outcomes(passed=2) 180 | 181 | 182 | def test_asyncio_mark_provides_class_scoped_loop_to_fixtures( 183 | pytester: pytest.Pytester, 184 | ): 185 | pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") 186 | pytester.makepyfile( 187 | dedent( 188 | """\ 189 | import asyncio 190 | 191 | import pytest 192 | import pytest_asyncio 193 | 194 | @pytest.mark.asyncio(loop_scope="class") 195 | class TestClassScopedLoop: 196 | loop: asyncio.AbstractEventLoop 197 | 198 | @pytest_asyncio.fixture 199 | async def my_fixture(self): 200 | TestClassScopedLoop.loop = asyncio.get_running_loop() 201 | 202 | @pytest.mark.asyncio 203 | async def test_runs_is_same_loop_as_fixture(self, my_fixture): 204 | assert asyncio.get_running_loop() is TestClassScopedLoop.loop 205 | """ 206 | ) 207 | ) 208 | result = pytester.runpytest_subprocess("--asyncio-mode=strict") 209 | result.assert_outcomes(passed=1) 210 | 211 | 212 | def test_asyncio_mark_allows_combining_class_scoped_fixture_with_function_scoped_test( 213 | pytester: pytest.Pytester, 214 | ): 215 | pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") 216 | pytester.makepyfile( 217 | dedent( 218 | """\ 219 | import asyncio 220 | 221 | import pytest 222 | import pytest_asyncio 223 | 224 | loop: asyncio.AbstractEventLoop 225 | 226 | class TestMixedScopes: 227 | @pytest_asyncio.fixture(loop_scope="class", scope="class") 228 | async def async_fixture(self): 229 | global loop 230 | loop = asyncio.get_running_loop() 231 | 232 | @pytest.mark.asyncio(loop_scope="function") 233 | async def test_runs_in_different_loop_as_fixture(self, async_fixture): 234 | global loop 235 | assert asyncio.get_running_loop() is not loop 236 | 237 | """ 238 | ), 239 | ) 240 | result = pytester.runpytest("--asyncio-mode=strict") 241 | result.assert_outcomes(passed=1) 242 | 243 | 244 | def test_asyncio_mark_handles_missing_event_loop_triggered_by_fixture( 245 | pytester: pytest.Pytester, 246 | ): 247 | pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") 248 | pytester.makepyfile( 249 | dedent( 250 | """\ 251 | import pytest 252 | import asyncio 253 | 254 | class TestClass: 255 | @pytest.fixture(scope="class") 256 | def sets_event_loop_to_none(self): 257 | # asyncio.run() creates a new event loop without closing the 258 | # existing one. For any test, but the first one, this leads to 259 | # a ResourceWarning when the discarded loop is destroyed by the 260 | # garbage collector. We close the current loop to avoid this. 261 | try: 262 | asyncio.get_event_loop().close() 263 | except RuntimeError: 264 | pass 265 | return asyncio.run(asyncio.sleep(0)) 266 | # asyncio.run() sets the current event loop to None when finished 267 | 268 | @pytest.mark.asyncio(loop_scope="class") 269 | # parametrization may impact fixture ordering 270 | @pytest.mark.parametrize("n", (0, 1)) 271 | async def test_does_not_fail(self, sets_event_loop_to_none, n): 272 | pass 273 | """ 274 | ) 275 | ) 276 | result = pytester.runpytest("--asyncio-mode=strict") 277 | result.assert_outcomes(passed=2) 278 | 279 | 280 | def test_standalone_test_does_not_trigger_warning_about_no_current_event_loop_being_set( 281 | pytester: pytest.Pytester, 282 | ): 283 | pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") 284 | pytester.makepyfile( 285 | dedent( 286 | """\ 287 | import pytest 288 | 289 | @pytest.mark.asyncio(loop_scope="class") 290 | class TestClass: 291 | async def test_anything(self): 292 | pass 293 | """ 294 | ) 295 | ) 296 | result = pytester.runpytest_subprocess("--asyncio-mode=strict") 297 | result.assert_outcomes(warnings=0, passed=1) 298 | -------------------------------------------------------------------------------- /tests/markers/test_function_scope.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from textwrap import dedent 4 | 5 | from pytest import Pytester 6 | 7 | 8 | def test_asyncio_mark_provides_function_scoped_loop_strict_mode(pytester: Pytester): 9 | pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") 10 | pytester.makepyfile( 11 | dedent( 12 | """\ 13 | import asyncio 14 | import pytest 15 | 16 | pytestmark = pytest.mark.asyncio 17 | 18 | loop: asyncio.AbstractEventLoop 19 | 20 | async def test_remember_loop(): 21 | global loop 22 | loop = asyncio.get_running_loop() 23 | 24 | async def test_does_not_run_in_same_loop(): 25 | global loop 26 | assert asyncio.get_running_loop() is not loop 27 | """ 28 | ) 29 | ) 30 | result = pytester.runpytest("--asyncio-mode=strict") 31 | result.assert_outcomes(passed=2) 32 | 33 | 34 | def test_loop_scope_function_provides_function_scoped_event_loop(pytester: Pytester): 35 | pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") 36 | pytester.makepyfile( 37 | dedent( 38 | """\ 39 | import asyncio 40 | import pytest 41 | 42 | pytestmark = pytest.mark.asyncio(loop_scope="function") 43 | 44 | loop: asyncio.AbstractEventLoop 45 | 46 | async def test_remember_loop(): 47 | global loop 48 | loop = asyncio.get_running_loop() 49 | 50 | async def test_does_not_run_in_same_loop(): 51 | global loop 52 | assert asyncio.get_running_loop() is not loop 53 | """ 54 | ) 55 | ) 56 | result = pytester.runpytest("--asyncio-mode=strict") 57 | result.assert_outcomes(passed=2) 58 | 59 | 60 | def test_raises_when_scope_and_loop_scope_arguments_are_present(pytester: Pytester): 61 | pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") 62 | pytester.makepyfile( 63 | dedent( 64 | """\ 65 | import pytest 66 | 67 | @pytest.mark.asyncio(scope="function", loop_scope="function") 68 | async def test_raises(): 69 | ... 70 | """ 71 | ) 72 | ) 73 | result = pytester.runpytest("--asyncio-mode=strict") 74 | result.assert_outcomes(errors=1) 75 | 76 | 77 | def test_warns_when_scope_argument_is_present(pytester: Pytester): 78 | pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") 79 | pytester.makepyfile( 80 | dedent( 81 | """\ 82 | import pytest 83 | 84 | @pytest.mark.asyncio(scope="function") 85 | async def test_warns(): 86 | ... 87 | """ 88 | ) 89 | ) 90 | result = pytester.runpytest_subprocess("--asyncio-mode=strict") 91 | result.assert_outcomes(passed=1, warnings=2) 92 | result.stdout.fnmatch_lines("*DeprecationWarning*") 93 | 94 | 95 | def test_asyncio_mark_respects_the_loop_policy( 96 | pytester: Pytester, 97 | ): 98 | pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") 99 | pytester.makepyfile( 100 | dedent( 101 | """\ 102 | import asyncio 103 | import pytest 104 | 105 | pytestmark = pytest.mark.asyncio 106 | 107 | class CustomEventLoopPolicy(asyncio.DefaultEventLoopPolicy): 108 | pass 109 | 110 | @pytest.fixture(scope="function") 111 | def event_loop_policy(): 112 | return CustomEventLoopPolicy() 113 | 114 | async def test_uses_custom_event_loop_policy(): 115 | assert isinstance( 116 | asyncio.get_event_loop_policy(), 117 | CustomEventLoopPolicy, 118 | ) 119 | """ 120 | ), 121 | ) 122 | result = pytester.runpytest("--asyncio-mode=strict") 123 | result.assert_outcomes(passed=1) 124 | 125 | 126 | def test_asyncio_mark_respects_parametrized_loop_policies( 127 | pytester: Pytester, 128 | ): 129 | pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") 130 | pytester.makepyfile( 131 | dedent( 132 | """\ 133 | import asyncio 134 | 135 | import pytest 136 | 137 | pytestmark = pytest.mark.asyncio 138 | 139 | class CustomEventLoopPolicy(asyncio.DefaultEventLoopPolicy): 140 | pass 141 | 142 | @pytest.fixture( 143 | scope="module", 144 | params=[ 145 | CustomEventLoopPolicy(), 146 | CustomEventLoopPolicy(), 147 | ], 148 | ) 149 | def event_loop_policy(request): 150 | return request.param 151 | 152 | async def test_parametrized_loop(): 153 | assert isinstance( 154 | asyncio.get_event_loop_policy(), 155 | CustomEventLoopPolicy, 156 | ) 157 | """ 158 | ) 159 | ) 160 | result = pytester.runpytest_subprocess("--asyncio-mode=strict") 161 | result.assert_outcomes(passed=2) 162 | 163 | 164 | def test_asyncio_mark_provides_function_scoped_loop_to_fixtures( 165 | pytester: Pytester, 166 | ): 167 | pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") 168 | pytester.makepyfile( 169 | dedent( 170 | """\ 171 | import asyncio 172 | 173 | import pytest 174 | import pytest_asyncio 175 | 176 | pytestmark = pytest.mark.asyncio 177 | 178 | loop: asyncio.AbstractEventLoop 179 | 180 | @pytest_asyncio.fixture 181 | async def my_fixture(): 182 | global loop 183 | loop = asyncio.get_running_loop() 184 | 185 | async def test_runs_is_same_loop_as_fixture(my_fixture): 186 | global loop 187 | assert asyncio.get_running_loop() is loop 188 | """ 189 | ) 190 | ) 191 | result = pytester.runpytest_subprocess("--asyncio-mode=strict") 192 | result.assert_outcomes(passed=1) 193 | 194 | 195 | def test_asyncio_mark_handles_missing_event_loop_triggered_by_fixture( 196 | pytester: Pytester, 197 | ): 198 | pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") 199 | pytester.makepyfile( 200 | dedent( 201 | """\ 202 | import pytest 203 | import asyncio 204 | 205 | @pytest.fixture 206 | def sets_event_loop_to_none(): 207 | # asyncio.run() creates a new event loop without closing the existing 208 | # one. For any test, but the first one, this leads to a ResourceWarning 209 | # when the discarded loop is destroyed by the garbage collector. 210 | # We close the current loop to avoid this 211 | try: 212 | asyncio.get_event_loop().close() 213 | except RuntimeError: 214 | pass 215 | return asyncio.run(asyncio.sleep(0)) 216 | # asyncio.run() sets the current event loop to None when finished 217 | 218 | @pytest.mark.asyncio 219 | # parametrization may impact fixture ordering 220 | @pytest.mark.parametrize("n", (0, 1)) 221 | async def test_does_not_fail(sets_event_loop_to_none, n): 222 | pass 223 | """ 224 | ) 225 | ) 226 | result = pytester.runpytest("--asyncio-mode=strict") 227 | result.assert_outcomes(passed=2) 228 | 229 | 230 | def test_standalone_test_does_not_trigger_warning_about_no_current_event_loop_being_set( 231 | pytester: Pytester, 232 | ): 233 | pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") 234 | pytester.makepyfile( 235 | dedent( 236 | """\ 237 | import pytest 238 | 239 | @pytest.mark.asyncio 240 | async def test_anything(): 241 | pass 242 | """ 243 | ) 244 | ) 245 | result = pytester.runpytest_subprocess("--asyncio-mode=strict") 246 | result.assert_outcomes(warnings=0, passed=1) 247 | 248 | 249 | def test_asyncio_mark_does_not_duplicate_other_marks_in_auto_mode( 250 | pytester: Pytester, 251 | ): 252 | pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") 253 | pytester.makeconftest( 254 | dedent( 255 | """\ 256 | def pytest_configure(config): 257 | config.addinivalue_line( 258 | "markers", "dummy_marker: mark used for testing purposes" 259 | ) 260 | """ 261 | ) 262 | ) 263 | pytester.makepyfile( 264 | dedent( 265 | """\ 266 | import pytest 267 | 268 | @pytest.mark.dummy_marker 269 | async def test_markers_not_duplicated(request): 270 | markers = [] 271 | for node, marker in request.node.iter_markers_with_node(): 272 | markers.append(marker) 273 | assert len(markers) == 2 274 | """ 275 | ) 276 | ) 277 | result = pytester.runpytest_subprocess("--asyncio-mode=auto") 278 | result.assert_outcomes(warnings=0, passed=1) 279 | -------------------------------------------------------------------------------- /tests/markers/test_invalid_arguments.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from textwrap import dedent 4 | 5 | import pytest 6 | 7 | 8 | def test_no_error_when_scope_passed_as_sole_keyword_argument( 9 | pytester: pytest.Pytester, 10 | ): 11 | pytester.makepyfile( 12 | dedent( 13 | """\ 14 | import pytest 15 | 16 | @pytest.mark.asyncio(loop_scope="session") 17 | async def test_anything(): 18 | pass 19 | """ 20 | ) 21 | ) 22 | result = pytester.runpytest_subprocess() 23 | result.assert_outcomes(passed=1) 24 | result.stdout.no_fnmatch_line("*ValueError*") 25 | 26 | 27 | def test_error_when_scope_passed_as_positional_argument( 28 | pytester: pytest.Pytester, 29 | ): 30 | pytester.makepyfile( 31 | dedent( 32 | """\ 33 | import pytest 34 | 35 | @pytest.mark.asyncio("session") 36 | async def test_anything(): 37 | pass 38 | """ 39 | ) 40 | ) 41 | result = pytester.runpytest_subprocess() 42 | result.assert_outcomes(errors=1) 43 | result.stdout.fnmatch_lines( 44 | ["*ValueError: mark.asyncio accepts only a keyword argument*"] 45 | ) 46 | 47 | 48 | def test_error_when_wrong_keyword_argument_is_passed( 49 | pytester: pytest.Pytester, 50 | ): 51 | pytester.makepyfile( 52 | dedent( 53 | """\ 54 | import pytest 55 | 56 | @pytest.mark.asyncio(cope="session") 57 | async def test_anything(): 58 | pass 59 | """ 60 | ) 61 | ) 62 | result = pytester.runpytest_subprocess() 63 | result.assert_outcomes(errors=1) 64 | result.stdout.fnmatch_lines( 65 | ["*ValueError: mark.asyncio accepts only a keyword argument 'loop_scope'*"] 66 | ) 67 | 68 | 69 | def test_error_when_additional_keyword_arguments_are_passed( 70 | pytester: pytest.Pytester, 71 | ): 72 | pytester.makepyfile( 73 | dedent( 74 | """\ 75 | import pytest 76 | 77 | @pytest.mark.asyncio(loop_scope="session", more="stuff") 78 | async def test_anything(): 79 | pass 80 | """ 81 | ) 82 | ) 83 | result = pytester.runpytest_subprocess() 84 | result.assert_outcomes(errors=1) 85 | result.stdout.fnmatch_lines( 86 | ["*ValueError: mark.asyncio accepts only a keyword argument*"] 87 | ) 88 | -------------------------------------------------------------------------------- /tests/markers/test_mixed_scope.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from textwrap import dedent 4 | 5 | from pytest import Pytester 6 | 7 | 8 | def test_function_scoped_loop_restores_previous_loop_scope(pytester: Pytester): 9 | pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") 10 | pytester.makepyfile( 11 | dedent( 12 | """\ 13 | import asyncio 14 | import pytest 15 | 16 | 17 | module_loop: asyncio.AbstractEventLoop 18 | 19 | @pytest.mark.asyncio(loop_scope="module") 20 | async def test_remember_loop(): 21 | global module_loop 22 | module_loop = asyncio.get_running_loop() 23 | 24 | @pytest.mark.asyncio(loop_scope="function") 25 | async def test_with_function_scoped_loop(): 26 | pass 27 | 28 | @pytest.mark.asyncio(loop_scope="module") 29 | async def test_runs_in_same_loop(): 30 | global module_loop 31 | assert asyncio.get_running_loop() is module_loop 32 | """ 33 | ) 34 | ) 35 | result = pytester.runpytest("--asyncio-mode=strict") 36 | result.assert_outcomes(passed=3) 37 | -------------------------------------------------------------------------------- /tests/markers/test_module_scope.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from textwrap import dedent 4 | 5 | from pytest import Pytester 6 | 7 | 8 | def test_asyncio_mark_provides_module_scoped_loop_strict_mode(pytester: Pytester): 9 | pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") 10 | pytester.makepyfile( 11 | dedent( 12 | """\ 13 | import asyncio 14 | import pytest 15 | 16 | pytestmark = pytest.mark.asyncio(loop_scope="module") 17 | 18 | loop: asyncio.AbstractEventLoop 19 | 20 | async def test_remember_loop(): 21 | global loop 22 | loop = asyncio.get_running_loop() 23 | 24 | async def test_this_runs_in_same_loop(): 25 | global loop 26 | assert asyncio.get_running_loop() is loop 27 | 28 | class TestClassA: 29 | async def test_this_runs_in_same_loop(self): 30 | global loop 31 | assert asyncio.get_running_loop() is loop 32 | """ 33 | ) 34 | ) 35 | result = pytester.runpytest("--asyncio-mode=strict") 36 | result.assert_outcomes(passed=3) 37 | 38 | 39 | def test_asyncio_mark_respects_the_loop_policy( 40 | pytester: Pytester, 41 | ): 42 | pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") 43 | pytester.makepyfile( 44 | __init__="", 45 | custom_policy=dedent( 46 | """\ 47 | import asyncio 48 | 49 | class CustomEventLoopPolicy(asyncio.DefaultEventLoopPolicy): 50 | pass 51 | """ 52 | ), 53 | test_uses_custom_policy=dedent( 54 | """\ 55 | import asyncio 56 | import pytest 57 | 58 | from .custom_policy import CustomEventLoopPolicy 59 | 60 | pytestmark = pytest.mark.asyncio(loop_scope="module") 61 | 62 | @pytest.fixture(scope="module") 63 | def event_loop_policy(): 64 | return CustomEventLoopPolicy() 65 | 66 | async def test_uses_custom_event_loop_policy(): 67 | assert isinstance( 68 | asyncio.get_event_loop_policy(), 69 | CustomEventLoopPolicy, 70 | ) 71 | """ 72 | ), 73 | test_does_not_use_custom_policy=dedent( 74 | """\ 75 | import asyncio 76 | import pytest 77 | 78 | from .custom_policy import CustomEventLoopPolicy 79 | 80 | pytestmark = pytest.mark.asyncio(loop_scope="module") 81 | 82 | async def test_does_not_use_custom_event_loop_policy(): 83 | assert not isinstance( 84 | asyncio.get_event_loop_policy(), 85 | CustomEventLoopPolicy, 86 | ) 87 | """ 88 | ), 89 | ) 90 | result = pytester.runpytest("--asyncio-mode=strict") 91 | result.assert_outcomes(passed=2) 92 | 93 | 94 | def test_asyncio_mark_respects_parametrized_loop_policies( 95 | pytester: Pytester, 96 | ): 97 | pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") 98 | pytester.makepyfile( 99 | dedent( 100 | """\ 101 | import asyncio 102 | 103 | import pytest 104 | 105 | pytestmark = pytest.mark.asyncio(loop_scope="module") 106 | 107 | @pytest.fixture( 108 | scope="module", 109 | params=[ 110 | asyncio.DefaultEventLoopPolicy(), 111 | asyncio.DefaultEventLoopPolicy(), 112 | ], 113 | ) 114 | def event_loop_policy(request): 115 | return request.param 116 | 117 | async def test_parametrized_loop(): 118 | pass 119 | """ 120 | ) 121 | ) 122 | result = pytester.runpytest_subprocess("--asyncio-mode=strict") 123 | result.assert_outcomes(passed=2) 124 | 125 | 126 | def test_asyncio_mark_provides_module_scoped_loop_to_fixtures( 127 | pytester: Pytester, 128 | ): 129 | pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") 130 | pytester.makepyfile( 131 | dedent( 132 | """\ 133 | import asyncio 134 | 135 | import pytest 136 | import pytest_asyncio 137 | 138 | pytestmark = pytest.mark.asyncio(loop_scope="module") 139 | 140 | loop: asyncio.AbstractEventLoop 141 | 142 | @pytest_asyncio.fixture(loop_scope="module", scope="module") 143 | async def my_fixture(): 144 | global loop 145 | loop = asyncio.get_running_loop() 146 | 147 | async def test_runs_is_same_loop_as_fixture(my_fixture): 148 | global loop 149 | assert asyncio.get_running_loop() is loop 150 | """ 151 | ) 152 | ) 153 | result = pytester.runpytest_subprocess("--asyncio-mode=strict") 154 | result.assert_outcomes(passed=1) 155 | 156 | 157 | def test_asyncio_mark_allows_combining_module_scoped_fixture_with_class_scoped_test( 158 | pytester: Pytester, 159 | ): 160 | pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") 161 | pytester.makepyfile( 162 | dedent( 163 | """\ 164 | import asyncio 165 | 166 | import pytest 167 | import pytest_asyncio 168 | 169 | loop: asyncio.AbstractEventLoop 170 | 171 | @pytest_asyncio.fixture(loop_scope="module", scope="module") 172 | async def async_fixture(): 173 | global loop 174 | loop = asyncio.get_running_loop() 175 | 176 | @pytest.mark.asyncio(loop_scope="class") 177 | class TestMixedScopes: 178 | async def test_runs_in_different_loop_as_fixture(self, async_fixture): 179 | global loop 180 | assert asyncio.get_running_loop() is not loop 181 | 182 | """ 183 | ), 184 | ) 185 | result = pytester.runpytest("--asyncio-mode=strict") 186 | result.assert_outcomes(passed=1) 187 | 188 | 189 | def test_asyncio_mark_allows_combining_module_scoped_fixture_with_function_scoped_test( 190 | pytester: Pytester, 191 | ): 192 | pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") 193 | pytester.makepyfile( 194 | __init__="", 195 | test_mixed_scopes=dedent( 196 | """\ 197 | import asyncio 198 | 199 | import pytest 200 | import pytest_asyncio 201 | 202 | loop: asyncio.AbstractEventLoop 203 | 204 | @pytest_asyncio.fixture(loop_scope="module", scope="module") 205 | async def async_fixture(): 206 | global loop 207 | loop = asyncio.get_running_loop() 208 | 209 | @pytest.mark.asyncio(loop_scope="function") 210 | async def test_runs_in_different_loop_as_fixture(async_fixture): 211 | global loop 212 | assert asyncio.get_running_loop() is not loop 213 | """ 214 | ), 215 | ) 216 | result = pytester.runpytest("--asyncio-mode=strict") 217 | result.assert_outcomes(passed=1) 218 | 219 | 220 | def test_allows_combining_module_scoped_asyncgen_fixture_with_function_scoped_test( 221 | pytester: Pytester, 222 | ): 223 | pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") 224 | pytester.makepyfile( 225 | dedent( 226 | """\ 227 | import asyncio 228 | 229 | import pytest 230 | import pytest_asyncio 231 | 232 | loop: asyncio.AbstractEventLoop 233 | 234 | @pytest_asyncio.fixture(loop_scope="module", scope="module") 235 | async def async_fixture(): 236 | global loop 237 | loop = asyncio.get_running_loop() 238 | yield 239 | 240 | @pytest.mark.asyncio(loop_scope="function") 241 | async def test_runs_in_different_loop_as_fixture(async_fixture): 242 | global loop 243 | assert asyncio.get_running_loop() is not loop 244 | """ 245 | ), 246 | ) 247 | result = pytester.runpytest("--asyncio-mode=strict") 248 | result.assert_outcomes(passed=1) 249 | 250 | 251 | def test_asyncio_mark_handles_missing_event_loop_triggered_by_fixture( 252 | pytester: Pytester, 253 | ): 254 | pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") 255 | pytester.makepyfile( 256 | dedent( 257 | """\ 258 | import pytest 259 | import asyncio 260 | 261 | @pytest.fixture(scope="module") 262 | def sets_event_loop_to_none(): 263 | # asyncio.run() creates a new event loop without closing the existing 264 | # one. For any test, but the first one, this leads to a ResourceWarning 265 | # when the discarded loop is destroyed by the garbage collector. 266 | # We close the current loop to avoid this 267 | try: 268 | asyncio.get_event_loop().close() 269 | except RuntimeError: 270 | pass 271 | return asyncio.run(asyncio.sleep(0)) 272 | # asyncio.run() sets the current event loop to None when finished 273 | 274 | @pytest.mark.asyncio(loop_scope="module") 275 | # parametrization may impact fixture ordering 276 | @pytest.mark.parametrize("n", (0, 1)) 277 | async def test_does_not_fail(sets_event_loop_to_none, n): 278 | pass 279 | """ 280 | ) 281 | ) 282 | result = pytester.runpytest("--asyncio-mode=strict") 283 | result.assert_outcomes(passed=2) 284 | 285 | 286 | def test_standalone_test_does_not_trigger_warning_about_no_current_event_loop_being_set( 287 | pytester: Pytester, 288 | ): 289 | pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") 290 | pytester.makepyfile( 291 | dedent( 292 | """\ 293 | import pytest 294 | 295 | @pytest.mark.asyncio(loop_scope="module") 296 | async def test_anything(): 297 | pass 298 | """ 299 | ) 300 | ) 301 | result = pytester.runpytest_subprocess("--asyncio-mode=strict") 302 | result.assert_outcomes(warnings=0, passed=1) 303 | -------------------------------------------------------------------------------- /tests/markers/test_package_scope.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from textwrap import dedent 4 | 5 | from pytest import Pytester 6 | 7 | 8 | def test_asyncio_mark_provides_package_scoped_loop_strict_mode(pytester: Pytester): 9 | package_name = pytester.path.name 10 | subpackage_name = "subpkg" 11 | pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") 12 | pytester.makepyfile( 13 | __init__="", 14 | shared_module=dedent( 15 | """\ 16 | import asyncio 17 | 18 | loop: asyncio.AbstractEventLoop = None 19 | """ 20 | ), 21 | test_module_one=dedent( 22 | f"""\ 23 | import asyncio 24 | import pytest 25 | 26 | from {package_name} import shared_module 27 | 28 | @pytest.mark.asyncio(loop_scope="package") 29 | async def test_remember_loop(): 30 | shared_module.loop = asyncio.get_running_loop() 31 | """ 32 | ), 33 | test_module_two=dedent( 34 | f"""\ 35 | import asyncio 36 | import pytest 37 | 38 | from {package_name} import shared_module 39 | 40 | pytestmark = pytest.mark.asyncio(loop_scope="package") 41 | 42 | async def test_this_runs_in_same_loop(): 43 | assert asyncio.get_running_loop() is shared_module.loop 44 | 45 | class TestClassA: 46 | async def test_this_runs_in_same_loop(self): 47 | assert asyncio.get_running_loop() is shared_module.loop 48 | """ 49 | ), 50 | ) 51 | subpkg = pytester.mkpydir(subpackage_name) 52 | subpkg.joinpath("__init__.py").touch() 53 | subpkg.joinpath("test_subpkg.py").write_text( 54 | dedent( 55 | f"""\ 56 | import asyncio 57 | import pytest 58 | 59 | from {package_name} import shared_module 60 | 61 | pytestmark = pytest.mark.asyncio(loop_scope="package") 62 | 63 | async def test_subpackage_runs_in_different_loop(): 64 | assert asyncio.get_running_loop() is not shared_module.loop 65 | """ 66 | ) 67 | ) 68 | result = pytester.runpytest("--asyncio-mode=strict") 69 | result.assert_outcomes(passed=4) 70 | 71 | 72 | def test_asyncio_mark_respects_the_loop_policy( 73 | pytester: Pytester, 74 | ): 75 | pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") 76 | pytester.makepyfile( 77 | __init__="", 78 | conftest=dedent( 79 | """\ 80 | import pytest 81 | 82 | from .custom_policy import CustomEventLoopPolicy 83 | 84 | @pytest.fixture(scope="package") 85 | def event_loop_policy(): 86 | return CustomEventLoopPolicy() 87 | """ 88 | ), 89 | custom_policy=dedent( 90 | """\ 91 | import asyncio 92 | 93 | class CustomEventLoopPolicy(asyncio.DefaultEventLoopPolicy): 94 | pass 95 | """ 96 | ), 97 | test_uses_custom_policy=dedent( 98 | """\ 99 | import asyncio 100 | import pytest 101 | 102 | from .custom_policy import CustomEventLoopPolicy 103 | 104 | pytestmark = pytest.mark.asyncio(loop_scope="package") 105 | 106 | async def test_uses_custom_event_loop_policy(): 107 | assert isinstance( 108 | asyncio.get_event_loop_policy(), 109 | CustomEventLoopPolicy, 110 | ) 111 | """ 112 | ), 113 | test_also_uses_custom_policy=dedent( 114 | """\ 115 | import asyncio 116 | import pytest 117 | 118 | from .custom_policy import CustomEventLoopPolicy 119 | 120 | pytestmark = pytest.mark.asyncio(loop_scope="package") 121 | 122 | async def test_also_uses_custom_event_loop_policy(): 123 | assert isinstance( 124 | asyncio.get_event_loop_policy(), 125 | CustomEventLoopPolicy, 126 | ) 127 | """ 128 | ), 129 | ) 130 | result = pytester.runpytest("--asyncio-mode=strict") 131 | result.assert_outcomes(passed=2) 132 | 133 | 134 | def test_asyncio_mark_respects_parametrized_loop_policies( 135 | pytester: Pytester, 136 | ): 137 | pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") 138 | pytester.makepyfile( 139 | __init__="", 140 | test_parametrization=dedent( 141 | """\ 142 | import asyncio 143 | 144 | import pytest 145 | 146 | pytestmark = pytest.mark.asyncio(loop_scope="package") 147 | 148 | @pytest.fixture( 149 | scope="package", 150 | params=[ 151 | asyncio.DefaultEventLoopPolicy(), 152 | asyncio.DefaultEventLoopPolicy(), 153 | ], 154 | ) 155 | def event_loop_policy(request): 156 | return request.param 157 | 158 | async def test_parametrized_loop(): 159 | pass 160 | """ 161 | ), 162 | ) 163 | result = pytester.runpytest_subprocess("--asyncio-mode=strict") 164 | result.assert_outcomes(passed=2) 165 | 166 | 167 | def test_asyncio_mark_provides_package_scoped_loop_to_fixtures( 168 | pytester: Pytester, 169 | ): 170 | pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") 171 | package_name = pytester.path.name 172 | pytester.makepyfile( 173 | __init__="", 174 | conftest=dedent( 175 | f"""\ 176 | import asyncio 177 | 178 | import pytest_asyncio 179 | 180 | from {package_name} import shared_module 181 | 182 | @pytest_asyncio.fixture(loop_scope="package", scope="package") 183 | async def my_fixture(): 184 | shared_module.loop = asyncio.get_running_loop() 185 | """ 186 | ), 187 | shared_module=dedent( 188 | """\ 189 | import asyncio 190 | 191 | loop: asyncio.AbstractEventLoop = None 192 | """ 193 | ), 194 | test_fixture_runs_in_scoped_loop=dedent( 195 | f"""\ 196 | import asyncio 197 | 198 | import pytest 199 | import pytest_asyncio 200 | 201 | from {package_name} import shared_module 202 | 203 | pytestmark = pytest.mark.asyncio(loop_scope="package") 204 | 205 | async def test_runs_in_same_loop_as_fixture(my_fixture): 206 | assert asyncio.get_running_loop() is shared_module.loop 207 | """ 208 | ), 209 | ) 210 | result = pytester.runpytest_subprocess("--asyncio-mode=strict") 211 | result.assert_outcomes(passed=1) 212 | 213 | 214 | def test_asyncio_mark_allows_combining_package_scoped_fixture_with_module_scoped_test( 215 | pytester: Pytester, 216 | ): 217 | pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") 218 | pytester.makepyfile( 219 | __init__="", 220 | test_mixed_scopes=dedent( 221 | """\ 222 | import asyncio 223 | 224 | import pytest 225 | import pytest_asyncio 226 | 227 | loop: asyncio.AbstractEventLoop 228 | 229 | @pytest_asyncio.fixture(loop_scope="package", scope="package") 230 | async def async_fixture(): 231 | global loop 232 | loop = asyncio.get_running_loop() 233 | 234 | @pytest.mark.asyncio(loop_scope="module") 235 | async def test_runs_in_different_loop_as_fixture(async_fixture): 236 | global loop 237 | assert asyncio.get_running_loop() is not loop 238 | """ 239 | ), 240 | ) 241 | result = pytester.runpytest("--asyncio-mode=strict") 242 | result.assert_outcomes(passed=1) 243 | 244 | 245 | def test_asyncio_mark_allows_combining_package_scoped_fixture_with_class_scoped_test( 246 | pytester: Pytester, 247 | ): 248 | pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") 249 | pytester.makepyfile( 250 | __init__="", 251 | test_mixed_scopes=dedent( 252 | """\ 253 | import asyncio 254 | 255 | import pytest 256 | import pytest_asyncio 257 | 258 | loop: asyncio.AbstractEventLoop 259 | 260 | @pytest_asyncio.fixture(loop_scope="package", scope="package") 261 | async def async_fixture(): 262 | global loop 263 | loop = asyncio.get_running_loop() 264 | 265 | @pytest.mark.asyncio(loop_scope="class") 266 | class TestMixedScopes: 267 | async def test_runs_in_different_loop_as_fixture(self, async_fixture): 268 | global loop 269 | assert asyncio.get_running_loop() is not loop 270 | """ 271 | ), 272 | ) 273 | result = pytester.runpytest("--asyncio-mode=strict") 274 | result.assert_outcomes(passed=1) 275 | 276 | 277 | def test_asyncio_mark_allows_combining_package_scoped_fixture_with_function_scoped_test( 278 | pytester: Pytester, 279 | ): 280 | pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") 281 | pytester.makepyfile( 282 | __init__="", 283 | test_mixed_scopes=dedent( 284 | """\ 285 | import asyncio 286 | 287 | import pytest 288 | import pytest_asyncio 289 | 290 | loop: asyncio.AbstractEventLoop 291 | 292 | @pytest_asyncio.fixture(loop_scope="package", scope="package") 293 | async def async_fixture(): 294 | global loop 295 | loop = asyncio.get_running_loop() 296 | 297 | @pytest.mark.asyncio 298 | async def test_runs_in_different_loop_as_fixture(async_fixture): 299 | global loop 300 | assert asyncio.get_running_loop() is not loop 301 | """ 302 | ), 303 | ) 304 | result = pytester.runpytest("--asyncio-mode=strict") 305 | result.assert_outcomes(passed=1) 306 | 307 | 308 | def test_asyncio_mark_handles_missing_event_loop_triggered_by_fixture( 309 | pytester: Pytester, 310 | ): 311 | pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") 312 | pytester.makepyfile( 313 | __init__="", 314 | test_loop_is_none=dedent( 315 | """\ 316 | import pytest 317 | import asyncio 318 | 319 | @pytest.fixture(scope="package") 320 | def sets_event_loop_to_none(): 321 | # asyncio.run() creates a new event loop without closing the existing 322 | # one. For any test, but the first one, this leads to a ResourceWarning 323 | # when the discarded loop is destroyed by the garbage collector. 324 | # We close the current loop to avoid this 325 | try: 326 | asyncio.get_event_loop().close() 327 | except RuntimeError: 328 | pass 329 | return asyncio.run(asyncio.sleep(0)) 330 | # asyncio.run() sets the current event loop to None when finished 331 | 332 | @pytest.mark.asyncio(loop_scope="package") 333 | # parametrization may impact fixture ordering 334 | @pytest.mark.parametrize("n", (0, 1)) 335 | async def test_does_not_fail(sets_event_loop_to_none, n): 336 | pass 337 | """ 338 | ), 339 | ) 340 | result = pytester.runpytest("--asyncio-mode=strict") 341 | result.assert_outcomes(passed=2) 342 | -------------------------------------------------------------------------------- /tests/markers/test_session_scope.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from textwrap import dedent 4 | 5 | from pytest import Pytester 6 | 7 | 8 | def test_asyncio_mark_provides_session_scoped_loop_strict_mode(pytester: Pytester): 9 | package_name = pytester.path.name 10 | pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") 11 | pytester.makepyfile( 12 | __init__="", 13 | shared_module=dedent( 14 | """\ 15 | import asyncio 16 | 17 | loop: asyncio.AbstractEventLoop = None 18 | """ 19 | ), 20 | test_module_one=dedent( 21 | f"""\ 22 | import asyncio 23 | import pytest 24 | 25 | from {package_name} import shared_module 26 | 27 | @pytest.mark.asyncio(loop_scope="session") 28 | async def test_remember_loop(): 29 | shared_module.loop = asyncio.get_running_loop() 30 | """ 31 | ), 32 | test_module_two=dedent( 33 | f"""\ 34 | import asyncio 35 | import pytest 36 | 37 | from {package_name} import shared_module 38 | 39 | pytestmark = pytest.mark.asyncio(loop_scope="session") 40 | 41 | async def test_this_runs_in_same_loop(): 42 | assert asyncio.get_running_loop() is shared_module.loop 43 | 44 | class TestClassA: 45 | async def test_this_runs_in_same_loop(self): 46 | assert asyncio.get_running_loop() is shared_module.loop 47 | """ 48 | ), 49 | ) 50 | 51 | # subpackage_name must alphabetically come after test_module_one.py 52 | subpackage_name = "z_subpkg" 53 | subpkg = pytester.mkpydir(subpackage_name) 54 | subpkg.joinpath("test_subpkg.py").write_text( 55 | dedent( 56 | f"""\ 57 | import asyncio 58 | import pytest 59 | 60 | from {package_name} import shared_module 61 | 62 | pytestmark = pytest.mark.asyncio(loop_scope="session") 63 | 64 | async def test_subpackage_runs_in_same_loop(): 65 | assert asyncio.get_running_loop() is shared_module.loop 66 | """ 67 | ) 68 | ) 69 | result = pytester.runpytest("--asyncio-mode=strict") 70 | result.assert_outcomes(passed=4) 71 | 72 | 73 | def test_asyncio_mark_respects_the_loop_policy( 74 | pytester: Pytester, 75 | ): 76 | pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") 77 | pytester.makepyfile( 78 | __init__="", 79 | conftest=dedent( 80 | """\ 81 | import pytest 82 | 83 | from .custom_policy import CustomEventLoopPolicy 84 | 85 | @pytest.fixture(scope="session") 86 | def event_loop_policy(): 87 | return CustomEventLoopPolicy() 88 | """ 89 | ), 90 | custom_policy=dedent( 91 | """\ 92 | import asyncio 93 | 94 | class CustomEventLoopPolicy(asyncio.DefaultEventLoopPolicy): 95 | pass 96 | """ 97 | ), 98 | test_uses_custom_policy=dedent( 99 | """\ 100 | import asyncio 101 | import pytest 102 | 103 | from .custom_policy import CustomEventLoopPolicy 104 | 105 | pytestmark = pytest.mark.asyncio(loop_scope="session") 106 | 107 | async def test_uses_custom_event_loop_policy(): 108 | assert isinstance( 109 | asyncio.get_event_loop_policy(), 110 | CustomEventLoopPolicy, 111 | ) 112 | """ 113 | ), 114 | test_also_uses_custom_policy=dedent( 115 | """\ 116 | import asyncio 117 | import pytest 118 | 119 | from .custom_policy import CustomEventLoopPolicy 120 | 121 | pytestmark = pytest.mark.asyncio(loop_scope="session") 122 | 123 | async def test_also_uses_custom_event_loop_policy(): 124 | assert isinstance( 125 | asyncio.get_event_loop_policy(), 126 | CustomEventLoopPolicy, 127 | ) 128 | """ 129 | ), 130 | ) 131 | result = pytester.runpytest("--asyncio-mode=strict") 132 | result.assert_outcomes(passed=2) 133 | 134 | 135 | def test_asyncio_mark_respects_parametrized_loop_policies( 136 | pytester: Pytester, 137 | ): 138 | pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") 139 | pytester.makepyfile( 140 | __init__="", 141 | test_parametrization=dedent( 142 | """\ 143 | import asyncio 144 | 145 | import pytest 146 | 147 | pytestmark = pytest.mark.asyncio(loop_scope="session") 148 | 149 | @pytest.fixture( 150 | scope="session", 151 | params=[ 152 | asyncio.DefaultEventLoopPolicy(), 153 | asyncio.DefaultEventLoopPolicy(), 154 | ], 155 | ) 156 | def event_loop_policy(request): 157 | return request.param 158 | 159 | async def test_parametrized_loop(): 160 | pass 161 | """ 162 | ), 163 | ) 164 | result = pytester.runpytest_subprocess("--asyncio-mode=strict") 165 | result.assert_outcomes(passed=2) 166 | 167 | 168 | def test_asyncio_mark_provides_session_scoped_loop_to_fixtures( 169 | pytester: Pytester, 170 | ): 171 | package_name = pytester.path.name 172 | pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") 173 | pytester.makepyfile( 174 | __init__="", 175 | conftest=dedent( 176 | f"""\ 177 | import asyncio 178 | 179 | import pytest_asyncio 180 | 181 | from {package_name} import shared_module 182 | 183 | @pytest_asyncio.fixture(loop_scope="session", scope="session") 184 | async def my_fixture(): 185 | shared_module.loop = asyncio.get_running_loop() 186 | """ 187 | ), 188 | shared_module=dedent( 189 | """\ 190 | import asyncio 191 | 192 | loop: asyncio.AbstractEventLoop = None 193 | """ 194 | ), 195 | ) 196 | subpackage_name = "subpkg" 197 | subpkg = pytester.mkpydir(subpackage_name) 198 | subpkg.joinpath("test_subpkg.py").write_text( 199 | dedent( 200 | f"""\ 201 | import asyncio 202 | 203 | import pytest 204 | import pytest_asyncio 205 | 206 | from {package_name} import shared_module 207 | 208 | pytestmark = pytest.mark.asyncio(loop_scope="session") 209 | 210 | async def test_runs_in_same_loop_as_fixture(my_fixture): 211 | assert asyncio.get_running_loop() is shared_module.loop 212 | """ 213 | ) 214 | ) 215 | result = pytester.runpytest_subprocess("--asyncio-mode=strict") 216 | result.assert_outcomes(passed=1) 217 | 218 | 219 | def test_asyncio_mark_allows_combining_session_scoped_fixture_with_package_scoped_test( 220 | pytester: Pytester, 221 | ): 222 | pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") 223 | pytester.makepyfile( 224 | __init__="", 225 | test_mixed_scopes=dedent( 226 | """\ 227 | import asyncio 228 | 229 | import pytest 230 | import pytest_asyncio 231 | 232 | loop: asyncio.AbstractEventLoop 233 | 234 | @pytest_asyncio.fixture(loop_scope="session", scope="session") 235 | async def async_fixture(): 236 | global loop 237 | loop = asyncio.get_running_loop() 238 | 239 | @pytest.mark.asyncio(loop_scope="package") 240 | async def test_runs_in_different_loop_as_fixture(async_fixture): 241 | global loop 242 | assert asyncio.get_running_loop() is not loop 243 | """ 244 | ), 245 | ) 246 | result = pytester.runpytest("--asyncio-mode=strict") 247 | result.assert_outcomes(passed=1) 248 | 249 | 250 | def test_asyncio_mark_allows_combining_session_scoped_fixture_with_module_scoped_test( 251 | pytester: Pytester, 252 | ): 253 | pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") 254 | pytester.makepyfile( 255 | __init__="", 256 | test_mixed_scopes=dedent( 257 | """\ 258 | import asyncio 259 | 260 | import pytest 261 | import pytest_asyncio 262 | 263 | loop: asyncio.AbstractEventLoop 264 | 265 | @pytest_asyncio.fixture(loop_scope="session", scope="session") 266 | async def async_fixture(): 267 | global loop 268 | loop = asyncio.get_running_loop() 269 | 270 | @pytest.mark.asyncio(loop_scope="module") 271 | async def test_runs_in_different_loop_as_fixture(async_fixture): 272 | global loop 273 | assert asyncio.get_running_loop() is not loop 274 | """ 275 | ), 276 | ) 277 | result = pytester.runpytest("--asyncio-mode=strict") 278 | result.assert_outcomes(passed=1) 279 | 280 | 281 | def test_asyncio_mark_allows_combining_session_scoped_fixture_with_class_scoped_test( 282 | pytester: Pytester, 283 | ): 284 | pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") 285 | pytester.makepyfile( 286 | __init__="", 287 | test_mixed_scopes=dedent( 288 | """\ 289 | import asyncio 290 | 291 | import pytest 292 | import pytest_asyncio 293 | 294 | loop: asyncio.AbstractEventLoop 295 | 296 | @pytest_asyncio.fixture(loop_scope="session", scope="session") 297 | async def async_fixture(): 298 | global loop 299 | loop = asyncio.get_running_loop() 300 | 301 | @pytest.mark.asyncio(loop_scope="class") 302 | class TestMixedScopes: 303 | async def test_runs_in_different_loop_as_fixture(self, async_fixture): 304 | global loop 305 | assert asyncio.get_running_loop() is not loop 306 | """ 307 | ), 308 | ) 309 | result = pytester.runpytest("--asyncio-mode=strict") 310 | result.assert_outcomes(passed=1) 311 | 312 | 313 | def test_asyncio_mark_allows_combining_session_scoped_fixture_with_function_scoped_test( 314 | pytester: Pytester, 315 | ): 316 | pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") 317 | pytester.makepyfile( 318 | __init__="", 319 | test_mixed_scopes=dedent( 320 | """\ 321 | import asyncio 322 | 323 | import pytest 324 | import pytest_asyncio 325 | 326 | loop: asyncio.AbstractEventLoop 327 | 328 | @pytest_asyncio.fixture(loop_scope="session", scope="session") 329 | async def async_fixture(): 330 | global loop 331 | loop = asyncio.get_running_loop() 332 | 333 | @pytest.mark.asyncio 334 | async def test_runs_in_different_loop_as_fixture(async_fixture): 335 | global loop 336 | assert asyncio.get_running_loop() is not loop 337 | """ 338 | ), 339 | ) 340 | result = pytester.runpytest("--asyncio-mode=strict") 341 | result.assert_outcomes(passed=1) 342 | 343 | 344 | def test_allows_combining_session_scoped_asyncgen_fixture_with_function_scoped_test( 345 | pytester: Pytester, 346 | ): 347 | pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") 348 | pytester.makepyfile( 349 | __init__="", 350 | test_mixed_scopes=dedent( 351 | """\ 352 | import asyncio 353 | 354 | import pytest 355 | import pytest_asyncio 356 | 357 | loop: asyncio.AbstractEventLoop 358 | 359 | @pytest_asyncio.fixture(loop_scope="session", scope="session") 360 | async def async_fixture(): 361 | global loop 362 | loop = asyncio.get_running_loop() 363 | yield 364 | 365 | @pytest.mark.asyncio 366 | async def test_runs_in_different_loop_as_fixture(async_fixture): 367 | global loop 368 | assert asyncio.get_running_loop() is not loop 369 | """ 370 | ), 371 | ) 372 | result = pytester.runpytest("--asyncio-mode=strict") 373 | result.assert_outcomes(passed=1) 374 | 375 | 376 | def test_asyncio_mark_handles_missing_event_loop_triggered_by_fixture( 377 | pytester: Pytester, 378 | ): 379 | pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") 380 | pytester.makepyfile( 381 | dedent( 382 | """\ 383 | import pytest 384 | import asyncio 385 | 386 | @pytest.fixture(scope="session") 387 | def sets_event_loop_to_none(): 388 | # asyncio.run() creates a new event loop without closing the existing 389 | # one. For any test, but the first one, this leads to a ResourceWarning 390 | # when the discarded loop is destroyed by the garbage collector. 391 | # We close the current loop to avoid this 392 | try: 393 | asyncio.get_event_loop().close() 394 | except RuntimeError: 395 | pass 396 | return asyncio.run(asyncio.sleep(0)) 397 | # asyncio.run() sets the current event loop to None when finished 398 | 399 | @pytest.mark.asyncio(loop_scope="session") 400 | # parametrization may impact fixture ordering 401 | @pytest.mark.parametrize("n", (0, 1)) 402 | async def test_does_not_fail(sets_event_loop_to_none, n): 403 | pass 404 | """ 405 | ) 406 | ) 407 | result = pytester.runpytest("--asyncio-mode=strict") 408 | result.assert_outcomes(passed=2) 409 | 410 | 411 | def test_standalone_test_does_not_trigger_warning_about_no_current_event_loop_being_set( 412 | pytester: Pytester, 413 | ): 414 | pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") 415 | pytester.makepyfile( 416 | dedent( 417 | """\ 418 | import pytest 419 | 420 | @pytest.mark.asyncio(loop_scope="session") 421 | async def test_anything(): 422 | pass 423 | """ 424 | ) 425 | ) 426 | result = pytester.runpytest_subprocess("--asyncio-mode=strict") 427 | result.assert_outcomes(warnings=0, passed=1) 428 | -------------------------------------------------------------------------------- /tests/modes/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pytest-dev/pytest-asyncio/bd741b82e22030732e42a92eedebdb133cc3be36/tests/modes/__init__.py -------------------------------------------------------------------------------- /tests/modes/test_auto_mode.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from textwrap import dedent 4 | 5 | from pytest import Pytester 6 | 7 | 8 | def test_auto_mode_cmdline(pytester: Pytester): 9 | pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") 10 | pytester.makepyfile( 11 | dedent( 12 | """\ 13 | import asyncio 14 | import pytest 15 | 16 | pytest_plugins = 'pytest_asyncio' 17 | 18 | async def test_a(): 19 | await asyncio.sleep(0) 20 | """ 21 | ) 22 | ) 23 | result = pytester.runpytest("--asyncio-mode=auto") 24 | result.assert_outcomes(passed=1) 25 | 26 | 27 | def test_auto_mode_cfg(pytester: Pytester): 28 | pytester.makeini( 29 | dedent( 30 | """\ 31 | [pytest] 32 | asyncio_default_fixture_loop_scope = function 33 | asyncio_mode = auto 34 | """ 35 | ) 36 | ) 37 | pytester.makepyfile( 38 | dedent( 39 | """\ 40 | import asyncio 41 | import pytest 42 | 43 | pytest_plugins = 'pytest_asyncio' 44 | 45 | async def test_a(): 46 | await asyncio.sleep(0) 47 | """ 48 | ) 49 | ) 50 | result = pytester.runpytest("--asyncio-mode=auto") 51 | result.assert_outcomes(passed=1) 52 | 53 | 54 | def test_auto_mode_async_fixture(pytester: Pytester): 55 | pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") 56 | pytester.makepyfile( 57 | dedent( 58 | """\ 59 | import asyncio 60 | import pytest 61 | 62 | pytest_plugins = 'pytest_asyncio' 63 | 64 | @pytest.fixture 65 | async def fixture_a(): 66 | await asyncio.sleep(0) 67 | return 1 68 | 69 | async def test_a(fixture_a): 70 | await asyncio.sleep(0) 71 | assert fixture_a == 1 72 | """ 73 | ) 74 | ) 75 | result = pytester.runpytest("--asyncio-mode=auto") 76 | result.assert_outcomes(passed=1) 77 | 78 | 79 | def test_auto_mode_method_fixture(pytester: Pytester): 80 | pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") 81 | pytester.makepyfile( 82 | dedent( 83 | """\ 84 | import asyncio 85 | import pytest 86 | 87 | pytest_plugins = 'pytest_asyncio' 88 | 89 | 90 | class TestA: 91 | 92 | @pytest.fixture 93 | async def fixture_a(self): 94 | await asyncio.sleep(0) 95 | return 1 96 | 97 | async def test_a(self, fixture_a): 98 | await asyncio.sleep(0) 99 | assert fixture_a == 1 100 | """ 101 | ) 102 | ) 103 | result = pytester.runpytest("--asyncio-mode=auto") 104 | result.assert_outcomes(passed=1) 105 | 106 | 107 | def test_auto_mode_static_method(pytester: Pytester): 108 | pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") 109 | pytester.makepyfile( 110 | dedent( 111 | """\ 112 | import asyncio 113 | 114 | pytest_plugins = 'pytest_asyncio' 115 | 116 | 117 | class TestA: 118 | 119 | @staticmethod 120 | async def test_a(): 121 | await asyncio.sleep(0) 122 | """ 123 | ) 124 | ) 125 | result = pytester.runpytest("--asyncio-mode=auto") 126 | result.assert_outcomes(passed=1) 127 | 128 | 129 | def test_auto_mode_static_method_fixture(pytester: Pytester): 130 | pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") 131 | pytester.makepyfile( 132 | dedent( 133 | """\ 134 | import asyncio 135 | import pytest 136 | 137 | pytest_plugins = 'pytest_asyncio' 138 | 139 | 140 | class TestA: 141 | 142 | @staticmethod 143 | @pytest.fixture 144 | async def fixture_a(): 145 | await asyncio.sleep(0) 146 | return 1 147 | 148 | @staticmethod 149 | async def test_a(fixture_a): 150 | await asyncio.sleep(0) 151 | assert fixture_a == 1 152 | """ 153 | ) 154 | ) 155 | result = pytester.runpytest("--asyncio-mode=auto") 156 | result.assert_outcomes(passed=1) 157 | -------------------------------------------------------------------------------- /tests/modes/test_strict_mode.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from textwrap import dedent 4 | 5 | from pytest import Pytester 6 | 7 | 8 | def test_strict_mode_cmdline(pytester: Pytester): 9 | pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") 10 | pytester.makepyfile( 11 | dedent( 12 | """\ 13 | import asyncio 14 | import pytest 15 | 16 | pytest_plugins = 'pytest_asyncio' 17 | 18 | @pytest.mark.asyncio 19 | async def test_a(): 20 | await asyncio.sleep(0) 21 | """ 22 | ) 23 | ) 24 | result = pytester.runpytest("--asyncio-mode=strict") 25 | result.assert_outcomes(passed=1) 26 | 27 | 28 | def test_strict_mode_cfg(pytester: Pytester): 29 | pytester.makeini( 30 | dedent( 31 | """\ 32 | [pytest] 33 | asyncio_default_fixture_loop_scope = function 34 | asyncio_mode = strict 35 | """ 36 | ) 37 | ) 38 | pytester.makepyfile( 39 | dedent( 40 | """\ 41 | import asyncio 42 | import pytest 43 | 44 | pytest_plugins = 'pytest_asyncio' 45 | 46 | @pytest.mark.asyncio 47 | async def test_a(): 48 | await asyncio.sleep(0) 49 | """ 50 | ) 51 | ) 52 | result = pytester.runpytest() 53 | result.assert_outcomes(passed=1) 54 | 55 | 56 | def test_strict_mode_method_fixture(pytester: Pytester): 57 | pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") 58 | pytester.makepyfile( 59 | dedent( 60 | """\ 61 | import asyncio 62 | import pytest 63 | import pytest_asyncio 64 | 65 | pytest_plugins = 'pytest_asyncio' 66 | 67 | class TestA: 68 | 69 | @pytest_asyncio.fixture 70 | async def fixture_a(self): 71 | await asyncio.sleep(0) 72 | return 1 73 | 74 | @pytest.mark.asyncio 75 | async def test_a(self, fixture_a): 76 | await asyncio.sleep(0) 77 | assert fixture_a == 1 78 | """ 79 | ) 80 | ) 81 | result = pytester.runpytest("--asyncio-mode=auto") 82 | result.assert_outcomes(passed=1) 83 | 84 | 85 | def test_strict_mode_ignores_unmarked_coroutine(pytester: Pytester): 86 | pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") 87 | pytester.makepyfile( 88 | dedent( 89 | """\ 90 | import pytest 91 | 92 | async def test_anything(): 93 | pass 94 | """ 95 | ) 96 | ) 97 | result = pytester.runpytest_subprocess("--asyncio-mode=strict", "-W default") 98 | result.assert_outcomes(skipped=1, warnings=1) 99 | result.stdout.fnmatch_lines(["*async def functions are not natively supported*"]) 100 | 101 | 102 | def test_strict_mode_ignores_unmarked_fixture(pytester: Pytester): 103 | pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") 104 | pytester.makepyfile( 105 | dedent( 106 | """\ 107 | import pytest 108 | 109 | # Not using pytest_asyncio.fixture 110 | @pytest.fixture() 111 | async def any_fixture(): 112 | raise RuntimeError() 113 | 114 | async def test_anything(any_fixture): 115 | pass 116 | """ 117 | ) 118 | ) 119 | result = pytester.runpytest_subprocess("--asyncio-mode=strict", "-W default") 120 | result.assert_outcomes(skipped=1, warnings=2) 121 | result.stdout.fnmatch_lines( 122 | [ 123 | "*async def functions are not natively supported*", 124 | "*coroutine 'any_fixture' was never awaited*", 125 | ], 126 | ) 127 | 128 | 129 | def test_strict_mode_marked_test_unmarked_fixture_warning(pytester: Pytester): 130 | pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") 131 | pytester.makepyfile( 132 | dedent( 133 | """\ 134 | import pytest 135 | 136 | # Not using pytest_asyncio.fixture 137 | @pytest.fixture() 138 | async def any_fixture(): 139 | pass 140 | 141 | @pytest.mark.asyncio 142 | async def test_anything(any_fixture): 143 | # suppress unawaited coroutine warning 144 | try: 145 | any_fixture.send(None) 146 | except StopIteration: 147 | pass 148 | """ 149 | ) 150 | ) 151 | result = pytester.runpytest_subprocess("--asyncio-mode=strict", "-W default") 152 | result.assert_outcomes(passed=1, failed=0, skipped=0, warnings=1) 153 | result.stdout.fnmatch_lines( 154 | [ 155 | "*warnings summary*", 156 | ( 157 | "test_strict_mode_marked_test_unmarked_fixture_warning.py::" 158 | "test_anything" 159 | ), 160 | ( 161 | "*/pytest_asyncio/plugin.py:*: PytestDeprecationWarning: " 162 | "asyncio test 'test_anything' requested async " 163 | "@pytest.fixture 'any_fixture' in strict mode. " 164 | "You might want to use @pytest_asyncio.fixture or switch to " 165 | "auto mode. " 166 | "This will become an error in future versions of flake8-asyncio." 167 | ), 168 | ], 169 | ) 170 | 171 | 172 | # autouse is not handled in any special way currently 173 | def test_strict_mode_marked_test_unmarked_autouse_fixture_warning(pytester: Pytester): 174 | pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") 175 | pytester.makepyfile( 176 | dedent( 177 | """\ 178 | import pytest 179 | 180 | # Not using pytest_asyncio.fixture 181 | @pytest.fixture(autouse=True) 182 | async def any_fixture(): 183 | pass 184 | 185 | @pytest.mark.asyncio 186 | async def test_anything(any_fixture): 187 | # suppress unawaited coroutine warning 188 | try: 189 | any_fixture.send(None) 190 | except StopIteration: 191 | pass 192 | """ 193 | ) 194 | ) 195 | result = pytester.runpytest_subprocess("--asyncio-mode=strict", "-W default") 196 | result.assert_outcomes(passed=1, warnings=1) 197 | result.stdout.fnmatch_lines( 198 | [ 199 | "*warnings summary*", 200 | ( 201 | "test_strict_mode_marked_test_unmarked_autouse_fixture_warning.py::" 202 | "test_anything" 203 | ), 204 | ( 205 | "*/pytest_asyncio/plugin.py:*: PytestDeprecationWarning: " 206 | "*asyncio test 'test_anything' requested async " 207 | "@pytest.fixture 'any_fixture' in strict mode. " 208 | "You might want to use @pytest_asyncio.fixture or switch to " 209 | "auto mode. " 210 | "This will become an error in future versions of flake8-asyncio." 211 | ), 212 | ], 213 | ) 214 | -------------------------------------------------------------------------------- /tests/test_asyncio_fixture.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import asyncio 4 | from textwrap import dedent 5 | 6 | import pytest 7 | from pytest import Pytester 8 | 9 | import pytest_asyncio 10 | 11 | 12 | @pytest_asyncio.fixture 13 | async def fixture_bare(): 14 | await asyncio.sleep(0) 15 | return 1 16 | 17 | 18 | @pytest.mark.asyncio 19 | async def test_bare_fixture(fixture_bare): 20 | await asyncio.sleep(0) 21 | assert fixture_bare == 1 22 | 23 | 24 | @pytest_asyncio.fixture(name="new_fixture_name") 25 | async def fixture_with_name(request): 26 | await asyncio.sleep(0) 27 | return request.fixturename 28 | 29 | 30 | @pytest.mark.asyncio 31 | async def test_fixture_with_name(new_fixture_name): 32 | await asyncio.sleep(0) 33 | assert new_fixture_name == "new_fixture_name" 34 | 35 | 36 | @pytest_asyncio.fixture(params=[2, 4]) 37 | async def fixture_with_params(request): 38 | await asyncio.sleep(0) 39 | return request.param 40 | 41 | 42 | @pytest.mark.asyncio 43 | async def test_fixture_with_params(fixture_with_params): 44 | await asyncio.sleep(0) 45 | assert fixture_with_params % 2 == 0 46 | 47 | 48 | @pytest.mark.parametrize("mode", ("auto", "strict")) 49 | def test_sync_function_uses_async_fixture(pytester: Pytester, mode): 50 | pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") 51 | pytester.makepyfile( 52 | dedent( 53 | """\ 54 | import pytest_asyncio 55 | 56 | pytest_plugins = 'pytest_asyncio' 57 | 58 | @pytest_asyncio.fixture 59 | async def always_true(): 60 | return True 61 | 62 | def test_sync_function_uses_async_fixture(always_true): 63 | assert always_true is True 64 | """ 65 | ) 66 | ) 67 | result = pytester.runpytest(f"--asyncio-mode={mode}") 68 | result.assert_outcomes(passed=1) 69 | -------------------------------------------------------------------------------- /tests/test_asyncio_mark.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from textwrap import dedent 4 | 5 | from pytest import Pytester 6 | 7 | 8 | def test_asyncio_mark_on_sync_function_emits_warning(pytester: Pytester): 9 | pytester.makepyfile( 10 | dedent( 11 | """\ 12 | import pytest 13 | 14 | @pytest.mark.asyncio 15 | def test_a(): 16 | pass 17 | """ 18 | ) 19 | ) 20 | result = pytester.runpytest_subprocess("--asyncio-mode=strict", "-W default") 21 | result.assert_outcomes(passed=1) 22 | result.stdout.fnmatch_lines( 23 | ["*is marked with '@pytest.mark.asyncio' but it is not an async function.*"] 24 | ) 25 | 26 | 27 | def test_asyncio_mark_on_async_generator_function_emits_warning_in_strict_mode( 28 | pytester: Pytester, 29 | ): 30 | pytester.makepyfile( 31 | dedent( 32 | """\ 33 | import pytest 34 | 35 | @pytest.mark.asyncio 36 | async def test_a(): 37 | yield 38 | """ 39 | ) 40 | ) 41 | result = pytester.runpytest_subprocess("--asyncio-mode=strict", "-W default") 42 | result.assert_outcomes(xfailed=1, warnings=1) 43 | result.stdout.fnmatch_lines( 44 | ["*Tests based on asynchronous generators are not supported*"] 45 | ) 46 | 47 | 48 | def test_asyncio_mark_on_async_generator_function_emits_warning_in_auto_mode( 49 | pytester: Pytester, 50 | ): 51 | pytester.makepyfile( 52 | dedent( 53 | """\ 54 | async def test_a(): 55 | yield 56 | """ 57 | ) 58 | ) 59 | result = pytester.runpytest_subprocess("--asyncio-mode=auto", "-W default") 60 | result.assert_outcomes(xfailed=1, warnings=1) 61 | result.stdout.fnmatch_lines( 62 | ["*Tests based on asynchronous generators are not supported*"] 63 | ) 64 | 65 | 66 | def test_asyncio_mark_on_async_generator_method_emits_warning_in_strict_mode( 67 | pytester: Pytester, 68 | ): 69 | pytester.makepyfile( 70 | dedent( 71 | """\ 72 | import pytest 73 | 74 | class TestAsyncGenerator: 75 | @pytest.mark.asyncio 76 | async def test_a(self): 77 | yield 78 | """ 79 | ) 80 | ) 81 | result = pytester.runpytest_subprocess("--asyncio-mode=strict", "-W default") 82 | result.assert_outcomes(xfailed=1, warnings=1) 83 | result.stdout.fnmatch_lines( 84 | ["*Tests based on asynchronous generators are not supported*"] 85 | ) 86 | 87 | 88 | def test_asyncio_mark_on_async_generator_method_emits_warning_in_auto_mode( 89 | pytester: Pytester, 90 | ): 91 | pytester.makepyfile( 92 | dedent( 93 | """\ 94 | class TestAsyncGenerator: 95 | @staticmethod 96 | async def test_a(): 97 | yield 98 | """ 99 | ) 100 | ) 101 | result = pytester.runpytest_subprocess("--asyncio-mode=auto", "-W default") 102 | result.assert_outcomes(xfailed=1, warnings=1) 103 | result.stdout.fnmatch_lines( 104 | ["*Tests based on asynchronous generators are not supported*"] 105 | ) 106 | 107 | 108 | def test_asyncio_mark_on_async_generator_staticmethod_emits_warning_in_strict_mode( 109 | pytester: Pytester, 110 | ): 111 | pytester.makepyfile( 112 | dedent( 113 | """\ 114 | import pytest 115 | 116 | class TestAsyncGenerator: 117 | @staticmethod 118 | @pytest.mark.asyncio 119 | async def test_a(): 120 | yield 121 | """ 122 | ) 123 | ) 124 | result = pytester.runpytest_subprocess("--asyncio-mode=strict", "-W default") 125 | result.assert_outcomes(xfailed=1, warnings=1) 126 | result.stdout.fnmatch_lines( 127 | ["*Tests based on asynchronous generators are not supported*"] 128 | ) 129 | 130 | 131 | def test_asyncio_mark_on_async_generator_staticmethod_emits_warning_in_auto_mode( 132 | pytester: Pytester, 133 | ): 134 | pytester.makepyfile( 135 | dedent( 136 | """\ 137 | class TestAsyncGenerator: 138 | @staticmethod 139 | async def test_a(): 140 | yield 141 | """ 142 | ) 143 | ) 144 | result = pytester.runpytest_subprocess("--asyncio-mode=auto", "-W default") 145 | result.assert_outcomes(xfailed=1, warnings=1) 146 | result.stdout.fnmatch_lines( 147 | ["*Tests based on asynchronous generators are not supported*"] 148 | ) 149 | 150 | 151 | def test_asyncio_marker_fallbacks_to_configured_default_loop_scope_if_not_set( 152 | pytester: Pytester, 153 | ): 154 | pytester.makeini( 155 | dedent( 156 | """\ 157 | [pytest] 158 | asyncio_default_fixture_loop_scope = function 159 | asyncio_default_test_loop_scope = session 160 | """ 161 | ) 162 | ) 163 | 164 | pytester.makepyfile( 165 | dedent( 166 | """\ 167 | import asyncio 168 | import pytest_asyncio 169 | import pytest 170 | 171 | loop: asyncio.AbstractEventLoop 172 | 173 | @pytest_asyncio.fixture(loop_scope="session", scope="session") 174 | async def session_loop_fixture(): 175 | global loop 176 | loop = asyncio.get_running_loop() 177 | 178 | async def test_a(session_loop_fixture): 179 | global loop 180 | assert asyncio.get_running_loop() is loop 181 | """ 182 | ) 183 | ) 184 | 185 | result = pytester.runpytest("--asyncio-mode=auto") 186 | result.assert_outcomes(passed=1) 187 | 188 | 189 | def test_asyncio_marker_uses_marker_loop_scope_even_if_config_is_set( 190 | pytester: Pytester, 191 | ): 192 | pytester.makeini( 193 | dedent( 194 | """\ 195 | [pytest] 196 | asyncio_default_fixture_loop_scope = function 197 | asyncio_default_test_loop_scope = module 198 | """ 199 | ) 200 | ) 201 | 202 | pytester.makepyfile( 203 | dedent( 204 | """\ 205 | import asyncio 206 | import pytest_asyncio 207 | import pytest 208 | 209 | loop: asyncio.AbstractEventLoop 210 | 211 | @pytest_asyncio.fixture(loop_scope="session", scope="session") 212 | async def session_loop_fixture(): 213 | global loop 214 | loop = asyncio.get_running_loop() 215 | 216 | @pytest.mark.asyncio(loop_scope="session") 217 | async def test_a(session_loop_fixture): 218 | global loop 219 | assert asyncio.get_running_loop() is loop 220 | """ 221 | ) 222 | ) 223 | 224 | result = pytester.runpytest("--asyncio-mode=auto") 225 | result.assert_outcomes(passed=1) 226 | -------------------------------------------------------------------------------- /tests/test_doctest.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from textwrap import dedent 4 | 5 | from pytest import Pytester 6 | 7 | 8 | def test_plugin_does_not_interfere_with_doctest_collection(pytester: Pytester): 9 | pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") 10 | pytester.makepyfile( 11 | dedent( 12 | '''\ 13 | def any_function(): 14 | """ 15 | >>> 42 16 | 42 17 | """ 18 | ''' 19 | ), 20 | ) 21 | result = pytester.runpytest("--asyncio-mode=strict", "--doctest-modules") 22 | result.assert_outcomes(passed=1) 23 | 24 | 25 | def test_plugin_does_not_interfere_with_doctest_textfile_collection(pytester: Pytester): 26 | pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") 27 | pytester.makefile(".txt", "") # collected as DoctestTextfile 28 | pytester.makepyfile( 29 | __init__="", 30 | test_python_file=dedent( 31 | """\ 32 | import pytest 33 | 34 | pytest_plugins = "pytest_asyncio" 35 | 36 | @pytest.mark.asyncio 37 | async def test_anything(): 38 | pass 39 | """ 40 | ), 41 | ) 42 | result = pytester.runpytest("--asyncio-mode=strict") 43 | result.assert_outcomes(passed=1) 44 | -------------------------------------------------------------------------------- /tests/test_event_loop_fixture.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from textwrap import dedent 4 | 5 | from pytest import Pytester 6 | 7 | 8 | def test_event_loop_fixture_respects_event_loop_policy(pytester: Pytester): 9 | pytester.makeconftest( 10 | dedent( 11 | """\ 12 | '''Defines and sets a custom event loop policy''' 13 | import asyncio 14 | from asyncio import DefaultEventLoopPolicy, SelectorEventLoop 15 | 16 | class TestEventLoop(SelectorEventLoop): 17 | pass 18 | 19 | class TestEventLoopPolicy(DefaultEventLoopPolicy): 20 | def new_event_loop(self): 21 | return TestEventLoop() 22 | 23 | # This statement represents a code which sets a custom event loop policy 24 | asyncio.set_event_loop_policy(TestEventLoopPolicy()) 25 | """ 26 | ) 27 | ) 28 | pytester.makepyfile( 29 | dedent( 30 | """\ 31 | '''Tests that any externally provided event loop policy remains unaltered''' 32 | import asyncio 33 | 34 | import pytest 35 | 36 | 37 | @pytest.mark.asyncio 38 | async def test_uses_loop_provided_by_custom_policy(): 39 | '''Asserts that test cases use the event loop 40 | provided by the custom event loop policy''' 41 | assert type(asyncio.get_event_loop()).__name__ == "TestEventLoop" 42 | 43 | 44 | @pytest.mark.asyncio 45 | async def test_custom_policy_is_not_overwritten(): 46 | ''' 47 | Asserts that any custom event loop policy stays the same 48 | across test cases. 49 | ''' 50 | assert type(asyncio.get_event_loop()).__name__ == "TestEventLoop" 51 | """ 52 | ) 53 | ) 54 | result = pytester.runpytest_subprocess("--asyncio-mode=strict") 55 | result.assert_outcomes(passed=2) 56 | 57 | 58 | def test_event_loop_fixture_handles_unclosed_async_gen( 59 | pytester: Pytester, 60 | ): 61 | pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") 62 | pytester.makepyfile( 63 | dedent( 64 | """\ 65 | import asyncio 66 | import pytest 67 | 68 | pytest_plugins = 'pytest_asyncio' 69 | 70 | @pytest.mark.asyncio 71 | async def test_something(): 72 | async def generator_fn(): 73 | yield 74 | yield 75 | 76 | gen = generator_fn() 77 | await gen.__anext__() 78 | """ 79 | ) 80 | ) 81 | result = pytester.runpytest_subprocess("--asyncio-mode=strict", "-W", "default") 82 | result.assert_outcomes(passed=1, warnings=0) 83 | 84 | 85 | def test_event_loop_already_closed( 86 | pytester: Pytester, 87 | ): 88 | pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") 89 | pytester.makepyfile( 90 | dedent( 91 | """\ 92 | import asyncio 93 | import pytest 94 | import pytest_asyncio 95 | pytest_plugins = 'pytest_asyncio' 96 | 97 | @pytest_asyncio.fixture 98 | async def _event_loop(): 99 | return asyncio.get_running_loop() 100 | 101 | @pytest.fixture 102 | def cleanup_after(_event_loop): 103 | yield 104 | # fixture has its own cleanup code 105 | _event_loop.close() 106 | 107 | @pytest.mark.asyncio 108 | async def test_something(cleanup_after): 109 | await asyncio.sleep(0.01) 110 | """ 111 | ) 112 | ) 113 | result = pytester.runpytest_subprocess("--asyncio-mode=strict", "-W", "default") 114 | result.assert_outcomes(passed=1, warnings=0) 115 | 116 | 117 | def test_event_loop_fixture_asyncgen_error( 118 | pytester: Pytester, 119 | ): 120 | pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") 121 | pytester.makepyfile( 122 | dedent( 123 | """\ 124 | import asyncio 125 | import pytest 126 | 127 | pytest_plugins = 'pytest_asyncio' 128 | 129 | @pytest.mark.asyncio 130 | async def test_something(): 131 | # mock shutdown_asyncgen failure 132 | loop = asyncio.get_running_loop() 133 | async def fail(): 134 | raise RuntimeError("mock error cleaning up...") 135 | loop.shutdown_asyncgens = fail 136 | """ 137 | ) 138 | ) 139 | result = pytester.runpytest_subprocess("--asyncio-mode=strict", "-W", "default") 140 | result.assert_outcomes(passed=1, warnings=1) 141 | -------------------------------------------------------------------------------- /tests/test_fixture_loop_scopes.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from textwrap import dedent 4 | 5 | import pytest 6 | from pytest import Pytester 7 | 8 | 9 | @pytest.mark.parametrize( 10 | "fixture_scope", ("session", "package", "module", "class", "function") 11 | ) 12 | def test_loop_scope_session_is_independent_of_fixture_scope( 13 | pytester: Pytester, 14 | fixture_scope: str, 15 | ): 16 | pytester.makepyfile( 17 | dedent( 18 | f"""\ 19 | import asyncio 20 | import pytest 21 | import pytest_asyncio 22 | 23 | loop: asyncio.AbstractEventLoop = None 24 | 25 | @pytest_asyncio.fixture(scope="{fixture_scope}", loop_scope="session") 26 | async def fixture(): 27 | global loop 28 | loop = asyncio.get_running_loop() 29 | 30 | @pytest.mark.asyncio(loop_scope="session") 31 | async def test_runs_in_same_loop_as_fixture(fixture): 32 | global loop 33 | assert loop == asyncio.get_running_loop() 34 | """ 35 | ) 36 | ) 37 | result = pytester.runpytest_subprocess("--asyncio-mode=strict") 38 | result.assert_outcomes(passed=1) 39 | 40 | 41 | @pytest.mark.parametrize("default_loop_scope", ("function", "module", "session")) 42 | def test_default_loop_scope_config_option_changes_fixture_loop_scope( 43 | pytester: Pytester, 44 | default_loop_scope: str, 45 | ): 46 | pytester.makeini( 47 | dedent( 48 | f"""\ 49 | [pytest] 50 | asyncio_default_fixture_loop_scope = {default_loop_scope} 51 | """ 52 | ) 53 | ) 54 | pytester.makepyfile( 55 | dedent( 56 | f"""\ 57 | import asyncio 58 | import pytest 59 | import pytest_asyncio 60 | 61 | @pytest_asyncio.fixture 62 | async def fixture_loop(): 63 | return asyncio.get_running_loop() 64 | 65 | @pytest.mark.asyncio(loop_scope="{default_loop_scope}") 66 | async def test_runs_in_fixture_loop(fixture_loop): 67 | assert asyncio.get_running_loop() is fixture_loop 68 | """ 69 | ) 70 | ) 71 | result = pytester.runpytest_subprocess("--asyncio-mode=strict") 72 | result.assert_outcomes(passed=1) 73 | 74 | 75 | def test_default_class_loop_scope_config_option_changes_fixture_loop_scope( 76 | pytester: Pytester, 77 | ): 78 | pytester.makeini( 79 | dedent( 80 | """\ 81 | [pytest] 82 | asyncio_default_fixture_loop_scope = class 83 | """ 84 | ) 85 | ) 86 | pytester.makepyfile( 87 | dedent( 88 | """\ 89 | import asyncio 90 | import pytest 91 | import pytest_asyncio 92 | 93 | class TestClass: 94 | @pytest_asyncio.fixture 95 | async def fixture_loop(self): 96 | return asyncio.get_running_loop() 97 | 98 | @pytest.mark.asyncio(loop_scope="class") 99 | async def test_runs_in_fixture_loop(self, fixture_loop): 100 | assert asyncio.get_running_loop() is fixture_loop 101 | """ 102 | ) 103 | ) 104 | result = pytester.runpytest_subprocess("--asyncio-mode=strict") 105 | result.assert_outcomes(passed=1) 106 | 107 | 108 | def test_default_package_loop_scope_config_option_changes_fixture_loop_scope( 109 | pytester: Pytester, 110 | ): 111 | pytester.makeini( 112 | dedent( 113 | """\ 114 | [pytest] 115 | asyncio_default_fixture_loop_scope = package 116 | """ 117 | ) 118 | ) 119 | pytester.makepyfile( 120 | __init__="", 121 | test_a=dedent( 122 | """\ 123 | import asyncio 124 | import pytest 125 | import pytest_asyncio 126 | 127 | @pytest_asyncio.fixture 128 | async def fixture_loop(): 129 | return asyncio.get_running_loop() 130 | 131 | @pytest.mark.asyncio(loop_scope="package") 132 | async def test_runs_in_fixture_loop(fixture_loop): 133 | assert asyncio.get_running_loop() is fixture_loop 134 | """ 135 | ), 136 | ) 137 | result = pytester.runpytest_subprocess("--asyncio-mode=strict") 138 | result.assert_outcomes(passed=1) 139 | -------------------------------------------------------------------------------- /tests/test_import.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from textwrap import dedent 4 | 5 | from pytest import Pytester 6 | 7 | 8 | def test_import_warning_does_not_cause_internal_error(pytester: Pytester): 9 | pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") 10 | pytester.makepyfile( 11 | dedent( 12 | """\ 13 | raise ImportWarning() 14 | 15 | async def test_errors_out(): 16 | pass 17 | """ 18 | ) 19 | ) 20 | result = pytester.runpytest("--asyncio-mode=auto") 21 | result.assert_outcomes(errors=1) 22 | 23 | 24 | def test_import_warning_in_package_does_not_cause_internal_error(pytester: Pytester): 25 | pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") 26 | pytester.makepyfile( 27 | __init__=dedent( 28 | """\ 29 | raise ImportWarning() 30 | """ 31 | ), 32 | test_a=dedent( 33 | """\ 34 | async def test_errors_out(): 35 | pass 36 | """ 37 | ), 38 | ) 39 | result = pytester.runpytest("--asyncio-mode=auto") 40 | result.assert_outcomes(errors=1) 41 | 42 | 43 | def test_does_not_import_unrelated_packages(pytester: Pytester): 44 | pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") 45 | pkg_dir = pytester.mkpydir("mypkg") 46 | pkg_dir.joinpath("__init__.py").write_text( 47 | dedent( 48 | """\ 49 | raise ImportError() 50 | """ 51 | ), 52 | ) 53 | test_dir = pytester.mkdir("tests") 54 | test_dir.joinpath("test_a.py").write_text( 55 | dedent( 56 | """\ 57 | async def test_passes(): 58 | pass 59 | """ 60 | ), 61 | ) 62 | result = pytester.runpytest("--asyncio-mode=auto") 63 | result.assert_outcomes(passed=1) 64 | -------------------------------------------------------------------------------- /tests/test_is_async_test.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from textwrap import dedent 4 | 5 | from pytest import Pytester 6 | 7 | 8 | def test_returns_false_for_sync_item(pytester: Pytester): 9 | pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") 10 | pytester.makepyfile( 11 | dedent( 12 | """\ 13 | import pytest 14 | import pytest_asyncio 15 | 16 | def test_sync(): 17 | pass 18 | 19 | def pytest_collection_modifyitems(items): 20 | async_tests = [ 21 | item 22 | for item in items 23 | if pytest_asyncio.is_async_test(item) 24 | ] 25 | assert len(async_tests) == 0 26 | """ 27 | ) 28 | ) 29 | result = pytester.runpytest("--asyncio-mode=strict") 30 | result.assert_outcomes(passed=1) 31 | 32 | 33 | def test_returns_true_for_marked_coroutine_item_in_strict_mode(pytester: Pytester): 34 | pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") 35 | pytester.makepyfile( 36 | dedent( 37 | """\ 38 | import pytest 39 | import pytest_asyncio 40 | 41 | @pytest.mark.asyncio 42 | async def test_coro(): 43 | pass 44 | 45 | def pytest_collection_modifyitems(items): 46 | async_tests = [ 47 | item 48 | for item in items 49 | if pytest_asyncio.is_async_test(item) 50 | ] 51 | assert len(async_tests) == 1 52 | """ 53 | ) 54 | ) 55 | result = pytester.runpytest("--asyncio-mode=strict") 56 | result.assert_outcomes(passed=1) 57 | 58 | 59 | def test_returns_false_for_unmarked_coroutine_item_in_strict_mode(pytester: Pytester): 60 | pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") 61 | pytester.makepyfile( 62 | dedent( 63 | """\ 64 | import pytest 65 | import pytest_asyncio 66 | 67 | async def test_coro(): 68 | pass 69 | 70 | def pytest_collection_modifyitems(items): 71 | async_tests = [ 72 | item 73 | for item in items 74 | if pytest_asyncio.is_async_test(item) 75 | ] 76 | assert len(async_tests) == 0 77 | """ 78 | ) 79 | ) 80 | result = pytester.runpytest("--asyncio-mode=strict") 81 | result.assert_outcomes(failed=1) 82 | 83 | 84 | def test_returns_true_for_unmarked_coroutine_item_in_auto_mode(pytester: Pytester): 85 | pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") 86 | pytester.makepyfile( 87 | dedent( 88 | """\ 89 | import pytest 90 | import pytest_asyncio 91 | 92 | async def test_coro(): 93 | pass 94 | 95 | def pytest_collection_modifyitems(items): 96 | async_tests = [ 97 | item 98 | for item in items 99 | if pytest_asyncio.is_async_test(item) 100 | ] 101 | assert len(async_tests) == 1 102 | """ 103 | ) 104 | ) 105 | result = pytester.runpytest("--asyncio-mode=auto") 106 | result.assert_outcomes(passed=1) 107 | -------------------------------------------------------------------------------- /tests/test_port_factories.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from textwrap import dedent 4 | 5 | from pytest import Pytester 6 | 7 | import pytest_asyncio.plugin 8 | 9 | 10 | def test_unused_tcp_port_selects_unused_port(pytester: Pytester): 11 | pytester.makepyfile( 12 | dedent( 13 | """\ 14 | import asyncio 15 | 16 | import pytest 17 | 18 | @pytest.mark.asyncio 19 | async def test_unused_port_fixture(unused_tcp_port): 20 | async def closer(_, writer): 21 | writer.close() 22 | 23 | server1 = await asyncio.start_server( 24 | closer, host="localhost", port=unused_tcp_port 25 | ) 26 | 27 | with pytest.raises(IOError): 28 | await asyncio.start_server( 29 | closer, host="localhost", port=unused_tcp_port 30 | ) 31 | 32 | server1.close() 33 | await server1.wait_closed() 34 | """ 35 | ) 36 | ) 37 | 38 | 39 | def test_unused_udp_port_selects_unused_port(pytester: Pytester): 40 | pytester.makepyfile( 41 | dedent( 42 | """\ 43 | @pytest.mark.asyncio 44 | async def test_unused_udp_port_fixture(unused_udp_port): 45 | class Closer: 46 | def connection_made(self, transport): 47 | pass 48 | 49 | def connection_lost(self, *arg, **kwd): 50 | pass 51 | 52 | event_loop = asyncio.get_running_loop() 53 | transport1, _ = await event_loop.create_datagram_endpoint( 54 | Closer, 55 | local_addr=("127.0.0.1", unused_udp_port), 56 | reuse_port=False, 57 | ) 58 | 59 | with pytest.raises(IOError): 60 | await event_loop.create_datagram_endpoint( 61 | Closer, 62 | local_addr=("127.0.0.1", unused_udp_port), 63 | reuse_port=False, 64 | ) 65 | 66 | transport1.abort() 67 | """ 68 | ) 69 | ) 70 | 71 | 72 | def test_unused_tcp_port_factory_selects_unused_port(pytester: Pytester): 73 | pytester.makepyfile( 74 | dedent( 75 | """\ 76 | @pytest.mark.asyncio 77 | async def test_unused_port_factory_fixture(unused_tcp_port_factory): 78 | async def closer(_, writer): 79 | writer.close() 80 | 81 | port1, port2, port3 = ( 82 | unused_tcp_port_factory(), 83 | unused_tcp_port_factory(), 84 | unused_tcp_port_factory(), 85 | ) 86 | 87 | server1 = await asyncio.start_server( 88 | closer, host="localhost", port=port1 89 | ) 90 | server2 = await asyncio.start_server( 91 | closer, host="localhost", port=port2 92 | ) 93 | server3 = await asyncio.start_server( 94 | closer, host="localhost", port=port3 95 | ) 96 | 97 | for port in port1, port2, port3: 98 | with pytest.raises(IOError): 99 | await asyncio.start_server(closer, host="localhost", port=port) 100 | 101 | server1.close() 102 | await server1.wait_closed() 103 | server2.close() 104 | await server2.wait_closed() 105 | server3.close() 106 | await server3.wait_closed() 107 | """ 108 | ) 109 | ) 110 | 111 | 112 | def test_unused_udp_port_factory_selects_unused_port(pytester: Pytester): 113 | pytester.makepyfile( 114 | dedent( 115 | """\ 116 | @pytest.mark.asyncio 117 | async def test_unused_udp_port_factory_fixture(unused_udp_port_factory): 118 | class Closer: 119 | def connection_made(self, transport): 120 | pass 121 | 122 | def connection_lost(self, *arg, **kwd): 123 | pass 124 | 125 | port1, port2, port3 = ( 126 | unused_udp_port_factory(), 127 | unused_udp_port_factory(), 128 | unused_udp_port_factory(), 129 | ) 130 | 131 | event_loop = asyncio.get_running_loop() 132 | transport1, _ = await event_loop.create_datagram_endpoint( 133 | Closer, 134 | local_addr=("127.0.0.1", port1), 135 | reuse_port=False, 136 | ) 137 | transport2, _ = await event_loop.create_datagram_endpoint( 138 | Closer, 139 | local_addr=("127.0.0.1", port2), 140 | reuse_port=False, 141 | ) 142 | transport3, _ = await event_loop.create_datagram_endpoint( 143 | Closer, 144 | local_addr=("127.0.0.1", port3), 145 | reuse_port=False, 146 | ) 147 | 148 | for port in port1, port2, port3: 149 | with pytest.raises(IOError): 150 | await event_loop.create_datagram_endpoint( 151 | Closer, 152 | local_addr=("127.0.0.1", port), 153 | reuse_port=False, 154 | ) 155 | 156 | transport1.abort() 157 | transport2.abort() 158 | transport3.abort() 159 | """ 160 | ) 161 | ) 162 | 163 | 164 | def test_unused_port_factory_duplicate(unused_tcp_port_factory, monkeypatch): 165 | """Test correct avoidance of duplicate ports.""" 166 | counter = 0 167 | 168 | def mock_unused_tcp_port(_ignored): 169 | """Force some duplicate ports.""" 170 | nonlocal counter 171 | counter += 1 172 | if counter < 5: 173 | return 10000 174 | else: 175 | return 10000 + counter 176 | 177 | monkeypatch.setattr(pytest_asyncio.plugin, "_unused_port", mock_unused_tcp_port) 178 | 179 | assert unused_tcp_port_factory() == 10000 180 | assert unused_tcp_port_factory() > 10000 181 | 182 | 183 | def test_unused_udp_port_factory_duplicate(unused_udp_port_factory, monkeypatch): 184 | """Test correct avoidance of duplicate UDP ports.""" 185 | counter = 0 186 | 187 | def mock_unused_udp_port(_ignored): 188 | """Force some duplicate ports.""" 189 | nonlocal counter 190 | counter += 1 191 | if counter < 5: 192 | return 10000 193 | else: 194 | return 10000 + counter 195 | 196 | monkeypatch.setattr(pytest_asyncio.plugin, "_unused_port", mock_unused_udp_port) 197 | 198 | assert unused_udp_port_factory() == 10000 199 | assert unused_udp_port_factory() > 10000 200 | -------------------------------------------------------------------------------- /tests/test_simple.py: -------------------------------------------------------------------------------- 1 | """Quick'n'dirty unit tests for provided fixtures and markers.""" 2 | 3 | from __future__ import annotations 4 | 5 | import asyncio 6 | from textwrap import dedent 7 | 8 | import pytest 9 | from pytest import Pytester 10 | 11 | 12 | async def async_coro(): 13 | await asyncio.sleep(0) 14 | return "ok" 15 | 16 | 17 | @pytest.mark.asyncio 18 | async def test_asyncio_marker(): 19 | """Test the asyncio pytest marker.""" 20 | await asyncio.sleep(0) 21 | 22 | 23 | def test_asyncio_marker_compatibility_with_xfail(pytester: Pytester): 24 | pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") 25 | pytester.makepyfile( 26 | dedent( 27 | """\ 28 | import pytest 29 | 30 | pytest_plugins = "pytest_asyncio" 31 | 32 | @pytest.mark.xfail(reason="need a failure", strict=True) 33 | @pytest.mark.asyncio 34 | async def test_asyncio_marker_fail(): 35 | raise AssertionError 36 | """ 37 | ) 38 | ) 39 | result = pytester.runpytest("--asyncio-mode=strict") 40 | result.assert_outcomes(xfailed=1) 41 | 42 | 43 | def test_asyncio_auto_mode_compatibility_with_xfail(pytester: Pytester): 44 | pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") 45 | pytester.makepyfile( 46 | dedent( 47 | """\ 48 | import pytest 49 | 50 | pytest_plugins = "pytest_asyncio" 51 | 52 | @pytest.mark.xfail(reason="need a failure", strict=True) 53 | async def test_asyncio_marker_fail(): 54 | raise AssertionError 55 | """ 56 | ) 57 | ) 58 | result = pytester.runpytest("--asyncio-mode=auto") 59 | result.assert_outcomes(xfailed=1) 60 | 61 | 62 | @pytest.mark.asyncio 63 | async def test_asyncio_marker_with_default_param(a_param=None): 64 | """Test the asyncio pytest marker.""" 65 | await asyncio.sleep(0) 66 | 67 | 68 | class TestMarkerInClassBasedTests: 69 | """Test that asyncio marked functions work for methods of test classes.""" 70 | 71 | @pytest.mark.asyncio 72 | async def test_asyncio_marker_with_implicit_loop_fixture(self): 73 | """ 74 | Test the "asyncio" marker works on a method in 75 | a class-based test with implicit loop fixture. 76 | """ 77 | ret = await async_coro() 78 | assert ret == "ok" 79 | 80 | 81 | class TestEventLoopStartedBeforeFixtures: 82 | @pytest.fixture 83 | async def loop(self): 84 | return asyncio.get_event_loop() 85 | 86 | @staticmethod 87 | def foo(): 88 | return 1 89 | 90 | @pytest.mark.asyncio 91 | async def test_no_event_loop(self, loop): 92 | assert await loop.run_in_executor(None, self.foo) == 1 93 | 94 | @pytest.mark.asyncio 95 | async def test_event_loop_after_fixture(self, loop): 96 | assert await loop.run_in_executor(None, self.foo) == 1 97 | 98 | @pytest.mark.asyncio 99 | async def test_event_loop_before_fixture(self, loop): 100 | assert await loop.run_in_executor(None, self.foo) == 1 101 | 102 | 103 | def test_invalid_asyncio_mode(pytester): 104 | pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") 105 | result = pytester.runpytest("-o", "asyncio_mode=True") 106 | result.stderr.no_fnmatch_line("INTERNALERROR> *") 107 | result.stderr.fnmatch_lines( 108 | "ERROR: 'True' is not a valid asyncio_mode. Valid modes: auto, strict." 109 | ) 110 | -------------------------------------------------------------------------------- /tests/test_skips.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from textwrap import dedent 4 | 5 | from pytest import Pytester 6 | 7 | 8 | def test_asyncio_strict_mode_skip(pytester: Pytester): 9 | pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") 10 | pytester.makepyfile( 11 | dedent( 12 | """\ 13 | import pytest 14 | 15 | pytest_plugins = "pytest_asyncio" 16 | 17 | @pytest.mark.asyncio 18 | async def test_no_warning_on_skip(): 19 | pytest.skip("Test a skip error inside asyncio") 20 | """ 21 | ) 22 | ) 23 | result = pytester.runpytest("--asyncio-mode=strict") 24 | result.assert_outcomes(skipped=1) 25 | 26 | 27 | def test_asyncio_auto_mode_skip(pytester: Pytester): 28 | pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") 29 | pytester.makepyfile( 30 | dedent( 31 | """\ 32 | import pytest 33 | 34 | pytest_plugins = "pytest_asyncio" 35 | 36 | async def test_no_warning_on_skip(): 37 | pytest.skip("Test a skip error inside asyncio") 38 | """ 39 | ) 40 | ) 41 | result = pytester.runpytest("--asyncio-mode=auto") 42 | result.assert_outcomes(skipped=1) 43 | 44 | 45 | def test_asyncio_strict_mode_module_level_skip(pytester: Pytester): 46 | pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") 47 | pytester.makepyfile( 48 | dedent( 49 | """\ 50 | import pytest 51 | 52 | pytest.skip("Skip all tests", allow_module_level=True) 53 | 54 | @pytest.mark.asyncio 55 | async def test_is_skipped(): 56 | pass 57 | """ 58 | ) 59 | ) 60 | result = pytester.runpytest("--asyncio-mode=strict") 61 | result.assert_outcomes(skipped=1) 62 | 63 | 64 | def test_asyncio_auto_mode_module_level_skip(pytester: Pytester): 65 | pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") 66 | pytester.makepyfile( 67 | dedent( 68 | """\ 69 | import pytest 70 | 71 | pytest.skip("Skip all tests", allow_module_level=True) 72 | 73 | async def test_is_skipped(): 74 | pass 75 | """ 76 | ) 77 | ) 78 | result = pytester.runpytest("--asyncio-mode=auto") 79 | result.assert_outcomes(skipped=1) 80 | 81 | 82 | def test_asyncio_auto_mode_wrong_skip_usage(pytester: Pytester): 83 | pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") 84 | pytester.makepyfile( 85 | dedent( 86 | """\ 87 | import pytest 88 | 89 | pytest.skip("Skip all tests") 90 | 91 | async def test_is_skipped(): 92 | pass 93 | """ 94 | ) 95 | ) 96 | result = pytester.runpytest("--asyncio-mode=auto") 97 | result.assert_outcomes(errors=1) 98 | 99 | 100 | def test_unittest_skiptest_compatibility(pytester: Pytester): 101 | pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") 102 | pytester.makepyfile( 103 | dedent( 104 | """\ 105 | from unittest import SkipTest 106 | 107 | raise SkipTest("Skip all tests") 108 | 109 | async def test_is_skipped(): 110 | pass 111 | """ 112 | ) 113 | ) 114 | result = pytester.runpytest("--asyncio-mode=auto") 115 | result.assert_outcomes(skipped=1) 116 | 117 | 118 | def test_skip_in_module_does_not_skip_package(pytester: Pytester): 119 | pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") 120 | pytester.makepyfile( 121 | __init__="", 122 | test_skip=dedent( 123 | """\ 124 | import pytest 125 | 126 | pytest.skip("Skip all tests", allow_module_level=True) 127 | 128 | def test_a(): 129 | pass 130 | 131 | def test_b(): 132 | pass 133 | """ 134 | ), 135 | test_something=dedent( 136 | """\ 137 | import pytest 138 | 139 | @pytest.mark.asyncio 140 | async def test_something(): 141 | pass 142 | """ 143 | ), 144 | ) 145 | result = pytester.runpytest("--asyncio-mode=strict") 146 | result.assert_outcomes(passed=1, skipped=1) 147 | -------------------------------------------------------------------------------- /tests/test_subprocess.py: -------------------------------------------------------------------------------- 1 | """Tests for using subprocesses in tests.""" 2 | 3 | from __future__ import annotations 4 | 5 | import asyncio.subprocess 6 | import sys 7 | 8 | import pytest 9 | 10 | 11 | @pytest.mark.asyncio 12 | async def test_subprocess(): 13 | """Starting a subprocess should be possible.""" 14 | proc = await asyncio.subprocess.create_subprocess_exec( 15 | sys.executable, "--version", stdout=asyncio.subprocess.PIPE 16 | ) 17 | await proc.communicate() 18 | -------------------------------------------------------------------------------- /tools/get-version.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import json 4 | import sys 5 | from importlib import metadata 6 | 7 | from packaging.version import parse as parse_version 8 | 9 | 10 | def main(): 11 | version_string = metadata.version("pytest-asyncio") 12 | version = parse_version(version_string) 13 | print(f"version={version}") 14 | prerelease = json.dumps(version.is_prerelease) 15 | print(f"prerelease={prerelease}") 16 | 17 | 18 | if __name__ == "__main__": 19 | sys.exit(main()) 20 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | minversion = 4.9.0 3 | envlist = py39, py310, py311, py312, py313, py314, pytest-min, docs 4 | isolated_build = true 5 | passenv = 6 | CI 7 | 8 | [testenv] 9 | extras = testing 10 | install_command = python -m pip install \ 11 | --requirement dependencies/default/requirements.txt \ 12 | --constraint dependencies/default/constraints.txt \ 13 | {opts} {packages} 14 | commands = make test 15 | allowlist_externals = 16 | make 17 | 18 | [testenv:pytest-min] 19 | extras = testing 20 | install_command = python -m pip install \ 21 | --requirement dependencies/pytest-min/requirements.txt \ 22 | --constraint dependencies/pytest-min/constraints.txt \ 23 | {opts} {packages} 24 | commands = make test 25 | allowlist_externals = 26 | make 27 | 28 | [testenv:docs] 29 | allowlist_externals = 30 | git 31 | extras = docs 32 | deps = 33 | --requirement dependencies/docs/requirements.txt 34 | --constraint dependencies/docs/constraints.txt 35 | change_dir = docs 36 | description = Build The Docs with {basepython} 37 | commands = 38 | # Retrieve possibly missing commits: 39 | -git fetch --unshallow 40 | -git fetch --tags 41 | 42 | # Build the html docs with Sphinx: 43 | {envpython} -Im sphinx \ 44 | -j auto \ 45 | {tty:--color} \ 46 | -a \ 47 | -T \ 48 | -n \ 49 | -W --keep-going \ 50 | -d "{temp_dir}{/}.doctrees" \ 51 | . \ 52 | {posargs:"{envdir}{/}docs_out" -b html} 53 | 54 | # Print out the output docs dir and a way to serve html: 55 | -{envpython} -c\ 56 | 'import pathlib;\ 57 | docs_dir = pathlib.Path(r"{envdir}") / "docs_out";\ 58 | index_file = docs_dir / "index.html";\ 59 | print("\n" + "=" * 120 +\ 60 | f"\n\nOpen the documentation with:\n\n\ 61 | \t$ python3 -Im webbrowser \N\{QUOTATION MARK\}file://\{index_file\}\N\{QUOTATION MARK\}\n\n\ 62 | To serve docs, use\n\n\ 63 | \t$ python3 -Im http.server --directory \ 64 | \N\{QUOTATION MARK\}\{docs_dir\}\N\{QUOTATION MARK\} 0\n\n" +\ 65 | "=" * 120)' 66 | changedir = {toxinidir}{/}docs 67 | isolated_build = true 68 | passenv = 69 | SSH_AUTH_SOCK 70 | skip_install = false 71 | 72 | [gh-actions] 73 | python = 74 | 3.9: py39, pytest-min 75 | 3.10: py310 76 | 3.11: py311 77 | 3.12: py312 78 | 3.13: py313 79 | 3.14-dev: py314 80 | pypy3: pypy3 81 | --------------------------------------------------------------------------------