├── .github ├── dependabot.yml └── workflows │ ├── auto-merge.yml │ ├── ci.yaml │ └── codeql.yml ├── .gitignore ├── .mypy.ini ├── .pre-commit-config.yaml ├── CHANGES.rst ├── LICENSE ├── MANIFEST.in ├── README.rst ├── pyproject.toml ├── pytest_aiohttp ├── __init__.py ├── plugin.py └── py.typed ├── requirements-dev.txt ├── requirements.txt ├── setup.cfg ├── setup.py └── tests ├── test_fixtures.py └── test_switch_mode.py /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2 3 | updates: 4 | - package-ecosystem: pip 5 | directory: / 6 | schedule: 7 | interval: daily 8 | 9 | - package-ecosystem: github-actions 10 | directory: / 11 | schedule: 12 | interval: monthly 13 | -------------------------------------------------------------------------------- /.github/workflows/auto-merge.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Dependabot auto-merge 3 | on: pull_request_target 4 | 5 | permissions: 6 | pull-requests: write 7 | contents: write 8 | 9 | jobs: 10 | dependabot: 11 | runs-on: ubuntu-latest 12 | if: ${{ github.actor == 'dependabot[bot]' }} 13 | steps: 14 | - name: Dependabot metadata 15 | id: metadata 16 | uses: dependabot/fetch-metadata@v2 17 | with: 18 | github-token: ${{ secrets.GITHUB_TOKEN }} 19 | - name: Enable auto-merge for Dependabot PRs 20 | run: gh pr merge --auto --squash "$PR_URL" 21 | env: 22 | PR_URL: ${{github.event.pull_request.html_url}} 23 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 24 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: CI 3 | 4 | on: 5 | merge_group: 6 | push: 7 | branches: [master] 8 | tags: [v*] 9 | pull_request: 10 | 11 | jobs: 12 | lint: 13 | name: Run linters 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4.2.2 17 | - uses: actions/setup-python@v5 18 | with: 19 | python-version: 3.11 20 | cache: pip 21 | cache-dependency-path: '**/requirements*.txt' 22 | - name: Install dependencies 23 | uses: py-actions/py-dependency-install@v4 24 | with: 25 | path: requirements-dev.txt 26 | - name: Run mypy 27 | run: mypy 28 | - name: Install check-wheel-content, pre-commit, and twine 29 | run: python -m pip install build pre-commit check-wheel-contents twine 30 | - name: Build package 31 | run: python -m build 32 | - name: Run linter 33 | run: python -m pre_commit run --all-files --show-diff-on-failure 34 | - name: Check wheel contents 35 | run: check-wheel-contents dist/*.whl 36 | - name: Check by twine 37 | run: python -m twine check dist/* 38 | 39 | test: 40 | strategy: 41 | matrix: 42 | pyver: ['3.9', '3.10', '3.11', '3.12', '3.13'] 43 | runs-on: ubuntu-latest 44 | 45 | steps: 46 | - name: Checkout 47 | uses: actions/checkout@v4.2.2 48 | - name: Setup Python ${{ matrix.pyver }} 49 | uses: actions/setup-python@v5 50 | with: 51 | python-version: ${{ matrix.pyver }} 52 | cache: pip 53 | cache-dependency-path: '**/requirements*.txt' 54 | - name: Install dependencies 55 | uses: py-actions/py-dependency-install@v4 56 | with: 57 | path: requirements.txt 58 | - name: Run pytest 59 | run: pytest 60 | 61 | 62 | check: # This job does nothing and is only used for the branch protection 63 | if: always() 64 | 65 | needs: 66 | - lint 67 | - test 68 | 69 | runs-on: ubuntu-latest 70 | 71 | steps: 72 | - name: Decide whether the needed jobs succeeded or failed 73 | uses: re-actors/alls-green@release/v1 74 | with: 75 | jobs: ${{ toJSON(needs) }} 76 | 77 | 78 | deploy: 79 | name: Deploy 80 | environment: release 81 | # Run only on pushing a tag 82 | if: github.event_name == 'push' && contains(github.ref, 'refs/tags/') 83 | needs: [lint, test] 84 | runs-on: ubuntu-latest 85 | steps: 86 | - name: Checkout 87 | uses: actions/checkout@v4.2.2 88 | with: 89 | fetch-depth: 0 90 | - name: Install build 91 | run: python -m pip install build pre-commit check-wheel-contents twine 92 | - name: Build package 93 | run: python -m build 94 | - name: PyPI upload 95 | uses: pypa/gh-action-pypi-publish@v1.12.4 96 | with: 97 | packages_dir: dist 98 | password: ${{ secrets.PYPI_API_TOKEN }} 99 | - name: GitHub Release 100 | uses: ncipollo/release-action@v1 101 | with: 102 | name: pytest-aiohttp ${{ github.ref_name }} 103 | artifacts: dist/* 104 | bodyFile: README.rst 105 | token: ${{ secrets.GITHUB_TOKEN }} 106 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: CodeQL 3 | 4 | on: 5 | push: 6 | branches: [master] 7 | pull_request: 8 | branches: [master] 9 | schedule: 10 | - cron: 30 13 * * 6 11 | 12 | jobs: 13 | analyze: 14 | name: Analyze 15 | runs-on: ubuntu-latest 16 | permissions: 17 | actions: read 18 | contents: read 19 | security-events: write 20 | 21 | strategy: 22 | fail-fast: false 23 | matrix: 24 | language: [python] 25 | 26 | steps: 27 | - name: Checkout 28 | uses: actions/checkout@v4.2.2 29 | 30 | - name: Initialize CodeQL 31 | uses: github/codeql-action/init@v3 32 | with: 33 | languages: ${{ matrix.language }} 34 | queries: +security-and-quality 35 | 36 | - name: Autobuild 37 | uses: github/codeql-action/autobuild@v3 38 | 39 | - name: Perform CodeQL Analysis 40 | uses: github/codeql-action/analyze@v3 41 | with: 42 | category: /language:${{ matrix.language }} 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | local_settings.py 55 | 56 | # Flask stuff: 57 | instance/ 58 | .webassets-cache 59 | 60 | # Scrapy stuff: 61 | .scrapy 62 | 63 | # Sphinx documentation 64 | docs/_build/ 65 | 66 | # PyBuilder 67 | target/ 68 | 69 | # IPython Notebook 70 | .ipynb_checkpoints 71 | 72 | # pyenv 73 | .python-version 74 | 75 | # celery beat schedule file 76 | celerybeat-schedule 77 | 78 | # dotenv 79 | .env 80 | 81 | # virtualenv 82 | venv/ 83 | ENV/ 84 | 85 | # Spyder project settings 86 | .spyderproject 87 | 88 | # Rope project settings 89 | .ropeproject 90 | 91 | .python-version 92 | 93 | # generated by setuptools_scm 94 | pytest_aiohttp/_version.py 95 | -------------------------------------------------------------------------------- /.mypy.ini: -------------------------------------------------------------------------------- 1 | [mypy] 2 | files = pytest_aiohttp, tests 3 | check_untyped_defs = True 4 | follow_imports_for_stubs = True 5 | disallow_any_decorated = True 6 | disallow_any_generics = True 7 | disallow_any_unimported = True 8 | disallow_incomplete_defs = True 9 | disallow_subclassing_any = True 10 | disallow_untyped_calls = True 11 | disallow_untyped_decorators = True 12 | disallow_untyped_defs = True 13 | # TODO(PY312): explicit-override 14 | enable_error_code = ignore-without-code, possibly-undefined, redundant-expr, redundant-self, truthy-bool, truthy-iterable, unused-awaitable 15 | extra_checks = True 16 | implicit_reexport = False 17 | no_implicit_optional = True 18 | pretty = True 19 | show_column_numbers = True 20 | show_error_codes = True 21 | show_error_code_links = True 22 | strict_equality = True 23 | warn_incomplete_stub = True 24 | warn_redundant_casts = True 25 | warn_return_any = True 26 | warn_unreachable = True 27 | warn_unused_ignores = True 28 | -------------------------------------------------------------------------------- /.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/asottile/yesqa 9 | rev: v1.5.0 10 | hooks: 11 | - id: yesqa 12 | - repo: https://github.com/Zac-HD/shed 13 | rev: 2024.10.1 14 | hooks: 15 | - id: shed 16 | args: 17 | - --refactor 18 | types_or: 19 | - python 20 | - markdown 21 | - rst 22 | - repo: https://github.com/jumanjihouse/pre-commit-hook-yamlfmt 23 | rev: 0.2.3 24 | hooks: 25 | - id: yamlfmt 26 | args: [--mapping, '2', --sequence, '2', --offset, '0'] 27 | - repo: https://github.com/pre-commit/pre-commit-hooks 28 | rev: v5.0.0 29 | hooks: 30 | - id: trailing-whitespace 31 | - id: end-of-file-fixer 32 | - id: fix-encoding-pragma 33 | args: [--remove] 34 | - id: check-case-conflict 35 | - id: check-json 36 | - id: check-xml 37 | - id: check-yaml 38 | - id: debug-statements 39 | - repo: https://github.com/PyCQA/flake8 40 | rev: 7.2.0 41 | hooks: 42 | - id: flake8 43 | language_version: python3 44 | - repo: https://github.com/pre-commit/pygrep-hooks 45 | rev: v1.10.0 46 | hooks: 47 | - id: python-use-type-annotations 48 | - repo: https://github.com/python-jsonschema/check-jsonschema 49 | rev: 0.33.0 50 | hooks: 51 | - id: check-github-actions 52 | - id: check-github-workflows 53 | ci: 54 | skip: 55 | - check-github-actions 56 | - check-github-workflows 57 | -------------------------------------------------------------------------------- /CHANGES.rst: -------------------------------------------------------------------------------- 1 | CHANGES 2 | ======= 3 | 4 | 1.1.0 (2025-01-23) 5 | ------------------ 6 | 7 | - Drop Python 3.8 (#57) 8 | 9 | - Export the plugin types at top-level (#60, #61) 10 | 11 | - Add host parameter to aiohttp_server fixture (#63) 12 | 13 | 1.0.5 (2023-09-06) 14 | ------------------ 15 | 16 | - Fix some compatibility with Pytest 7. 17 | 18 | 1.0.4 (2022-02-12) 19 | ------------------ 20 | 21 | - Fix failure with ``aiohttp_client`` fixture usage when ``asyncio_mode=strict``. 22 | `#25 `_ 23 | 24 | 1.0.3 (2022-01-03) 25 | ------------------ 26 | 27 | - Fix ``loop`` and ``proactor_loop`` fixtures. 28 | `#22 `_ 29 | 30 | 1.0.2 (2022-01-20) 31 | ------------------ 32 | 33 | - Restore implicit switch to ``asyncio_mode = auto`` if *legacy* mode is detected. 34 | 35 | 1.0.1 (2022-01-20) 36 | ------------------ 37 | 38 | - Don't implicitly switch from legacy to auto asyncio_mode, the integration doesn't work 39 | well. 40 | 41 | 1.0.0 (2022-1-20) 42 | ------------------ 43 | 44 | - The plugin is compatible with ``pytest-asyncio`` now. It uses ``pytest-asyncio`` for 45 | async tests running and async fixtures support, providing by itself only fixtures for 46 | creating aiohttp test server and client. 47 | 48 | 0.2.0 (2017-11-30) 49 | ------------------ 50 | 51 | - Fix backward incompatibility changes introduced by `pytest` 3.3+ 52 | 53 | 0.1.3 (2016-09-08) 54 | ------------------ 55 | 56 | - Add MANIFEST.in file 57 | 58 | 0.1.2 (2016-08-07) 59 | ------------------ 60 | 61 | - Fix README markup 62 | 63 | 0.1.1 (2016-07-22) 64 | ------------------ 65 | 66 | - Fix an url in setup.py 67 | 68 | 0.1.0 (2016-07-22) 69 | ------------------ 70 | 71 | - Initial release 72 | -------------------------------------------------------------------------------- /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 2016-2018 Andrew Svetlov and aiohttp team 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 | grasp pytest_aiohttp 2 | include LICENSE 3 | include CHANGES.rst 4 | include README.rst 5 | global-exclude *.pyc 6 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | pytest-aiohttp 2 | ============== 3 | 4 | pytest plugin for aiohttp support 5 | 6 | The library provides useful fixtures for creation test aiohttp server and client. 7 | 8 | 9 | Installation 10 | ------------ 11 | 12 | .. code-block:: console 13 | 14 | $ pip install pytest-aiohttp 15 | 16 | Add ``asyncio_mode = auto`` line to `pytest configuration 17 | `_ (see `pytest-asyncio modes 18 | `_ for details). The plugin works 19 | with ``strict`` mode also. 20 | 21 | 22 | 23 | Usage 24 | ----- 25 | 26 | Write tests in `pytest-asyncio `_ style 27 | using provided fixtures for aiohttp test server and client creation. The plugin provides 28 | resources cleanup out-of-the-box. 29 | 30 | The simple usage example: 31 | 32 | .. code-block:: python 33 | 34 | from aiohttp import web 35 | 36 | 37 | async def hello(request): 38 | return web.Response(body=b"Hello, world") 39 | 40 | 41 | def create_app(): 42 | app = web.Application() 43 | app.router.add_route("GET", "/", hello) 44 | return app 45 | 46 | 47 | async def test_hello(aiohttp_client): 48 | client = await aiohttp_client(create_app()) 49 | resp = await client.get("/") 50 | assert resp.status == 200 51 | text = await resp.text() 52 | assert "Hello, world" in text 53 | 54 | 55 | See `aiohttp documentation ` for 56 | more details about fixtures usage. 57 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = [ 3 | "setuptools>=51.0", 4 | "wheel>=0.36", 5 | "setuptools_scm>=6.2" 6 | ] 7 | build-backend = "setuptools.build_meta" 8 | 9 | [tool.setuptools_scm] 10 | write_to = "pytest_aiohttp/_version.py" 11 | -------------------------------------------------------------------------------- /pytest_aiohttp/__init__.py: -------------------------------------------------------------------------------- 1 | from ._version import version as __version__ # noqa: F401 2 | from .plugin import ( # noqa: F401 3 | AiohttpClient as AiohttpClient, 4 | AiohttpRawServer as AiohttpRawServer, 5 | AiohttpServer as AiohttpServer, 6 | ) 7 | -------------------------------------------------------------------------------- /pytest_aiohttp/plugin.py: -------------------------------------------------------------------------------- 1 | from collections.abc import AsyncIterator, Awaitable 2 | from typing import ( 3 | Any, 4 | Dict, 5 | Optional, 6 | Protocol, 7 | Type, 8 | TypeVar, 9 | Union, 10 | overload, 11 | ) 12 | 13 | import pytest 14 | import pytest_asyncio 15 | from aiohttp.test_utils import BaseTestServer, RawTestServer, TestClient, TestServer 16 | from aiohttp.web import Application, BaseRequest, Request 17 | from aiohttp.web_protocol import _RequestHandler 18 | 19 | _Request = TypeVar("_Request", bound=BaseRequest) 20 | 21 | 22 | class AiohttpClient(Protocol): 23 | # TODO(PY311): Use Unpack to specify ClientSession kwargs. 24 | @overload 25 | async def __call__( 26 | self, 27 | __param: Application, 28 | *, 29 | server_kwargs: Optional[Dict[str, Any]] = None, 30 | **kwargs: Any, 31 | ) -> TestClient[Request, Application]: ... 32 | 33 | @overload 34 | async def __call__( 35 | self, 36 | __param: BaseTestServer, # TODO(aiohttp4): BaseTestServer[_Request] 37 | *, 38 | server_kwargs: Optional[Dict[str, Any]] = None, 39 | **kwargs: Any, 40 | ) -> TestClient[_Request, None]: ... 41 | 42 | 43 | class AiohttpServer(Protocol): 44 | def __call__( 45 | self, app: Application, *, port: Optional[int] = None, **kwargs: Any 46 | ) -> Awaitable[TestServer]: ... 47 | 48 | 49 | class AiohttpRawServer(Protocol): 50 | def __call__( 51 | self, 52 | handler: _RequestHandler, # TODO(aiohttp4): _RequestHandler[BaseRequest] 53 | *, 54 | port: Optional[int] = None, 55 | **kwargs: Any, 56 | ) -> Awaitable[RawTestServer]: ... 57 | 58 | 59 | LEGACY_MODE = DeprecationWarning( 60 | "The 'asyncio_mode' is 'legacy', switching to 'auto' for the sake of " 61 | "pytest-aiohttp backward compatibility. " 62 | "Please explicitly use 'asyncio_mode=strict' or 'asyncio_mode=auto' " 63 | "in pytest configuration file." 64 | ) 65 | 66 | 67 | @pytest.hookimpl(tryfirst=True) 68 | def pytest_configure(config: pytest.Config) -> None: 69 | val = config.getoption("asyncio_mode") 70 | if val is None: 71 | val = config.getini("asyncio_mode") 72 | if val == "legacy": 73 | config.option.asyncio_mode = "auto" 74 | config.issue_config_time_warning(LEGACY_MODE, stacklevel=2) 75 | 76 | 77 | @pytest_asyncio.fixture 78 | async def aiohttp_server() -> AsyncIterator[AiohttpServer]: 79 | """Factory to create a TestServer instance, given an app. 80 | 81 | aiohttp_server(app, **kwargs) 82 | """ 83 | servers = [] 84 | 85 | async def go( 86 | app: Application, 87 | *, 88 | host: str = "127.0.0.1", 89 | port: Optional[int] = None, 90 | **kwargs: Any, 91 | ) -> TestServer: 92 | server = TestServer(app, host=host, port=port) 93 | await server.start_server(**kwargs) 94 | servers.append(server) 95 | return server 96 | 97 | yield go 98 | 99 | while servers: 100 | await servers.pop().close() 101 | 102 | 103 | @pytest_asyncio.fixture 104 | async def aiohttp_raw_server() -> AsyncIterator[AiohttpRawServer]: 105 | """Factory to create a RawTestServer instance, given a web handler. 106 | 107 | aiohttp_raw_server(handler, **kwargs) 108 | """ 109 | servers = [] 110 | 111 | async def go( 112 | handler: _RequestHandler, # TODO(aiohttp4): _RequestHandler[BaseRequest] 113 | *, 114 | port: Optional[int] = None, 115 | **kwargs: Any, 116 | ) -> RawTestServer: 117 | server = RawTestServer(handler, port=port) 118 | await server.start_server(**kwargs) 119 | servers.append(server) 120 | return server 121 | 122 | yield go 123 | 124 | while servers: 125 | await servers.pop().close() 126 | 127 | 128 | @pytest.fixture 129 | def aiohttp_client_cls() -> Type[TestClient[Any, Any]]: # type: ignore[misc] 130 | """ 131 | Client class to use in ``aiohttp_client`` factory. 132 | 133 | Use it for passing custom ``TestClient`` implementations. 134 | 135 | Example:: 136 | 137 | class MyClient(TestClient): 138 | async def login(self, *, user, pw): 139 | payload = {"username": user, "password": pw} 140 | return await self.post("/login", json=payload) 141 | 142 | @pytest.fixture 143 | def aiohttp_client_cls(): 144 | return MyClient 145 | 146 | def test_login(aiohttp_client): 147 | app = web.Application() 148 | client = await aiohttp_client(app) 149 | await client.login(user="admin", pw="s3cr3t") 150 | 151 | """ 152 | return TestClient 153 | 154 | 155 | @pytest_asyncio.fixture 156 | async def aiohttp_client( # type: ignore[misc] 157 | aiohttp_client_cls: Type[TestClient[Any, Any]], 158 | ) -> AsyncIterator[AiohttpClient]: 159 | """Factory to create a TestClient instance. 160 | 161 | aiohttp_client(app, **kwargs) 162 | aiohttp_client(server, **kwargs) 163 | aiohttp_client(raw_server, **kwargs) 164 | """ 165 | clients = [] 166 | 167 | @overload 168 | async def go( 169 | __param: Application, 170 | *, 171 | server_kwargs: Optional[Dict[str, Any]] = None, 172 | **kwargs: Any, 173 | ) -> TestClient[Request, Application]: ... 174 | 175 | @overload 176 | async def go( 177 | __param: BaseTestServer, # TODO(aiohttp4): BaseTestServer[_Request] 178 | *, 179 | server_kwargs: Optional[Dict[str, Any]] = None, 180 | **kwargs: Any, 181 | ) -> TestClient[_Request, None]: ... 182 | 183 | async def go( 184 | __param: Union[ 185 | Application, BaseTestServer 186 | ], # TODO(aiohttp4): BaseTestServer[Any] 187 | *, 188 | server_kwargs: Optional[Dict[str, Any]] = None, 189 | **kwargs: Any, 190 | ) -> TestClient[Any, Any]: 191 | # TODO(PY311): Use Unpack to specify ClientSession kwargs and server_kwargs. 192 | if isinstance(__param, Application): 193 | server_kwargs = server_kwargs or {} 194 | server = TestServer(__param, **server_kwargs) 195 | client = aiohttp_client_cls(server, **kwargs) 196 | elif isinstance(__param, BaseTestServer): 197 | client = aiohttp_client_cls(__param, **kwargs) 198 | else: 199 | raise ValueError(f"Unknown argument type: {type(__param)!r}") 200 | 201 | await client.start_server() 202 | clients.append(client) 203 | return client 204 | 205 | yield go 206 | 207 | while clients: 208 | await clients.pop().close() 209 | -------------------------------------------------------------------------------- /pytest_aiohttp/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aio-libs/pytest-aiohttp/1a82ae1fbca8f6ece064489dcbce1e899e6a90e8/pytest_aiohttp/py.typed -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | -r requirements.txt 2 | 3 | mypy==1.16.0 4 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | -e . 2 | aiohttp==3.12.9 3 | pytest==8.3.5 4 | pytest-asyncio==0.26.0 5 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = pytest-aiohttp 3 | version = attr: pytest_aiohttp.__version__ 4 | url = https://github.com/aio-libs/pytest-aiohttp 5 | project_urls = 6 | GitHub = https://github.com/aio-libs/pytest-aiohttp 7 | Changelog = https://github.com/aio-libs/pytest-aiohttp/blob/master/CHANGES.rst 8 | description = Pytest plugin for aiohttp support 9 | long_description = file: README.rst 10 | long_description_content_type = text/x-rst 11 | maintainer = aiohttp team 12 | maintainer_email = team@aiohttp.org 13 | license = Apache 2.0 14 | license_file = LICENSE 15 | classifiers = 16 | Development Status :: 4 - Beta 17 | 18 | Intended Audience :: Developers 19 | 20 | License :: OSI Approved :: Apache Software License 21 | 22 | Programming Language :: Python :: 3.9 23 | Programming Language :: Python :: 3.10 24 | Programming Language :: Python :: 3.11 25 | Programming Language :: Python :: 3.12 26 | Programming Language :: Python :: 3.13 27 | 28 | Topic :: Software Development :: Testing 29 | 30 | Framework :: AsyncIO 31 | Framework :: Pytest 32 | Framework :: aiohttp 33 | Typing :: Typed 34 | 35 | [options] 36 | python_requires = >=3.9 37 | packages = find: 38 | include_package_data = True 39 | 40 | setup_requires = 41 | setuptools_scm >= 6.2 42 | 43 | install_requires = 44 | pytest >= 6.1.0 45 | aiohttp >= 3.11.0 46 | pytest-asyncio >= 0.17.2 47 | 48 | [options.extras_require] 49 | testing = 50 | coverage == 7.8.2 51 | mypy == 1.16.0 52 | 53 | [options.entry_points] 54 | pytest11 = 55 | aiohttp = pytest_aiohttp.plugin 56 | 57 | [coverage:run] 58 | source = pytest_aiohttp 59 | branch = true 60 | 61 | [coverage:report] 62 | show_missing = true 63 | 64 | [tool:pytest] 65 | addopts = -rsx --tb=short 66 | testpaths = tests 67 | asyncio_mode = auto 68 | junit_family=xunit2 69 | filterwarnings = error 70 | 71 | [flake8] 72 | max-line-length = 88 73 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup() 4 | -------------------------------------------------------------------------------- /tests/test_fixtures.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | pytest_plugins = "pytester" 4 | 5 | 6 | def test_aiohttp_plugin(testdir: pytest.Testdir) -> None: 7 | testdir.makepyfile( 8 | """\ 9 | import pytest 10 | from unittest import mock 11 | 12 | from aiohttp import web 13 | 14 | value = web.AppKey('value', str) 15 | 16 | 17 | async def hello(request): 18 | return web.Response(body=b'Hello, world') 19 | 20 | 21 | async def create_app(): 22 | app = web.Application() 23 | app.router.add_route('GET', '/', hello) 24 | return app 25 | 26 | 27 | async def test_hello(aiohttp_client) -> None: 28 | client = await aiohttp_client(await create_app()) 29 | resp = await client.get('/') 30 | assert resp.status == 200 31 | text = await resp.text() 32 | assert 'Hello, world' in text 33 | 34 | 35 | async def test_hello_from_app(aiohttp_client) -> None: 36 | app = web.Application() 37 | app.router.add_get('/', hello) 38 | client = await aiohttp_client(app) 39 | resp = await client.get('/') 40 | assert resp.status == 200 41 | text = await resp.text() 42 | assert 'Hello, world' in text 43 | 44 | 45 | async def test_hello_with_loop(aiohttp_client) -> None: 46 | client = await aiohttp_client(await create_app()) 47 | resp = await client.get('/') 48 | assert resp.status == 200 49 | text = await resp.text() 50 | assert 'Hello, world' in text 51 | 52 | 53 | async def test_noop() -> None: 54 | pass 55 | 56 | 57 | async def previous(request): 58 | if request.method == 'POST': 59 | with pytest.deprecated_call(): # FIXME: this isn't actually called 60 | request.app[value] = (await request.post())['value'] 61 | return web.Response(body=b'thanks for the data') 62 | else: 63 | v = request.app.get(value, 'unknown') 64 | return web.Response(body='value: {}'.format(v).encode()) 65 | 66 | 67 | def create_stateful_app(): 68 | app = web.Application() 69 | app.router.add_route('*', '/', previous) 70 | return app 71 | 72 | 73 | @pytest.fixture 74 | async def cli(aiohttp_client): 75 | return await aiohttp_client(create_stateful_app()) 76 | 77 | 78 | def test_noncoro() -> None: 79 | assert True 80 | 81 | 82 | async def test_failed_to_create_client(aiohttp_client) -> None: 83 | 84 | def make_app(): 85 | raise RuntimeError() 86 | 87 | with pytest.raises(RuntimeError): 88 | await aiohttp_client(make_app()) 89 | 90 | 91 | async def test_custom_port_aiohttp_client(aiohttp_client, unused_tcp_port): 92 | client = await aiohttp_client(await create_app(), 93 | server_kwargs={'port': unused_tcp_port}) 94 | assert client.port == unused_tcp_port 95 | resp = await client.get('/') 96 | assert resp.status == 200 97 | text = await resp.text() 98 | assert 'Hello, world' in text 99 | 100 | 101 | async def test_custom_port_test_server(aiohttp_server, unused_tcp_port): 102 | app = await create_app() 103 | server = await aiohttp_server(app, port=unused_tcp_port) 104 | assert server.port == unused_tcp_port 105 | """ 106 | ) 107 | result = testdir.runpytest("--asyncio-mode=auto") 108 | result.assert_outcomes(passed=8) 109 | 110 | 111 | def test_aiohttp_raw_server(testdir: pytest.Testdir) -> None: 112 | testdir.makepyfile( 113 | """\ 114 | import pytest 115 | 116 | from aiohttp import web 117 | 118 | 119 | async def handler(request): 120 | return web.Response(text="OK") 121 | 122 | 123 | @pytest.fixture 124 | async def server(aiohttp_raw_server): 125 | return await aiohttp_raw_server(handler) 126 | 127 | 128 | @pytest.fixture 129 | async def cli(aiohttp_client, server): 130 | client = await aiohttp_client(server) 131 | return client 132 | 133 | 134 | async def test_hello(cli) -> None: 135 | resp = await cli.get('/') 136 | assert resp.status == 200 137 | text = await resp.text() 138 | assert 'OK' in text 139 | """ 140 | ) 141 | result = testdir.runpytest("--asyncio-mode=auto") 142 | result.assert_outcomes(passed=1) 143 | 144 | 145 | def test_aiohttp_client_cls_fixture_custom_client_used(testdir: pytest.Testdir) -> None: 146 | testdir.makepyfile( 147 | """ 148 | import pytest 149 | from aiohttp.web import Application 150 | from aiohttp.test_utils import TestClient 151 | 152 | 153 | class CustomClient(TestClient): 154 | pass 155 | 156 | 157 | @pytest.fixture 158 | def aiohttp_client_cls(): 159 | return CustomClient 160 | 161 | 162 | async def test_hello(aiohttp_client) -> None: 163 | client = await aiohttp_client(Application()) 164 | assert isinstance(client, CustomClient) 165 | 166 | """ 167 | ) 168 | result = testdir.runpytest("--asyncio-mode=auto") 169 | result.assert_outcomes(passed=1) 170 | 171 | 172 | def test_aiohttp_client_cls_fixture_factory(testdir: pytest.Testdir) -> None: 173 | testdir.makeconftest( 174 | """\ 175 | 176 | def pytest_configure(config): 177 | config.addinivalue_line("markers", "rest: RESTful API tests") 178 | config.addinivalue_line("markers", "graphql: GraphQL API tests") 179 | 180 | """ 181 | ) 182 | testdir.makepyfile( 183 | """ 184 | import pytest 185 | from aiohttp.web import Application 186 | from aiohttp.test_utils import TestClient 187 | 188 | 189 | class RESTfulClient(TestClient): 190 | pass 191 | 192 | 193 | class GraphQLClient(TestClient): 194 | pass 195 | 196 | 197 | @pytest.fixture 198 | def aiohttp_client_cls(request): 199 | if request.node.get_closest_marker('rest') is not None: 200 | return RESTfulClient 201 | elif request.node.get_closest_marker('graphql') is not None: 202 | return GraphQLClient 203 | return TestClient 204 | 205 | 206 | @pytest.mark.rest 207 | async def test_rest(aiohttp_client) -> None: 208 | client = await aiohttp_client(Application()) 209 | assert isinstance(client, RESTfulClient) 210 | 211 | 212 | @pytest.mark.graphql 213 | async def test_graphql(aiohttp_client) -> None: 214 | client = await aiohttp_client(Application()) 215 | assert isinstance(client, GraphQLClient) 216 | 217 | """ 218 | ) 219 | result = testdir.runpytest("--asyncio-mode=auto") 220 | result.assert_outcomes(passed=2) 221 | -------------------------------------------------------------------------------- /tests/test_switch_mode.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from pytest_aiohttp.plugin import LEGACY_MODE 4 | 5 | pytest_plugins: str = "pytester" 6 | 7 | 8 | def test_warning_for_legacy_mode(testdir: pytest.Testdir) -> None: 9 | testdir.makepyfile( 10 | """\ 11 | async def test_a(): 12 | pass 13 | 14 | """ 15 | ) 16 | result = testdir.runpytest_subprocess("--asyncio-mode=legacy") 17 | result.assert_outcomes(passed=1) 18 | result.stdout.fnmatch_lines(["*" + str(LEGACY_MODE) + "*"]) 19 | 20 | 21 | def test_auto_mode(testdir: pytest.Testdir) -> None: 22 | testdir.makepyfile( 23 | """\ 24 | async def test_a(): 25 | pass 26 | 27 | """ 28 | ) 29 | result = testdir.runpytest_subprocess("--asyncio-mode=auto") 30 | result.assert_outcomes(passed=1) 31 | result.stdout.no_fnmatch_line("*" + str(LEGACY_MODE) + "*") 32 | 33 | 34 | def test_strict_mode(testdir: pytest.Testdir) -> None: 35 | testdir.makepyfile( 36 | """\ 37 | import pytest 38 | 39 | 40 | @pytest.mark.asyncio 41 | async def test_a(): 42 | pass 43 | 44 | """ 45 | ) 46 | result = testdir.runpytest_subprocess("--asyncio-mode=strict") 47 | result.assert_outcomes(passed=1) 48 | result.stdout.no_fnmatch_line("*" + str(LEGACY_MODE) + "*") 49 | --------------------------------------------------------------------------------