From 6978539eb2e4d32bc031f0bb1e411c2d6fa119c6 Mon Sep 17 00:00:00 2001 From: Michael Seifert Date: Wed, 12 Jul 2023 12:09:01 +0200 Subject: [PATCH 001/151] Prepare release of v0.21.1 (#582) * [docs] Add release date of v0.21.1 to changelog. Signed-off-by: Michael Seifert * [docs] Fixed typo. Signed-off-by: Michael Seifert --------- Signed-off-by: Michael Seifert --- docs/source/reference/changelog.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/source/reference/changelog.rst b/docs/source/reference/changelog.rst index f68a63c2..d57a4a3d 100644 --- a/docs/source/reference/changelog.rst +++ b/docs/source/reference/changelog.rst @@ -2,12 +2,12 @@ Changelog ========= -0.22.0 (UNRELEASED) +0.21.1 (2023-07-12) =================== - Output a proper error message when an invalid ``asyncio_mode`` is selected. - Extend warning message about unclosed event loops with additional possible cause. `#531 `_ -- Previously, some tests reported "skipped" or "xfailed" as a result. Now all tests report a "success" results. +- Previously, some tests reported "skipped" or "xfailed" as a result. Now all tests report a "success" result. 0.21.0 (2023-03-19) =================== From 99a14d4fd3b884804944b83cb5fdac09bd0f7480 Mon Sep 17 00:00:00 2001 From: Michael Seifert Date: Wed, 12 Jul 2023 11:23:49 +0200 Subject: [PATCH 002/151] [feat!] Remove support for Python 3.7. Signed-off-by: Michael Seifert --- .github/workflows/main.yml | 2 +- dependencies/default/constraints.txt | 1 - dependencies/default/requirements.txt | 1 - dependencies/pytest-min/requirements.txt | 1 - docs/source/reference/changelog.rst | 4 ++++ pytest_asyncio/plugin.py | 7 +------ setup.cfg | 4 +--- tests/test_subprocess.py | 12 ------------ tox.ini | 5 ++--- 9 files changed, 9 insertions(+), 28 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 2b9e03af..fb09af02 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -60,7 +60,7 @@ jobs: strategy: matrix: - python-version: ['3.7', '3.8', '3.9', '3.10', '3.11', 3.12-dev] + python-version: ['3.8', '3.9', '3.10', '3.11', 3.12-dev] steps: - uses: actions/checkout@v3 diff --git a/dependencies/default/constraints.txt b/dependencies/default/constraints.txt index 04999c70..2eaae3ce 100644 --- a/dependencies/default/constraints.txt +++ b/dependencies/default/constraints.txt @@ -20,5 +20,4 @@ sortedcontainers==2.4.0 tomli==2.0.1 trio==0.22.1 typed-ast==1.5.5 -typing_extensions==4.7.1 zipp==3.15.0 diff --git a/dependencies/default/requirements.txt b/dependencies/default/requirements.txt index a0009a85..0828607f 100644 --- a/dependencies/default/requirements.txt +++ b/dependencies/default/requirements.txt @@ -1,4 +1,3 @@ # Always adjust install_requires in setup.cfg and pytest-min-requirements.txt # when changing runtime dependencies pytest >= 7.0.0 -typing-extensions >= 3.7.2; python_version < "3.8" diff --git a/dependencies/pytest-min/requirements.txt b/dependencies/pytest-min/requirements.txt index 4152d2f8..9fb33e96 100644 --- a/dependencies/pytest-min/requirements.txt +++ b/dependencies/pytest-min/requirements.txt @@ -1,4 +1,3 @@ # Always adjust install_requires in setup.cfg and requirements.txt # when changing minimum version dependencies pytest[testing] == 7.0.0 -typing-extensions >= 3.7.2; python_version < "3.8" diff --git a/docs/source/reference/changelog.rst b/docs/source/reference/changelog.rst index d57a4a3d..c85d87e9 100644 --- a/docs/source/reference/changelog.rst +++ b/docs/source/reference/changelog.rst @@ -2,6 +2,10 @@ Changelog ========= +1.0.0 (UNRELEASED) +================== +- Remove support for Python 3.7 + 0.21.1 (2023-07-12) =================== - Output a proper error message when an invalid ``asyncio_mode`` is selected. diff --git a/pytest_asyncio/plugin.py b/pytest_asyncio/plugin.py index 12669791..c07dfced 100644 --- a/pytest_asyncio/plugin.py +++ b/pytest_asyncio/plugin.py @@ -5,7 +5,6 @@ import functools import inspect import socket -import sys import warnings from textwrap import dedent from typing import ( @@ -17,6 +16,7 @@ Iterable, Iterator, List, + Literal, Optional, Set, TypeVar, @@ -36,11 +36,6 @@ Session, ) -if sys.version_info >= (3, 8): - from typing import Literal -else: - from typing_extensions import Literal - _R = TypeVar("_R") _ScopeName = Literal["session", "package", "module", "class", "function"] diff --git a/setup.cfg b/setup.cfg index 3712ec16..3f624fd7 100644 --- a/setup.cfg +++ b/setup.cfg @@ -21,7 +21,6 @@ classifiers = License :: OSI Approved :: Apache Software License - Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 Programming Language :: Python :: 3.10 @@ -34,14 +33,13 @@ classifiers = Typing :: Typed [options] -python_requires = >=3.7 +python_requires = >=3.8 packages = find: include_package_data = True # Always adjust requirements.txt and pytest-min-requirements.txt when changing runtime dependencies install_requires = pytest >= 7.0.0 - typing-extensions >= 3.7.2; python_version < "3.8" [options.extras_require] testing = diff --git a/tests/test_subprocess.py b/tests/test_subprocess.py index 79c5109d..8f1caee5 100644 --- a/tests/test_subprocess.py +++ b/tests/test_subprocess.py @@ -15,18 +15,6 @@ def event_loop(): loop.close() -@pytest.mark.skipif( - sys.version_info < (3, 8), - reason=""" - When run with Python 3.7 asyncio.subprocess.create_subprocess_exec seems to be - affected by an issue that prevents correct cleanup. Tests using pytest-trio - will report that signal handling is already performed by another library and - fail. [1] This is possibly a bug in CPython 3.7, so we ignore this test for - that Python version. - - [1] https://github.com/python-trio/pytest-trio/issues/126 - """, -) @pytest.mark.asyncio async def test_subprocess(event_loop): """Starting a subprocess should be possible.""" diff --git a/tox.ini b/tox.ini index 4987355b..33e0c931 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] minversion = 3.14.0 -envlist = py37, py38, py39, py310, py311, py312, pytest-min +envlist = py38, py39, py310, py311, py312, pytest-min isolated_build = true passenv = CI @@ -25,8 +25,7 @@ allowlist_externals = [gh-actions] python = - 3.7: py37, pytest-min - 3.8: py38 + 3.8: py38, pytest-min 3.9: py39 3.10: py310 3.11: py311 From 974fe00357e9a4a74cd02e59605b27e1e3cdcb0f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 Jul 2023 18:26:45 +0000 Subject: [PATCH 003/151] Build(deps): Bump importlib-metadata in /dependencies/default Bumps [importlib-metadata](https://github.com/python/importlib_metadata) from 6.7.0 to 6.8.0. - [Release notes](https://github.com/python/importlib_metadata/releases) - [Changelog](https://github.com/python/importlib_metadata/blob/main/NEWS.rst) - [Commits](https://github.com/python/importlib_metadata/compare/v6.7.0...v6.8.0) --- updated-dependencies: - dependency-name: importlib-metadata dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- dependencies/default/constraints.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies/default/constraints.txt b/dependencies/default/constraints.txt index 2eaae3ce..e67e90fa 100644 --- a/dependencies/default/constraints.txt +++ b/dependencies/default/constraints.txt @@ -5,7 +5,7 @@ exceptiongroup==1.1.2 flaky==3.7.0 hypothesis==6.79.4 idna==3.4 -importlib-metadata==6.7.0 +importlib-metadata==6.8.0 iniconfig==2.0.0 mypy==1.4.1 mypy-extensions==1.0.0 From 6b07115acc90f59f93ffcb05a45a8d907ecb0d99 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 Jul 2023 18:26:42 +0000 Subject: [PATCH 004/151] Build(deps): Bump zipp from 3.15.0 to 3.16.2 in /dependencies/default Bumps [zipp](https://github.com/jaraco/zipp) from 3.15.0 to 3.16.2. - [Release notes](https://github.com/jaraco/zipp/releases) - [Changelog](https://github.com/jaraco/zipp/blob/main/NEWS.rst) - [Commits](https://github.com/jaraco/zipp/compare/v3.15.0...v3.16.2) --- updated-dependencies: - dependency-name: zipp dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- dependencies/default/constraints.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies/default/constraints.txt b/dependencies/default/constraints.txt index e67e90fa..57734254 100644 --- a/dependencies/default/constraints.txt +++ b/dependencies/default/constraints.txt @@ -20,4 +20,4 @@ sortedcontainers==2.4.0 tomli==2.0.1 trio==0.22.1 typed-ast==1.5.5 -zipp==3.15.0 +zipp==3.16.2 From de8dca1e57564de3713a0ba12c76452d94a1e9c5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 Jul 2023 18:26:37 +0000 Subject: [PATCH 005/151] Build(deps): Bump trio from 0.22.1 to 0.22.2 in /dependencies/default Bumps [trio](https://github.com/python-trio/trio) from 0.22.1 to 0.22.2. - [Release notes](https://github.com/python-trio/trio/releases) - [Commits](https://github.com/python-trio/trio/compare/v0.22.1...v0.22.2) --- updated-dependencies: - dependency-name: trio dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- dependencies/default/constraints.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies/default/constraints.txt b/dependencies/default/constraints.txt index 57734254..77870a88 100644 --- a/dependencies/default/constraints.txt +++ b/dependencies/default/constraints.txt @@ -18,6 +18,6 @@ pytest-trio==0.8.0 sniffio==1.3.0 sortedcontainers==2.4.0 tomli==2.0.1 -trio==0.22.1 +trio==0.22.2 typed-ast==1.5.5 zipp==3.16.2 From 5731bd26a75cc3fbc4d5d87596c562e0019387ec Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 Jul 2023 18:26:33 +0000 Subject: [PATCH 006/151] Build(deps): Bump hypothesis in /dependencies/default Bumps [hypothesis](https://github.com/HypothesisWorks/hypothesis) from 6.79.4 to 6.81.2. - [Release notes](https://github.com/HypothesisWorks/hypothesis/releases) - [Commits](https://github.com/HypothesisWorks/hypothesis/compare/hypothesis-python-6.79.4...hypothesis-python-6.81.2) --- updated-dependencies: - dependency-name: hypothesis dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- dependencies/default/constraints.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies/default/constraints.txt b/dependencies/default/constraints.txt index 77870a88..7c19cc51 100644 --- a/dependencies/default/constraints.txt +++ b/dependencies/default/constraints.txt @@ -3,7 +3,7 @@ attrs==23.1.0 coverage==7.2.7 exceptiongroup==1.1.2 flaky==3.7.0 -hypothesis==6.79.4 +hypothesis==6.81.2 idna==3.4 importlib-metadata==6.8.0 iniconfig==2.0.0 From 1fd929acc6615caa794c7a348807fcc607c35cbb Mon Sep 17 00:00:00 2001 From: Michael Seifert Date: Sun, 23 Jul 2023 08:36:58 +0200 Subject: [PATCH 007/151] [build] Declare support for Python 3.12. Signed-off-by: Michael Seifert --- docs/source/reference/changelog.rst | 1 + setup.cfg | 1 + 2 files changed, 2 insertions(+) diff --git a/docs/source/reference/changelog.rst b/docs/source/reference/changelog.rst index c85d87e9..77204145 100644 --- a/docs/source/reference/changelog.rst +++ b/docs/source/reference/changelog.rst @@ -5,6 +5,7 @@ Changelog 1.0.0 (UNRELEASED) ================== - Remove support for Python 3.7 +- Declare support for Python 3.12 0.21.1 (2023-07-12) =================== diff --git a/setup.cfg b/setup.cfg index 3f624fd7..5dd6a63e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -25,6 +25,7 @@ classifiers = Programming Language :: Python :: 3.9 Programming Language :: Python :: 3.10 Programming Language :: Python :: 3.11 + Programming Language :: Python :: 3.12 Topic :: Software Development :: Testing From c31931794df195ef4ca1077fac3e0d5026661783 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Jul 2023 18:46:21 +0000 Subject: [PATCH 008/151] Build(deps): Bump hypothesis in /dependencies/default Bumps [hypothesis](https://github.com/HypothesisWorks/hypothesis) from 6.81.2 to 6.82.0. - [Release notes](https://github.com/HypothesisWorks/hypothesis/releases) - [Commits](https://github.com/HypothesisWorks/hypothesis/compare/hypothesis-python-6.81.2...hypothesis-python-6.82.0) --- updated-dependencies: - dependency-name: hypothesis dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- dependencies/default/constraints.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies/default/constraints.txt b/dependencies/default/constraints.txt index 7c19cc51..68e019ec 100644 --- a/dependencies/default/constraints.txt +++ b/dependencies/default/constraints.txt @@ -3,7 +3,7 @@ attrs==23.1.0 coverage==7.2.7 exceptiongroup==1.1.2 flaky==3.7.0 -hypothesis==6.81.2 +hypothesis==6.82.0 idna==3.4 importlib-metadata==6.8.0 iniconfig==2.0.0 From 17ab1124dbc7a75a01186635842b9ad217936136 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 31 Jul 2023 18:07:13 +0000 Subject: [PATCH 009/151] Build(deps): Bump pyparsing from 3.1.0 to 3.1.1 in /dependencies/default Bumps [pyparsing](https://github.com/pyparsing/pyparsing) from 3.1.0 to 3.1.1. - [Release notes](https://github.com/pyparsing/pyparsing/releases) - [Changelog](https://github.com/pyparsing/pyparsing/blob/master/CHANGES) - [Commits](https://github.com/pyparsing/pyparsing/compare/3.1.0...3.1.1) --- updated-dependencies: - dependency-name: pyparsing dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- dependencies/default/constraints.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies/default/constraints.txt b/dependencies/default/constraints.txt index 68e019ec..26318311 100644 --- a/dependencies/default/constraints.txt +++ b/dependencies/default/constraints.txt @@ -12,7 +12,7 @@ mypy-extensions==1.0.0 outcome==1.2.0 packaging==23.1 pluggy==1.2.0 -pyparsing==3.1.0 +pyparsing==3.1.1 pytest==7.4.0 pytest-trio==0.8.0 sniffio==1.3.0 From 186172c51126c028abae9328857a18be30685e9e Mon Sep 17 00:00:00 2001 From: Michael Seifert Date: Sun, 23 Jul 2023 08:27:29 +0200 Subject: [PATCH 010/151] [docs] Added how-to guide for testing with uvloop. Signed-off-by: Michael Seifert --- docs/source/how-to-guides/index.rst | 10 ++++++++++ docs/source/how-to-guides/uvloop.rst | 13 +++++++++++++ docs/source/index.rst | 1 + 3 files changed, 24 insertions(+) create mode 100644 docs/source/how-to-guides/index.rst create mode 100644 docs/source/how-to-guides/uvloop.rst diff --git a/docs/source/how-to-guides/index.rst b/docs/source/how-to-guides/index.rst new file mode 100644 index 00000000..922fac91 --- /dev/null +++ b/docs/source/how-to-guides/index.rst @@ -0,0 +1,10 @@ +============= +How-To Guides +============= + +.. toctree:: + :hidden: + + uvloop + +This section of the documentation provides code snippets and recipes to accomplish specific tasks with pytest-asyncio. diff --git a/docs/source/how-to-guides/uvloop.rst b/docs/source/how-to-guides/uvloop.rst new file mode 100644 index 00000000..14353365 --- /dev/null +++ b/docs/source/how-to-guides/uvloop.rst @@ -0,0 +1,13 @@ +======================= +How to test with uvloop +======================= + +Replace the default event loop policy in your *conftest.py:* + +.. code-block:: python + + import asyncio + + import uvloop + + asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) diff --git a/docs/source/index.rst b/docs/source/index.rst index e6b33033..a5096c56 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -7,6 +7,7 @@ Welcome to pytest-asyncio! :hidden: concepts + how-to-guides/index reference/index support From 95def23aee250ed139cb44d44b5b8813aee26327 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 7 Aug 2023 18:19:32 +0000 Subject: [PATCH 011/151] Build(deps): Bump hypothesis in /dependencies/default Bumps [hypothesis](https://github.com/HypothesisWorks/hypothesis) from 6.82.0 to 6.82.2. - [Release notes](https://github.com/HypothesisWorks/hypothesis/releases) - [Commits](https://github.com/HypothesisWorks/hypothesis/compare/hypothesis-python-6.82.0...hypothesis-python-6.82.2) --- updated-dependencies: - dependency-name: hypothesis dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- dependencies/default/constraints.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies/default/constraints.txt b/dependencies/default/constraints.txt index 26318311..49411a79 100644 --- a/dependencies/default/constraints.txt +++ b/dependencies/default/constraints.txt @@ -3,7 +3,7 @@ attrs==23.1.0 coverage==7.2.7 exceptiongroup==1.1.2 flaky==3.7.0 -hypothesis==6.82.0 +hypothesis==6.82.2 idna==3.4 importlib-metadata==6.8.0 iniconfig==2.0.0 From 59402ad848db556c19095567700ce7a70dc9e553 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 10 Aug 2023 18:06:20 +0000 Subject: [PATCH 012/151] Build(deps): Bump pypa/gh-action-pypi-publish from 1.8.8 to 1.8.9 Bumps [pypa/gh-action-pypi-publish](https://github.com/pypa/gh-action-pypi-publish) from 1.8.8 to 1.8.9. - [Release notes](https://github.com/pypa/gh-action-pypi-publish/releases) - [Commits](https://github.com/pypa/gh-action-pypi-publish/compare/v1.8.8...v1.8.9) --- updated-dependencies: - dependency-name: pypa/gh-action-pypi-publish dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index fb09af02..3e090d3e 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -145,7 +145,7 @@ jobs: run: | pandoc -s -o README.md README.rst - name: PyPI upload - uses: pypa/gh-action-pypi-publish@v1.8.8 + uses: pypa/gh-action-pypi-publish@v1.8.9 with: packages_dir: dist password: ${{ secrets.PYPI_API_TOKEN }} From 8e9cbc2c53c25fc57c9dfc047402698a87c0d3b1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 11 Aug 2023 18:39:01 +0000 Subject: [PATCH 013/151] Build(deps): Bump pypa/gh-action-pypi-publish from 1.8.9 to 1.8.10 Bumps [pypa/gh-action-pypi-publish](https://github.com/pypa/gh-action-pypi-publish) from 1.8.9 to 1.8.10. - [Release notes](https://github.com/pypa/gh-action-pypi-publish/releases) - [Commits](https://github.com/pypa/gh-action-pypi-publish/compare/v1.8.9...v1.8.10) --- updated-dependencies: - dependency-name: pypa/gh-action-pypi-publish dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 3e090d3e..0971073e 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -145,7 +145,7 @@ jobs: run: | pandoc -s -o README.md README.rst - name: PyPI upload - uses: pypa/gh-action-pypi-publish@v1.8.9 + uses: pypa/gh-action-pypi-publish@v1.8.10 with: packages_dir: dist password: ${{ secrets.PYPI_API_TOKEN }} From cb2ab13014f3da9265898fc7cc2fdb5fad784b2b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Aug 2023 18:39:32 +0000 Subject: [PATCH 014/151] Build(deps): Bump coverage from 7.2.7 to 7.3.0 in /dependencies/default Bumps [coverage](https://github.com/nedbat/coveragepy) from 7.2.7 to 7.3.0. - [Release notes](https://github.com/nedbat/coveragepy/releases) - [Changelog](https://github.com/nedbat/coveragepy/blob/master/CHANGES.rst) - [Commits](https://github.com/nedbat/coveragepy/compare/7.2.7...7.3.0) --- updated-dependencies: - dependency-name: coverage dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- dependencies/default/constraints.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies/default/constraints.txt b/dependencies/default/constraints.txt index 49411a79..49219a79 100644 --- a/dependencies/default/constraints.txt +++ b/dependencies/default/constraints.txt @@ -1,6 +1,6 @@ async-generator==1.10 attrs==23.1.0 -coverage==7.2.7 +coverage==7.3.0 exceptiongroup==1.1.2 flaky==3.7.0 hypothesis==6.82.2 From 1ae6db68b00f4d7c17ce97bc71719abce17b2d7f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Aug 2023 18:39:21 +0000 Subject: [PATCH 015/151] Build(deps): Bump hypothesis in /dependencies/default Bumps [hypothesis](https://github.com/HypothesisWorks/hypothesis) from 6.82.2 to 6.82.4. - [Release notes](https://github.com/HypothesisWorks/hypothesis/releases) - [Commits](https://github.com/HypothesisWorks/hypothesis/compare/hypothesis-python-6.82.2...hypothesis-python-6.82.4) --- updated-dependencies: - dependency-name: hypothesis dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- dependencies/default/constraints.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies/default/constraints.txt b/dependencies/default/constraints.txt index 49219a79..86f2a65b 100644 --- a/dependencies/default/constraints.txt +++ b/dependencies/default/constraints.txt @@ -3,7 +3,7 @@ attrs==23.1.0 coverage==7.3.0 exceptiongroup==1.1.2 flaky==3.7.0 -hypothesis==6.82.2 +hypothesis==6.82.4 idna==3.4 importlib-metadata==6.8.0 iniconfig==2.0.0 From a929697431c55ce13988270704e957bb2b4c2948 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Aug 2023 18:39:02 +0000 Subject: [PATCH 016/151] Build(deps): Bump mypy from 1.4.1 to 1.5.0 in /dependencies/default Bumps [mypy](https://github.com/python/mypy) from 1.4.1 to 1.5.0. - [Commits](https://github.com/python/mypy/compare/v1.4.1...v1.5.0) --- updated-dependencies: - dependency-name: mypy dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- dependencies/default/constraints.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies/default/constraints.txt b/dependencies/default/constraints.txt index 86f2a65b..d4f72ebd 100644 --- a/dependencies/default/constraints.txt +++ b/dependencies/default/constraints.txt @@ -7,7 +7,7 @@ hypothesis==6.82.4 idna==3.4 importlib-metadata==6.8.0 iniconfig==2.0.0 -mypy==1.4.1 +mypy==1.5.0 mypy-extensions==1.0.0 outcome==1.2.0 packaging==23.1 From c1c00bd7514dbd7ed23a5fad99d9ed22cdf78a82 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 15 Aug 2023 07:36:58 +0000 Subject: [PATCH 017/151] Build(deps): Bump exceptiongroup in /dependencies/default Bumps [exceptiongroup](https://github.com/agronholm/exceptiongroup) from 1.1.2 to 1.1.3. - [Changelog](https://github.com/agronholm/exceptiongroup/blob/main/CHANGES.rst) - [Commits](https://github.com/agronholm/exceptiongroup/compare/1.1.2...1.1.3) --- updated-dependencies: - dependency-name: exceptiongroup dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- dependencies/default/constraints.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies/default/constraints.txt b/dependencies/default/constraints.txt index d4f72ebd..e09db500 100644 --- a/dependencies/default/constraints.txt +++ b/dependencies/default/constraints.txt @@ -1,7 +1,7 @@ async-generator==1.10 attrs==23.1.0 coverage==7.3.0 -exceptiongroup==1.1.2 +exceptiongroup==1.1.3 flaky==3.7.0 hypothesis==6.82.4 idna==3.4 From 23a3f364983fe100ab70eec90cc8903a27901332 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 21 Aug 2023 18:54:21 +0000 Subject: [PATCH 018/151] Build(deps): Bump hypothesis in /dependencies/default Bumps [hypothesis](https://github.com/HypothesisWorks/hypothesis) from 6.82.4 to 6.82.6. - [Release notes](https://github.com/HypothesisWorks/hypothesis/releases) - [Commits](https://github.com/HypothesisWorks/hypothesis/compare/hypothesis-python-6.82.4...hypothesis-python-6.82.6) --- updated-dependencies: - dependency-name: hypothesis dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- dependencies/default/constraints.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies/default/constraints.txt b/dependencies/default/constraints.txt index e09db500..a19a021c 100644 --- a/dependencies/default/constraints.txt +++ b/dependencies/default/constraints.txt @@ -3,7 +3,7 @@ attrs==23.1.0 coverage==7.3.0 exceptiongroup==1.1.3 flaky==3.7.0 -hypothesis==6.82.4 +hypothesis==6.82.6 idna==3.4 importlib-metadata==6.8.0 iniconfig==2.0.0 From 5a2aaa6f74925fe9981d962958d864252bb914dc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 24 Aug 2023 16:56:04 +0000 Subject: [PATCH 019/151] Build(deps): Bump mypy from 1.5.0 to 1.5.1 in /dependencies/default Bumps [mypy](https://github.com/python/mypy) from 1.5.0 to 1.5.1. - [Commits](https://github.com/python/mypy/compare/v1.5.0...v1.5.1) --- updated-dependencies: - dependency-name: mypy dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- dependencies/default/constraints.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies/default/constraints.txt b/dependencies/default/constraints.txt index a19a021c..a8e0c8d2 100644 --- a/dependencies/default/constraints.txt +++ b/dependencies/default/constraints.txt @@ -7,7 +7,7 @@ hypothesis==6.82.6 idna==3.4 importlib-metadata==6.8.0 iniconfig==2.0.0 -mypy==1.5.0 +mypy==1.5.1 mypy-extensions==1.0.0 outcome==1.2.0 packaging==23.1 From a0fa69957907cf3e54ca2da89add292f1ecf8e92 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 28 Aug 2023 18:47:38 +0000 Subject: [PATCH 020/151] Build(deps): Bump pluggy from 1.2.0 to 1.3.0 in /dependencies/default Bumps [pluggy](https://github.com/pytest-dev/pluggy) from 1.2.0 to 1.3.0. - [Changelog](https://github.com/pytest-dev/pluggy/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pluggy/compare/1.2.0...1.3.0) --- updated-dependencies: - dependency-name: pluggy dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- dependencies/default/constraints.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies/default/constraints.txt b/dependencies/default/constraints.txt index a8e0c8d2..346e3356 100644 --- a/dependencies/default/constraints.txt +++ b/dependencies/default/constraints.txt @@ -11,7 +11,7 @@ mypy==1.5.1 mypy-extensions==1.0.0 outcome==1.2.0 packaging==23.1 -pluggy==1.2.0 +pluggy==1.3.0 pyparsing==3.1.1 pytest==7.4.0 pytest-trio==0.8.0 From fd57e55db1170c029324a7a9c56f86f14468217e Mon Sep 17 00:00:00 2001 From: Michael Seifert Date: Mon, 18 Sep 2023 09:50:47 +0200 Subject: [PATCH 021/151] [test] Addresses a Hypothesis health check that leads to failing tests. Class-based tests that inherit a Hypothesis test case emit a Hypothesis health check warning starting from hypothesis-6.83.2 [0][1]. This is due to inherited tests being run by different Hypothesis executors and may cause issues when replaying examples [2]. Inheriting Hypothesis tests in subclasses is clearly not wanted, so it makes sense to remove the pytest-asyncio test that tests for this feature. [0] https://hypothesis.readthedocs.io/en/latest/changes.html#v6-83-2 [1] https://github.com/HypothesisWorks/hypothesis/pull/3720 [2] https://github.com/HypothesisWorks/hypothesis/issues/3446 Signed-off-by: Michael Seifert --- tests/hypothesis/test_inherited_test.py | 20 -------------------- 1 file changed, 20 deletions(-) delete mode 100644 tests/hypothesis/test_inherited_test.py diff --git a/tests/hypothesis/test_inherited_test.py b/tests/hypothesis/test_inherited_test.py deleted file mode 100644 index a7762264..00000000 --- a/tests/hypothesis/test_inherited_test.py +++ /dev/null @@ -1,20 +0,0 @@ -import hypothesis.strategies as st -import pytest -from hypothesis import given - - -class BaseClass: - @pytest.mark.asyncio - @given(value=st.integers()) - async def test_hypothesis(self, value: int) -> None: - pass - - -class TestOne(BaseClass): - """During the first execution the Hypothesis test - is wrapped in a synchronous function.""" - - -class TestTwo(BaseClass): - """Execute the test a second time to ensure that - the test receives a fresh event loop.""" From 6ded4fc03ab1cf78feb55310f00a50bcc96eebc5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 18 Sep 2023 07:57:33 +0000 Subject: [PATCH 022/151] Build(deps): Bump hypothesis in /dependencies/default Bumps [hypothesis](https://github.com/HypothesisWorks/hypothesis) from 6.82.6 to 6.86.1. - [Release notes](https://github.com/HypothesisWorks/hypothesis/releases) - [Commits](https://github.com/HypothesisWorks/hypothesis/compare/hypothesis-python-6.82.6...hypothesis-python-6.86.1) --- updated-dependencies: - dependency-name: hypothesis dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- dependencies/default/constraints.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies/default/constraints.txt b/dependencies/default/constraints.txt index 346e3356..6fe8aac0 100644 --- a/dependencies/default/constraints.txt +++ b/dependencies/default/constraints.txt @@ -3,7 +3,7 @@ attrs==23.1.0 coverage==7.3.0 exceptiongroup==1.1.3 flaky==3.7.0 -hypothesis==6.82.6 +hypothesis==6.86.1 idna==3.4 importlib-metadata==6.8.0 iniconfig==2.0.0 From a921cbd6592c0ed83db3c81773519b4abf761b7a Mon Sep 17 00:00:00 2001 From: Michael Seifert Date: Mon, 18 Sep 2023 10:11:02 +0200 Subject: [PATCH 023/151] [ci] Start CI workflow when PR is added to a merge group. Signed-off-by: Michael Seifert --- .github/workflows/main.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 0971073e..82564487 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -7,6 +7,7 @@ on: tags: [v*] pull_request: branches: [main] + merge_group: workflow_dispatch: env: From fccda74be4fb4fc4a4ae44ceb8effbe1ad27f56c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 18 Sep 2023 08:02:14 +0000 Subject: [PATCH 024/151] Build(deps): Bump coverage from 7.3.0 to 7.3.1 in /dependencies/default Bumps [coverage](https://github.com/nedbat/coveragepy) from 7.3.0 to 7.3.1. - [Release notes](https://github.com/nedbat/coveragepy/releases) - [Changelog](https://github.com/nedbat/coveragepy/blob/master/CHANGES.rst) - [Commits](https://github.com/nedbat/coveragepy/compare/7.3.0...7.3.1) --- updated-dependencies: - dependency-name: coverage dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- dependencies/default/constraints.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies/default/constraints.txt b/dependencies/default/constraints.txt index 6fe8aac0..964e3294 100644 --- a/dependencies/default/constraints.txt +++ b/dependencies/default/constraints.txt @@ -1,6 +1,6 @@ async-generator==1.10 attrs==23.1.0 -coverage==7.3.0 +coverage==7.3.1 exceptiongroup==1.1.3 flaky==3.7.0 hypothesis==6.86.1 From 7312ae7a7fcc2f724a0318be35c45f5d1c76277f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 18 Sep 2023 08:16:08 +0000 Subject: [PATCH 025/151] Build(deps): Bump actions/checkout from 3 to 4 Bumps [actions/checkout](https://github.com/actions/checkout) from 3 to 4. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v3...v4) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/main.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 82564487..a68d0a11 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -21,7 +21,7 @@ jobs: version: ${{ steps.version.outputs.version }} prerelease: ${{ steps.version.outputs.prerelease }} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 0 - uses: actions/setup-python@v4 @@ -64,7 +64,7 @@ jobs: python-version: ['3.8', '3.9', '3.10', '3.11', 3.12-dev] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: actions/setup-python@v4 if: "!endsWith(matrix.python-version, '-dev')" with: @@ -99,7 +99,7 @@ jobs: uses: re-actors/alls-green@release/v1 with: jobs: ${{ toJSON(needs) }} - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: actions/setup-python@v4 with: python-version: ${{ env.PYTHON_LATEST }} @@ -133,7 +133,7 @@ jobs: run: | sudo apt-get install -y pandoc - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Download distributions uses: actions/download-artifact@v3 with: From 96b2f2adb3455410a031ce6eb3f0974df13b0580 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 18 Sep 2023 08:16:12 +0000 Subject: [PATCH 026/151] Build(deps): Bump pytest from 7.4.0 to 7.4.2 in /dependencies/default Bumps [pytest](https://github.com/pytest-dev/pytest) from 7.4.0 to 7.4.2. - [Release notes](https://github.com/pytest-dev/pytest/releases) - [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest/compare/7.4.0...7.4.2) --- updated-dependencies: - dependency-name: pytest dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- dependencies/default/constraints.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies/default/constraints.txt b/dependencies/default/constraints.txt index 964e3294..2e59e388 100644 --- a/dependencies/default/constraints.txt +++ b/dependencies/default/constraints.txt @@ -13,7 +13,7 @@ outcome==1.2.0 packaging==23.1 pluggy==1.3.0 pyparsing==3.1.1 -pytest==7.4.0 +pytest==7.4.2 pytest-trio==0.8.0 sniffio==1.3.0 sortedcontainers==2.4.0 From 717f674feac9ac38751545dde23f549053bd8565 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 18 Sep 2023 18:34:55 +0000 Subject: [PATCH 027/151] Build(deps): Bump hypothesis in /dependencies/default Bumps [hypothesis](https://github.com/HypothesisWorks/hypothesis) from 6.86.1 to 6.86.2. - [Release notes](https://github.com/HypothesisWorks/hypothesis/releases) - [Commits](https://github.com/HypothesisWorks/hypothesis/compare/hypothesis-python-6.86.1...hypothesis-python-6.86.2) --- updated-dependencies: - dependency-name: hypothesis dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- dependencies/default/constraints.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies/default/constraints.txt b/dependencies/default/constraints.txt index 2e59e388..ae99e6e4 100644 --- a/dependencies/default/constraints.txt +++ b/dependencies/default/constraints.txt @@ -3,7 +3,7 @@ attrs==23.1.0 coverage==7.3.1 exceptiongroup==1.1.3 flaky==3.7.0 -hypothesis==6.86.1 +hypothesis==6.86.2 idna==3.4 importlib-metadata==6.8.0 iniconfig==2.0.0 From c99ef9355a3bacef0ffc9689b65ec88b9f13d88a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 18 Sep 2023 18:35:01 +0000 Subject: [PATCH 028/151] Build(deps): Bump zipp from 3.16.2 to 3.17.0 in /dependencies/default Bumps [zipp](https://github.com/jaraco/zipp) from 3.16.2 to 3.17.0. - [Release notes](https://github.com/jaraco/zipp/releases) - [Changelog](https://github.com/jaraco/zipp/blob/main/NEWS.rst) - [Commits](https://github.com/jaraco/zipp/compare/v3.16.2...v3.17.0) --- updated-dependencies: - dependency-name: zipp dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- dependencies/default/constraints.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies/default/constraints.txt b/dependencies/default/constraints.txt index ae99e6e4..5fd075da 100644 --- a/dependencies/default/constraints.txt +++ b/dependencies/default/constraints.txt @@ -20,4 +20,4 @@ sortedcontainers==2.4.0 tomli==2.0.1 trio==0.22.2 typed-ast==1.5.5 -zipp==3.16.2 +zipp==3.17.0 From 0883c41e1c1d3d69213d09ae6700983d94b7e8a5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 25 Sep 2023 18:18:04 +0000 Subject: [PATCH 029/151] Build(deps): Bump hypothesis in /dependencies/default Bumps [hypothesis](https://github.com/HypothesisWorks/hypothesis) from 6.86.2 to 6.87.0. - [Release notes](https://github.com/HypothesisWorks/hypothesis/releases) - [Commits](https://github.com/HypothesisWorks/hypothesis/compare/hypothesis-python-6.86.2...hypothesis-python-6.87.0) --- updated-dependencies: - dependency-name: hypothesis dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- dependencies/default/constraints.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies/default/constraints.txt b/dependencies/default/constraints.txt index 5fd075da..3673bdc0 100644 --- a/dependencies/default/constraints.txt +++ b/dependencies/default/constraints.txt @@ -3,7 +3,7 @@ attrs==23.1.0 coverage==7.3.1 exceptiongroup==1.1.3 flaky==3.7.0 -hypothesis==6.86.2 +hypothesis==6.87.0 idna==3.4 importlib-metadata==6.8.0 iniconfig==2.0.0 From 089e36ed42a1584fca9591e9cc2b8ab98595d532 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Oct 2023 18:39:40 +0000 Subject: [PATCH 030/151] Build(deps): Bump hypothesis in /dependencies/default Bumps [hypothesis](https://github.com/HypothesisWorks/hypothesis) from 6.87.0 to 6.87.1. - [Release notes](https://github.com/HypothesisWorks/hypothesis/releases) - [Commits](https://github.com/HypothesisWorks/hypothesis/compare/hypothesis-python-6.87.0...hypothesis-python-6.87.1) --- updated-dependencies: - dependency-name: hypothesis dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- dependencies/default/constraints.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies/default/constraints.txt b/dependencies/default/constraints.txt index 3673bdc0..306919e2 100644 --- a/dependencies/default/constraints.txt +++ b/dependencies/default/constraints.txt @@ -3,7 +3,7 @@ attrs==23.1.0 coverage==7.3.1 exceptiongroup==1.1.3 flaky==3.7.0 -hypothesis==6.87.0 +hypothesis==6.87.1 idna==3.4 importlib-metadata==6.8.0 iniconfig==2.0.0 From 939e792a6c55dede1d27144c203937fb970dcdf7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Oct 2023 18:39:52 +0000 Subject: [PATCH 031/151] Build(deps): Bump coverage from 7.3.1 to 7.3.2 in /dependencies/default Bumps [coverage](https://github.com/nedbat/coveragepy) from 7.3.1 to 7.3.2. - [Release notes](https://github.com/nedbat/coveragepy/releases) - [Changelog](https://github.com/nedbat/coveragepy/blob/master/CHANGES.rst) - [Commits](https://github.com/nedbat/coveragepy/compare/7.3.1...7.3.2) --- updated-dependencies: - dependency-name: coverage dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- dependencies/default/constraints.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies/default/constraints.txt b/dependencies/default/constraints.txt index 306919e2..1a6681ca 100644 --- a/dependencies/default/constraints.txt +++ b/dependencies/default/constraints.txt @@ -1,6 +1,6 @@ async-generator==1.10 attrs==23.1.0 -coverage==7.3.1 +coverage==7.3.2 exceptiongroup==1.1.3 flaky==3.7.0 hypothesis==6.87.1 From 8b2ee641c9f3437a1a9ee553e79bb70bcf137c82 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Oct 2023 18:39:25 +0000 Subject: [PATCH 032/151] Build(deps): Bump packaging from 23.1 to 23.2 in /dependencies/default Bumps [packaging](https://github.com/pypa/packaging) from 23.1 to 23.2. - [Release notes](https://github.com/pypa/packaging/releases) - [Changelog](https://github.com/pypa/packaging/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pypa/packaging/compare/23.1...23.2) --- updated-dependencies: - dependency-name: packaging dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- dependencies/default/constraints.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies/default/constraints.txt b/dependencies/default/constraints.txt index 1a6681ca..5e32763b 100644 --- a/dependencies/default/constraints.txt +++ b/dependencies/default/constraints.txt @@ -10,7 +10,7 @@ iniconfig==2.0.0 mypy==1.5.1 mypy-extensions==1.0.0 outcome==1.2.0 -packaging==23.1 +packaging==23.2 pluggy==1.3.0 pyparsing==3.1.1 pytest==7.4.2 From 03a054a8eb861ca43d40a09bc5ee6af60324d08e Mon Sep 17 00:00:00 2001 From: Michael Seifert Date: Tue, 3 Oct 2023 16:47:10 +0200 Subject: [PATCH 033/151] [ci] Use the release version rather than the deadsnakes -dev version of CPython 3.12 for CI. Signed-off-by: Michael Seifert --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index a68d0a11..96322810 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -61,7 +61,7 @@ jobs: strategy: matrix: - python-version: ['3.8', '3.9', '3.10', '3.11', 3.12-dev] + python-version: ['3.8', '3.9', '3.10', '3.11', '3.12'] steps: - uses: actions/checkout@v4 From b0870ed90b8f42defcf24c1bdf41f6e6a20cbc7b Mon Sep 17 00:00:00 2001 From: Michael Seifert Date: Tue, 3 Oct 2023 16:47:40 +0200 Subject: [PATCH 034/151] [ci] Run CI jobs with Python 3.12. Signed-off-by: Michael Seifert --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 96322810..17800cff 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -11,7 +11,7 @@ on: workflow_dispatch: env: - PYTHON_LATEST: 3.11 + PYTHON_LATEST: 3.12 jobs: lint: From a4596399b06b7d2f30f0612f1a74a70f7a8ac400 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 Oct 2023 18:39:15 +0000 Subject: [PATCH 035/151] Build(deps): Bump hypothesis in /dependencies/default Bumps [hypothesis](https://github.com/HypothesisWorks/hypothesis) from 6.87.1 to 6.87.3. - [Release notes](https://github.com/HypothesisWorks/hypothesis/releases) - [Commits](https://github.com/HypothesisWorks/hypothesis/compare/hypothesis-python-6.87.1...hypothesis-python-6.87.3) --- updated-dependencies: - dependency-name: hypothesis dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- dependencies/default/constraints.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies/default/constraints.txt b/dependencies/default/constraints.txt index 5e32763b..1006461d 100644 --- a/dependencies/default/constraints.txt +++ b/dependencies/default/constraints.txt @@ -3,7 +3,7 @@ attrs==23.1.0 coverage==7.3.2 exceptiongroup==1.1.3 flaky==3.7.0 -hypothesis==6.87.1 +hypothesis==6.87.3 idna==3.4 importlib-metadata==6.8.0 iniconfig==2.0.0 From 1ac176675f462a7a678f0a190ff541b417857075 Mon Sep 17 00:00:00 2001 From: Michael Seifert Date: Wed, 20 Sep 2023 15:57:28 +0200 Subject: [PATCH 036/151] [feat] Introduce the asyncio_event_loop mark which provides a class-scoped asyncio event loop when a class has the mark. Signed-off-by: Michael Seifert --- docs/source/reference/markers.rst | 52 +++++++++++++++++++ pytest_asyncio/plugin.py | 33 ++++++++++++ tests/markers/test_class_marker.py | 81 ++++++++++++++++++++++++++++++ 3 files changed, 166 insertions(+) diff --git a/docs/source/reference/markers.rst b/docs/source/reference/markers.rst index eb89592c..d3bc291c 100644 --- a/docs/source/reference/markers.rst +++ b/docs/source/reference/markers.rst @@ -30,5 +30,57 @@ In *auto* mode, the ``pytest.mark.asyncio`` marker can be omitted, the marker is automatically to *async* test functions. +``pytest.mark.asyncio_event_loop`` +================================== +Test classes with this mark provide a class-scoped asyncio event loop. + +This functionality is orthogonal to the `asyncio` mark. +That means the presence of this mark does not imply that async test functions inside the class are collected by pytest-asyncio. +The collection happens automatically in `auto` mode. +However, if you're using strict mode, you still have to apply the `asyncio` mark to your async test functions. + +The following code example uses the `asyncio_event_loop` mark to provide a shared event loop for all tests in `TestClassScopedLoop`: + +.. code-block:: python + + import asyncio + + import pytest + + + @pytest.mark.asyncio_event_loop + class TestClassScopedLoop: + loop: asyncio.AbstractEventLoop + + @pytest.mark.asyncio + async def test_remember_loop(self): + TestClassScopedLoop.loop = asyncio.get_running_loop() + + @pytest.mark.asyncio + async def test_this_runs_in_same_loop(self): + assert asyncio.get_running_loop() is TestClassScopedLoop.loop + +In *auto* mode, the ``pytest.mark.asyncio`` marker can be omitted: + +.. code-block:: python + + import asyncio + + import pytest + + + @pytest.mark.asyncio_event_loop + class TestClassScopedLoop: + loop: asyncio.AbstractEventLoop + + async def test_remember_loop(self): + TestClassScopedLoop.loop = asyncio.get_running_loop() + + async def test_this_runs_in_same_loop(self): + assert asyncio.get_running_loop() is TestClassScopedLoop.loop + + + + .. |pytestmark| replace:: ``pytestmark`` .. _pytestmark: http://doc.pytest.org/en/latest/example/markers.html#marking-whole-classes-or-modules diff --git a/pytest_asyncio/plugin.py b/pytest_asyncio/plugin.py index c07dfced..d0ff0c7c 100644 --- a/pytest_asyncio/plugin.py +++ b/pytest_asyncio/plugin.py @@ -26,6 +26,7 @@ ) import pytest +from _pytest.mark.structures import get_unpacked_marks from pytest import ( Config, FixtureRequest, @@ -176,6 +177,11 @@ def pytest_configure(config: Config) -> None: "mark the test as a coroutine, it will be " "run using an asyncio event loop", ) + config.addinivalue_line( + "markers", + "asyncio_event_loop: " + "Provides an asyncio event loop in the scope of the marked test class", + ) @pytest.hookimpl(tryfirst=True) @@ -339,6 +345,33 @@ def pytest_pycollect_makeitem( return None +@pytest.hookimpl +def pytest_collectstart(collector: pytest.Collector): + if not isinstance(collector, pytest.Class): + return + # pytest.Collector.own_markers is empty at this point, + # so we rely on _pytest.mark.structures.get_unpacked_marks + marks = get_unpacked_marks(collector.obj, consider_mro=True) + for mark in marks: + if not mark.name == "asyncio_event_loop": + continue + + @pytest.fixture( + scope="class", + name="event_loop", + ) + def scoped_event_loop(cls) -> Iterator[asyncio.AbstractEventLoop]: + loop = asyncio.get_event_loop_policy().new_event_loop() + yield loop + loop.close() + + # @pytest.fixture does not register the fixture anywhere, so pytest doesn't + # know it exists. We work around this by attaching the fixture function to the + # collected Python class, where it will be picked up by pytest.Class.collect() + collector.obj.__pytest_asyncio_scoped_event_loop = scoped_event_loop + break + + def pytest_collection_modifyitems( session: Session, config: Config, items: List[Item] ) -> None: diff --git a/tests/markers/test_class_marker.py b/tests/markers/test_class_marker.py index d46c3af7..19645747 100644 --- a/tests/markers/test_class_marker.py +++ b/tests/markers/test_class_marker.py @@ -1,5 +1,6 @@ """Test if pytestmark works when defined on a class.""" import asyncio +from textwrap import dedent import pytest @@ -23,3 +24,83 @@ async def inc(): @pytest.fixture def sample_fixture(): return None + + +def test_asyncio_event_loop_mark_provides_class_scoped_loop_strict_mode( + pytester: pytest.Pytester, +): + pytester.makepyfile( + dedent( + """\ + import asyncio + import pytest + + @pytest.mark.asyncio_event_loop + class TestClassScopedLoop: + loop: asyncio.AbstractEventLoop + + @pytest.mark.asyncio + async def test_remember_loop(self): + TestClassScopedLoop.loop = asyncio.get_running_loop() + + @pytest.mark.asyncio + async def test_this_runs_in_same_loop(self): + assert asyncio.get_running_loop() is TestClassScopedLoop.loop + """ + ) + ) + result = pytester.runpytest("--asyncio-mode=strict") + result.assert_outcomes(passed=2) + + +def test_asyncio_event_loop_mark_provides_class_scoped_loop_auto_mode( + pytester: pytest.Pytester, +): + pytester.makepyfile( + dedent( + """\ + import asyncio + import pytest + + @pytest.mark.asyncio_event_loop + class TestClassScopedLoop: + loop: asyncio.AbstractEventLoop + + async def test_remember_loop(self): + TestClassScopedLoop.loop = asyncio.get_running_loop() + + async def test_this_runs_in_same_loop(self): + assert asyncio.get_running_loop() is TestClassScopedLoop.loop + """ + ) + ) + result = pytester.runpytest("--asyncio-mode=auto") + result.assert_outcomes(passed=2) + + +def test_asyncio_event_loop_mark_is_inherited_to_subclasses(pytester: pytest.Pytester): + pytester.makepyfile( + dedent( + """\ + import asyncio + import pytest + + @pytest.mark.asyncio_event_loop + class TestSuperClassWithMark: + pass + + class TestWithoutMark(TestSuperClassWithMark): + loop: asyncio.AbstractEventLoop + + @pytest.mark.asyncio + async def test_remember_loop(self): + TestWithoutMark.loop = asyncio.get_running_loop() + + @pytest.mark.asyncio + async def test_this_runs_in_same_loop(self): + assert asyncio.get_running_loop() is TestWithoutMark.loop + """ + ) + ) + result = pytester.runpytest("--asyncio-mode=strict") + result.assert_outcomes(passed=2) From 158f51776d1e7b8198ac9c53288b46d14fb243ca Mon Sep 17 00:00:00 2001 From: Michael Seifert Date: Tue, 26 Sep 2023 15:03:09 +0200 Subject: [PATCH 037/151] [refactor] Existing tests in test_module_marker are executed with pytest.Pytester to avoid applying pytestmark to subsequent tests in the test module. Signed-off-by: Michael Seifert --- tests/markers/test_module_marker.py | 65 +++++++++++++++++------------ 1 file changed, 39 insertions(+), 26 deletions(-) diff --git a/tests/markers/test_module_marker.py b/tests/markers/test_module_marker.py index 2f69dbc9..c870edb7 100644 --- a/tests/markers/test_module_marker.py +++ b/tests/markers/test_module_marker.py @@ -1,39 +1,52 @@ -"""Test if pytestmark works when defined in a module.""" -import asyncio +from textwrap import dedent -import pytest +from pytest import Pytester -pytestmark = pytest.mark.asyncio +def test_asyncio_mark_works_on_module_level(pytester: Pytester): + pytester.makepyfile( + dedent( + """\ + import asyncio -class TestPyTestMark: - async def test_is_asyncio(self, event_loop, sample_fixture): - assert asyncio.get_event_loop() + import pytest - counter = 1 + pytestmark = pytest.mark.asyncio - async def inc(): - nonlocal counter - counter += 1 - await asyncio.sleep(0) - await asyncio.ensure_future(inc()) - assert counter == 2 + class TestPyTestMark: + async def test_is_asyncio(self, event_loop, sample_fixture): + assert asyncio.get_event_loop() + counter = 1 -async def test_is_asyncio(event_loop, sample_fixture): - assert asyncio.get_event_loop() - counter = 1 + async def inc(): + nonlocal counter + counter += 1 + await asyncio.sleep(0) - async def inc(): - nonlocal counter - counter += 1 - await asyncio.sleep(0) + await asyncio.ensure_future(inc()) + assert counter == 2 - await asyncio.ensure_future(inc()) - assert counter == 2 + async def test_is_asyncio(event_loop, sample_fixture): + assert asyncio.get_event_loop() + counter = 1 -@pytest.fixture -def sample_fixture(): - return None + async def inc(): + nonlocal counter + counter += 1 + await asyncio.sleep(0) + + await asyncio.ensure_future(inc()) + assert counter == 2 + + + @pytest.fixture + def sample_fixture(): + return None + """ + ) + ) + result = pytester.runpytest("--asyncio-mode=strict") + result.assert_outcomes(passed=2) From 68a17d680223a2d56c106f73bfcd47ec48eca0c3 Mon Sep 17 00:00:00 2001 From: Michael Seifert Date: Tue, 3 Oct 2023 12:54:47 +0200 Subject: [PATCH 038/151] [feat] The asyncio_event_loop mark provides a module-scoped asyncio event loop when a module has the mark. Signed-off-by: Michael Seifert --- docs/source/reference/markers.rst | 29 ++++++++++++- pytest_asyncio/plugin.py | 12 ++++-- tests/markers/test_module_marker.py | 63 +++++++++++++++++++++++++++++ 3 files changed, 98 insertions(+), 6 deletions(-) diff --git a/docs/source/reference/markers.rst b/docs/source/reference/markers.rst index d3bc291c..9c4edc28 100644 --- a/docs/source/reference/markers.rst +++ b/docs/source/reference/markers.rst @@ -32,10 +32,10 @@ automatically to *async* test functions. ``pytest.mark.asyncio_event_loop`` ================================== -Test classes with this mark provide a class-scoped asyncio event loop. +Test classes or modules with this mark provide a class-scoped or module-scoped asyncio event loop. This functionality is orthogonal to the `asyncio` mark. -That means the presence of this mark does not imply that async test functions inside the class are collected by pytest-asyncio. +That means the presence of this mark does not imply that async test functions inside the class or module are collected by pytest-asyncio. The collection happens automatically in `auto` mode. However, if you're using strict mode, you still have to apply the `asyncio` mark to your async test functions. @@ -79,8 +79,33 @@ In *auto* mode, the ``pytest.mark.asyncio`` marker can be omitted: async def test_this_runs_in_same_loop(self): assert asyncio.get_running_loop() is TestClassScopedLoop.loop +Similarly, a module-scoped loop is provided when adding the `asyncio_event_loop` mark to the module: +.. code-block:: python + + import asyncio + + import pytest + + pytestmark = pytest.mark.asyncio_event_loop + + loop: asyncio.AbstractEventLoop + async def test_remember_loop(): + global loop + loop = asyncio.get_running_loop() + + + async def test_this_runs_in_same_loop(): + global loop + assert asyncio.get_running_loop() is loop + + + class TestClassA: + async def test_this_runs_in_same_loop(self): + global loop + assert asyncio.get_running_loop() is loop + .. |pytestmark| replace:: ``pytestmark`` .. _pytestmark: http://doc.pytest.org/en/latest/example/markers.html#marking-whole-classes-or-modules diff --git a/pytest_asyncio/plugin.py b/pytest_asyncio/plugin.py index d0ff0c7c..794a3088 100644 --- a/pytest_asyncio/plugin.py +++ b/pytest_asyncio/plugin.py @@ -180,7 +180,8 @@ def pytest_configure(config: Config) -> None: config.addinivalue_line( "markers", "asyncio_event_loop: " - "Provides an asyncio event loop in the scope of the marked test class", + "Provides an asyncio event loop in the scope of the marked test " + "class or module", ) @@ -347,7 +348,7 @@ def pytest_pycollect_makeitem( @pytest.hookimpl def pytest_collectstart(collector: pytest.Collector): - if not isinstance(collector, pytest.Class): + if not isinstance(collector, (pytest.Class, pytest.Module)): return # pytest.Collector.own_markers is empty at this point, # so we rely on _pytest.mark.structures.get_unpacked_marks @@ -357,10 +358,12 @@ def pytest_collectstart(collector: pytest.Collector): continue @pytest.fixture( - scope="class", + scope="class" if isinstance(collector, pytest.Class) else "module", name="event_loop", ) - def scoped_event_loop(cls) -> Iterator[asyncio.AbstractEventLoop]: + def scoped_event_loop( + *args, # Function needs to accept "cls" when collected by pytest.Class + ) -> Iterator[asyncio.AbstractEventLoop]: loop = asyncio.get_event_loop_policy().new_event_loop() yield loop loop.close() @@ -368,6 +371,7 @@ def scoped_event_loop(cls) -> Iterator[asyncio.AbstractEventLoop]: # @pytest.fixture does not register the fixture anywhere, so pytest doesn't # know it exists. We work around this by attaching the fixture function to the # collected Python class, where it will be picked up by pytest.Class.collect() + # or pytest.Module.collect(), respectively collector.obj.__pytest_asyncio_scoped_event_loop = scoped_event_loop break diff --git a/tests/markers/test_module_marker.py b/tests/markers/test_module_marker.py index c870edb7..8a5e9338 100644 --- a/tests/markers/test_module_marker.py +++ b/tests/markers/test_module_marker.py @@ -50,3 +50,66 @@ def sample_fixture(): ) result = pytester.runpytest("--asyncio-mode=strict") result.assert_outcomes(passed=2) + + +def test_asyncio_mark_provides_module_scoped_loop_strict_mode(pytester: Pytester): + pytester.makepyfile( + dedent( + """\ + import asyncio + import pytest + + pytestmark = pytest.mark.asyncio_event_loop + + loop: asyncio.AbstractEventLoop + + @pytest.mark.asyncio + async def test_remember_loop(): + global loop + loop = asyncio.get_running_loop() + + @pytest.mark.asyncio + async def test_this_runs_in_same_loop(): + global loop + assert asyncio.get_running_loop() is loop + + class TestClassA: + @pytest.mark.asyncio + async def test_this_runs_in_same_loop(self): + global loop + assert asyncio.get_running_loop() is loop + """ + ) + ) + result = pytester.runpytest("--asyncio-mode=strict") + result.assert_outcomes(passed=3) + + +def test_asyncio_mark_provides_class_scoped_loop_auto_mode(pytester: Pytester): + pytester.makepyfile( + dedent( + """\ + import asyncio + import pytest + + pytestmark = pytest.mark.asyncio_event_loop + + loop: asyncio.AbstractEventLoop + + async def test_remember_loop(): + global loop + loop = asyncio.get_running_loop() + + async def test_this_runs_in_same_loop(): + global loop + assert asyncio.get_running_loop() is loop + + class TestClassA: + async def test_this_runs_in_same_loop(self): + global loop + assert asyncio.get_running_loop() is loop + """ + ) + ) + result = pytester.runpytest("--asyncio-mode=auto") + result.assert_outcomes(passed=3) From e98d3aab9c917df978099d930d1972437577dacf Mon Sep 17 00:00:00 2001 From: Michael Seifert Date: Tue, 3 Oct 2023 14:14:54 +0200 Subject: [PATCH 039/151] [refactor] Run multiloop test inside Pytester to avoid the custom event loop implementation to pollute the test environment. Signed-off-by: Michael Seifert --- tests/multiloop/conftest.py | 15 ----- tests/multiloop/test_alternative_loops.py | 16 ------ tests/test_multiloop.py | 70 +++++++++++++++++++++++ 3 files changed, 70 insertions(+), 31 deletions(-) delete mode 100644 tests/multiloop/conftest.py delete mode 100644 tests/multiloop/test_alternative_loops.py create mode 100644 tests/test_multiloop.py diff --git a/tests/multiloop/conftest.py b/tests/multiloop/conftest.py deleted file mode 100644 index ebcb627a..00000000 --- a/tests/multiloop/conftest.py +++ /dev/null @@ -1,15 +0,0 @@ -import asyncio - -import pytest - - -class CustomSelectorLoop(asyncio.SelectorEventLoop): - """A subclass with no overrides, just to test for presence.""" - - -@pytest.fixture -def event_loop(): - """Create an instance of the default event loop for each test case.""" - loop = CustomSelectorLoop() - yield loop - loop.close() diff --git a/tests/multiloop/test_alternative_loops.py b/tests/multiloop/test_alternative_loops.py deleted file mode 100644 index 5f66c967..00000000 --- a/tests/multiloop/test_alternative_loops.py +++ /dev/null @@ -1,16 +0,0 @@ -"""Unit tests for overriding the event loop.""" -import asyncio - -import pytest - - -@pytest.mark.asyncio -async def test_for_custom_loop(): - """This test should be executed using the custom loop.""" - await asyncio.sleep(0.01) - assert type(asyncio.get_event_loop()).__name__ == "CustomSelectorLoop" - - -@pytest.mark.asyncio -async def test_dependent_fixture(dependent_fixture): - await asyncio.sleep(0.1) diff --git a/tests/test_multiloop.py b/tests/test_multiloop.py new file mode 100644 index 00000000..6c47d68c --- /dev/null +++ b/tests/test_multiloop.py @@ -0,0 +1,70 @@ +from textwrap import dedent + +from pytest import Pytester + + +def test_event_loop_override(pytester: Pytester): + pytester.makeconftest( + dedent( + '''\ + import asyncio + + import pytest + + + @pytest.fixture + def dependent_fixture(event_loop): + """A fixture dependent on the event_loop fixture, doing some cleanup.""" + counter = 0 + + async def just_a_sleep(): + """Just sleep a little while.""" + nonlocal event_loop + await asyncio.sleep(0.1) + nonlocal counter + counter += 1 + + event_loop.run_until_complete(just_a_sleep()) + yield + event_loop.run_until_complete(just_a_sleep()) + + assert counter == 2 + + + class CustomSelectorLoop(asyncio.SelectorEventLoop): + """A subclass with no overrides, just to test for presence.""" + + + @pytest.fixture + def event_loop(): + """Create an instance of the default event loop for each test case.""" + loop = CustomSelectorLoop() + yield loop + loop.close() + ''' + ) + ) + pytester.makepyfile( + dedent( + '''\ + """Unit tests for overriding the event loop.""" + import asyncio + + import pytest + + + @pytest.mark.asyncio + async def test_for_custom_loop(): + """This test should be executed using the custom loop.""" + await asyncio.sleep(0.01) + assert type(asyncio.get_event_loop()).__name__ == "CustomSelectorLoop" + + + @pytest.mark.asyncio + async def test_dependent_fixture(dependent_fixture): + await asyncio.sleep(0.1) + ''' + ) + ) + result = pytester.runpytest_subprocess("--asyncio-mode=strict") + result.assert_outcomes(passed=2) From 5821031fde37bdfd4ab511a483a3f595cb31237e Mon Sep 17 00:00:00 2001 From: Michael Seifert Date: Tue, 3 Oct 2023 15:53:07 +0200 Subject: [PATCH 040/151] [refactor] Run parametrized loop test inside Pytester to prevent the event loop implementation from polluting the test environment. Signed-off-by: Michael Seifert --- .../async_fixtures/test_parametrized_loop.py | 57 ++++++++++++------- 1 file changed, 36 insertions(+), 21 deletions(-) diff --git a/tests/async_fixtures/test_parametrized_loop.py b/tests/async_fixtures/test_parametrized_loop.py index 2fb8befa..2bdbe5e8 100644 --- a/tests/async_fixtures/test_parametrized_loop.py +++ b/tests/async_fixtures/test_parametrized_loop.py @@ -1,31 +1,46 @@ -import asyncio +from textwrap import dedent -import pytest +from pytest import Pytester -TESTS_COUNT = 0 +def test_event_loop_parametrization(pytester: Pytester): + pytester.makepyfile( + dedent( + """\ + import asyncio -def teardown_module(): - # parametrized 2 * 2 times: 2 for 'event_loop' and 2 for 'fix' - assert TESTS_COUNT == 4 + import pytest + import pytest_asyncio + TESTS_COUNT = 0 -@pytest.fixture(scope="module", params=[1, 2]) -def event_loop(request): - request.param - loop = asyncio.new_event_loop() - yield loop - loop.close() + def teardown_module(): + # parametrized 2 * 2 times: 2 for 'event_loop' and 2 for 'fix' + assert TESTS_COUNT == 4 -@pytest.fixture(params=["a", "b"]) -async def fix(request): - await asyncio.sleep(0) - return request.param + @pytest.fixture(scope="module", params=[1, 2]) + def event_loop(request): + request.param + loop = asyncio.new_event_loop() + yield loop + loop.close() -@pytest.mark.asyncio -async def test_parametrized_loop(fix): - await asyncio.sleep(0) - global TESTS_COUNT - TESTS_COUNT += 1 + + @pytest_asyncio.fixture(params=["a", "b"]) + async def fix(request): + await asyncio.sleep(0) + return request.param + + + @pytest.mark.asyncio + async def test_parametrized_loop(fix): + await asyncio.sleep(0) + global TESTS_COUNT + TESTS_COUNT += 1 + """ + ) + ) + result = pytester.runpytest_subprocess("--asyncio-mode=strict") + result.assert_outcomes(passed=4) From 36b226936e17232535e88ca34f9707cdf211776b Mon Sep 17 00:00:00 2001 From: Michael Seifert Date: Tue, 25 Jul 2023 14:25:56 +0200 Subject: [PATCH 041/151] [refactor] The synchronization wrapper for coroutine and async generator tests no longer requires an explicit event loop argument. The wrapper retrieves the currently set loop via asyncio.get_event_loop, instead. Signed-off-by: Michael Seifert --- pytest_asyncio/plugin.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/pytest_asyncio/plugin.py b/pytest_asyncio/plugin.py index 794a3088..cdf160af 100644 --- a/pytest_asyncio/plugin.py +++ b/pytest_asyncio/plugin.py @@ -21,7 +21,6 @@ Set, TypeVar, Union, - cast, overload, ) @@ -509,19 +508,15 @@ def pytest_pyfunc_call(pyfuncitem: pytest.Function) -> Optional[object]: """ marker = pyfuncitem.get_closest_marker("asyncio") if marker is not None: - funcargs: Dict[str, object] = pyfuncitem.funcargs # type: ignore[name-defined] - loop = cast(asyncio.AbstractEventLoop, funcargs["event_loop"]) if _is_hypothesis_test(pyfuncitem.obj): pyfuncitem.obj.hypothesis.inner_test = wrap_in_sync( pyfuncitem, pyfuncitem.obj.hypothesis.inner_test, - _loop=loop, ) else: pyfuncitem.obj = wrap_in_sync( pyfuncitem, pyfuncitem.obj, - _loop=loop, ) yield @@ -533,7 +528,6 @@ def _is_hypothesis_test(function: Any) -> bool: def wrap_in_sync( pyfuncitem: pytest.Function, func: Callable[..., Awaitable[Any]], - _loop: asyncio.AbstractEventLoop, ): """Return a sync wrapper around an async function executing it in the current event loop.""" @@ -559,6 +553,7 @@ def inner(*args, **kwargs): ) ) return + _loop = asyncio.get_event_loop() task = asyncio.ensure_future(coro, loop=_loop) try: _loop.run_until_complete(task) From 93b37abec217b1d5c7f9db2ecb8daaabc65b2aa8 Mon Sep 17 00:00:00 2001 From: Michael Seifert Date: Mon, 9 Oct 2023 10:39:05 +0200 Subject: [PATCH 042/151] [feat] Class-scoped and module-scoped event loops no longer override the function-scoped event_loop fixture. They rather provide a fixture with a different name, based on the nodeid of the pytest.Collector that has the "asyncio_event_loop" mark. When a test requests the event_loop fixture and a dynamically generated event loop with class or module scope, pytest-asyncio will raise an error. Signed-off-by: Michael Seifert --- pytest_asyncio/plugin.py | 49 +++++++++++++++++++++++++++-- tests/markers/test_class_marker.py | 22 +++++++++++++ tests/markers/test_module_marker.py | 22 +++++++++++++ 3 files changed, 91 insertions(+), 2 deletions(-) diff --git a/pytest_asyncio/plugin.py b/pytest_asyncio/plugin.py index cdf160af..a7e70a78 100644 --- a/pytest_asyncio/plugin.py +++ b/pytest_asyncio/plugin.py @@ -34,6 +34,7 @@ Parser, PytestPluginManager, Session, + StashKey, ) _R = TypeVar("_R") @@ -55,6 +56,14 @@ SubRequest = Any +class PytestAsyncioError(Exception): + """Base class for exceptions raised by pytest-asyncio""" + + +class MultipleEventLoopsRequestedError(PytestAsyncioError): + """Raised when a test requests multiple asyncio event loops.""" + + class Mode(str, enum.Enum): AUTO = "auto" STRICT = "strict" @@ -345,6 +354,9 @@ def pytest_pycollect_makeitem( return None +_event_loop_fixture_id = StashKey[str] + + @pytest.hookimpl def pytest_collectstart(collector: pytest.Collector): if not isinstance(collector, (pytest.Class, pytest.Module)): @@ -356,9 +368,19 @@ def pytest_collectstart(collector: pytest.Collector): if not mark.name == "asyncio_event_loop": continue + # There seem to be issues when a fixture is shadowed by another fixture + # and both differ in their params. + # https://github.com/pytest-dev/pytest/issues/2043 + # https://github.com/pytest-dev/pytest/issues/11350 + # As such, we assign a unique name for each event_loop fixture. + # The fixture name is stored in the collector's Stash, so it can + # be injected when setting up the test + event_loop_fixture_id = f"{collector.nodeid}::" + collector.stash[_event_loop_fixture_id] = event_loop_fixture_id + @pytest.fixture( scope="class" if isinstance(collector, pytest.Class) else "module", - name="event_loop", + name=event_loop_fixture_id, ) def scoped_event_loop( *args, # Function needs to accept "cls" when collected by pytest.Class @@ -569,15 +591,38 @@ def inner(*args, **kwargs): return inner +_MULTIPLE_LOOPS_REQUESTED_ERROR = dedent( + """\ + Multiple asyncio event loops with different scopes have been requested + by %s. The test explicitly requests the event_loop fixture, while another + event loop is provided by %s. + Remove "event_loop" from the requested fixture in your test to run the test + in a larger-scoped event loop or remove the "asyncio_event_loop" mark to run + the test in a function-scoped event loop. + """ +) + + def pytest_runtest_setup(item: pytest.Item) -> None: marker = item.get_closest_marker("asyncio") if marker is None: return + event_loop_fixture_id = "event_loop" + for node, mark in item.iter_markers_with_node("asyncio_event_loop"): + scoped_event_loop_provider_node = node + event_loop_fixture_id = node.stash.get(_event_loop_fixture_id, None) + if event_loop_fixture_id: + break fixturenames = item.fixturenames # type: ignore[attr-defined] # inject an event loop fixture for all async tests if "event_loop" in fixturenames: + if event_loop_fixture_id != "event_loop": + raise MultipleEventLoopsRequestedError( + _MULTIPLE_LOOPS_REQUESTED_ERROR + % (item.nodeid, scoped_event_loop_provider_node.nodeid), + ) fixturenames.remove("event_loop") - fixturenames.insert(0, "event_loop") + fixturenames.insert(0, event_loop_fixture_id) obj = getattr(item, "obj", None) if not getattr(obj, "hypothesis", False) and getattr( obj, "is_hypothesis_test", False diff --git a/tests/markers/test_class_marker.py b/tests/markers/test_class_marker.py index 19645747..9b39382a 100644 --- a/tests/markers/test_class_marker.py +++ b/tests/markers/test_class_marker.py @@ -104,3 +104,25 @@ async def test_this_runs_in_same_loop(self): ) result = pytester.runpytest("--asyncio-mode=strict") result.assert_outcomes(passed=2) + + +def test_raise_when_event_loop_fixture_is_requested_in_addition_to_scoped_loop( + pytester: pytest.Pytester, +): + pytester.makepyfile( + dedent( + """\ + import asyncio + import pytest + + @pytest.mark.asyncio_event_loop + class TestClassScopedLoop: + @pytest.mark.asyncio + async def test_remember_loop(self, event_loop): + pass + """ + ) + ) + result = pytester.runpytest("--asyncio-mode=strict") + result.assert_outcomes(errors=1) + result.stdout.fnmatch_lines("*MultipleEventLoopsRequestedError: *") diff --git a/tests/markers/test_module_marker.py b/tests/markers/test_module_marker.py index 8a5e9338..53bfd7c1 100644 --- a/tests/markers/test_module_marker.py +++ b/tests/markers/test_module_marker.py @@ -113,3 +113,25 @@ async def test_this_runs_in_same_loop(self): ) result = pytester.runpytest("--asyncio-mode=auto") result.assert_outcomes(passed=3) + + +def test_raise_when_event_loop_fixture_is_requested_in_addition_to_scoped_loop( + pytester: Pytester, +): + pytester.makepyfile( + dedent( + """\ + import asyncio + import pytest + + pytestmark = pytest.mark.asyncio_event_loop + + @pytest.mark.asyncio + async def test_remember_loop(event_loop): + pass + """ + ) + ) + result = pytester.runpytest("--asyncio-mode=strict") + result.assert_outcomes(errors=1) + result.stdout.fnmatch_lines("*MultipleEventLoopsRequestedError: *") From f5d8f6eee827afb315ec4d7fd72e14a6e357e2ea Mon Sep 17 00:00:00 2001 From: Michael Seifert Date: Mon, 9 Oct 2023 12:33:43 +0200 Subject: [PATCH 043/151] [refactor] Tests for "loop_fixture_scope" create the custom event loop inside the event_loop fixture override rather than on the module-level. This prevents the custom loop from being created during test collection time. Signed-off-by: Michael Seifert --- tests/loop_fixture_scope/conftest.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/loop_fixture_scope/conftest.py b/tests/loop_fixture_scope/conftest.py index 223160c2..6b9a7649 100644 --- a/tests/loop_fixture_scope/conftest.py +++ b/tests/loop_fixture_scope/conftest.py @@ -7,11 +7,9 @@ class CustomSelectorLoop(asyncio.SelectorEventLoop): """A subclass with no overrides, just to test for presence.""" -loop = CustomSelectorLoop() - - @pytest.fixture(scope="module") def event_loop(): """Create an instance of the default event loop for each test case.""" + loop = CustomSelectorLoop() yield loop loop.close() From 684bae7a37daebdf20771e88648d7119f32e60a4 Mon Sep 17 00:00:00 2001 From: Michael Seifert Date: Mon, 9 Oct 2023 13:13:05 +0200 Subject: [PATCH 044/151] [feat] The asyncio_event_loop mark specifying an optional event loop policy. Signed-off-by: Michael Seifert --- docs/source/reference/markers.rst | 21 ++++++++++++ pytest_asyncio/plugin.py | 44 +++++++++++++++++++++---- tests/markers/test_class_marker.py | 35 ++++++++++++++++++++ tests/markers/test_module_marker.py | 50 +++++++++++++++++++++++++++++ 4 files changed, 143 insertions(+), 7 deletions(-) diff --git a/docs/source/reference/markers.rst b/docs/source/reference/markers.rst index 9c4edc28..4d4b0213 100644 --- a/docs/source/reference/markers.rst +++ b/docs/source/reference/markers.rst @@ -107,5 +107,26 @@ Similarly, a module-scoped loop is provided when adding the `asyncio_event_loop` global loop assert asyncio.get_running_loop() is loop +The `asyncio_event_loop` mark supports an optional `policy` keyword argument to set the asyncio event loop policy. + +.. code-block:: python + + import asyncio + + import pytest + + + class CustomEventLoopPolicy(asyncio.DefaultEventLoopPolicy): + pass + + + @pytest.mark.asyncio_event_loop(policy=CustomEventLoopPolicy()) + class TestUsesCustomEventLoopPolicy: + @pytest.mark.asyncio + async def test_uses_custom_event_loop_policy(self): + assert isinstance(asyncio.get_event_loop_policy(), CustomEventLoopPolicy) + +If no explicit policy is provided, the mark will use the loop policy returned by ``asyncio.get_event_loop_policy()``. + .. |pytestmark| replace:: ``pytestmark`` .. _pytestmark: http://doc.pytest.org/en/latest/example/markers.html#marking-whole-classes-or-modules diff --git a/pytest_asyncio/plugin.py b/pytest_asyncio/plugin.py index a7e70a78..63867ca1 100644 --- a/pytest_asyncio/plugin.py +++ b/pytest_asyncio/plugin.py @@ -31,6 +31,7 @@ FixtureRequest, Function, Item, + Metafunc, Parser, PytestPluginManager, Session, @@ -367,6 +368,7 @@ def pytest_collectstart(collector: pytest.Collector): for mark in marks: if not mark.name == "asyncio_event_loop": continue + event_loop_policy = mark.kwargs.get("policy", asyncio.get_event_loop_policy()) # There seem to be issues when a fixture is shadowed by another fixture # and both differ in their params. @@ -381,13 +383,23 @@ def pytest_collectstart(collector: pytest.Collector): @pytest.fixture( scope="class" if isinstance(collector, pytest.Class) else "module", name=event_loop_fixture_id, + params=(event_loop_policy,), + ids=(type(event_loop_policy).__name__,), ) def scoped_event_loop( *args, # Function needs to accept "cls" when collected by pytest.Class + request, ) -> Iterator[asyncio.AbstractEventLoop]: - loop = asyncio.get_event_loop_policy().new_event_loop() + new_loop_policy = request.param + old_loop_policy = asyncio.get_event_loop_policy() + old_loop = asyncio.get_event_loop() + asyncio.set_event_loop_policy(new_loop_policy) + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) yield loop loop.close() + asyncio.set_event_loop_policy(old_loop_policy) + asyncio.set_event_loop(old_loop) # @pytest.fixture does not register the fixture anywhere, so pytest doesn't # know it exists. We work around this by attaching the fixture function to the @@ -430,6 +442,30 @@ def _hypothesis_test_wraps_coroutine(function: Any) -> bool: return _is_coroutine(function.hypothesis.inner_test) +@pytest.hookimpl(tryfirst=True) +def pytest_generate_tests(metafunc: Metafunc) -> None: + for event_loop_provider_node, _ in metafunc.definition.iter_markers_with_node( + "asyncio_event_loop" + ): + event_loop_fixture_id = event_loop_provider_node.stash.get( + _event_loop_fixture_id, None + ) + if event_loop_fixture_id: + fixturemanager = metafunc.config.pluginmanager.get_plugin("funcmanage") + if "event_loop" in metafunc.fixturenames: + raise MultipleEventLoopsRequestedError( + _MULTIPLE_LOOPS_REQUESTED_ERROR + % (metafunc.definition.nodeid, event_loop_provider_node.nodeid), + ) + # Add the scoped event loop fixture to Metafunc's list of fixture names and + # fixturedefs and leave the actual parametrization to pytest + metafunc.fixturenames.insert(0, event_loop_fixture_id) + metafunc._arg2fixturedefs[ + event_loop_fixture_id + ] = fixturemanager._arg2fixturedefs[event_loop_fixture_id] + break + + @pytest.hookimpl(hookwrapper=True) def pytest_fixture_setup( fixturedef: FixtureDef, request: SubRequest @@ -609,18 +645,12 @@ def pytest_runtest_setup(item: pytest.Item) -> None: return event_loop_fixture_id = "event_loop" for node, mark in item.iter_markers_with_node("asyncio_event_loop"): - scoped_event_loop_provider_node = node event_loop_fixture_id = node.stash.get(_event_loop_fixture_id, None) if event_loop_fixture_id: break fixturenames = item.fixturenames # type: ignore[attr-defined] # inject an event loop fixture for all async tests if "event_loop" in fixturenames: - if event_loop_fixture_id != "event_loop": - raise MultipleEventLoopsRequestedError( - _MULTIPLE_LOOPS_REQUESTED_ERROR - % (item.nodeid, scoped_event_loop_provider_node.nodeid), - ) fixturenames.remove("event_loop") fixturenames.insert(0, event_loop_fixture_id) obj = getattr(item, "obj", None) diff --git a/tests/markers/test_class_marker.py b/tests/markers/test_class_marker.py index 9b39382a..dd8e2dc1 100644 --- a/tests/markers/test_class_marker.py +++ b/tests/markers/test_class_marker.py @@ -126,3 +126,38 @@ async def test_remember_loop(self, event_loop): result = pytester.runpytest("--asyncio-mode=strict") result.assert_outcomes(errors=1) result.stdout.fnmatch_lines("*MultipleEventLoopsRequestedError: *") + + +def test_asyncio_event_loop_mark_allows_specifying_the_loop_policy( + pytester: pytest.Pytester, +): + pytester.makepyfile( + dedent( + """\ + import asyncio + import pytest + + class CustomEventLoopPolicy(asyncio.DefaultEventLoopPolicy): + pass + + @pytest.mark.asyncio_event_loop(policy=CustomEventLoopPolicy()) + class TestUsesCustomEventLoopPolicy: + + @pytest.mark.asyncio + async def test_uses_custom_event_loop_policy(self): + assert isinstance( + asyncio.get_event_loop_policy(), + CustomEventLoopPolicy, + ) + + @pytest.mark.asyncio + async def test_does_not_use_custom_event_loop_policy(): + assert not isinstance( + asyncio.get_event_loop_policy(), + CustomEventLoopPolicy, + ) + """ + ) + ) + result = pytester.runpytest("--asyncio-mode=strict") + result.assert_outcomes(passed=2) diff --git a/tests/markers/test_module_marker.py b/tests/markers/test_module_marker.py index 53bfd7c1..781e4513 100644 --- a/tests/markers/test_module_marker.py +++ b/tests/markers/test_module_marker.py @@ -135,3 +135,53 @@ async def test_remember_loop(event_loop): result = pytester.runpytest("--asyncio-mode=strict") result.assert_outcomes(errors=1) result.stdout.fnmatch_lines("*MultipleEventLoopsRequestedError: *") + + +def test_asyncio_event_loop_mark_allows_specifying_the_loop_policy( + pytester: Pytester, +): + pytester.makepyfile( + __init__="", + custom_policy=dedent( + """\ + import asyncio + + class CustomEventLoopPolicy(asyncio.DefaultEventLoopPolicy): + pass + """ + ), + test_uses_custom_policy=dedent( + """\ + import asyncio + import pytest + + from .custom_policy import CustomEventLoopPolicy + + pytestmark = pytest.mark.asyncio_event_loop(policy=CustomEventLoopPolicy()) + + @pytest.mark.asyncio + async def test_uses_custom_event_loop_policy(): + assert isinstance( + asyncio.get_event_loop_policy(), + CustomEventLoopPolicy, + ) + """ + ), + test_does_not_use_custom_policy=dedent( + """\ + import asyncio + import pytest + + from .custom_policy import CustomEventLoopPolicy + + @pytest.mark.asyncio + async def test_does_not_use_custom_event_loop_policy(): + assert not isinstance( + asyncio.get_event_loop_policy(), + CustomEventLoopPolicy, + ) + """ + ), + ) + result = pytester.runpytest("--asyncio-mode=strict") + result.assert_outcomes(passed=2) From 79a0215ebce643d4ab55920bcde14424212da686 Mon Sep 17 00:00:00 2001 From: Michael Seifert Date: Mon, 9 Oct 2023 14:16:29 +0200 Subject: [PATCH 045/151] [feat] The "policy" keyword argument to asyncio_event_loop allows passing an iterable of policies. This causes tests under the _asyncio_event_loop_ mark to be parametrized with the different loop policies. Signed-off-by: Michael Seifert --- docs/source/reference/markers.rst | 24 ++++++++++++++++++++++++ pytest_asyncio/plugin.py | 9 +++++++-- tests/markers/test_class_marker.py | 27 +++++++++++++++++++++++++++ tests/markers/test_module_marker.py | 27 +++++++++++++++++++++++++++ 4 files changed, 85 insertions(+), 2 deletions(-) diff --git a/docs/source/reference/markers.rst b/docs/source/reference/markers.rst index 4d4b0213..7b304045 100644 --- a/docs/source/reference/markers.rst +++ b/docs/source/reference/markers.rst @@ -126,6 +126,30 @@ The `asyncio_event_loop` mark supports an optional `policy` keyword argument to async def test_uses_custom_event_loop_policy(self): assert isinstance(asyncio.get_event_loop_policy(), CustomEventLoopPolicy) + +The ``policy`` keyword argument may also take an iterable of event loop policies. This causes tests under by the `asyncio_event_loop` mark to be parametrized with different policies: + +.. code-block:: python + + import asyncio + + import pytest + + import pytest_asyncio + + + @pytest.mark.asyncio_event_loop( + policy=[ + asyncio.DefaultEventLoopPolicy(), + uvloop.EventLoopPolicy(), + ] + ) + class TestWithDifferentLoopPolicies: + @pytest.mark.asyncio + async def test_parametrized_loop(self): + pass + + If no explicit policy is provided, the mark will use the loop policy returned by ``asyncio.get_event_loop_policy()``. .. |pytestmark| replace:: ``pytestmark`` diff --git a/pytest_asyncio/plugin.py b/pytest_asyncio/plugin.py index 63867ca1..b2bde15f 100644 --- a/pytest_asyncio/plugin.py +++ b/pytest_asyncio/plugin.py @@ -369,6 +369,11 @@ def pytest_collectstart(collector: pytest.Collector): if not mark.name == "asyncio_event_loop": continue event_loop_policy = mark.kwargs.get("policy", asyncio.get_event_loop_policy()) + policy_params = ( + event_loop_policy + if isinstance(event_loop_policy, Iterable) + else (event_loop_policy,) + ) # There seem to be issues when a fixture is shadowed by another fixture # and both differ in their params. @@ -383,8 +388,8 @@ def pytest_collectstart(collector: pytest.Collector): @pytest.fixture( scope="class" if isinstance(collector, pytest.Class) else "module", name=event_loop_fixture_id, - params=(event_loop_policy,), - ids=(type(event_loop_policy).__name__,), + params=policy_params, + ids=tuple(type(policy).__name__ for policy in policy_params), ) def scoped_event_loop( *args, # Function needs to accept "cls" when collected by pytest.Class diff --git a/tests/markers/test_class_marker.py b/tests/markers/test_class_marker.py index dd8e2dc1..f9a2f680 100644 --- a/tests/markers/test_class_marker.py +++ b/tests/markers/test_class_marker.py @@ -161,3 +161,30 @@ async def test_does_not_use_custom_event_loop_policy(): ) result = pytester.runpytest("--asyncio-mode=strict") result.assert_outcomes(passed=2) + + +def test_asyncio_event_loop_mark_allows_specifying_multiple_loop_policies( + pytester: pytest.Pytester, +): + pytester.makepyfile( + dedent( + """\ + import asyncio + + import pytest + + @pytest.mark.asyncio_event_loop( + policy=[ + asyncio.DefaultEventLoopPolicy(), + asyncio.DefaultEventLoopPolicy(), + ] + ) + class TestWithDifferentLoopPolicies: + @pytest.mark.asyncio + async def test_parametrized_loop(self): + pass + """ + ) + ) + result = pytester.runpytest_subprocess("--asyncio-mode=strict") + result.assert_outcomes(passed=2) diff --git a/tests/markers/test_module_marker.py b/tests/markers/test_module_marker.py index 781e4513..b0926750 100644 --- a/tests/markers/test_module_marker.py +++ b/tests/markers/test_module_marker.py @@ -185,3 +185,30 @@ async def test_does_not_use_custom_event_loop_policy(): ) result = pytester.runpytest("--asyncio-mode=strict") result.assert_outcomes(passed=2) + + +def test_asyncio_event_loop_mark_allows_specifying_multiple_loop_policies( + pytester: Pytester, +): + pytester.makepyfile( + dedent( + """\ + import asyncio + + import pytest + + pytestmark = pytest.mark.asyncio_event_loop( + policy=[ + asyncio.DefaultEventLoopPolicy(), + asyncio.DefaultEventLoopPolicy(), + ] + ) + + @pytest.mark.asyncio + async def test_parametrized_loop(): + pass + """ + ) + ) + result = pytester.runpytest_subprocess("--asyncio-mode=strict") + result.assert_outcomes(passed=2) From d4a18e799407d75512ee9dcd72c1fe20d901b913 Mon Sep 17 00:00:00 2001 From: Michael Seifert Date: Mon, 9 Oct 2023 15:37:53 +0200 Subject: [PATCH 046/151] [feat] Fixtures and tests sharing the same asyncio_event_loop mark are executed in the same event loop. Signed-off-by: Michael Seifert --- docs/source/reference/markers.rst | 24 +++++++++++ pytest_asyncio/plugin.py | 62 +++++++++++++++++++---------- tests/markers/test_class_marker.py | 29 ++++++++++++++ tests/markers/test_module_marker.py | 31 +++++++++++++++ 4 files changed, 125 insertions(+), 21 deletions(-) diff --git a/docs/source/reference/markers.rst b/docs/source/reference/markers.rst index 7b304045..68d5efd3 100644 --- a/docs/source/reference/markers.rst +++ b/docs/source/reference/markers.rst @@ -152,5 +152,29 @@ The ``policy`` keyword argument may also take an iterable of event loop policies If no explicit policy is provided, the mark will use the loop policy returned by ``asyncio.get_event_loop_policy()``. +Fixtures and tests sharing the same `asyncio_event_loop` mark are executed in the same event loop: + +.. code-block:: python + + import asyncio + + import pytest + + import pytest_asyncio + + + @pytest.mark.asyncio_event_loop + class TestClassScopedLoop: + loop: asyncio.AbstractEventLoop + + @pytest_asyncio.fixture + async def my_fixture(self): + TestClassScopedLoop.loop = asyncio.get_running_loop() + + @pytest.mark.asyncio + async def test_runs_is_same_loop_as_fixture(self, my_fixture): + assert asyncio.get_running_loop() is TestClassScopedLoop.loop + + .. |pytestmark| replace:: ``pytestmark`` .. _pytestmark: http://doc.pytest.org/en/latest/example/markers.html#marking-whole-classes-or-modules diff --git a/pytest_asyncio/plugin.py b/pytest_asyncio/plugin.py index b2bde15f..7d41779f 100644 --- a/pytest_asyncio/plugin.py +++ b/pytest_asyncio/plugin.py @@ -27,6 +27,7 @@ import pytest from _pytest.mark.structures import get_unpacked_marks from pytest import ( + Collector, Config, FixtureRequest, Function, @@ -202,11 +203,17 @@ def pytest_report_header(config: Config) -> List[str]: def _preprocess_async_fixtures( - config: Config, + collector: Collector, processed_fixturedefs: Set[FixtureDef], ) -> None: + config = collector.config asyncio_mode = _get_asyncio_mode(config) fixturemanager = config.pluginmanager.get_plugin("funcmanage") + event_loop_fixture_id = "event_loop" + for node, mark in collector.iter_markers_with_node("asyncio_event_loop"): + event_loop_fixture_id = node.stash.get(_event_loop_fixture_id, None) + if event_loop_fixture_id: + break for fixtures in fixturemanager._arg2fixturedefs.values(): for fixturedef in fixtures: func = fixturedef.func @@ -219,37 +226,42 @@ def _preprocess_async_fixtures( # This applies to pytest_trio fixtures, for example continue _make_asyncio_fixture_function(func) - _inject_fixture_argnames(fixturedef) - _synchronize_async_fixture(fixturedef) + _inject_fixture_argnames(fixturedef, event_loop_fixture_id) + _synchronize_async_fixture(fixturedef, event_loop_fixture_id) assert _is_asyncio_fixture_function(fixturedef.func) processed_fixturedefs.add(fixturedef) -def _inject_fixture_argnames(fixturedef: FixtureDef) -> None: +def _inject_fixture_argnames( + fixturedef: FixtureDef, event_loop_fixture_id: str +) -> None: """ Ensures that `request` and `event_loop` are arguments of the specified fixture. """ to_add = [] - for name in ("request", "event_loop"): + for name in ("request", event_loop_fixture_id): if name not in fixturedef.argnames: to_add.append(name) if to_add: fixturedef.argnames += tuple(to_add) -def _synchronize_async_fixture(fixturedef: FixtureDef) -> None: +def _synchronize_async_fixture( + fixturedef: FixtureDef, event_loop_fixture_id: str +) -> None: """ Wraps the fixture function of an async fixture in a synchronous function. """ if inspect.isasyncgenfunction(fixturedef.func): - _wrap_asyncgen_fixture(fixturedef) + _wrap_asyncgen_fixture(fixturedef, event_loop_fixture_id) elif inspect.iscoroutinefunction(fixturedef.func): - _wrap_async_fixture(fixturedef) + _wrap_async_fixture(fixturedef, event_loop_fixture_id) def _add_kwargs( func: Callable[..., Any], kwargs: Dict[str, Any], + event_loop_fixture_id: str, event_loop: asyncio.AbstractEventLoop, request: SubRequest, ) -> Dict[str, Any]: @@ -257,8 +269,8 @@ def _add_kwargs( ret = kwargs.copy() if "request" in sig.parameters: ret["request"] = request - if "event_loop" in sig.parameters: - ret["event_loop"] = event_loop + if event_loop_fixture_id in sig.parameters: + ret[event_loop_fixture_id] = event_loop return ret @@ -281,17 +293,18 @@ def _perhaps_rebind_fixture_func( return func -def _wrap_asyncgen_fixture(fixturedef: FixtureDef) -> None: +def _wrap_asyncgen_fixture(fixturedef: FixtureDef, event_loop_fixture_id: str) -> None: fixture = fixturedef.func @functools.wraps(fixture) - def _asyncgen_fixture_wrapper( - event_loop: asyncio.AbstractEventLoop, request: SubRequest, **kwargs: Any - ): + def _asyncgen_fixture_wrapper(request: SubRequest, **kwargs: Any): func = _perhaps_rebind_fixture_func( fixture, request.instance, fixturedef.unittest ) - gen_obj = func(**_add_kwargs(func, kwargs, event_loop, request)) + event_loop = kwargs.pop(event_loop_fixture_id) + gen_obj = func( + **_add_kwargs(func, kwargs, event_loop_fixture_id, event_loop, request) + ) async def setup(): res = await gen_obj.__anext__() @@ -319,19 +332,20 @@ async def async_finalizer() -> None: fixturedef.func = _asyncgen_fixture_wrapper -def _wrap_async_fixture(fixturedef: FixtureDef) -> None: +def _wrap_async_fixture(fixturedef: FixtureDef, event_loop_fixture_id: str) -> None: fixture = fixturedef.func @functools.wraps(fixture) - def _async_fixture_wrapper( - event_loop: asyncio.AbstractEventLoop, request: SubRequest, **kwargs: Any - ): + def _async_fixture_wrapper(request: SubRequest, **kwargs: Any): func = _perhaps_rebind_fixture_func( fixture, request.instance, fixturedef.unittest ) + event_loop = kwargs.pop(event_loop_fixture_id) async def setup(): - res = await func(**_add_kwargs(func, kwargs, event_loop, request)) + res = await func( + **_add_kwargs(func, kwargs, event_loop_fixture_id, event_loop, request) + ) return res return event_loop.run_until_complete(setup()) @@ -351,7 +365,7 @@ def pytest_pycollect_makeitem( """A pytest hook to collect asyncio coroutines.""" if not collector.funcnamefilter(name): return None - _preprocess_async_fixtures(collector.config, _HOLDER) + _preprocess_async_fixtures(collector, _HOLDER) return None @@ -456,6 +470,12 @@ def pytest_generate_tests(metafunc: Metafunc) -> None: _event_loop_fixture_id, None ) if event_loop_fixture_id: + # This specific fixture name may already be in metafunc.argnames, if this + # test indirectly depends on the fixture. For example, this is the case + # when the test depends on an async fixture, both of which share the same + # asyncio_event_loop mark. + if event_loop_fixture_id in metafunc.fixturenames: + continue fixturemanager = metafunc.config.pluginmanager.get_plugin("funcmanage") if "event_loop" in metafunc.fixturenames: raise MultipleEventLoopsRequestedError( diff --git a/tests/markers/test_class_marker.py b/tests/markers/test_class_marker.py index f9a2f680..68425575 100644 --- a/tests/markers/test_class_marker.py +++ b/tests/markers/test_class_marker.py @@ -188,3 +188,32 @@ async def test_parametrized_loop(self): ) result = pytester.runpytest_subprocess("--asyncio-mode=strict") result.assert_outcomes(passed=2) + + +def test_asyncio_event_loop_mark_provides_class_scoped_loop_to_fixtures( + pytester: pytest.Pytester, +): + pytester.makepyfile( + dedent( + """\ + import asyncio + + import pytest + import pytest_asyncio + + @pytest.mark.asyncio_event_loop + class TestClassScopedLoop: + loop: asyncio.AbstractEventLoop + + @pytest_asyncio.fixture + async def my_fixture(self): + TestClassScopedLoop.loop = asyncio.get_running_loop() + + @pytest.mark.asyncio + async def test_runs_is_same_loop_as_fixture(self, my_fixture): + assert asyncio.get_running_loop() is TestClassScopedLoop.loop + """ + ) + ) + result = pytester.runpytest_subprocess("--asyncio-mode=strict") + result.assert_outcomes(passed=1) diff --git a/tests/markers/test_module_marker.py b/tests/markers/test_module_marker.py index b0926750..f6cd8762 100644 --- a/tests/markers/test_module_marker.py +++ b/tests/markers/test_module_marker.py @@ -212,3 +212,34 @@ async def test_parametrized_loop(): ) result = pytester.runpytest_subprocess("--asyncio-mode=strict") result.assert_outcomes(passed=2) + + +def test_asyncio_event_loop_mark_provides_module_scoped_loop_to_fixtures( + pytester: Pytester, +): + pytester.makepyfile( + dedent( + """\ + import asyncio + + import pytest + import pytest_asyncio + + pytestmark = pytest.mark.asyncio_event_loop + + loop: asyncio.AbstractEventLoop + + @pytest_asyncio.fixture + async def my_fixture(): + global loop + loop = asyncio.get_running_loop() + + @pytest.mark.asyncio + async def test_runs_is_same_loop_as_fixture(my_fixture): + global loop + assert asyncio.get_running_loop() is loop + """ + ) + ) + result = pytester.runpytest_subprocess("--asyncio-mode=strict") + result.assert_outcomes(passed=1) From 8ff46a61d68c926c1eb9a830c2908f96a437e47a Mon Sep 17 00:00:00 2001 From: Michael Seifert Date: Mon, 9 Oct 2023 16:21:57 +0200 Subject: [PATCH 047/151] [build] Update flake8 version in the pre-commit hooks to v6.1.0. Signed-off-by: Michael Seifert --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index fc81f2f5..4e5d2f8e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -42,7 +42,7 @@ repos: - id: mypy exclude: ^(docs|tests)/.* - repo: https://github.com/pycqa/flake8 - rev: 5.0.4 + rev: 6.1.0 hooks: - id: flake8 language_version: python3 From 617b9055aed2a6c863c53984b8149c9ba84aee5d Mon Sep 17 00:00:00 2001 From: Michael Seifert Date: Mon, 9 Oct 2023 16:46:58 +0200 Subject: [PATCH 048/151] [docs] Added changelog entry for the asyncio_event_loop mark. Signed-off-by: Michael Seifert --- docs/source/reference/changelog.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/source/reference/changelog.rst b/docs/source/reference/changelog.rst index 77204145..13c5080b 100644 --- a/docs/source/reference/changelog.rst +++ b/docs/source/reference/changelog.rst @@ -6,6 +6,8 @@ Changelog ================== - Remove support for Python 3.7 - Declare support for Python 3.12 +- Class-scoped and module-scoped event loops can be requested + via the _asyncio_event_loop_ mark. `#620 `_ 0.21.1 (2023-07-12) =================== From df5362a40cf44874bf2d357d5d7963825802ee30 Mon Sep 17 00:00:00 2001 From: Michael Seifert Date: Mon, 9 Oct 2023 17:11:18 +0200 Subject: [PATCH 049/151] [feat] Deprecate event loop fixture overrides. Signed-off-by: Michael Seifert --- docs/source/reference/changelog.rst | 7 +- docs/source/reference/fixtures.rst | 18 +---- pytest_asyncio/plugin.py | 29 +++++++ setup.cfg | 1 + tests/test_event_loop_fixture_finalizer.py | 4 +- ...event_loop_fixture_override_deprecation.py | 81 +++++++++++++++++++ tests/test_multiloop.py | 4 +- 7 files changed, 121 insertions(+), 23 deletions(-) create mode 100644 tests/test_event_loop_fixture_override_deprecation.py diff --git a/docs/source/reference/changelog.rst b/docs/source/reference/changelog.rst index 13c5080b..fb7c5d00 100644 --- a/docs/source/reference/changelog.rst +++ b/docs/source/reference/changelog.rst @@ -4,10 +4,13 @@ Changelog 1.0.0 (UNRELEASED) ================== -- Remove support for Python 3.7 -- Declare support for Python 3.12 - Class-scoped and module-scoped event loops can be requested via the _asyncio_event_loop_ mark. `#620 `_ +- Deprecate redefinition of the `event_loop` fixture. `#587 `_ + Users requiring a class-scoped or module-scoped asyncio event loop for their tests + should mark the corresponding class or module with `asyncio_event_loop`. +- Remove support for Python 3.7 +- Declare support for Python 3.12 0.21.1 (2023-07-12) =================== diff --git a/docs/source/reference/fixtures.rst b/docs/source/reference/fixtures.rst index adcc092d..d5032ba9 100644 --- a/docs/source/reference/fixtures.rst +++ b/docs/source/reference/fixtures.rst @@ -19,23 +19,7 @@ to ``function`` scope. Note that, when using the ``event_loop`` fixture, you need to interact with the event loop using methods like ``event_loop.run_until_complete``. If you want to *await* code inside your test function, you need to write a coroutine and use it as a test function. The `asyncio <#pytest-mark-asyncio>`__ marker is used to mark coroutines that should be treated as test functions. -The ``event_loop`` fixture can be overridden in any of the standard pytest locations, -e.g. directly in the test file, or in ``conftest.py``. This allows redefining the -fixture scope, for example: - -.. code-block:: python - - @pytest.fixture(scope="module") - def event_loop(): - policy = asyncio.get_event_loop_policy() - loop = policy.new_event_loop() - yield loop - loop.close() - -When defining multiple ``event_loop`` fixtures, you should ensure that their scopes don't overlap. -Each of the fixtures replace the running event loop, potentially without proper clean up. -This will emit a warning and likely lead to errors in your tests suite. -You can manually check for overlapping ``event_loop`` fixtures by running pytest with the ``--setup-show`` option. +If your tests require an asyncio event loop with class or module scope, apply the `asyncio_event_loop mark <./markers.html/#pytest-mark-asyncio-event-loop>`__ to the respective class or module. If you need to change the type of the event loop, prefer setting a custom event loop policy over redefining the ``event_loop`` fixture. diff --git a/pytest_asyncio/plugin.py b/pytest_asyncio/plugin.py index 7d41779f..3dba6c79 100644 --- a/pytest_asyncio/plugin.py +++ b/pytest_asyncio/plugin.py @@ -461,6 +461,18 @@ def _hypothesis_test_wraps_coroutine(function: Any) -> bool: return _is_coroutine(function.hypothesis.inner_test) +_REDEFINED_EVENT_LOOP_FIXTURE_WARNING = dedent( + """\ + The event_loop fixture provided by pytest-asyncio has been redefined in + %s:%d + Replacing the event_loop fixture with a custom implementation is deprecated + and will lead to errors in the future. + If you want to request an asyncio event loop with a class or module scope, + please attach the asyncio_event_loop mark to the respective class or module. + """ +) + + @pytest.hookimpl(tryfirst=True) def pytest_generate_tests(metafunc: Metafunc) -> None: for event_loop_provider_node, _ in metafunc.definition.iter_markers_with_node( @@ -497,6 +509,17 @@ def pytest_fixture_setup( ) -> Optional[object]: """Adjust the event loop policy when an event loop is produced.""" if fixturedef.argname == "event_loop": + # FixtureDef.baseid is an empty string when the Fixture was found in a plugin. + # This is also true, when the fixture was defined in a conftest.py + # at the rootdir. + fixture_filename = inspect.getsourcefile(fixturedef.func) + if not getattr(fixturedef.func, "__original_func", False): + _, fixture_line_number = inspect.getsourcelines(fixturedef.func) + warnings.warn( + _REDEFINED_EVENT_LOOP_FIXTURE_WARNING + % (fixture_filename, fixture_line_number), + DeprecationWarning, + ) # The use of a fixture finalizer is preferred over the # pytest_fixture_post_finalizer hook. The fixture finalizer is invoked once # for each fixture, whereas the hook may be invoked multiple times for @@ -691,6 +714,12 @@ def pytest_runtest_setup(item: pytest.Item) -> None: @pytest.fixture def event_loop(request: FixtureRequest) -> Iterator[asyncio.AbstractEventLoop]: """Create an instance of the default event loop for each test case.""" + # Add a magic value to the fixture function, so that we can check for overrides + # of this fixture in pytest_fixture_setup + # The magic value must be part of the function definition, because pytest may have + # multiple instances of the fixture function + event_loop.__original_func = True + loop = asyncio.get_event_loop_policy().new_event_loop() yield loop loop.close() diff --git a/setup.cfg b/setup.cfg index 5dd6a63e..7b7a1b6b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -71,6 +71,7 @@ asyncio_mode = auto junit_family=xunit2 filterwarnings = error + ignore:The event_loop fixture provided by pytest-asyncio has been redefined.*:DeprecationWarning [flake8] max-line-length = 88 diff --git a/tests/test_event_loop_fixture_finalizer.py b/tests/test_event_loop_fixture_finalizer.py index b676df2d..07b76501 100644 --- a/tests/test_event_loop_fixture_finalizer.py +++ b/tests/test_event_loop_fixture_finalizer.py @@ -111,7 +111,7 @@ async def test_ends_with_unclosed_loop(): ) ) result = pytester.runpytest("--asyncio-mode=strict", "-W", "default") - result.assert_outcomes(passed=1, warnings=1) + result.assert_outcomes(passed=1, warnings=2) result.stdout.fnmatch_lines("*unclosed event loop*") @@ -133,5 +133,5 @@ async def test_ends_with_unclosed_loop(): ) ) result = pytester.runpytest("--asyncio-mode=strict", "-W", "default") - result.assert_outcomes(passed=1, warnings=1) + result.assert_outcomes(passed=1, warnings=2) result.stdout.fnmatch_lines("*unclosed event loop*") diff --git a/tests/test_event_loop_fixture_override_deprecation.py b/tests/test_event_loop_fixture_override_deprecation.py new file mode 100644 index 00000000..b23ff642 --- /dev/null +++ b/tests/test_event_loop_fixture_override_deprecation.py @@ -0,0 +1,81 @@ +from textwrap import dedent + +from pytest import Pytester + + +def test_emit_warning_when_event_loop_fixture_is_redefined(pytester: Pytester): + pytester.makepyfile( + dedent( + """\ + import asyncio + import pytest + + @pytest.fixture + def event_loop(): + loop = asyncio.new_event_loop() + yield loop + loop.close() + + @pytest.mark.asyncio + async def test_emits_warning(): + pass + + @pytest.mark.asyncio + async def test_emits_warning_when_referenced_explicitly(event_loop): + pass + """ + ) + ) + result = pytester.runpytest("--asyncio-mode=strict") + result.assert_outcomes(passed=2, warnings=2) + + +def test_does_not_emit_warning_when_no_test_uses_the_event_loop_fixture( + pytester: Pytester, +): + pytester.makepyfile( + dedent( + """\ + import asyncio + import pytest + + @pytest.fixture + def event_loop(): + loop = asyncio.new_event_loop() + yield loop + loop.close() + + def test_emits_no_warning(): + pass + """ + ) + ) + result = pytester.runpytest("--asyncio-mode=strict") + result.assert_outcomes(passed=1, warnings=0) + + +def test_emit_warning_when_redefined_event_loop_is_used_by_fixture(pytester: Pytester): + pytester.makepyfile( + dedent( + """\ + import asyncio + import pytest + import pytest_asyncio + + @pytest.fixture + def event_loop(): + loop = asyncio.new_event_loop() + yield loop + loop.close() + + @pytest_asyncio.fixture + async def uses_event_loop(): + pass + + def test_emits_warning(uses_event_loop): + pass + """ + ) + ) + result = pytester.runpytest("--asyncio-mode=strict") + result.assert_outcomes(passed=1, warnings=1) diff --git a/tests/test_multiloop.py b/tests/test_multiloop.py index 6c47d68c..86a88eec 100644 --- a/tests/test_multiloop.py +++ b/tests/test_multiloop.py @@ -54,7 +54,7 @@ def event_loop(): @pytest.mark.asyncio - async def test_for_custom_loop(): + async def test_for_custom_loop(event_loop): """This test should be executed using the custom loop.""" await asyncio.sleep(0.01) assert type(asyncio.get_event_loop()).__name__ == "CustomSelectorLoop" @@ -67,4 +67,4 @@ async def test_dependent_fixture(dependent_fixture): ) ) result = pytester.runpytest_subprocess("--asyncio-mode=strict") - result.assert_outcomes(passed=2) + result.assert_outcomes(passed=2, warnings=2) From d85f012bac1ca8f4aafe6b652f6545e070772527 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 16 Oct 2023 18:29:38 +0000 Subject: [PATCH 050/151] Build(deps): Bump mypy from 1.5.1 to 1.6.0 in /dependencies/default Bumps [mypy](https://github.com/python/mypy) from 1.5.1 to 1.6.0. - [Commits](https://github.com/python/mypy/compare/v1.5.1...v1.6.0) --- updated-dependencies: - dependency-name: mypy dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- dependencies/default/constraints.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies/default/constraints.txt b/dependencies/default/constraints.txt index 1006461d..b093e320 100644 --- a/dependencies/default/constraints.txt +++ b/dependencies/default/constraints.txt @@ -7,7 +7,7 @@ hypothesis==6.87.3 idna==3.4 importlib-metadata==6.8.0 iniconfig==2.0.0 -mypy==1.5.1 +mypy==1.6.0 mypy-extensions==1.0.0 outcome==1.2.0 packaging==23.2 From 60127801d06a36cfa7d63c14248158026a9443dd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 16 Oct 2023 18:29:54 +0000 Subject: [PATCH 051/151] Build(deps): Bump hypothesis in /dependencies/default Bumps [hypothesis](https://github.com/HypothesisWorks/hypothesis) from 6.87.3 to 6.88.1. - [Release notes](https://github.com/HypothesisWorks/hypothesis/releases) - [Commits](https://github.com/HypothesisWorks/hypothesis/compare/hypothesis-python-6.87.3...hypothesis-python-6.88.1) --- updated-dependencies: - dependency-name: hypothesis dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- dependencies/default/constraints.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies/default/constraints.txt b/dependencies/default/constraints.txt index b093e320..4a909bbf 100644 --- a/dependencies/default/constraints.txt +++ b/dependencies/default/constraints.txt @@ -3,7 +3,7 @@ attrs==23.1.0 coverage==7.3.2 exceptiongroup==1.1.3 flaky==3.7.0 -hypothesis==6.87.3 +hypothesis==6.88.1 idna==3.4 importlib-metadata==6.8.0 iniconfig==2.0.0 From 4fba010e5b3d9d27685139452894d7b73c2798f2 Mon Sep 17 00:00:00 2001 From: Michael Seifert Date: Fri, 11 Aug 2023 14:19:12 +0200 Subject: [PATCH 052/151] [refactor] Introduced new item type "AsyncFunction" which represents pytest.Functions that are coroutines or async generators. Signed-off-by: Michael Seifert --- pytest_asyncio/plugin.py | 54 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 52 insertions(+), 2 deletions(-) diff --git a/pytest_asyncio/plugin.py b/pytest_asyncio/plugin.py index 3dba6c79..770dae6e 100644 --- a/pytest_asyncio/plugin.py +++ b/pytest_asyncio/plugin.py @@ -353,11 +353,17 @@ async def setup(): fixturedef.func = _async_fixture_wrapper +class AsyncFunction(pytest.Function): + """Pytest item that is a coroutine or an asynchronous generator""" + + _HOLDER: Set[FixtureDef] = set() -@pytest.hookimpl(tryfirst=True) -def pytest_pycollect_makeitem( +# The function name needs to start with "pytest_" +# see https://github.com/pytest-dev/pytest/issues/11307 +@pytest.hookimpl(specname="pytest_pycollect_makeitem", tryfirst=True) +def pytest_pycollect_makeitem_preprocess_async_fixtures( collector: Union[pytest.Module, pytest.Class], name: str, obj: object ) -> Union[ pytest.Item, pytest.Collector, List[Union[pytest.Item, pytest.Collector]], None @@ -369,6 +375,50 @@ def pytest_pycollect_makeitem( return None +# The function name needs to start with "pytest_" +# see https://github.com/pytest-dev/pytest/issues/11307 +@pytest.hookimpl(specname="pytest_pycollect_makeitem", hookwrapper=True) +def pytest_pycollect_makeitem_convert_async_functions_to_subclass( + collector: Union[pytest.Module, pytest.Class], name: str, obj: object +) -> Union[ + pytest.Item, pytest.Collector, List[Union[pytest.Item, pytest.Collector]], None +]: + """ + Converts coroutines and async generators collected as pytest.Functions + to AsyncFunction items. + """ + hook_result = yield + node_or_list_of_nodes = hook_result.get_result() + if not node_or_list_of_nodes: + return + try: + node_iterator = iter(node_or_list_of_nodes) + except TypeError: + # Treat single node as a single-element iterable + node_iterator = iter((node_or_list_of_nodes,)) + async_functions = [] + for collector_or_item in node_iterator: + if not ( + isinstance(collector_or_item, pytest.Function) + and _is_coroutine_or_asyncgen(obj) + ): + collector = collector_or_item + async_functions.append(collector) + continue + item = collector_or_item + async_function = AsyncFunction.from_parent( + item.parent, + name=item.name, + callspec=getattr(item, "callspec", None), + callobj=item.obj, + fixtureinfo=item._fixtureinfo, + keywords=item.keywords, + originalname=item.originalname, + ) + async_functions.append(async_function) + hook_result.force_result(async_functions) + + _event_loop_fixture_id = StashKey[str] From 6d0622614027c3327546fc1c276a493ae04a1728 Mon Sep 17 00:00:00 2001 From: Michael Seifert Date: Fri, 11 Aug 2023 14:45:13 +0200 Subject: [PATCH 053/151] [build] Added dependency on typing-extensions >= 4.0 for Python versions older than 3.11. Signed-off-by: Michael Seifert --- dependencies/default/constraints.txt | 1 + dependencies/default/requirements.txt | 1 + dependencies/pytest-min/constraints.txt | 1 + dependencies/pytest-min/requirements.txt | 1 + setup.cfg | 1 + 5 files changed, 5 insertions(+) diff --git a/dependencies/default/constraints.txt b/dependencies/default/constraints.txt index 4a909bbf..264d1472 100644 --- a/dependencies/default/constraints.txt +++ b/dependencies/default/constraints.txt @@ -20,4 +20,5 @@ sortedcontainers==2.4.0 tomli==2.0.1 trio==0.22.2 typed-ast==1.5.5 +typing-extensions==4.7.1 zipp==3.17.0 diff --git a/dependencies/default/requirements.txt b/dependencies/default/requirements.txt index 0828607f..e324ddfd 100644 --- a/dependencies/default/requirements.txt +++ b/dependencies/default/requirements.txt @@ -1,3 +1,4 @@ # Always adjust install_requires in setup.cfg and pytest-min-requirements.txt # when changing runtime dependencies pytest >= 7.0.0 +typing-extensions >= 4.0; python_version < "3.11" diff --git a/dependencies/pytest-min/constraints.txt b/dependencies/pytest-min/constraints.txt index 1f82dbaf..bd1ebe2b 100644 --- a/dependencies/pytest-min/constraints.txt +++ b/dependencies/pytest-min/constraints.txt @@ -19,4 +19,5 @@ requests==2.28.1 sortedcontainers==2.4.0 tomli==2.0.1 urllib3==1.26.12 +typing-extensions==4.7.1 xmlschema==2.1.1 diff --git a/dependencies/pytest-min/requirements.txt b/dependencies/pytest-min/requirements.txt index 9fb33e96..d7d1926d 100644 --- a/dependencies/pytest-min/requirements.txt +++ b/dependencies/pytest-min/requirements.txt @@ -1,3 +1,4 @@ # Always adjust install_requires in setup.cfg and requirements.txt # when changing minimum version dependencies pytest[testing] == 7.0.0 +typing-extensions >= 4.0; python_version < "3.11" diff --git a/setup.cfg b/setup.cfg index 7b7a1b6b..9af279cf 100644 --- a/setup.cfg +++ b/setup.cfg @@ -41,6 +41,7 @@ include_package_data = True # Always adjust requirements.txt and pytest-min-requirements.txt when changing runtime dependencies install_requires = pytest >= 7.0.0 + typing-extensions >= 4.0;python_version < "3.11" [options.extras_require] testing = From 9643e2f7b02862a34e0d472c2da3d9bab8374a84 Mon Sep 17 00:00:00 2001 From: Michael Seifert Date: Fri, 11 Aug 2023 14:59:00 +0200 Subject: [PATCH 054/151] [refactor] Added static factory method to AsyncFunction which instantiates an AsyncFunction object from a pytest.Function. Signed-off-by: Michael Seifert --- pytest_asyncio/plugin.py | 45 +++++++++++++++++++++------------------- 1 file changed, 24 insertions(+), 21 deletions(-) diff --git a/pytest_asyncio/plugin.py b/pytest_asyncio/plugin.py index 770dae6e..f5f24045 100644 --- a/pytest_asyncio/plugin.py +++ b/pytest_asyncio/plugin.py @@ -38,6 +38,7 @@ Session, StashKey, ) +from typing_extensions import Self _R = TypeVar("_R") @@ -356,6 +357,21 @@ async def setup(): class AsyncFunction(pytest.Function): """Pytest item that is a coroutine or an asynchronous generator""" + @classmethod + def from_function(cls, function: pytest.Function, /) -> Self: + """ + Instantiates an AsyncFunction from the specified pytest.Function item. + """ + return cls.from_parent( + function.parent, + name=function.name, + callspec=getattr(function, "callspec", None), + callobj=function.obj, + fixtureinfo=function._fixtureinfo, + keywords=function.keywords, + originalname=function.originalname, + ) + _HOLDER: Set[FixtureDef] = set() @@ -396,27 +412,14 @@ def pytest_pycollect_makeitem_convert_async_functions_to_subclass( except TypeError: # Treat single node as a single-element iterable node_iterator = iter((node_or_list_of_nodes,)) - async_functions = [] - for collector_or_item in node_iterator: - if not ( - isinstance(collector_or_item, pytest.Function) - and _is_coroutine_or_asyncgen(obj) - ): - collector = collector_or_item - async_functions.append(collector) - continue - item = collector_or_item - async_function = AsyncFunction.from_parent( - item.parent, - name=item.name, - callspec=getattr(item, "callspec", None), - callobj=item.obj, - fixtureinfo=item._fixtureinfo, - keywords=item.keywords, - originalname=item.originalname, - ) - async_functions.append(async_function) - hook_result.force_result(async_functions) + updated_node_collection = [] + for node in node_iterator: + if isinstance(node, pytest.Function) and _is_coroutine_or_asyncgen(obj): + async_function = AsyncFunction.from_function(node) + updated_node_collection.append(async_function) + else: + updated_node_collection.append(node) + hook_result.force_result(updated_node_collection) _event_loop_fixture_id = StashKey[str] From b1d0ce79dda37519831672c95ab77939aa948e2b Mon Sep 17 00:00:00 2001 From: Michael Seifert Date: Mon, 14 Aug 2023 12:59:43 +0200 Subject: [PATCH 055/151] [refactor] Moved code that generates a warning when marking a synchronous function with "asyncio" from the synchronization wrapper to the pytest_pyfunc_call hook. Whether to raise a warning due to unnecessary marker usage should be of no concern to the synchronization wrapper. The change implies that pytest-asyncio now checks that the test function is a coroutine or async generator, instead of checking that the return value of the test function is awaitable. Signed-off-by: Michael Seifert --- pytest_asyncio/plugin.py | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/pytest_asyncio/plugin.py b/pytest_asyncio/plugin.py index f5f24045..014a9510 100644 --- a/pytest_asyncio/plugin.py +++ b/pytest_asyncio/plugin.py @@ -672,11 +672,21 @@ def pytest_pyfunc_call(pyfuncitem: pytest.Function) -> Optional[object]: pyfuncitem, pyfuncitem.obj.hypothesis.inner_test, ) - else: + elif _is_coroutine_or_asyncgen(pyfuncitem.obj): pyfuncitem.obj = wrap_in_sync( pyfuncitem, pyfuncitem.obj, ) + else: + pyfuncitem.warn( + pytest.PytestWarning( + f"The test {pyfuncitem} is marked with '@pytest.mark.asyncio' " + "but it is not an async function. " + "Please remove asyncio marker. " + "If the test is not marked explicitly, " + "check for global markers applied via 'pytestmark'." + ) + ) yield @@ -701,17 +711,6 @@ def wrap_in_sync( @functools.wraps(func) def inner(*args, **kwargs): coro = func(*args, **kwargs) - if not inspect.isawaitable(coro): - pyfuncitem.warn( - pytest.PytestWarning( - f"The test {pyfuncitem} is marked with '@pytest.mark.asyncio' " - "but it is not an async function. " - "Please remove asyncio marker. " - "If the test is not marked explicitly, " - "check for global markers applied via 'pytestmark'." - ) - ) - return _loop = asyncio.get_event_loop() task = asyncio.ensure_future(coro, loop=_loop) try: From d83755533db0f4424a1e31ba12162fa80cc6f0b8 Mon Sep 17 00:00:00 2001 From: Michael Seifert Date: Tue, 15 Aug 2023 18:55:06 +0200 Subject: [PATCH 056/151] [refactor] Introduced new item type "AsyncHypothesisTest" which represents a coroutine or async generator to which a @hypothesis.given decorator was applied. The decorator returns a synchronous function and therefore needs special treatment by pytest-asyncio. Signed-off-by: Michael Seifert --- pytest_asyncio/plugin.py | 35 ++++++++++++++++++++++++++++++----- 1 file changed, 30 insertions(+), 5 deletions(-) diff --git a/pytest_asyncio/plugin.py b/pytest_asyncio/plugin.py index 014a9510..ed7a4bd2 100644 --- a/pytest_asyncio/plugin.py +++ b/pytest_asyncio/plugin.py @@ -373,6 +373,28 @@ def from_function(cls, function: pytest.Function, /) -> Self: ) +class AsyncHypothesisTest(pytest.Function): + """ + Pytest item that is coroutine or an asynchronous generator decorated by + @hypothesis.given. + """ + + @classmethod + def from_function(cls, function: pytest.Function, /) -> Self: + """ + Instantiates an AsyncFunction from the specified pytest.Function item. + """ + return cls.from_parent( + function.parent, + name=function.name, + callspec=getattr(function, "callspec", None), + callobj=function.obj, + fixtureinfo=function._fixtureinfo, + keywords=function.keywords, + originalname=function.originalname, + ) + + _HOLDER: Set[FixtureDef] = set() @@ -414,11 +436,14 @@ def pytest_pycollect_makeitem_convert_async_functions_to_subclass( node_iterator = iter((node_or_list_of_nodes,)) updated_node_collection = [] for node in node_iterator: - if isinstance(node, pytest.Function) and _is_coroutine_or_asyncgen(obj): - async_function = AsyncFunction.from_function(node) - updated_node_collection.append(async_function) - else: - updated_node_collection.append(node) + updated_item = node + if isinstance(node, pytest.Function): + if _is_coroutine_or_asyncgen(obj): + updated_item = AsyncFunction.from_function(node) + if _is_hypothesis_test(obj) and _hypothesis_test_wraps_coroutine(obj): + updated_item = AsyncHypothesisTest.from_function(node) + updated_node_collection.append(updated_item) + hook_result.force_result(updated_node_collection) From 1a249246277718c7d32adc3f15a3b5463ed641fa Mon Sep 17 00:00:00 2001 From: Michael Seifert Date: Sat, 26 Aug 2023 21:16:32 +0200 Subject: [PATCH 057/151] [refactor] Introduced new item type "AsyncStaticMethod" which represents a coroutine or async generator to which a @staticmethod decorator was applied. Pytest unbinds staticmethods, so unbound async staticmethods need special treatment by pytest-asyncio. Signed-off-by: Michael Seifert --- pytest_asyncio/plugin.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/pytest_asyncio/plugin.py b/pytest_asyncio/plugin.py index ed7a4bd2..bf67b6ec 100644 --- a/pytest_asyncio/plugin.py +++ b/pytest_asyncio/plugin.py @@ -373,6 +373,28 @@ def from_function(cls, function: pytest.Function, /) -> Self: ) +class AsyncStaticMethod(pytest.Function): + """ + Pytest item that is a coroutine or an asynchronous generator + decorated with staticmethod + """ + + @classmethod + def from_function(cls, function: pytest.Function, /) -> Self: + """ + Instantiates an AsyncStaticMethod from the specified pytest.Function item. + """ + return cls.from_parent( + function.parent, + name=function.name, + callspec=getattr(function, "callspec", None), + callobj=function.obj, + fixtureinfo=function._fixtureinfo, + keywords=function.keywords, + originalname=function.originalname, + ) + + class AsyncHypothesisTest(pytest.Function): """ Pytest item that is coroutine or an asynchronous generator decorated by @@ -438,6 +460,10 @@ def pytest_pycollect_makeitem_convert_async_functions_to_subclass( for node in node_iterator: updated_item = node if isinstance(node, pytest.Function): + if isinstance(obj, staticmethod) and _is_coroutine_or_asyncgen( + obj.__func__ + ): + updated_item = AsyncStaticMethod.from_function(node) if _is_coroutine_or_asyncgen(obj): updated_item = AsyncFunction.from_function(node) if _is_hypothesis_test(obj) and _hypothesis_test_wraps_coroutine(obj): From 6863959ee669fb306508f0e99b0d694ba3bb29b2 Mon Sep 17 00:00:00 2001 From: Michael Seifert Date: Fri, 8 Sep 2023 11:31:43 +0200 Subject: [PATCH 058/151] [refactor] The test execution of different pytest.Items are handled in their respective runtest methods, rather than in if clauses inside the pytest_pyfunc_call hook. Having the item-specific test run code grouped with the item leads to increased code locality, which, in turn makes the code easier to understand. Signed-off-by: Michael Seifert --- pytest_asyncio/plugin.py | 38 ++++++++++++++++++++++++++++---------- 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/pytest_asyncio/plugin.py b/pytest_asyncio/plugin.py index bf67b6ec..91e1f9fb 100644 --- a/pytest_asyncio/plugin.py +++ b/pytest_asyncio/plugin.py @@ -372,6 +372,14 @@ def from_function(cls, function: pytest.Function, /) -> Self: originalname=function.originalname, ) + def runtest(self) -> None: + if self.get_closest_marker("asyncio"): + self.obj = wrap_in_sync( + self, + self.obj, + ) + super().runtest() + class AsyncStaticMethod(pytest.Function): """ @@ -394,6 +402,14 @@ def from_function(cls, function: pytest.Function, /) -> Self: originalname=function.originalname, ) + def runtest(self) -> None: + if self.get_closest_marker("asyncio"): + self.obj = wrap_in_sync( + self, + self.obj, + ) + super().runtest() + class AsyncHypothesisTest(pytest.Function): """ @@ -416,6 +432,14 @@ def from_function(cls, function: pytest.Function, /) -> Self: originalname=function.originalname, ) + def runtest(self) -> None: + if self.get_closest_marker("asyncio"): + self.obj.hypothesis.inner_test = wrap_in_sync( + self, + self.obj.hypothesis.inner_test, + ) + super().runtest() + _HOLDER: Set[FixtureDef] = set() @@ -718,16 +742,10 @@ def pytest_pyfunc_call(pyfuncitem: pytest.Function) -> Optional[object]: """ marker = pyfuncitem.get_closest_marker("asyncio") if marker is not None: - if _is_hypothesis_test(pyfuncitem.obj): - pyfuncitem.obj.hypothesis.inner_test = wrap_in_sync( - pyfuncitem, - pyfuncitem.obj.hypothesis.inner_test, - ) - elif _is_coroutine_or_asyncgen(pyfuncitem.obj): - pyfuncitem.obj = wrap_in_sync( - pyfuncitem, - pyfuncitem.obj, - ) + if isinstance( + pyfuncitem, (AsyncFunction, AsyncHypothesisTest, AsyncStaticMethod) + ): + pass else: pyfuncitem.warn( pytest.PytestWarning( From d5d49607db2a2e234693f97a487093af76184ee0 Mon Sep 17 00:00:00 2001 From: Michael Seifert Date: Fri, 8 Sep 2023 11:36:24 +0200 Subject: [PATCH 059/151] [refactor] Simplified code for marking pytest.Items with "asyncio" in auto mode. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The implementation tests against the item type rather than testing against the test function itself. This prevents duplicate logic in identifying whether a test function is a staticmethod, a Hypothesis test, … Signed-off-by: Michael Seifert --- pytest_asyncio/plugin.py | 20 +++++--------------- 1 file changed, 5 insertions(+), 15 deletions(-) diff --git a/pytest_asyncio/plugin.py b/pytest_asyncio/plugin.py index 91e1f9fb..10231b3e 100644 --- a/pytest_asyncio/plugin.py +++ b/pytest_asyncio/plugin.py @@ -30,7 +30,6 @@ Collector, Config, FixtureRequest, - Function, Item, Metafunc, Parser, @@ -564,25 +563,16 @@ def pytest_collection_modifyitems( The mark is only applied in `AUTO` mode. It is applied to: - - coroutines - - staticmethods wrapping coroutines + - coroutines and async generators - Hypothesis tests wrapping coroutines + - staticmethods wrapping coroutines """ if _get_asyncio_mode(config) != Mode.AUTO: return - function_items = (item for item in items if isinstance(item, Function)) - for function_item in function_items: - function = function_item.obj - if isinstance(function, staticmethod): - # staticmethods need to be unwrapped. - function = function.__func__ - if ( - _is_coroutine(function) - or _is_hypothesis_test(function) - and _hypothesis_test_wraps_coroutine(function) - ): - function_item.add_marker("asyncio") + for item in items: + if isinstance(item, (AsyncFunction, AsyncHypothesisTest, AsyncStaticMethod)): + item.add_marker("asyncio") def _hypothesis_test_wraps_coroutine(function: Any) -> bool: From 2b541f581a6b37e81c5113069cc84fbffcd1390f Mon Sep 17 00:00:00 2001 From: Michael Seifert Date: Sat, 21 Oct 2023 11:12:29 +0200 Subject: [PATCH 060/151] [refactor] Moved logic to check whether a subclass of pytest.Function can substitute the Function item into the respective subclasses. This change allows all subclasses of function items to be treated the same when modifying pytest items. Signed-off-by: Michael Seifert --- pytest_asyncio/plugin.py | 28 +++++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/pytest_asyncio/plugin.py b/pytest_asyncio/plugin.py index 10231b3e..a9ba42a9 100644 --- a/pytest_asyncio/plugin.py +++ b/pytest_asyncio/plugin.py @@ -356,6 +356,12 @@ async def setup(): class AsyncFunction(pytest.Function): """Pytest item that is a coroutine or an asynchronous generator""" + @staticmethod + def can_substitute(item: pytest.Function) -> bool: + """Returns whether the specified function can be replaced by this class""" + func = item.obj + return _is_coroutine_or_asyncgen(func) + @classmethod def from_function(cls, function: pytest.Function, /) -> Self: """ @@ -386,6 +392,14 @@ class AsyncStaticMethod(pytest.Function): decorated with staticmethod """ + @staticmethod + def can_substitute(item: pytest.Function) -> bool: + """Returns whether the specified function can be replaced by this class""" + func = item.obj + return isinstance(func, staticmethod) and _is_coroutine_or_asyncgen( + func.__func__ + ) + @classmethod def from_function(cls, function: pytest.Function, /) -> Self: """ @@ -416,6 +430,12 @@ class AsyncHypothesisTest(pytest.Function): @hypothesis.given. """ + @staticmethod + def can_substitute(item: pytest.Function) -> bool: + """Returns whether the specified function can be replaced by this class""" + func = item.obj + return _is_hypothesis_test(func) and _hypothesis_test_wraps_coroutine(func) + @classmethod def from_function(cls, function: pytest.Function, /) -> Self: """ @@ -483,13 +503,11 @@ def pytest_pycollect_makeitem_convert_async_functions_to_subclass( for node in node_iterator: updated_item = node if isinstance(node, pytest.Function): - if isinstance(obj, staticmethod) and _is_coroutine_or_asyncgen( - obj.__func__ - ): + if AsyncStaticMethod.can_substitute(node): updated_item = AsyncStaticMethod.from_function(node) - if _is_coroutine_or_asyncgen(obj): + if AsyncFunction.can_substitute(node): updated_item = AsyncFunction.from_function(node) - if _is_hypothesis_test(obj) and _hypothesis_test_wraps_coroutine(obj): + if AsyncHypothesisTest.can_substitute(node): updated_item = AsyncHypothesisTest.from_function(node) updated_node_collection.append(updated_item) From b8fe7abf7c60d3d8a7aa6515fdf46314a9e3df39 Mon Sep 17 00:00:00 2001 From: Michael Seifert Date: Sat, 21 Oct 2023 11:22:13 +0200 Subject: [PATCH 061/151] [refactor] Extracted a base class for all types of pytest-asyncio function items. Signed-off-by: Michael Seifert --- pytest_asyncio/plugin.py | 69 +++++++++++++--------------------------- 1 file changed, 22 insertions(+), 47 deletions(-) diff --git a/pytest_asyncio/plugin.py b/pytest_asyncio/plugin.py index a9ba42a9..96fa407c 100644 --- a/pytest_asyncio/plugin.py +++ b/pytest_asyncio/plugin.py @@ -353,19 +353,14 @@ async def setup(): fixturedef.func = _async_fixture_wrapper -class AsyncFunction(pytest.Function): - """Pytest item that is a coroutine or an asynchronous generator""" - - @staticmethod - def can_substitute(item: pytest.Function) -> bool: - """Returns whether the specified function can be replaced by this class""" - func = item.obj - return _is_coroutine_or_asyncgen(func) +class PytestAsyncioFunction(pytest.Function): + """Base class for all test functions managed by pytest-asyncio.""" @classmethod def from_function(cls, function: pytest.Function, /) -> Self: """ - Instantiates an AsyncFunction from the specified pytest.Function item. + Instantiates this specific PytestAsyncioFunction type from the specified + pytest.Function item. """ return cls.from_parent( function.parent, @@ -377,6 +372,20 @@ def from_function(cls, function: pytest.Function, /) -> Self: originalname=function.originalname, ) + @staticmethod + def can_substitute(item: pytest.Function) -> bool: + """Returns whether the specified function can be replaced by this class""" + raise NotImplementedError() + + +class AsyncFunction(PytestAsyncioFunction): + """Pytest item that is a coroutine or an asynchronous generator""" + + @staticmethod + def can_substitute(item: pytest.Function) -> bool: + func = item.obj + return _is_coroutine_or_asyncgen(func) + def runtest(self) -> None: if self.get_closest_marker("asyncio"): self.obj = wrap_in_sync( @@ -386,7 +395,7 @@ def runtest(self) -> None: super().runtest() -class AsyncStaticMethod(pytest.Function): +class AsyncStaticMethod(PytestAsyncioFunction): """ Pytest item that is a coroutine or an asynchronous generator decorated with staticmethod @@ -394,27 +403,11 @@ class AsyncStaticMethod(pytest.Function): @staticmethod def can_substitute(item: pytest.Function) -> bool: - """Returns whether the specified function can be replaced by this class""" func = item.obj return isinstance(func, staticmethod) and _is_coroutine_or_asyncgen( func.__func__ ) - @classmethod - def from_function(cls, function: pytest.Function, /) -> Self: - """ - Instantiates an AsyncStaticMethod from the specified pytest.Function item. - """ - return cls.from_parent( - function.parent, - name=function.name, - callspec=getattr(function, "callspec", None), - callobj=function.obj, - fixtureinfo=function._fixtureinfo, - keywords=function.keywords, - originalname=function.originalname, - ) - def runtest(self) -> None: if self.get_closest_marker("asyncio"): self.obj = wrap_in_sync( @@ -424,7 +417,7 @@ def runtest(self) -> None: super().runtest() -class AsyncHypothesisTest(pytest.Function): +class AsyncHypothesisTest(PytestAsyncioFunction): """ Pytest item that is coroutine or an asynchronous generator decorated by @hypothesis.given. @@ -432,25 +425,9 @@ class AsyncHypothesisTest(pytest.Function): @staticmethod def can_substitute(item: pytest.Function) -> bool: - """Returns whether the specified function can be replaced by this class""" func = item.obj return _is_hypothesis_test(func) and _hypothesis_test_wraps_coroutine(func) - @classmethod - def from_function(cls, function: pytest.Function, /) -> Self: - """ - Instantiates an AsyncFunction from the specified pytest.Function item. - """ - return cls.from_parent( - function.parent, - name=function.name, - callspec=getattr(function, "callspec", None), - callobj=function.obj, - fixtureinfo=function._fixtureinfo, - keywords=function.keywords, - originalname=function.originalname, - ) - def runtest(self) -> None: if self.get_closest_marker("asyncio"): self.obj.hypothesis.inner_test = wrap_in_sync( @@ -589,7 +566,7 @@ def pytest_collection_modifyitems( if _get_asyncio_mode(config) != Mode.AUTO: return for item in items: - if isinstance(item, (AsyncFunction, AsyncHypothesisTest, AsyncStaticMethod)): + if isinstance(item, PytestAsyncioFunction): item.add_marker("asyncio") @@ -750,9 +727,7 @@ def pytest_pyfunc_call(pyfuncitem: pytest.Function) -> Optional[object]: """ marker = pyfuncitem.get_closest_marker("asyncio") if marker is not None: - if isinstance( - pyfuncitem, (AsyncFunction, AsyncHypothesisTest, AsyncStaticMethod) - ): + if isinstance(pyfuncitem, PytestAsyncioFunction): pass else: pyfuncitem.warn( From ee10388e36ee7b4b3fa467e3b103e8e616ff3e7e Mon Sep 17 00:00:00 2001 From: Michael Seifert Date: Sat, 21 Oct 2023 12:00:55 +0200 Subject: [PATCH 062/151] [refactor] Added a factory method to PytestAsyncioFunction. This avoids direct references to the subclasses of PytestAsyncioFunction, thus making the code more easily extendable with additional subclasses. Signed-off-by: Michael Seifert --- pytest_asyncio/plugin.py | 31 ++++++++++++++++++++----------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/pytest_asyncio/plugin.py b/pytest_asyncio/plugin.py index 96fa407c..45c22a6b 100644 --- a/pytest_asyncio/plugin.py +++ b/pytest_asyncio/plugin.py @@ -357,7 +357,21 @@ class PytestAsyncioFunction(pytest.Function): """Base class for all test functions managed by pytest-asyncio.""" @classmethod - def from_function(cls, function: pytest.Function, /) -> Self: + def substitute(cls, item: pytest.Function, /) -> pytest.Function: + """ + Returns a PytestAsyncioFunction if there is an implementation that can handle + the specified function item. + + If no implementation of PytestAsyncioFunction can handle the specified item, + the item is returned unchanged. + """ + for subclass in cls.__subclasses__(): + if subclass._can_substitute(item): + return subclass._from_function(item) + return item + + @classmethod + def _from_function(cls, function: pytest.Function, /) -> Self: """ Instantiates this specific PytestAsyncioFunction type from the specified pytest.Function item. @@ -373,7 +387,7 @@ def from_function(cls, function: pytest.Function, /) -> Self: ) @staticmethod - def can_substitute(item: pytest.Function) -> bool: + def _can_substitute(item: pytest.Function) -> bool: """Returns whether the specified function can be replaced by this class""" raise NotImplementedError() @@ -382,7 +396,7 @@ class AsyncFunction(PytestAsyncioFunction): """Pytest item that is a coroutine or an asynchronous generator""" @staticmethod - def can_substitute(item: pytest.Function) -> bool: + def _can_substitute(item: pytest.Function) -> bool: func = item.obj return _is_coroutine_or_asyncgen(func) @@ -402,7 +416,7 @@ class AsyncStaticMethod(PytestAsyncioFunction): """ @staticmethod - def can_substitute(item: pytest.Function) -> bool: + def _can_substitute(item: pytest.Function) -> bool: func = item.obj return isinstance(func, staticmethod) and _is_coroutine_or_asyncgen( func.__func__ @@ -424,7 +438,7 @@ class AsyncHypothesisTest(PytestAsyncioFunction): """ @staticmethod - def can_substitute(item: pytest.Function) -> bool: + def _can_substitute(item: pytest.Function) -> bool: func = item.obj return _is_hypothesis_test(func) and _hypothesis_test_wraps_coroutine(func) @@ -480,12 +494,7 @@ def pytest_pycollect_makeitem_convert_async_functions_to_subclass( for node in node_iterator: updated_item = node if isinstance(node, pytest.Function): - if AsyncStaticMethod.can_substitute(node): - updated_item = AsyncStaticMethod.from_function(node) - if AsyncFunction.can_substitute(node): - updated_item = AsyncFunction.from_function(node) - if AsyncHypothesisTest.can_substitute(node): - updated_item = AsyncHypothesisTest.from_function(node) + updated_item = PytestAsyncioFunction.substitute(node) updated_node_collection.append(updated_item) hook_result.force_result(updated_node_collection) From aa1b0bc2636be5a88650d0fb9b5d93374cf780d9 Mon Sep 17 00:00:00 2001 From: Michael Seifert Date: Sat, 21 Oct 2023 12:03:16 +0200 Subject: [PATCH 063/151] [refactor] Inlined function "_is_coroutine". There's not much benefit of this function, given that asyncio.iscoroutinefunction has an equally expressive name and isn't much longer to type out. Signed-off-by: Michael Seifert --- pytest_asyncio/plugin.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/pytest_asyncio/plugin.py b/pytest_asyncio/plugin.py index 45c22a6b..f1fa717f 100644 --- a/pytest_asyncio/plugin.py +++ b/pytest_asyncio/plugin.py @@ -157,13 +157,8 @@ def _make_asyncio_fixture_function(obj: Any) -> None: obj._force_asyncio_fixture = True -def _is_coroutine(obj: Any) -> bool: - """Check to see if an object is really an asyncio coroutine.""" - return asyncio.iscoroutinefunction(obj) - - def _is_coroutine_or_asyncgen(obj: Any) -> bool: - return _is_coroutine(obj) or inspect.isasyncgenfunction(obj) + return asyncio.iscoroutinefunction(obj) or inspect.isasyncgenfunction(obj) def _get_asyncio_mode(config: Config) -> Mode: @@ -580,7 +575,7 @@ def pytest_collection_modifyitems( def _hypothesis_test_wraps_coroutine(function: Any) -> bool: - return _is_coroutine(function.hypothesis.inner_test) + return asyncio.iscoroutinefunction(function.hypothesis.inner_test) _REDEFINED_EVENT_LOOP_FIXTURE_WARNING = dedent( From 1e4abfc99bae11941b4e96c6c02e13f83e53229b Mon Sep 17 00:00:00 2001 From: Michael Seifert Date: Sat, 21 Oct 2023 12:04:59 +0200 Subject: [PATCH 064/151] [refactor] Inlined function "_hypothesis_test_wraps_coroutine". This causes Hypothesis-specific code to be located in roughly the same place, increasing code locality. Signed-off-by: Michael Seifert --- pytest_asyncio/plugin.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/pytest_asyncio/plugin.py b/pytest_asyncio/plugin.py index f1fa717f..2137ee91 100644 --- a/pytest_asyncio/plugin.py +++ b/pytest_asyncio/plugin.py @@ -435,7 +435,9 @@ class AsyncHypothesisTest(PytestAsyncioFunction): @staticmethod def _can_substitute(item: pytest.Function) -> bool: func = item.obj - return _is_hypothesis_test(func) and _hypothesis_test_wraps_coroutine(func) + return _is_hypothesis_test(func) and asyncio.iscoroutinefunction( + func.hypothesis.inner_test + ) def runtest(self) -> None: if self.get_closest_marker("asyncio"): @@ -574,10 +576,6 @@ def pytest_collection_modifyitems( item.add_marker("asyncio") -def _hypothesis_test_wraps_coroutine(function: Any) -> bool: - return asyncio.iscoroutinefunction(function.hypothesis.inner_test) - - _REDEFINED_EVENT_LOOP_FIXTURE_WARNING = dedent( """\ The event_loop fixture provided by pytest-asyncio has been redefined in From b7e055537fe0026cf081997fa41f35a7b15819fe Mon Sep 17 00:00:00 2001 From: Michael Seifert Date: Sat, 21 Oct 2023 12:06:18 +0200 Subject: [PATCH 065/151] [refactor] Inlined function "_is_hypothesis_test". This causes Hypothesis-specific code to be located in roughly the same place, increasing code locality. Signed-off-by: Michael Seifert --- pytest_asyncio/plugin.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/pytest_asyncio/plugin.py b/pytest_asyncio/plugin.py index 2137ee91..dad0e856 100644 --- a/pytest_asyncio/plugin.py +++ b/pytest_asyncio/plugin.py @@ -435,9 +435,9 @@ class AsyncHypothesisTest(PytestAsyncioFunction): @staticmethod def _can_substitute(item: pytest.Function) -> bool: func = item.obj - return _is_hypothesis_test(func) and asyncio.iscoroutinefunction( - func.hypothesis.inner_test - ) + return getattr( + func, "is_hypothesis_test", False + ) and asyncio.iscoroutinefunction(func.hypothesis.inner_test) def runtest(self) -> None: if self.get_closest_marker("asyncio"): @@ -744,10 +744,6 @@ def pytest_pyfunc_call(pyfuncitem: pytest.Function) -> Optional[object]: yield -def _is_hypothesis_test(function: Any) -> bool: - return getattr(function, "is_hypothesis_test", False) - - def wrap_in_sync( pyfuncitem: pytest.Function, func: Callable[..., Awaitable[Any]], From 782c378c05dd1e9227db331a0a13285e9f3ba36a Mon Sep 17 00:00:00 2001 From: Michael Seifert Date: Sat, 21 Oct 2023 12:10:07 +0200 Subject: [PATCH 066/151] [refactor] Adjusted warning text towards consistent wording ("marker" -> "mark"). Signed-off-by: Michael Seifert --- pytest_asyncio/plugin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pytest_asyncio/plugin.py b/pytest_asyncio/plugin.py index dad0e856..e42b0c56 100644 --- a/pytest_asyncio/plugin.py +++ b/pytest_asyncio/plugin.py @@ -736,9 +736,9 @@ def pytest_pyfunc_call(pyfuncitem: pytest.Function) -> Optional[object]: pytest.PytestWarning( f"The test {pyfuncitem} is marked with '@pytest.mark.asyncio' " "but it is not an async function. " - "Please remove asyncio marker. " + "Please remove the asyncio mark. " "If the test is not marked explicitly, " - "check for global markers applied via 'pytestmark'." + "check for global marks applied via 'pytestmark'." ) ) yield From 3eb67c55a1d5cc332770f278a1eaa228111fca04 Mon Sep 17 00:00:00 2001 From: Michael Seifert Date: Sat, 21 Oct 2023 12:11:36 +0200 Subject: [PATCH 067/151] [refactor] Simplified implementation of pytest_pyfunc_call. Signed-off-by: Michael Seifert --- pytest_asyncio/plugin.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pytest_asyncio/plugin.py b/pytest_asyncio/plugin.py index e42b0c56..7db7b707 100644 --- a/pytest_asyncio/plugin.py +++ b/pytest_asyncio/plugin.py @@ -727,8 +727,7 @@ def pytest_pyfunc_call(pyfuncitem: pytest.Function) -> Optional[object]: Wraps marked tests in a synchronous function where the wrapped test coroutine is executed in an event loop. """ - marker = pyfuncitem.get_closest_marker("asyncio") - if marker is not None: + if pyfuncitem.get_closest_marker("asyncio") is not None: if isinstance(pyfuncitem, PytestAsyncioFunction): pass else: From 2acb8df5eb4f47ceca0af75b0f512fa8d78f8187 Mon Sep 17 00:00:00 2001 From: Michael Seifert Date: Sat, 21 Oct 2023 12:24:13 +0200 Subject: [PATCH 068/151] [refactor] Removed unused pytest item argument from _wrap_in_sync. Signed-off-by: Michael Seifert --- pytest_asyncio/plugin.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/pytest_asyncio/plugin.py b/pytest_asyncio/plugin.py index 7db7b707..a0b78179 100644 --- a/pytest_asyncio/plugin.py +++ b/pytest_asyncio/plugin.py @@ -398,7 +398,6 @@ def _can_substitute(item: pytest.Function) -> bool: def runtest(self) -> None: if self.get_closest_marker("asyncio"): self.obj = wrap_in_sync( - self, self.obj, ) super().runtest() @@ -420,7 +419,6 @@ def _can_substitute(item: pytest.Function) -> bool: def runtest(self) -> None: if self.get_closest_marker("asyncio"): self.obj = wrap_in_sync( - self, self.obj, ) super().runtest() @@ -442,7 +440,6 @@ def _can_substitute(item: pytest.Function) -> bool: def runtest(self) -> None: if self.get_closest_marker("asyncio"): self.obj.hypothesis.inner_test = wrap_in_sync( - self, self.obj.hypothesis.inner_test, ) super().runtest() @@ -744,7 +741,6 @@ def pytest_pyfunc_call(pyfuncitem: pytest.Function) -> Optional[object]: def wrap_in_sync( - pyfuncitem: pytest.Function, func: Callable[..., Awaitable[Any]], ): """Return a sync wrapper around an async function executing it in the From b9cdf0e616b1dbf73a1b738c9826e2f577424388 Mon Sep 17 00:00:00 2001 From: Michael Seifert Date: Sat, 21 Oct 2023 15:10:14 +0200 Subject: [PATCH 069/151] [refactor] Remove dependency on typing_extensions. Signed-off-by: Michael Seifert --- dependencies/default/constraints.txt | 1 - dependencies/default/requirements.txt | 1 - dependencies/pytest-min/constraints.txt | 1 - dependencies/pytest-min/requirements.txt | 1 - pytest_asyncio/plugin.py | 3 +-- setup.cfg | 1 - 6 files changed, 1 insertion(+), 7 deletions(-) diff --git a/dependencies/default/constraints.txt b/dependencies/default/constraints.txt index 264d1472..4a909bbf 100644 --- a/dependencies/default/constraints.txt +++ b/dependencies/default/constraints.txt @@ -20,5 +20,4 @@ sortedcontainers==2.4.0 tomli==2.0.1 trio==0.22.2 typed-ast==1.5.5 -typing-extensions==4.7.1 zipp==3.17.0 diff --git a/dependencies/default/requirements.txt b/dependencies/default/requirements.txt index e324ddfd..0828607f 100644 --- a/dependencies/default/requirements.txt +++ b/dependencies/default/requirements.txt @@ -1,4 +1,3 @@ # Always adjust install_requires in setup.cfg and pytest-min-requirements.txt # when changing runtime dependencies pytest >= 7.0.0 -typing-extensions >= 4.0; python_version < "3.11" diff --git a/dependencies/pytest-min/constraints.txt b/dependencies/pytest-min/constraints.txt index bd1ebe2b..1f82dbaf 100644 --- a/dependencies/pytest-min/constraints.txt +++ b/dependencies/pytest-min/constraints.txt @@ -19,5 +19,4 @@ requests==2.28.1 sortedcontainers==2.4.0 tomli==2.0.1 urllib3==1.26.12 -typing-extensions==4.7.1 xmlschema==2.1.1 diff --git a/dependencies/pytest-min/requirements.txt b/dependencies/pytest-min/requirements.txt index d7d1926d..9fb33e96 100644 --- a/dependencies/pytest-min/requirements.txt +++ b/dependencies/pytest-min/requirements.txt @@ -1,4 +1,3 @@ # Always adjust install_requires in setup.cfg and requirements.txt # when changing minimum version dependencies pytest[testing] == 7.0.0 -typing-extensions >= 4.0; python_version < "3.11" diff --git a/pytest_asyncio/plugin.py b/pytest_asyncio/plugin.py index a0b78179..340f3e76 100644 --- a/pytest_asyncio/plugin.py +++ b/pytest_asyncio/plugin.py @@ -37,7 +37,6 @@ Session, StashKey, ) -from typing_extensions import Self _R = TypeVar("_R") @@ -366,7 +365,7 @@ def substitute(cls, item: pytest.Function, /) -> pytest.Function: return item @classmethod - def _from_function(cls, function: pytest.Function, /) -> Self: + def _from_function(cls, function: pytest.Function, /) -> pytest.Function: """ Instantiates this specific PytestAsyncioFunction type from the specified pytest.Function item. diff --git a/setup.cfg b/setup.cfg index 9af279cf..7b7a1b6b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -41,7 +41,6 @@ include_package_data = True # Always adjust requirements.txt and pytest-min-requirements.txt when changing runtime dependencies install_requires = pytest >= 7.0.0 - typing-extensions >= 4.0;python_version < "3.11" [options.extras_require] testing = From 1da33391ab6257d4ac7d4fd3aff303c51cd657cc Mon Sep 17 00:00:00 2001 From: Michael Seifert Date: Sat, 21 Oct 2023 15:12:53 +0200 Subject: [PATCH 070/151] [refactor] Use "Function" type, instead of "pytest.Function". Signed-off-by: Michael Seifert --- pytest_asyncio/plugin.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/pytest_asyncio/plugin.py b/pytest_asyncio/plugin.py index 340f3e76..07b7d3b8 100644 --- a/pytest_asyncio/plugin.py +++ b/pytest_asyncio/plugin.py @@ -30,6 +30,7 @@ Collector, Config, FixtureRequest, + Function, Item, Metafunc, Parser, @@ -347,11 +348,11 @@ async def setup(): fixturedef.func = _async_fixture_wrapper -class PytestAsyncioFunction(pytest.Function): +class PytestAsyncioFunction(Function): """Base class for all test functions managed by pytest-asyncio.""" @classmethod - def substitute(cls, item: pytest.Function, /) -> pytest.Function: + def substitute(cls, item: Function, /) -> Function: """ Returns a PytestAsyncioFunction if there is an implementation that can handle the specified function item. @@ -365,10 +366,10 @@ def substitute(cls, item: pytest.Function, /) -> pytest.Function: return item @classmethod - def _from_function(cls, function: pytest.Function, /) -> pytest.Function: + def _from_function(cls, function: Function, /) -> Function: """ Instantiates this specific PytestAsyncioFunction type from the specified - pytest.Function item. + Function item. """ return cls.from_parent( function.parent, @@ -381,7 +382,7 @@ def _from_function(cls, function: pytest.Function, /) -> pytest.Function: ) @staticmethod - def _can_substitute(item: pytest.Function) -> bool: + def _can_substitute(item: Function) -> bool: """Returns whether the specified function can be replaced by this class""" raise NotImplementedError() @@ -390,7 +391,7 @@ class AsyncFunction(PytestAsyncioFunction): """Pytest item that is a coroutine or an asynchronous generator""" @staticmethod - def _can_substitute(item: pytest.Function) -> bool: + def _can_substitute(item: Function) -> bool: func = item.obj return _is_coroutine_or_asyncgen(func) @@ -409,7 +410,7 @@ class AsyncStaticMethod(PytestAsyncioFunction): """ @staticmethod - def _can_substitute(item: pytest.Function) -> bool: + def _can_substitute(item: Function) -> bool: func = item.obj return isinstance(func, staticmethod) and _is_coroutine_or_asyncgen( func.__func__ @@ -430,7 +431,7 @@ class AsyncHypothesisTest(PytestAsyncioFunction): """ @staticmethod - def _can_substitute(item: pytest.Function) -> bool: + def _can_substitute(item: Function) -> bool: func = item.obj return getattr( func, "is_hypothesis_test", False @@ -486,7 +487,7 @@ def pytest_pycollect_makeitem_convert_async_functions_to_subclass( updated_node_collection = [] for node in node_iterator: updated_item = node - if isinstance(node, pytest.Function): + if isinstance(node, Function): updated_item = PytestAsyncioFunction.substitute(node) updated_node_collection.append(updated_item) @@ -716,7 +717,7 @@ def _provide_clean_event_loop() -> None: @pytest.hookimpl(tryfirst=True, hookwrapper=True) -def pytest_pyfunc_call(pyfuncitem: pytest.Function) -> Optional[object]: +def pytest_pyfunc_call(pyfuncitem: Function) -> Optional[object]: """ Pytest hook called before a test case is run. From 106fa545a659a7e6a936b0f53d9d184287be8a13 Mon Sep 17 00:00:00 2001 From: Michael Seifert Date: Sat, 21 Oct 2023 15:14:29 +0200 Subject: [PATCH 071/151] [refactor] Address mypy complaint about unknown type. Signed-off-by: Michael Seifert --- pytest_asyncio/plugin.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pytest_asyncio/plugin.py b/pytest_asyncio/plugin.py index 07b7d3b8..f403ecb6 100644 --- a/pytest_asyncio/plugin.py +++ b/pytest_asyncio/plugin.py @@ -398,7 +398,8 @@ def _can_substitute(item: Function) -> bool: def runtest(self) -> None: if self.get_closest_marker("asyncio"): self.obj = wrap_in_sync( - self.obj, + # https://github.com/pytest-dev/pytest-asyncio/issues/596 + self.obj, # type: ignore[has-type] ) super().runtest() @@ -419,7 +420,8 @@ def _can_substitute(item: Function) -> bool: def runtest(self) -> None: if self.get_closest_marker("asyncio"): self.obj = wrap_in_sync( - self.obj, + # https://github.com/pytest-dev/pytest-asyncio/issues/596 + self.obj, # type: ignore[has-type] ) super().runtest() From b1d48eb6105cc636347b8a16567a537e46fa02aa Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Oct 2023 18:06:07 +0000 Subject: [PATCH 072/151] Build(deps): Bump mypy from 1.6.0 to 1.6.1 in /dependencies/default Bumps [mypy](https://github.com/python/mypy) from 1.6.0 to 1.6.1. - [Changelog](https://github.com/python/mypy/blob/master/CHANGELOG.md) - [Commits](https://github.com/python/mypy/compare/v1.6.0...v1.6.1) --- updated-dependencies: - dependency-name: mypy dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- dependencies/default/constraints.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies/default/constraints.txt b/dependencies/default/constraints.txt index 4a909bbf..212234d2 100644 --- a/dependencies/default/constraints.txt +++ b/dependencies/default/constraints.txt @@ -7,7 +7,7 @@ hypothesis==6.88.1 idna==3.4 importlib-metadata==6.8.0 iniconfig==2.0.0 -mypy==1.6.0 +mypy==1.6.1 mypy-extensions==1.0.0 outcome==1.2.0 packaging==23.2 From 660893d5b6fe33dd8caa2e0fb5c87ea9ee195fa1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Oct 2023 18:06:12 +0000 Subject: [PATCH 073/151] Build(deps): Bump outcome from 1.2.0 to 1.3.0 in /dependencies/default Bumps [outcome](https://github.com/python-trio/outcome) from 1.2.0 to 1.3.0. - [Commits](https://github.com/python-trio/outcome/compare/v1.2.0...v1.3.0) --- updated-dependencies: - dependency-name: outcome dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- dependencies/default/constraints.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies/default/constraints.txt b/dependencies/default/constraints.txt index 212234d2..d50a128b 100644 --- a/dependencies/default/constraints.txt +++ b/dependencies/default/constraints.txt @@ -9,7 +9,7 @@ importlib-metadata==6.8.0 iniconfig==2.0.0 mypy==1.6.1 mypy-extensions==1.0.0 -outcome==1.2.0 +outcome==1.3.0 packaging==23.2 pluggy==1.3.0 pyparsing==3.1.1 From 5a474bdc9e8596d1db159c7a0f81d64683f11477 Mon Sep 17 00:00:00 2001 From: Michael Seifert Date: Sat, 21 Oct 2023 10:31:09 +0200 Subject: [PATCH 074/151] [refactor] Moved test for warning generated when sync functions are marked with "asyncio" into a separate test module. The "test_simple" module is named too general. Thus, it serves as a magnet for all kinds of tests that aren't connected to each other. This is one step to break up the "test_simple" module into more coherent test modules. Signed-off-by: Michael Seifert --- tests/test_asyncio_mark_on_sync_function.py | 33 +++++++++++++++++++++ tests/test_simple.py | 32 -------------------- 2 files changed, 33 insertions(+), 32 deletions(-) create mode 100644 tests/test_asyncio_mark_on_sync_function.py diff --git a/tests/test_asyncio_mark_on_sync_function.py b/tests/test_asyncio_mark_on_sync_function.py new file mode 100644 index 00000000..7de7ec2f --- /dev/null +++ b/tests/test_asyncio_mark_on_sync_function.py @@ -0,0 +1,33 @@ +from textwrap import dedent + + +def test_warn_asyncio_marker_for_regular_func(testdir): + testdir.makepyfile( + dedent( + """\ + import pytest + + pytest_plugins = 'pytest_asyncio' + + @pytest.mark.asyncio + def test_a(): + pass + """ + ) + ) + testdir.makefile( + ".ini", + pytest=dedent( + """\ + [pytest] + asyncio_mode = strict + filterwarnings = + default + """ + ), + ) + result = testdir.runpytest() + result.assert_outcomes(passed=1) + result.stdout.fnmatch_lines( + ["*is marked with '@pytest.mark.asyncio' but it is not an async function.*"] + ) diff --git a/tests/test_simple.py b/tests/test_simple.py index 5e6a0d20..81fcd14b 100644 --- a/tests/test_simple.py +++ b/tests/test_simple.py @@ -300,38 +300,6 @@ async def test_no_warning_on_skip(): result.assert_outcomes(skipped=1) -def test_warn_asyncio_marker_for_regular_func(testdir): - testdir.makepyfile( - dedent( - """\ - import pytest - - pytest_plugins = 'pytest_asyncio' - - @pytest.mark.asyncio - def test_a(): - pass - """ - ) - ) - testdir.makefile( - ".ini", - pytest=dedent( - """\ - [pytest] - asyncio_mode = strict - filterwarnings = - default - """ - ), - ) - result = testdir.runpytest() - result.assert_outcomes(passed=1) - result.stdout.fnmatch_lines( - ["*is marked with '@pytest.mark.asyncio' but it is not an async function.*"] - ) - - def test_invalid_asyncio_mode(testdir): result = testdir.runpytest("-o", "asyncio_mode=True") result.stderr.no_fnmatch_line("INTERNALERROR> *") From d697a120ecd1d6eeb7a5a850eb5cce153bf69017 Mon Sep 17 00:00:00 2001 From: Michael Seifert Date: Sat, 21 Oct 2023 10:32:32 +0200 Subject: [PATCH 075/151] [refactor] test_warn_asyncio_marker_for_regular_func uses pytester, instead of the older "testdir" fixture. Signed-off-by: Michael Seifert --- tests/test_asyncio_mark_on_sync_function.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/tests/test_asyncio_mark_on_sync_function.py b/tests/test_asyncio_mark_on_sync_function.py index 7de7ec2f..70152b47 100644 --- a/tests/test_asyncio_mark_on_sync_function.py +++ b/tests/test_asyncio_mark_on_sync_function.py @@ -1,8 +1,10 @@ from textwrap import dedent +from pytest import Pytester -def test_warn_asyncio_marker_for_regular_func(testdir): - testdir.makepyfile( + +def test_warn_asyncio_marker_for_regular_func(pytester: Pytester): + pytester.makepyfile( dedent( """\ import pytest @@ -15,7 +17,7 @@ def test_a(): """ ) ) - testdir.makefile( + pytester.makefile( ".ini", pytest=dedent( """\ @@ -26,7 +28,7 @@ def test_a(): """ ), ) - result = testdir.runpytest() + result = pytester.runpytest() result.assert_outcomes(passed=1) result.stdout.fnmatch_lines( ["*is marked with '@pytest.mark.asyncio' but it is not an async function.*"] From 4bca1d707edeca7d2ffb15f067ba42297bf5299e Mon Sep 17 00:00:00 2001 From: Michael Seifert Date: Mon, 23 Oct 2023 12:12:18 +0200 Subject: [PATCH 076/151] [feat] Test items based on asynchronous generators always exit with *xfail* status and emit a warning during the collection phase. This behavior is consistent with synchronous yield tests. Signed-off-by: Michael Seifert --- docs/source/reference/changelog.rst | 1 + pytest_asyncio/plugin.py | 29 ++- tests/test_asyncio_mark.py | 223 ++++++++++++++++++++ tests/test_asyncio_mark_on_sync_function.py | 35 --- 4 files changed, 250 insertions(+), 38 deletions(-) create mode 100644 tests/test_asyncio_mark.py delete mode 100644 tests/test_asyncio_mark_on_sync_function.py diff --git a/docs/source/reference/changelog.rst b/docs/source/reference/changelog.rst index fb7c5d00..7da71868 100644 --- a/docs/source/reference/changelog.rst +++ b/docs/source/reference/changelog.rst @@ -9,6 +9,7 @@ Changelog - Deprecate redefinition of the `event_loop` fixture. `#587 `_ Users requiring a class-scoped or module-scoped asyncio event loop for their tests should mark the corresponding class or module with `asyncio_event_loop`. +- Test items based on asynchronous generators always exit with *xfail* status and emit a warning during the collection phase. This behavior is consistent with synchronous yield tests. `#642 `__ - Remove support for Python 3.7 - Declare support for Python 3.12 diff --git a/pytest_asyncio/plugin.py b/pytest_asyncio/plugin.py index f403ecb6..2babd96a 100644 --- a/pytest_asyncio/plugin.py +++ b/pytest_asyncio/plugin.py @@ -34,6 +34,7 @@ Item, Metafunc, Parser, + PytestCollectionWarning, PytestPluginManager, Session, StashKey, @@ -387,13 +388,13 @@ def _can_substitute(item: Function) -> bool: raise NotImplementedError() -class AsyncFunction(PytestAsyncioFunction): - """Pytest item that is a coroutine or an asynchronous generator""" +class Coroutine(PytestAsyncioFunction): + """Pytest item created by a coroutine""" @staticmethod def _can_substitute(item: Function) -> bool: func = item.obj - return _is_coroutine_or_asyncgen(func) + return asyncio.iscoroutinefunction(func) def runtest(self) -> None: if self.get_closest_marker("asyncio"): @@ -404,6 +405,28 @@ def runtest(self) -> None: super().runtest() +class AsyncGenerator(PytestAsyncioFunction): + """Pytest item created by an asynchronous generator""" + + @staticmethod + def _can_substitute(item: Function) -> bool: + func = item.obj + return inspect.isasyncgenfunction(func) + + @classmethod + def _from_function(cls, function: Function, /) -> Function: + async_gen_item = super()._from_function(function) + unsupported_item_type_message = ( + f"Tests based on asynchronous generators are not supported. " + f"{function.name} will be ignored." + ) + async_gen_item.warn(PytestCollectionWarning(unsupported_item_type_message)) + async_gen_item.add_marker( + pytest.mark.xfail(run=False, reason=unsupported_item_type_message) + ) + return async_gen_item + + class AsyncStaticMethod(PytestAsyncioFunction): """ Pytest item that is a coroutine or an asynchronous generator diff --git a/tests/test_asyncio_mark.py b/tests/test_asyncio_mark.py new file mode 100644 index 00000000..65e54861 --- /dev/null +++ b/tests/test_asyncio_mark.py @@ -0,0 +1,223 @@ +from textwrap import dedent + +from pytest import Pytester + + +def test_asyncio_mark_on_sync_function_emits_warning(pytester: Pytester): + pytester.makepyfile( + dedent( + """\ + import pytest + + @pytest.mark.asyncio + def test_a(): + pass + """ + ) + ) + pytester.makefile( + ".ini", + pytest=dedent( + """\ + [pytest] + asyncio_mode = strict + filterwarnings = + default + """ + ), + ) + result = pytester.runpytest() + result.assert_outcomes(passed=1) + result.stdout.fnmatch_lines( + ["*is marked with '@pytest.mark.asyncio' but it is not an async function.*"] + ) + + +def test_asyncio_mark_on_async_generator_function_emits_warning_in_strict_mode( + pytester: Pytester, +): + pytester.makepyfile( + dedent( + """\ + import pytest + + @pytest.mark.asyncio + async def test_a(): + yield + """ + ) + ) + pytester.makefile( + ".ini", + pytest=dedent( + """\ + [pytest] + asyncio_mode = strict + filterwarnings = + default + """ + ), + ) + result = pytester.runpytest() + result.assert_outcomes(xfailed=1, warnings=1) + result.stdout.fnmatch_lines( + ["*Tests based on asynchronous generators are not supported*"] + ) + + +def test_asyncio_mark_on_async_generator_function_emits_warning_in_auto_mode( + pytester: Pytester, +): + pytester.makepyfile( + dedent( + """\ + async def test_a(): + yield + """ + ) + ) + pytester.makefile( + ".ini", + pytest=dedent( + """\ + [pytest] + asyncio_mode = auto + filterwarnings = + default + """ + ), + ) + result = pytester.runpytest() + result.assert_outcomes(xfailed=1, warnings=1) + result.stdout.fnmatch_lines( + ["*Tests based on asynchronous generators are not supported*"] + ) + + +def test_asyncio_mark_on_async_generator_method_emits_warning_in_strict_mode( + pytester: Pytester, +): + pytester.makepyfile( + dedent( + """\ + import pytest + + class TestAsyncGenerator: + @pytest.mark.asyncio + async def test_a(self): + yield + """ + ) + ) + pytester.makefile( + ".ini", + pytest=dedent( + """\ + [pytest] + asyncio_mode = strict + filterwarnings = + default + """ + ), + ) + result = pytester.runpytest() + result.assert_outcomes(xfailed=1, warnings=1) + result.stdout.fnmatch_lines( + ["*Tests based on asynchronous generators are not supported*"] + ) + + +def test_asyncio_mark_on_async_generator_method_emits_warning_in_auto_mode( + pytester: Pytester, +): + pytester.makepyfile( + dedent( + """\ + class TestAsyncGenerator: + @staticmethod + async def test_a(): + yield + """ + ) + ) + pytester.makefile( + ".ini", + pytest=dedent( + """\ + [pytest] + asyncio_mode = auto + filterwarnings = + default + """ + ), + ) + result = pytester.runpytest() + result.assert_outcomes(xfailed=1, warnings=1) + result.stdout.fnmatch_lines( + ["*Tests based on asynchronous generators are not supported*"] + ) + + +def test_asyncio_mark_on_async_generator_staticmethod_emits_warning_in_strict_mode( + pytester: Pytester, +): + pytester.makepyfile( + dedent( + """\ + import pytest + + class TestAsyncGenerator: + @staticmethod + @pytest.mark.asyncio + async def test_a(): + yield + """ + ) + ) + pytester.makefile( + ".ini", + pytest=dedent( + """\ + [pytest] + asyncio_mode = strict + filterwarnings = + default + """ + ), + ) + result = pytester.runpytest() + result.assert_outcomes(xfailed=1, warnings=1) + result.stdout.fnmatch_lines( + ["*Tests based on asynchronous generators are not supported*"] + ) + + +def test_asyncio_mark_on_async_generator_staticmethod_emits_warning_in_auto_mode( + pytester: Pytester, +): + pytester.makepyfile( + dedent( + """\ + class TestAsyncGenerator: + @staticmethod + async def test_a(): + yield + """ + ) + ) + pytester.makefile( + ".ini", + pytest=dedent( + """\ + [pytest] + asyncio_mode = auto + filterwarnings = + default + """ + ), + ) + result = pytester.runpytest() + result.assert_outcomes(xfailed=1, warnings=1) + result.stdout.fnmatch_lines( + ["*Tests based on asynchronous generators are not supported*"] + ) diff --git a/tests/test_asyncio_mark_on_sync_function.py b/tests/test_asyncio_mark_on_sync_function.py deleted file mode 100644 index 70152b47..00000000 --- a/tests/test_asyncio_mark_on_sync_function.py +++ /dev/null @@ -1,35 +0,0 @@ -from textwrap import dedent - -from pytest import Pytester - - -def test_warn_asyncio_marker_for_regular_func(pytester: Pytester): - pytester.makepyfile( - dedent( - """\ - import pytest - - pytest_plugins = 'pytest_asyncio' - - @pytest.mark.asyncio - def test_a(): - pass - """ - ) - ) - pytester.makefile( - ".ini", - pytest=dedent( - """\ - [pytest] - asyncio_mode = strict - filterwarnings = - default - """ - ), - ) - result = pytester.runpytest() - result.assert_outcomes(passed=1) - result.stdout.fnmatch_lines( - ["*is marked with '@pytest.mark.asyncio' but it is not an async function.*"] - ) From b6b2c7f0b666af84aa604df52764c46f763d71ed Mon Sep 17 00:00:00 2001 From: Michael Seifert Date: Mon, 23 Oct 2023 12:32:04 +0200 Subject: [PATCH 077/151] [refactor] Simplified code in test_asyncio_mark. Tests use command-line arguments to set the asyncio mode and warnings filter, instead of a .ini file. This reduces the number of lines in the test module significantly. Signed-off-by: Michael Seifert --- tests/test_asyncio_mark.py | 91 +++----------------------------------- 1 file changed, 7 insertions(+), 84 deletions(-) diff --git a/tests/test_asyncio_mark.py b/tests/test_asyncio_mark.py index 65e54861..b514cbcd 100644 --- a/tests/test_asyncio_mark.py +++ b/tests/test_asyncio_mark.py @@ -15,18 +15,7 @@ def test_a(): """ ) ) - pytester.makefile( - ".ini", - pytest=dedent( - """\ - [pytest] - asyncio_mode = strict - filterwarnings = - default - """ - ), - ) - result = pytester.runpytest() + result = pytester.runpytest("--asyncio-mode=strict", "-W default") result.assert_outcomes(passed=1) result.stdout.fnmatch_lines( ["*is marked with '@pytest.mark.asyncio' but it is not an async function.*"] @@ -47,18 +36,7 @@ async def test_a(): """ ) ) - pytester.makefile( - ".ini", - pytest=dedent( - """\ - [pytest] - asyncio_mode = strict - filterwarnings = - default - """ - ), - ) - result = pytester.runpytest() + result = pytester.runpytest("--asyncio-mode=strict", "-W default") result.assert_outcomes(xfailed=1, warnings=1) result.stdout.fnmatch_lines( ["*Tests based on asynchronous generators are not supported*"] @@ -76,18 +54,7 @@ async def test_a(): """ ) ) - pytester.makefile( - ".ini", - pytest=dedent( - """\ - [pytest] - asyncio_mode = auto - filterwarnings = - default - """ - ), - ) - result = pytester.runpytest() + result = pytester.runpytest("--asyncio-mode=auto", "-W default") result.assert_outcomes(xfailed=1, warnings=1) result.stdout.fnmatch_lines( ["*Tests based on asynchronous generators are not supported*"] @@ -109,18 +76,7 @@ async def test_a(self): """ ) ) - pytester.makefile( - ".ini", - pytest=dedent( - """\ - [pytest] - asyncio_mode = strict - filterwarnings = - default - """ - ), - ) - result = pytester.runpytest() + result = pytester.runpytest("--asyncio-mode=strict", "-W default") result.assert_outcomes(xfailed=1, warnings=1) result.stdout.fnmatch_lines( ["*Tests based on asynchronous generators are not supported*"] @@ -140,18 +96,7 @@ async def test_a(): """ ) ) - pytester.makefile( - ".ini", - pytest=dedent( - """\ - [pytest] - asyncio_mode = auto - filterwarnings = - default - """ - ), - ) - result = pytester.runpytest() + result = pytester.runpytest("--asyncio-mode=auto", "-W default") result.assert_outcomes(xfailed=1, warnings=1) result.stdout.fnmatch_lines( ["*Tests based on asynchronous generators are not supported*"] @@ -174,18 +119,7 @@ async def test_a(): """ ) ) - pytester.makefile( - ".ini", - pytest=dedent( - """\ - [pytest] - asyncio_mode = strict - filterwarnings = - default - """ - ), - ) - result = pytester.runpytest() + result = pytester.runpytest("--asyncio-mode=strict", "-W default") result.assert_outcomes(xfailed=1, warnings=1) result.stdout.fnmatch_lines( ["*Tests based on asynchronous generators are not supported*"] @@ -205,18 +139,7 @@ async def test_a(): """ ) ) - pytester.makefile( - ".ini", - pytest=dedent( - """\ - [pytest] - asyncio_mode = auto - filterwarnings = - default - """ - ), - ) - result = pytester.runpytest() + result = pytester.runpytest("--asyncio-mode=auto", "-W default") result.assert_outcomes(xfailed=1, warnings=1) result.stdout.fnmatch_lines( ["*Tests based on asynchronous generators are not supported*"] From 62bf444c60ee9b0f54b46c0b5843afd605724f3f Mon Sep 17 00:00:00 2001 From: Michael Seifert Date: Mon, 23 Oct 2023 10:12:16 +0200 Subject: [PATCH 078/151] [docs] Extracted most Python code examples into separate files. This allows for auto-formatting using the usual tools and enables running the code examples as tests to ensure they work. Tests that perform network operations or require additional test dependencies have been excluded. Signed-off-by: Michael Seifert --- .../decorators/fixture_strict_mode_example.py | 14 ++ .../{decorators.rst => decorators/index.rst} | 16 +- .../reference/fixtures/event_loop_example.py | 5 + .../{fixtures.rst => fixtures/index.rst} | 8 +- docs/source/reference/index.rst | 6 +- docs/source/reference/markers.rst | 180 ------------------ .../class_scoped_loop_auto_mode_example.py | 14 ++ ...oop_custom_policies_strict_mode_example.py | 15 ++ ..._loop_custom_policy_strict_mode_example.py | 14 ++ .../class_scoped_loop_strict_mode_example.py | 16 ++ ...d_loop_with_fixture_strict_mode_example.py | 18 ++ docs/source/reference/markers/index.rst | 65 +++++++ .../module_scoped_loop_auto_mode_example.py | 23 +++ .../pytestmark_asyncio_strict_mode_example.py | 11 ++ 14 files changed, 202 insertions(+), 203 deletions(-) create mode 100644 docs/source/reference/decorators/fixture_strict_mode_example.py rename docs/source/reference/{decorators.rst => decorators/index.rst} (66%) create mode 100644 docs/source/reference/fixtures/event_loop_example.py rename docs/source/reference/{fixtures.rst => fixtures/index.rst} (90%) delete mode 100644 docs/source/reference/markers.rst create mode 100644 docs/source/reference/markers/class_scoped_loop_auto_mode_example.py create mode 100644 docs/source/reference/markers/class_scoped_loop_custom_policies_strict_mode_example.py create mode 100644 docs/source/reference/markers/class_scoped_loop_custom_policy_strict_mode_example.py create mode 100644 docs/source/reference/markers/class_scoped_loop_strict_mode_example.py create mode 100644 docs/source/reference/markers/class_scoped_loop_with_fixture_strict_mode_example.py create mode 100644 docs/source/reference/markers/index.rst create mode 100644 docs/source/reference/markers/module_scoped_loop_auto_mode_example.py create mode 100644 docs/source/reference/markers/pytestmark_asyncio_strict_mode_example.py diff --git a/docs/source/reference/decorators/fixture_strict_mode_example.py b/docs/source/reference/decorators/fixture_strict_mode_example.py new file mode 100644 index 00000000..6442c103 --- /dev/null +++ b/docs/source/reference/decorators/fixture_strict_mode_example.py @@ -0,0 +1,14 @@ +import asyncio + +import pytest_asyncio + + +@pytest_asyncio.fixture +async def async_gen_fixture(): + await asyncio.sleep(0.1) + yield "a value" + + +@pytest_asyncio.fixture(scope="module") +async def async_fixture(): + return await asyncio.sleep(0.1) diff --git a/docs/source/reference/decorators.rst b/docs/source/reference/decorators/index.rst similarity index 66% rename from docs/source/reference/decorators.rst rename to docs/source/reference/decorators/index.rst index 977ed6b8..5c96cf4b 100644 --- a/docs/source/reference/decorators.rst +++ b/docs/source/reference/decorators/index.rst @@ -3,20 +3,8 @@ Decorators ========== Asynchronous fixtures are defined just like ordinary pytest fixtures, except they should be decorated with ``@pytest_asyncio.fixture``. -.. code-block:: python3 - - import pytest_asyncio - - - @pytest_asyncio.fixture - async def async_gen_fixture(): - await asyncio.sleep(0.1) - yield "a value" - - - @pytest_asyncio.fixture(scope="module") - async def async_fixture(): - return await asyncio.sleep(0.1) +.. include:: fixture_strict_mode_example.py + :code: python All scopes are supported, but if you use a non-function scope you will need to redefine the ``event_loop`` fixture to have the same or broader scope. diff --git a/docs/source/reference/fixtures/event_loop_example.py b/docs/source/reference/fixtures/event_loop_example.py new file mode 100644 index 00000000..b5a82b62 --- /dev/null +++ b/docs/source/reference/fixtures/event_loop_example.py @@ -0,0 +1,5 @@ +import asyncio + + +def test_event_loop_fixture(event_loop): + event_loop.run_until_complete(asyncio.sleep(0)) diff --git a/docs/source/reference/fixtures.rst b/docs/source/reference/fixtures/index.rst similarity index 90% rename from docs/source/reference/fixtures.rst rename to docs/source/reference/fixtures/index.rst index d5032ba9..98fe5382 100644 --- a/docs/source/reference/fixtures.rst +++ b/docs/source/reference/fixtures/index.rst @@ -9,12 +9,8 @@ is available as the return value of this fixture or via `asyncio.get_running_loo The event loop is closed when the fixture scope ends. The fixture scope defaults to ``function`` scope. -.. code-block:: python - - def test_http_client(event_loop): - url = "http://httpbin.org/get" - resp = event_loop.run_until_complete(http_client(url)) - assert b"HTTP/1.1 200 OK" in resp +.. include:: event_loop_example.py + :code: python Note that, when using the ``event_loop`` fixture, you need to interact with the event loop using methods like ``event_loop.run_until_complete``. If you want to *await* code inside your test function, you need to write a coroutine and use it as a test function. The `asyncio <#pytest-mark-asyncio>`__ marker is used to mark coroutines that should be treated as test functions. diff --git a/docs/source/reference/index.rst b/docs/source/reference/index.rst index c07d0e19..5fdc2724 100644 --- a/docs/source/reference/index.rst +++ b/docs/source/reference/index.rst @@ -6,9 +6,9 @@ Reference :hidden: configuration - fixtures - markers - decorators + fixtures/index + markers/index + decorators/index changelog This section of the documentation provides descriptions of the individual parts provided by pytest-asyncio. diff --git a/docs/source/reference/markers.rst b/docs/source/reference/markers.rst deleted file mode 100644 index 68d5efd3..00000000 --- a/docs/source/reference/markers.rst +++ /dev/null @@ -1,180 +0,0 @@ -======= -Markers -======= - -``pytest.mark.asyncio`` -======================= -A coroutine or async generator with this marker will be treated as a test function by pytest. The marked function will be executed as an -asyncio task in the event loop provided by the ``event_loop`` fixture. - -In order to make your test code a little more concise, the pytest |pytestmark|_ -feature can be used to mark entire modules or classes with this marker. -Only test coroutines will be affected (by default, coroutines prefixed by -``test_``), so, for example, fixtures are safe to define. - -.. code-block:: python - - import asyncio - - import pytest - - # All test coroutines will be treated as marked. - pytestmark = pytest.mark.asyncio - - - async def test_example(event_loop): - """No marker!""" - await asyncio.sleep(0, loop=event_loop) - -In *auto* mode, the ``pytest.mark.asyncio`` marker can be omitted, the marker is added -automatically to *async* test functions. - - -``pytest.mark.asyncio_event_loop`` -================================== -Test classes or modules with this mark provide a class-scoped or module-scoped asyncio event loop. - -This functionality is orthogonal to the `asyncio` mark. -That means the presence of this mark does not imply that async test functions inside the class or module are collected by pytest-asyncio. -The collection happens automatically in `auto` mode. -However, if you're using strict mode, you still have to apply the `asyncio` mark to your async test functions. - -The following code example uses the `asyncio_event_loop` mark to provide a shared event loop for all tests in `TestClassScopedLoop`: - -.. code-block:: python - - import asyncio - - import pytest - - - @pytest.mark.asyncio_event_loop - class TestClassScopedLoop: - loop: asyncio.AbstractEventLoop - - @pytest.mark.asyncio - async def test_remember_loop(self): - TestClassScopedLoop.loop = asyncio.get_running_loop() - - @pytest.mark.asyncio - async def test_this_runs_in_same_loop(self): - assert asyncio.get_running_loop() is TestClassScopedLoop.loop - -In *auto* mode, the ``pytest.mark.asyncio`` marker can be omitted: - -.. code-block:: python - - import asyncio - - import pytest - - - @pytest.mark.asyncio_event_loop - class TestClassScopedLoop: - loop: asyncio.AbstractEventLoop - - async def test_remember_loop(self): - TestClassScopedLoop.loop = asyncio.get_running_loop() - - async def test_this_runs_in_same_loop(self): - assert asyncio.get_running_loop() is TestClassScopedLoop.loop - -Similarly, a module-scoped loop is provided when adding the `asyncio_event_loop` mark to the module: - -.. code-block:: python - - import asyncio - - import pytest - - pytestmark = pytest.mark.asyncio_event_loop - - loop: asyncio.AbstractEventLoop - - - async def test_remember_loop(): - global loop - loop = asyncio.get_running_loop() - - - async def test_this_runs_in_same_loop(): - global loop - assert asyncio.get_running_loop() is loop - - - class TestClassA: - async def test_this_runs_in_same_loop(self): - global loop - assert asyncio.get_running_loop() is loop - -The `asyncio_event_loop` mark supports an optional `policy` keyword argument to set the asyncio event loop policy. - -.. code-block:: python - - import asyncio - - import pytest - - - class CustomEventLoopPolicy(asyncio.DefaultEventLoopPolicy): - pass - - - @pytest.mark.asyncio_event_loop(policy=CustomEventLoopPolicy()) - class TestUsesCustomEventLoopPolicy: - @pytest.mark.asyncio - async def test_uses_custom_event_loop_policy(self): - assert isinstance(asyncio.get_event_loop_policy(), CustomEventLoopPolicy) - - -The ``policy`` keyword argument may also take an iterable of event loop policies. This causes tests under by the `asyncio_event_loop` mark to be parametrized with different policies: - -.. code-block:: python - - import asyncio - - import pytest - - import pytest_asyncio - - - @pytest.mark.asyncio_event_loop( - policy=[ - asyncio.DefaultEventLoopPolicy(), - uvloop.EventLoopPolicy(), - ] - ) - class TestWithDifferentLoopPolicies: - @pytest.mark.asyncio - async def test_parametrized_loop(self): - pass - - -If no explicit policy is provided, the mark will use the loop policy returned by ``asyncio.get_event_loop_policy()``. - -Fixtures and tests sharing the same `asyncio_event_loop` mark are executed in the same event loop: - -.. code-block:: python - - import asyncio - - import pytest - - import pytest_asyncio - - - @pytest.mark.asyncio_event_loop - class TestClassScopedLoop: - loop: asyncio.AbstractEventLoop - - @pytest_asyncio.fixture - async def my_fixture(self): - TestClassScopedLoop.loop = asyncio.get_running_loop() - - @pytest.mark.asyncio - async def test_runs_is_same_loop_as_fixture(self, my_fixture): - assert asyncio.get_running_loop() is TestClassScopedLoop.loop - - -.. |pytestmark| replace:: ``pytestmark`` -.. _pytestmark: http://doc.pytest.org/en/latest/example/markers.html#marking-whole-classes-or-modules diff --git a/docs/source/reference/markers/class_scoped_loop_auto_mode_example.py b/docs/source/reference/markers/class_scoped_loop_auto_mode_example.py new file mode 100644 index 00000000..a839e571 --- /dev/null +++ b/docs/source/reference/markers/class_scoped_loop_auto_mode_example.py @@ -0,0 +1,14 @@ +import asyncio + +import pytest + + +@pytest.mark.asyncio_event_loop +class TestClassScopedLoop: + loop: asyncio.AbstractEventLoop + + async def test_remember_loop(self): + TestClassScopedLoop.loop = asyncio.get_running_loop() + + async def test_this_runs_in_same_loop(self): + assert asyncio.get_running_loop() is TestClassScopedLoop.loop diff --git a/docs/source/reference/markers/class_scoped_loop_custom_policies_strict_mode_example.py b/docs/source/reference/markers/class_scoped_loop_custom_policies_strict_mode_example.py new file mode 100644 index 00000000..85ccc3a1 --- /dev/null +++ b/docs/source/reference/markers/class_scoped_loop_custom_policies_strict_mode_example.py @@ -0,0 +1,15 @@ +import asyncio + +import pytest + + +@pytest.mark.asyncio_event_loop( + policy=[ + asyncio.DefaultEventLoopPolicy(), + asyncio.DefaultEventLoopPolicy(), + ] +) +class TestWithDifferentLoopPolicies: + @pytest.mark.asyncio + async def test_parametrized_loop(self): + pass diff --git a/docs/source/reference/markers/class_scoped_loop_custom_policy_strict_mode_example.py b/docs/source/reference/markers/class_scoped_loop_custom_policy_strict_mode_example.py new file mode 100644 index 00000000..b4525ca4 --- /dev/null +++ b/docs/source/reference/markers/class_scoped_loop_custom_policy_strict_mode_example.py @@ -0,0 +1,14 @@ +import asyncio + +import pytest + + +class CustomEventLoopPolicy(asyncio.DefaultEventLoopPolicy): + pass + + +@pytest.mark.asyncio_event_loop(policy=CustomEventLoopPolicy()) +class TestUsesCustomEventLoopPolicy: + @pytest.mark.asyncio + async def test_uses_custom_event_loop_policy(self): + assert isinstance(asyncio.get_event_loop_policy(), CustomEventLoopPolicy) diff --git a/docs/source/reference/markers/class_scoped_loop_strict_mode_example.py b/docs/source/reference/markers/class_scoped_loop_strict_mode_example.py new file mode 100644 index 00000000..c33b34b8 --- /dev/null +++ b/docs/source/reference/markers/class_scoped_loop_strict_mode_example.py @@ -0,0 +1,16 @@ +import asyncio + +import pytest + + +@pytest.mark.asyncio_event_loop +class TestClassScopedLoop: + loop: asyncio.AbstractEventLoop + + @pytest.mark.asyncio + async def test_remember_loop(self): + TestClassScopedLoop.loop = asyncio.get_running_loop() + + @pytest.mark.asyncio + async def test_this_runs_in_same_loop(self): + assert asyncio.get_running_loop() is TestClassScopedLoop.loop diff --git a/docs/source/reference/markers/class_scoped_loop_with_fixture_strict_mode_example.py b/docs/source/reference/markers/class_scoped_loop_with_fixture_strict_mode_example.py new file mode 100644 index 00000000..c70a4bc6 --- /dev/null +++ b/docs/source/reference/markers/class_scoped_loop_with_fixture_strict_mode_example.py @@ -0,0 +1,18 @@ +import asyncio + +import pytest + +import pytest_asyncio + + +@pytest.mark.asyncio_event_loop +class TestClassScopedLoop: + loop: asyncio.AbstractEventLoop + + @pytest_asyncio.fixture + async def my_fixture(self): + TestClassScopedLoop.loop = asyncio.get_running_loop() + + @pytest.mark.asyncio + async def test_runs_is_same_loop_as_fixture(self, my_fixture): + assert asyncio.get_running_loop() is TestClassScopedLoop.loop diff --git a/docs/source/reference/markers/index.rst b/docs/source/reference/markers/index.rst new file mode 100644 index 00000000..6c3e5253 --- /dev/null +++ b/docs/source/reference/markers/index.rst @@ -0,0 +1,65 @@ +======= +Markers +======= + +``pytest.mark.asyncio`` +======================= +A coroutine or async generator with this marker will be treated as a test function by pytest. The marked function will be executed as an +asyncio task in the event loop provided by the ``event_loop`` fixture. + +In order to make your test code a little more concise, the pytest |pytestmark|_ +feature can be used to mark entire modules or classes with this marker. +Only test coroutines will be affected (by default, coroutines prefixed by +``test_``), so, for example, fixtures are safe to define. + +.. include:: pytestmark_asyncio_strict_mode_example.py + :code: python + +In *auto* mode, the ``pytest.mark.asyncio`` marker can be omitted, the marker is added +automatically to *async* test functions. + + +``pytest.mark.asyncio_event_loop`` +================================== +Test classes or modules with this mark provide a class-scoped or module-scoped asyncio event loop. + +This functionality is orthogonal to the `asyncio` mark. +That means the presence of this mark does not imply that async test functions inside the class or module are collected by pytest-asyncio. +The collection happens automatically in `auto` mode. +However, if you're using strict mode, you still have to apply the `asyncio` mark to your async test functions. + +The following code example uses the `asyncio_event_loop` mark to provide a shared event loop for all tests in `TestClassScopedLoop`: + +.. include:: class_scoped_loop_strict_mode_example.py + :code: python + +In *auto* mode, the ``pytest.mark.asyncio`` marker can be omitted: + +.. include:: class_scoped_loop_auto_mode_example.py + :code: python + +Similarly, a module-scoped loop is provided when adding the `asyncio_event_loop` mark to the module: + +.. include:: module_scoped_loop_auto_mode_example.py + :code: python + +The `asyncio_event_loop` mark supports an optional `policy` keyword argument to set the asyncio event loop policy. + +.. include:: class_scoped_loop_custom_policy_strict_mode_example.py + :code: python + + +The ``policy`` keyword argument may also take an iterable of event loop policies. This causes tests under by the `asyncio_event_loop` mark to be parametrized with different policies: + +.. include:: class_scoped_loop_custom_policies_strict_mode_example.py + :code: python + +If no explicit policy is provided, the mark will use the loop policy returned by ``asyncio.get_event_loop_policy()``. + +Fixtures and tests sharing the same `asyncio_event_loop` mark are executed in the same event loop: + +.. include:: class_scoped_loop_with_fixture_strict_mode_example.py + :code: python + +.. |pytestmark| replace:: ``pytestmark`` +.. _pytestmark: http://doc.pytest.org/en/latest/example/markers.html#marking-whole-classes-or-modules diff --git a/docs/source/reference/markers/module_scoped_loop_auto_mode_example.py b/docs/source/reference/markers/module_scoped_loop_auto_mode_example.py new file mode 100644 index 00000000..e38bdeff --- /dev/null +++ b/docs/source/reference/markers/module_scoped_loop_auto_mode_example.py @@ -0,0 +1,23 @@ +import asyncio + +import pytest + +pytestmark = pytest.mark.asyncio_event_loop + +loop: asyncio.AbstractEventLoop + + +async def test_remember_loop(): + global loop + loop = asyncio.get_running_loop() + + +async def test_this_runs_in_same_loop(): + global loop + assert asyncio.get_running_loop() is loop + + +class TestClassA: + async def test_this_runs_in_same_loop(self): + global loop + assert asyncio.get_running_loop() is loop diff --git a/docs/source/reference/markers/pytestmark_asyncio_strict_mode_example.py b/docs/source/reference/markers/pytestmark_asyncio_strict_mode_example.py new file mode 100644 index 00000000..f1465728 --- /dev/null +++ b/docs/source/reference/markers/pytestmark_asyncio_strict_mode_example.py @@ -0,0 +1,11 @@ +import asyncio + +import pytest + +# All test coroutines will be treated as marked. +pytestmark = pytest.mark.asyncio + + +async def test_example(): + """No marker!""" + await asyncio.sleep(0) From b7c66dbf19d692b54eb24f0918b73ec85437ec17 Mon Sep 17 00:00:00 2001 From: Michael Seifert Date: Mon, 23 Oct 2023 10:13:57 +0200 Subject: [PATCH 079/151] [build] Run code examples in docs as tests. Signed-off-by: Michael Seifert --- Makefile | 2 +- setup.cfg | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 8bc58c49..e1ef5d27 100644 --- a/Makefile +++ b/Makefile @@ -21,7 +21,7 @@ clean-test: ## remove test and coverage artifacts rm -fr htmlcov/ test: - coverage run --parallel-mode --omit */_version.py -m pytest tests + coverage run --parallel-mode --omit */_version.py -m pytest install: pip install -U pre-commit diff --git a/setup.cfg b/setup.cfg index 7b7a1b6b..d027a942 100644 --- a/setup.cfg +++ b/setup.cfg @@ -65,8 +65,9 @@ branch = true show_missing = true [tool:pytest] +python_files = test_*.py *_example.py addopts = -rsx --tb=short -testpaths = tests +testpaths = docs/source tests asyncio_mode = auto junit_family=xunit2 filterwarnings = From 9f75b3cc7e748112783bc64c45089d7d8e8014da Mon Sep 17 00:00:00 2001 From: Michael Seifert Date: Tue, 24 Oct 2023 08:56:14 +0200 Subject: [PATCH 080/151] [fix] Fixes an issue that causes DeprecationWarnings for event loop overrides to be emitted, even though the fixture was not overridden. Signed-off-by: Michael Seifert --- pytest_asyncio/plugin.py | 33 +++++++++++----------- tests/test_event_loop_fixture_finalizer.py | 2 +- 2 files changed, 17 insertions(+), 18 deletions(-) diff --git a/pytest_asyncio/plugin.py b/pytest_asyncio/plugin.py index 2babd96a..b58197e4 100644 --- a/pytest_asyncio/plugin.py +++ b/pytest_asyncio/plugin.py @@ -646,17 +646,6 @@ def pytest_fixture_setup( ) -> Optional[object]: """Adjust the event loop policy when an event loop is produced.""" if fixturedef.argname == "event_loop": - # FixtureDef.baseid is an empty string when the Fixture was found in a plugin. - # This is also true, when the fixture was defined in a conftest.py - # at the rootdir. - fixture_filename = inspect.getsourcefile(fixturedef.func) - if not getattr(fixturedef.func, "__original_func", False): - _, fixture_line_number = inspect.getsourcelines(fixturedef.func) - warnings.warn( - _REDEFINED_EVENT_LOOP_FIXTURE_WARNING - % (fixture_filename, fixture_line_number), - DeprecationWarning, - ) # The use of a fixture finalizer is preferred over the # pytest_fixture_post_finalizer hook. The fixture finalizer is invoked once # for each fixture, whereas the hook may be invoked multiple times for @@ -669,6 +658,16 @@ def pytest_fixture_setup( ) outcome = yield loop = outcome.get_result() + # Weird behavior was observed when checking for an attribute of FixtureDef.func + # Instead, we now check for a special attribute of the returned event loop + fixture_filename = inspect.getsourcefile(fixturedef.func) + if not getattr(loop, "__original_fixture_loop", False): + _, fixture_line_number = inspect.getsourcelines(fixturedef.func) + warnings.warn( + _REDEFINED_EVENT_LOOP_FIXTURE_WARNING + % (fixture_filename, fixture_line_number), + DeprecationWarning, + ) policy = asyncio.get_event_loop_policy() try: with warnings.catch_warnings(): @@ -836,13 +835,13 @@ def pytest_runtest_setup(item: pytest.Item) -> None: @pytest.fixture def event_loop(request: FixtureRequest) -> Iterator[asyncio.AbstractEventLoop]: """Create an instance of the default event loop for each test case.""" - # Add a magic value to the fixture function, so that we can check for overrides - # of this fixture in pytest_fixture_setup - # The magic value must be part of the function definition, because pytest may have - # multiple instances of the fixture function - event_loop.__original_func = True - loop = asyncio.get_event_loop_policy().new_event_loop() + # Add a magic value to the event loop, so pytest-asyncio can determine if the + # event_loop fixture was overridden. Other implementations of event_loop don't + # set this value. + # The magic value must be set as part of the function definition, because pytest + # seems to have multiple instances of the same FixtureDef or fixture function + loop.__original_fixture_loop = True # type: ignore[attr-defined] yield loop loop.close() diff --git a/tests/test_event_loop_fixture_finalizer.py b/tests/test_event_loop_fixture_finalizer.py index 07b76501..5f8f9574 100644 --- a/tests/test_event_loop_fixture_finalizer.py +++ b/tests/test_event_loop_fixture_finalizer.py @@ -133,5 +133,5 @@ async def test_ends_with_unclosed_loop(): ) ) result = pytester.runpytest("--asyncio-mode=strict", "-W", "default") - result.assert_outcomes(passed=1, warnings=2) + result.assert_outcomes(passed=1, warnings=1) result.stdout.fnmatch_lines("*unclosed event loop*") From 044e568c5dcf911a5a06699865d45f0e8c03f9a9 Mon Sep 17 00:00:00 2001 From: Michael Seifert Date: Tue, 24 Oct 2023 12:32:21 +0200 Subject: [PATCH 081/151] [feat] Emit a deprecation warning when the event_loop fixture is explicitly requested by a coroutine or an async fixture. Signed-off-by: Michael Seifert --- docs/source/concepts.rst | 13 +- docs/source/reference/fixtures/index.rst | 2 +- pytest_asyncio/plugin.py | 23 ++- .../test_async_fixtures_with_finalizer.py | 6 +- tests/async_fixtures/test_nested.py | 2 +- tests/markers/test_class_marker.py | 2 +- ...event_loop_fixture_override_deprecation.py | 34 +++- ...est_explicit_event_loop_fixture_request.py | 159 ++++++++++++++++++ tests/test_multiloop.py | 2 +- tests/test_simple.py | 21 +-- tests/test_subprocess.py | 2 +- 11 files changed, 238 insertions(+), 28 deletions(-) create mode 100644 tests/test_explicit_event_loop_fixture_request.py diff --git a/docs/source/concepts.rst b/docs/source/concepts.rst index eb08bae6..e774791e 100644 --- a/docs/source/concepts.rst +++ b/docs/source/concepts.rst @@ -4,14 +4,19 @@ Concepts asyncio event loops =================== -pytest-asyncio runs each test item in its own asyncio event loop. The loop can be accessed via the ``event_loop`` fixture, which is automatically requested by all async tests. +pytest-asyncio runs each test item in its own asyncio event loop. The loop can be accessed via ``asyncio.get_running_loop()``. .. code-block:: python - async def test_provided_loop_is_running_loop(event_loop): - assert event_loop is asyncio.get_running_loop() + async def test_runs_in_a_loop(): + assert asyncio.get_running_loop() -You can think of `event_loop` as an autouse fixture for async tests. +Synchronous test functions can get access to an asyncio event loop via the `event_loop` fixture. + +.. code-block:: python + + def test_can_access_current_loop(event_loop): + assert event_loop Test discovery modes ==================== diff --git a/docs/source/reference/fixtures/index.rst b/docs/source/reference/fixtures/index.rst index 98fe5382..c0bfd300 100644 --- a/docs/source/reference/fixtures/index.rst +++ b/docs/source/reference/fixtures/index.rst @@ -5,7 +5,7 @@ Fixtures event_loop ========== Creates a new asyncio event loop based on the current event loop policy. The new loop -is available as the return value of this fixture or via `asyncio.get_running_loop `__. +is available as the return value of this fixture for synchronous functions, or via `asyncio.get_running_loop `__ for asynchronous functions. The event loop is closed when the fixture scope ends. The fixture scope defaults to ``function`` scope. diff --git a/pytest_asyncio/plugin.py b/pytest_asyncio/plugin.py index b58197e4..18c86869 100644 --- a/pytest_asyncio/plugin.py +++ b/pytest_asyncio/plugin.py @@ -35,6 +35,7 @@ Metafunc, Parser, PytestCollectionWarning, + PytestDeprecationWarning, PytestPluginManager, Session, StashKey, @@ -222,6 +223,16 @@ def _preprocess_async_fixtures( # This applies to pytest_trio fixtures, for example continue _make_asyncio_fixture_function(func) + function_signature = inspect.signature(func) + if "event_loop" in function_signature.parameters: + warnings.warn( + PytestDeprecationWarning( + f"{func.__name__} is asynchronous and explicitly " + f'requests the "event_loop" fixture. Asynchronous fixtures and ' + f'test functions should use "asyncio.get_running_loop()" ' + f"instead." + ) + ) _inject_fixture_argnames(fixturedef, event_loop_fixture_id) _synchronize_async_fixture(fixturedef, event_loop_fixture_id) assert _is_asyncio_fixture_function(fixturedef.func) @@ -372,7 +383,7 @@ def _from_function(cls, function: Function, /) -> Function: Instantiates this specific PytestAsyncioFunction type from the specified Function item. """ - return cls.from_parent( + subclass_instance = cls.from_parent( function.parent, name=function.name, callspec=getattr(function, "callspec", None), @@ -381,6 +392,16 @@ def _from_function(cls, function: Function, /) -> Function: keywords=function.keywords, originalname=function.originalname, ) + subclassed_function_signature = inspect.signature(subclass_instance.obj) + if "event_loop" in subclassed_function_signature.parameters: + subclass_instance.warn( + PytestDeprecationWarning( + f"{subclass_instance.name} is asynchronous and explicitly " + f'requests the "event_loop" fixture. Asynchronous fixtures and ' + f'test functions should use "asyncio.get_running_loop()" instead.' + ) + ) + return subclass_instance @staticmethod def _can_substitute(item: Function) -> bool: diff --git a/tests/async_fixtures/test_async_fixtures_with_finalizer.py b/tests/async_fixtures/test_async_fixtures_with_finalizer.py index 2e72d5de..aa2ce3d7 100644 --- a/tests/async_fixtures/test_async_fixtures_with_finalizer.py +++ b/tests/async_fixtures/test_async_fixtures_with_finalizer.py @@ -26,14 +26,14 @@ def event_loop(): @pytest.fixture(scope="module") -async def port_with_event_loop_finalizer(request, event_loop): +async def port_with_event_loop_finalizer(request): def port_finalizer(finalizer): async def port_afinalizer(): # await task using loop provided by event_loop fixture # RuntimeError is raised if task is created on a different loop await finalizer - event_loop.run_until_complete(port_afinalizer()) + asyncio.get_event_loop().run_until_complete(port_afinalizer()) worker = asyncio.ensure_future(asyncio.sleep(0.2)) request.addfinalizer(functools.partial(port_finalizer, worker)) @@ -41,7 +41,7 @@ async def port_afinalizer(): @pytest.fixture(scope="module") -async def port_with_get_event_loop_finalizer(request, event_loop): +async def port_with_get_event_loop_finalizer(request): def port_finalizer(finalizer): async def port_afinalizer(): # await task using current loop retrieved from the event loop policy diff --git a/tests/async_fixtures/test_nested.py b/tests/async_fixtures/test_nested.py index e81e7824..da7ee3a1 100644 --- a/tests/async_fixtures/test_nested.py +++ b/tests/async_fixtures/test_nested.py @@ -12,7 +12,7 @@ async def async_inner_fixture(): @pytest.fixture() -async def async_fixture_outer(async_inner_fixture, event_loop): +async def async_fixture_outer(async_inner_fixture): await asyncio.sleep(0.01) print("outer start") assert async_inner_fixture is True diff --git a/tests/markers/test_class_marker.py b/tests/markers/test_class_marker.py index 68425575..f8cf4ca0 100644 --- a/tests/markers/test_class_marker.py +++ b/tests/markers/test_class_marker.py @@ -8,7 +8,7 @@ class TestPyTestMark: pytestmark = pytest.mark.asyncio - async def test_is_asyncio(self, event_loop, sample_fixture): + async def test_is_asyncio(self, sample_fixture): assert asyncio.get_event_loop() counter = 1 diff --git a/tests/test_event_loop_fixture_override_deprecation.py b/tests/test_event_loop_fixture_override_deprecation.py index b23ff642..3484ef76 100644 --- a/tests/test_event_loop_fixture_override_deprecation.py +++ b/tests/test_event_loop_fixture_override_deprecation.py @@ -19,15 +19,45 @@ def event_loop(): @pytest.mark.asyncio async def test_emits_warning(): pass + """ + ) + ) + result = pytester.runpytest("--asyncio-mode=strict") + result.assert_outcomes(passed=1, warnings=1) + result.stdout.fnmatch_lines( + ["*event_loop fixture provided by pytest-asyncio has been redefined*"] + ) + + +def test_emit_warning_when_event_loop_fixture_is_redefined_explicit_request( + pytester: Pytester, +): + pytester.makepyfile( + dedent( + """\ + import asyncio + import pytest + + @pytest.fixture + def event_loop(): + loop = asyncio.new_event_loop() + yield loop + loop.close() @pytest.mark.asyncio - async def test_emits_warning_when_referenced_explicitly(event_loop): + async def test_emits_warning_when_requested_explicitly(event_loop): pass """ ) ) result = pytester.runpytest("--asyncio-mode=strict") - result.assert_outcomes(passed=2, warnings=2) + result.assert_outcomes(passed=1, warnings=2) + result.stdout.fnmatch_lines( + ["*event_loop fixture provided by pytest-asyncio has been redefined*"] + ) + result.stdout.fnmatch_lines( + ['*is asynchronous and explicitly requests the "event_loop" fixture*'] + ) def test_does_not_emit_warning_when_no_test_uses_the_event_loop_fixture( diff --git a/tests/test_explicit_event_loop_fixture_request.py b/tests/test_explicit_event_loop_fixture_request.py new file mode 100644 index 00000000..8c4b732c --- /dev/null +++ b/tests/test_explicit_event_loop_fixture_request.py @@ -0,0 +1,159 @@ +from textwrap import dedent + +from pytest import Pytester + + +def test_emit_warning_when_event_loop_is_explicitly_requested_in_coroutine( + pytester: Pytester, +): + pytester.makepyfile( + dedent( + """\ + import pytest + + @pytest.mark.asyncio + async def test_coroutine_emits_warning(event_loop): + pass + """ + ) + ) + result = pytester.runpytest("--asyncio-mode=strict") + result.assert_outcomes(passed=1, warnings=1) + result.stdout.fnmatch_lines( + ['*is asynchronous and explicitly requests the "event_loop" fixture*'] + ) + + +def test_emit_warning_when_event_loop_is_explicitly_requested_in_coroutine_method( + pytester: Pytester, +): + pytester.makepyfile( + dedent( + """\ + import pytest + + class TestEmitsWarning: + @pytest.mark.asyncio + async def test_coroutine_emits_warning(self, event_loop): + pass + """ + ) + ) + result = pytester.runpytest("--asyncio-mode=strict") + result.assert_outcomes(passed=1, warnings=1) + result.stdout.fnmatch_lines( + ['*is asynchronous and explicitly requests the "event_loop" fixture*'] + ) + + +def test_emit_warning_when_event_loop_is_explicitly_requested_in_coroutine_staticmethod( + pytester: Pytester, +): + pytester.makepyfile( + dedent( + """\ + import pytest + + class TestEmitsWarning: + @staticmethod + @pytest.mark.asyncio + async def test_coroutine_emits_warning(event_loop): + pass + """ + ) + ) + result = pytester.runpytest("--asyncio-mode=strict") + result.assert_outcomes(passed=1, warnings=1) + result.stdout.fnmatch_lines( + ['*is asynchronous and explicitly requests the "event_loop" fixture*'] + ) + + +def test_emit_warning_when_event_loop_is_explicitly_requested_in_coroutine_fixture( + pytester: Pytester, +): + pytester.makepyfile( + dedent( + """\ + import pytest + import pytest_asyncio + + @pytest_asyncio.fixture + async def emits_warning(event_loop): + pass + + @pytest.mark.asyncio + async def test_uses_fixture(emits_warning): + pass + """ + ) + ) + result = pytester.runpytest("--asyncio-mode=strict") + result.assert_outcomes(passed=1, warnings=1) + result.stdout.fnmatch_lines( + ['*is asynchronous and explicitly requests the "event_loop" fixture*'] + ) + + +def test_emit_warning_when_event_loop_is_explicitly_requested_in_async_gen_fixture( + pytester: Pytester, +): + pytester.makepyfile( + dedent( + """\ + import pytest + import pytest_asyncio + + @pytest_asyncio.fixture + async def emits_warning(event_loop): + yield + + @pytest.mark.asyncio + async def test_uses_fixture(emits_warning): + pass + """ + ) + ) + result = pytester.runpytest("--asyncio-mode=strict") + result.assert_outcomes(passed=1, warnings=1) + result.stdout.fnmatch_lines( + ['*is asynchronous and explicitly requests the "event_loop" fixture*'] + ) + + +def test_does_not_emit_warning_when_event_loop_is_explicitly_requested_in_sync_function( + pytester: Pytester, +): + pytester.makepyfile( + dedent( + """\ + import pytest + + def test_uses_fixture(event_loop): + pass + """ + ) + ) + result = pytester.runpytest("--asyncio-mode=strict") + result.assert_outcomes(passed=1) + + +def test_does_not_emit_warning_when_event_loop_is_explicitly_requested_in_sync_fixture( + pytester: Pytester, +): + pytester.makepyfile( + dedent( + """\ + import pytest + + @pytest.fixture + def any_fixture(event_loop): + pass + + def test_uses_fixture(any_fixture): + pass + """ + ) + ) + result = pytester.runpytest("--asyncio-mode=strict") + result.assert_outcomes(passed=1) diff --git a/tests/test_multiloop.py b/tests/test_multiloop.py index 86a88eec..c3713cc9 100644 --- a/tests/test_multiloop.py +++ b/tests/test_multiloop.py @@ -54,7 +54,7 @@ def event_loop(): @pytest.mark.asyncio - async def test_for_custom_loop(event_loop): + async def test_for_custom_loop(): """This test should be executed using the custom loop.""" await asyncio.sleep(0.01) assert type(asyncio.get_event_loop()).__name__ == "CustomSelectorLoop" diff --git a/tests/test_simple.py b/tests/test_simple.py index 81fcd14b..b6020c69 100644 --- a/tests/test_simple.py +++ b/tests/test_simple.py @@ -70,7 +70,7 @@ async def test_asyncio_marker_with_default_param(a_param=None): @pytest.mark.asyncio -async def test_unused_port_fixture(unused_tcp_port, event_loop): +async def test_unused_port_fixture(unused_tcp_port): """Test the unused TCP port fixture.""" async def closer(_, writer): @@ -86,7 +86,7 @@ async def closer(_, writer): @pytest.mark.asyncio -async def test_unused_udp_port_fixture(unused_udp_port, event_loop): +async def test_unused_udp_port_fixture(unused_udp_port): """Test the unused TCP port fixture.""" class Closer: @@ -96,6 +96,7 @@ def connection_made(self, transport): def connection_lost(self, *arg, **kwd): pass + event_loop = asyncio.get_running_loop() transport1, _ = await event_loop.create_datagram_endpoint( Closer, local_addr=("127.0.0.1", unused_udp_port), @@ -113,7 +114,7 @@ def connection_lost(self, *arg, **kwd): @pytest.mark.asyncio -async def test_unused_port_factory_fixture(unused_tcp_port_factory, event_loop): +async def test_unused_port_factory_fixture(unused_tcp_port_factory): """Test the unused TCP port factory fixture.""" async def closer(_, writer): @@ -142,7 +143,7 @@ async def closer(_, writer): @pytest.mark.asyncio -async def test_unused_udp_port_factory_fixture(unused_udp_port_factory, event_loop): +async def test_unused_udp_port_factory_fixture(unused_udp_port_factory): """Test the unused UDP port factory fixture.""" class Closer: @@ -158,6 +159,7 @@ def connection_lost(self, *arg, **kwd): unused_udp_port_factory(), ) + event_loop = asyncio.get_running_loop() transport1, _ = await event_loop.create_datagram_endpoint( Closer, local_addr=("127.0.0.1", port1), @@ -228,13 +230,6 @@ def mock_unused_udp_port(_ignored): class TestMarkerInClassBasedTests: """Test that asyncio marked functions work for methods of test classes.""" - @pytest.mark.asyncio - async def test_asyncio_marker_with_explicit_loop_fixture(self, event_loop): - """Test the "asyncio" marker works on a method in - a class-based test with explicit loop fixture.""" - ret = await async_coro() - assert ret == "ok" - @pytest.mark.asyncio async def test_asyncio_marker_with_implicit_loop_fixture(self): """Test the "asyncio" marker works on a method in @@ -257,11 +252,11 @@ async def test_no_event_loop(self, loop): assert await loop.run_in_executor(None, self.foo) == 1 @pytest.mark.asyncio - async def test_event_loop_after_fixture(self, loop, event_loop): + async def test_event_loop_after_fixture(self, loop): assert await loop.run_in_executor(None, self.foo) == 1 @pytest.mark.asyncio - async def test_event_loop_before_fixture(self, event_loop, loop): + async def test_event_loop_before_fixture(self, loop): assert await loop.run_in_executor(None, self.foo) == 1 diff --git a/tests/test_subprocess.py b/tests/test_subprocess.py index 8f1caee5..14d3498a 100644 --- a/tests/test_subprocess.py +++ b/tests/test_subprocess.py @@ -16,7 +16,7 @@ def event_loop(): @pytest.mark.asyncio -async def test_subprocess(event_loop): +async def test_subprocess(): """Starting a subprocess should be possible.""" proc = await asyncio.subprocess.create_subprocess_exec( sys.executable, "--version", stdout=asyncio.subprocess.PIPE From 494601d6b32ec4a28af4f19eb4d823b61cf9e428 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 30 Oct 2023 18:42:05 +0000 Subject: [PATCH 082/151] Build(deps): Bump pytest from 7.4.2 to 7.4.3 in /dependencies/default Bumps [pytest](https://github.com/pytest-dev/pytest) from 7.4.2 to 7.4.3. - [Release notes](https://github.com/pytest-dev/pytest/releases) - [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest/compare/7.4.2...7.4.3) --- updated-dependencies: - dependency-name: pytest dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- dependencies/default/constraints.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies/default/constraints.txt b/dependencies/default/constraints.txt index d50a128b..ecf8d1d5 100644 --- a/dependencies/default/constraints.txt +++ b/dependencies/default/constraints.txt @@ -13,7 +13,7 @@ outcome==1.3.0 packaging==23.2 pluggy==1.3.0 pyparsing==3.1.1 -pytest==7.4.2 +pytest==7.4.3 pytest-trio==0.8.0 sniffio==1.3.0 sortedcontainers==2.4.0 From 349d9c8077c1ca1ff411094c154e6de6f12aa87d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 30 Oct 2023 18:42:16 +0000 Subject: [PATCH 083/151] Build(deps): Bump outcome in /dependencies/default Bumps [outcome](https://github.com/python-trio/outcome) from 1.3.0 to 1.3.0.post0. - [Commits](https://github.com/python-trio/outcome/compare/v1.3.0...v1.3.0.post0) --- updated-dependencies: - dependency-name: outcome dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- dependencies/default/constraints.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies/default/constraints.txt b/dependencies/default/constraints.txt index ecf8d1d5..74eee9b6 100644 --- a/dependencies/default/constraints.txt +++ b/dependencies/default/constraints.txt @@ -9,7 +9,7 @@ importlib-metadata==6.8.0 iniconfig==2.0.0 mypy==1.6.1 mypy-extensions==1.0.0 -outcome==1.3.0 +outcome==1.3.0.post0 packaging==23.2 pluggy==1.3.0 pyparsing==3.1.1 From a950f553681a2fbc2a026372021f944a35cf5cee Mon Sep 17 00:00:00 2001 From: Michael Seifert Date: Tue, 31 Oct 2023 07:56:30 +0100 Subject: [PATCH 084/151] Prepare for release of v0.22.0. Signed-off-by: Michael Seifert --- docs/source/reference/changelog.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/source/reference/changelog.rst b/docs/source/reference/changelog.rst index 7da71868..a528c07e 100644 --- a/docs/source/reference/changelog.rst +++ b/docs/source/reference/changelog.rst @@ -2,8 +2,8 @@ Changelog ========= -1.0.0 (UNRELEASED) -================== +0.22.0 (2023-10-31) +=================== - Class-scoped and module-scoped event loops can be requested via the _asyncio_event_loop_ mark. `#620 `_ - Deprecate redefinition of the `event_loop` fixture. `#587 `_ From 32759571c8ae4cb9f566e7534a98a681b7173924 Mon Sep 17 00:00:00 2001 From: Michael Seifert Date: Tue, 31 Oct 2023 09:00:19 +0100 Subject: [PATCH 085/151] [build] Added dedicated requirements and constraints files for docs dependencies. Signed-off-by: Michael Seifert --- .github/dependabot.yml | 6 ++++++ dependencies/docs/constraints.txt | 23 +++++++++++++++++++++++ dependencies/docs/requirements.txt | 2 ++ 3 files changed, 31 insertions(+) create mode 100644 dependencies/docs/constraints.txt create mode 100644 dependencies/docs/requirements.txt diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 9a663d8e..3d725a50 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -7,6 +7,12 @@ updates: interval: weekly open-pull-requests-limit: 10 target-branch: main +- package-ecosystem: pip + directory: /dependencies/docs + schedule: + interval: weekly + open-pull-requests-limit: 10 + target-branch: main - package-ecosystem: github-actions directory: / schedule: diff --git a/dependencies/docs/constraints.txt b/dependencies/docs/constraints.txt new file mode 100644 index 00000000..97602b7b --- /dev/null +++ b/dependencies/docs/constraints.txt @@ -0,0 +1,23 @@ +alabaster==0.7.13 +Babel==2.13.1 +certifi==2023.7.22 +charset-normalizer==3.3.1 +docutils==0.18.1 +idna==3.4 +imagesize==1.4.1 +Jinja2==3.1.2 +MarkupSafe==2.1.3 +packaging==23.2 +Pygments==2.16.1 +requests==2.31.0 +snowballstemmer==2.2.0 +Sphinx==7.2.6 +sphinx-rtd-theme==1.3.0 +sphinxcontrib-applehelp==1.0.7 +sphinxcontrib-devhelp==1.0.5 +sphinxcontrib-htmlhelp==2.0.4 +sphinxcontrib-jquery==4.1 +sphinxcontrib-jsmath==1.0.1 +sphinxcontrib-qthelp==1.0.6 +sphinxcontrib-serializinghtml==1.1.9 +urllib3==2.0.7 diff --git a/dependencies/docs/requirements.txt b/dependencies/docs/requirements.txt new file mode 100644 index 00000000..1bfd7f50 --- /dev/null +++ b/dependencies/docs/requirements.txt @@ -0,0 +1,2 @@ +sphinx >= 5.3 +sphinx-rtd-theme >= 1.0 From 6cc90f268e50f9550dcbe94460a76954d2aaa6fb Mon Sep 17 00:00:00 2001 From: Michael Seifert Date: Tue, 31 Oct 2023 08:47:52 +0100 Subject: [PATCH 086/151] [docs] Adjust release version and copyright year in docs. Signed-off-by: Michael Seifert --- docs/source/conf.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index b61a6679..f0611997 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -7,9 +7,9 @@ # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information project = "pytest-asyncio" -copyright = "2022, pytest-asyncio contributors" +copyright = "2023, pytest-asyncio contributors" author = "Tin Tvrtković" -release = "v0.20.1" +release = "v0.22.0" # -- General configuration --------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration From 5de9d84c7d63e80d16063fd74e1d0f0913e8edaf Mon Sep 17 00:00:00 2001 From: Michael Seifert Date: Tue, 31 Oct 2023 08:39:33 +0100 Subject: [PATCH 087/151] [docs] Add .readthedocs.yaml config. This fixes an issue that prevents the documentation from being built. See https://blog.readthedocs.com/migrate-configuration-v2/ Signed-off-by: Michael Seifert --- .readthedocs.yaml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 .readthedocs.yaml diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 00000000..d825e855 --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,18 @@ +--- +# Read the Docs configuration file for Sphinx projects +# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details + +version: 2 +build: + os: ubuntu-22.04 + tools: + python: '3.12' + +sphinx: + configuration: docs/source/conf.py + fail_on_warning: true + +python: + install: + - requirements: dependencies/default/constraints.txt + - requirements: dependencies/docs/constraints.txt From 549950ff61a57824d3fee435409401ac4ee7916a Mon Sep 17 00:00:00 2001 From: Michael Seifert Date: Tue, 31 Oct 2023 09:06:54 +0100 Subject: [PATCH 088/151] [docs] Remove reference to empty "_static" directory to silence Sphinx warning. Signed-off-by: Michael Seifert --- docs/source/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index f0611997..b85bc156 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -24,4 +24,4 @@ # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output html_theme = "sphinx_rtd_theme" -html_static_path = ["_static"] +html_static_path = [] From 587dac5f46266a622ec7b1007c38dc24a7a4d1a9 Mon Sep 17 00:00:00 2001 From: Michael Seifert Date: Tue, 31 Oct 2023 14:17:59 +0100 Subject: [PATCH 089/151] [build] Fixes a bug that prevented the pytest-min tox environment from correctly using the minimum pytest version. The "deps" option does not specify the package dependencies, but a list of packages that should be installed before the package under test. However, it's possible ot use "install_command" to specify a constraints file. see https://github.com/tox-dev/tox/issues/1939#issuecomment-814851730 Signed-off-by: Michael Seifert --- tox.ini | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/tox.ini b/tox.ini index 33e0c931..5acc30e2 100644 --- a/tox.ini +++ b/tox.ini @@ -7,18 +7,20 @@ passenv = [testenv] extras = testing -deps = - --requirement dependencies/default/requirements.txt - --constraint dependencies/default/constraints.txt +install_command = python -m pip install \ + --requirement dependencies/default/requirements.txt \ + --constraint dependencies/default/constraints.txt \ + {opts} {packages} commands = make test allowlist_externals = make [testenv:pytest-min] extras = testing -deps = - --requirement dependencies/pytest-min/requirements.txt - --constraint dependencies/pytest-min/constraints.txt +install_command = python -m pip install \ + --requirement dependencies/pytest-min/requirements.txt \ + --constraint dependencies/pytest-min/constraints.txt \ + {opts} {packages} commands = make test allowlist_externals = make From b96918ac545fda960fff354434932738826d4330 Mon Sep 17 00:00:00 2001 From: Michael Seifert Date: Tue, 31 Oct 2023 14:47:41 +0100 Subject: [PATCH 090/151] [fix] Fixes a bug that broke compatibility with pytest>=7.0,<7.2. The "consider_mro" keyword argument to _pytest.mark.structures.get_unpacked_marks was only introduced in pytest-7.2.0. Removing the explicit argument from the function call will adjust the behavior of pytest-asyncio marks to that of pytest. Signed-off-by: Michael Seifert --- docs/source/reference/changelog.rst | 4 ++++ pytest_asyncio/plugin.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/source/reference/changelog.rst b/docs/source/reference/changelog.rst index a528c07e..985580e8 100644 --- a/docs/source/reference/changelog.rst +++ b/docs/source/reference/changelog.rst @@ -2,6 +2,10 @@ Changelog ========= +0.22.1 (UNRELEASED) +=================== +- Fixes a bug that broke compatibility with pytest>=7.0,<7.2. `#654 `_ + 0.22.0 (2023-10-31) =================== - Class-scoped and module-scoped event loops can be requested diff --git a/pytest_asyncio/plugin.py b/pytest_asyncio/plugin.py index 18c86869..b39a47b8 100644 --- a/pytest_asyncio/plugin.py +++ b/pytest_asyncio/plugin.py @@ -549,7 +549,7 @@ def pytest_collectstart(collector: pytest.Collector): return # pytest.Collector.own_markers is empty at this point, # so we rely on _pytest.mark.structures.get_unpacked_marks - marks = get_unpacked_marks(collector.obj, consider_mro=True) + marks = get_unpacked_marks(collector.obj) for mark in marks: if not mark.name == "asyncio_event_loop": continue From d8b005b574bc4254ba3e5d9b8f4c87f4d64fe3ae Mon Sep 17 00:00:00 2001 From: Michael Seifert Date: Tue, 31 Oct 2023 15:50:11 +0100 Subject: [PATCH 091/151] [test] Pin trio to v0.21 in the pytest-min test environment to avoid unhandled deprecation warnings. Signed-off-by: Michael Seifert --- dependencies/pytest-min/constraints.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/dependencies/pytest-min/constraints.txt b/dependencies/pytest-min/constraints.txt index 1f82dbaf..3cdda3a6 100644 --- a/dependencies/pytest-min/constraints.txt +++ b/dependencies/pytest-min/constraints.txt @@ -18,5 +18,6 @@ pytest==7.0.0 requests==2.28.1 sortedcontainers==2.4.0 tomli==2.0.1 +trio==0.21.0 urllib3==1.26.12 xmlschema==2.1.1 From fcc5fec9cad348f4792e3574fb29a01f434561a2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 Nov 2023 18:25:52 +0000 Subject: [PATCH 092/151] Build(deps): Bump charset-normalizer in /dependencies/docs Bumps [charset-normalizer](https://github.com/Ousret/charset_normalizer) from 3.3.1 to 3.3.2. - [Release notes](https://github.com/Ousret/charset_normalizer/releases) - [Changelog](https://github.com/Ousret/charset_normalizer/blob/master/CHANGELOG.md) - [Commits](https://github.com/Ousret/charset_normalizer/compare/3.3.1...3.3.2) --- updated-dependencies: - dependency-name: charset-normalizer dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- dependencies/docs/constraints.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies/docs/constraints.txt b/dependencies/docs/constraints.txt index 97602b7b..21439273 100644 --- a/dependencies/docs/constraints.txt +++ b/dependencies/docs/constraints.txt @@ -1,7 +1,7 @@ alabaster==0.7.13 Babel==2.13.1 certifi==2023.7.22 -charset-normalizer==3.3.1 +charset-normalizer==3.3.2 docutils==0.18.1 idna==3.4 imagesize==1.4.1 From 5fa5042b3b5ffddd5747f2528312519cfe1a94c1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 Nov 2023 18:53:02 +0000 Subject: [PATCH 093/151] Build(deps): Bump trio from 0.22.2 to 0.23.1 in /dependencies/default Bumps [trio](https://github.com/python-trio/trio) from 0.22.2 to 0.23.1. - [Release notes](https://github.com/python-trio/trio/releases) - [Commits](https://github.com/python-trio/trio/compare/v0.22.2...v0.23.1) --- updated-dependencies: - dependency-name: trio dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- dependencies/default/constraints.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies/default/constraints.txt b/dependencies/default/constraints.txt index 74eee9b6..7fc94d02 100644 --- a/dependencies/default/constraints.txt +++ b/dependencies/default/constraints.txt @@ -18,6 +18,6 @@ pytest-trio==0.8.0 sniffio==1.3.0 sortedcontainers==2.4.0 tomli==2.0.1 -trio==0.22.2 +trio==0.23.1 typed-ast==1.5.5 zipp==3.17.0 From e55317aea67a3c99cbf9058ecfca6d793d91e0cc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 Nov 2023 18:53:11 +0000 Subject: [PATCH 094/151] Build(deps): Bump hypothesis in /dependencies/default Bumps [hypothesis](https://github.com/HypothesisWorks/hypothesis) from 6.88.1 to 6.88.3. - [Release notes](https://github.com/HypothesisWorks/hypothesis/releases) - [Commits](https://github.com/HypothesisWorks/hypothesis/compare/hypothesis-python-6.88.1...hypothesis-python-6.88.3) --- updated-dependencies: - dependency-name: hypothesis dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- dependencies/default/constraints.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies/default/constraints.txt b/dependencies/default/constraints.txt index 7fc94d02..e3e5f1cc 100644 --- a/dependencies/default/constraints.txt +++ b/dependencies/default/constraints.txt @@ -3,7 +3,7 @@ attrs==23.1.0 coverage==7.3.2 exceptiongroup==1.1.3 flaky==3.7.0 -hypothesis==6.88.1 +hypothesis==6.88.3 idna==3.4 importlib-metadata==6.8.0 iniconfig==2.0.0 From 973eaa45e1d6e63ed08bd0620268c5fc12d4c1c5 Mon Sep 17 00:00:00 2001 From: Michael Seifert Date: Tue, 31 Oct 2023 15:45:17 +0100 Subject: [PATCH 095/151] [test] Remove pytest-trio from test dependencies. trio v0.22 deprecated trio.MultiError. This adjustment requires at least pytest-trio v0.8.0, [0] which, in turn, depends on pytest>=7.2. Pytest-asyncio currently supports pytest>=7.0. Therefore, we either need to juggle with the constraints in the pytest-min test environment or remove the pytest-trio dependency entirely. I opted for the latter. Pytest-asyncio should be compatible with other plugins, but it should not be responsible to test that compatibility. If every pytest plugin wrote tests against other plugins, we ended up in dependency hell and a lot of work would be done multiple times. There's a dedicated section in pytest for testing plugin integration. This is the right place to do it. [0] https://pytest-trio.readthedocs.io/en/v0.8.0/history.html#misc Signed-off-by: Michael Seifert --- dependencies/default/constraints.txt | 11 +------ dependencies/pytest-min/constraints.txt | 37 ++++++++++++---------- docs/source/reference/changelog.rst | 4 +-- setup.cfg | 1 - tests/modes/test_strict_mode.py | 42 +++++++++++++++++++++++++ tests/trio/test_fixtures.py | 25 --------------- 6 files changed, 65 insertions(+), 55 deletions(-) delete mode 100644 tests/trio/test_fixtures.py diff --git a/dependencies/default/constraints.txt b/dependencies/default/constraints.txt index e3e5f1cc..f8fb873f 100644 --- a/dependencies/default/constraints.txt +++ b/dependencies/default/constraints.txt @@ -1,23 +1,14 @@ -async-generator==1.10 attrs==23.1.0 coverage==7.3.2 exceptiongroup==1.1.3 flaky==3.7.0 hypothesis==6.88.3 -idna==3.4 -importlib-metadata==6.8.0 iniconfig==2.0.0 mypy==1.6.1 mypy-extensions==1.0.0 -outcome==1.3.0.post0 packaging==23.2 pluggy==1.3.0 -pyparsing==3.1.1 pytest==7.4.3 -pytest-trio==0.8.0 -sniffio==1.3.0 sortedcontainers==2.4.0 tomli==2.0.1 -trio==0.23.1 -typed-ast==1.5.5 -zipp==3.17.0 +typing_extensions==4.8.0 diff --git a/dependencies/pytest-min/constraints.txt b/dependencies/pytest-min/constraints.txt index 3cdda3a6..133398b3 100644 --- a/dependencies/pytest-min/constraints.txt +++ b/dependencies/pytest-min/constraints.txt @@ -1,23 +1,26 @@ -argcomplete==2.0.0 -attrs==22.1.0 -certifi==2022.9.24 -charset-normalizer==2.1.1 -elementpath==3.0.2 -exceptiongroup==1.0.0rc9 -hypothesis==6.56.3 +argcomplete==3.1.2 +attrs==23.1.0 +certifi==2023.7.22 +charset-normalizer==3.3.1 +coverage==7.3.2 +elementpath==4.1.5 +exceptiongroup==1.1.3 +flaky==3.7.0 +hypothesis==6.88.3 idna==3.4 -iniconfig==1.1.1 -mock==4.0.3 +iniconfig==2.0.0 +mock==5.1.0 +mypy==1.6.1 +mypy-extensions==1.0.0 nose==1.3.7 -packaging==21.3 -pluggy==1.0.0 +packaging==23.2 +pluggy==1.3.0 py==1.11.0 -Pygments==2.13.0 -pyparsing==3.0.9 +Pygments==2.16.1 pytest==7.0.0 -requests==2.28.1 +requests==2.31.0 sortedcontainers==2.4.0 tomli==2.0.1 -trio==0.21.0 -urllib3==1.26.12 -xmlschema==2.1.1 +typing_extensions==4.8.0 +urllib3==2.0.7 +xmlschema==2.5.0 diff --git a/docs/source/reference/changelog.rst b/docs/source/reference/changelog.rst index 985580e8..39721dc6 100644 --- a/docs/source/reference/changelog.rst +++ b/docs/source/reference/changelog.rst @@ -2,9 +2,9 @@ Changelog ========= -0.22.1 (UNRELEASED) +0.23.0 (UNRELEASED) =================== -- Fixes a bug that broke compatibility with pytest>=7.0,<7.2. `#654 `_ +- Removes pytest-trio from the test dependencies `#620 `_ 0.22.0 (2023-10-31) =================== diff --git a/setup.cfg b/setup.cfg index d027a942..fcd7477b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -48,7 +48,6 @@ testing = hypothesis >= 5.7.1 flaky >= 3.5.0 mypy >= 0.931 - pytest-trio >= 0.7.0 docs = sphinx >= 5.3 sphinx-rtd-theme >= 1.0 diff --git a/tests/modes/test_strict_mode.py b/tests/modes/test_strict_mode.py index 3b6487c7..3afc9f5b 100644 --- a/tests/modes/test_strict_mode.py +++ b/tests/modes/test_strict_mode.py @@ -66,3 +66,45 @@ async def test_a(self, fixture_a): ) result = testdir.runpytest("--asyncio-mode=auto") result.assert_outcomes(passed=1) + + +def test_strict_mode_ignores_unmarked_coroutine(testdir): + testdir.makepyfile( + dedent( + """\ + import pytest + + async def test_anything(): + pass + """ + ) + ) + result = testdir.runpytest("--asyncio-mode=strict", "-W default") + result.assert_outcomes(skipped=1, warnings=1) + result.stdout.fnmatch_lines(["*async def functions are not natively supported*"]) + + +def test_strict_mode_ignores_unmarked_fixture(testdir): + testdir.makepyfile( + dedent( + """\ + import pytest + + # Not using pytest_asyncio.fixture + @pytest.fixture() + async def any_fixture(): + raise RuntimeError() + + async def test_anything(any_fixture): + pass + """ + ) + ) + result = testdir.runpytest("--asyncio-mode=strict", "-W default") + result.assert_outcomes(skipped=1, warnings=2) + result.stdout.fnmatch_lines( + [ + "*async def functions are not natively supported*", + "*coroutine 'any_fixture' was never awaited*", + ], + ) diff --git a/tests/trio/test_fixtures.py b/tests/trio/test_fixtures.py deleted file mode 100644 index 42b28437..00000000 --- a/tests/trio/test_fixtures.py +++ /dev/null @@ -1,25 +0,0 @@ -from textwrap import dedent - - -def test_strict_mode_ignores_trio_fixtures(testdir): - testdir.makepyfile( - dedent( - """\ - import pytest - import pytest_asyncio - import pytest_trio - - pytest_plugins = ["pytest_asyncio", "pytest_trio"] - - @pytest_trio.trio_fixture - async def any_fixture(): - return True - - @pytest.mark.trio - async def test_anything(any_fixture): - pass - """ - ) - ) - result = testdir.runpytest("--asyncio-mode=strict") - result.assert_outcomes(passed=1) From 9d364a8052f8533b6bce28dfa743532d2b0efb16 Mon Sep 17 00:00:00 2001 From: Michael Seifert Date: Fri, 3 Nov 2023 17:21:30 +0100 Subject: [PATCH 096/151] [feat] Introduce the event_loop_policy fixture. Signed-off-by: Michael Seifert --- docs/source/how-to-guides/uvloop.rst | 11 +- docs/source/reference/changelog.rst | 1 + .../fixtures/event_loop_policy_example.py | 17 ++ .../event_loop_policy_parametrized_example.py | 23 +++ docs/source/reference/fixtures/index.rst | 18 +++ ...oop_custom_policies_strict_mode_example.py | 8 +- ..._loop_custom_policy_strict_mode_example.py | 7 +- pytest_asyncio/plugin.py | 39 +++-- tests/markers/test_class_marker.py | 17 +- tests/markers/test_function_scope.py | 147 ++++++++++++++++++ tests/markers/test_module_marker.py | 19 ++- 11 files changed, 281 insertions(+), 26 deletions(-) create mode 100644 docs/source/reference/fixtures/event_loop_policy_example.py create mode 100644 docs/source/reference/fixtures/event_loop_policy_parametrized_example.py create mode 100644 tests/markers/test_function_scope.py diff --git a/docs/source/how-to-guides/uvloop.rst b/docs/source/how-to-guides/uvloop.rst index 14353365..889c0f9d 100644 --- a/docs/source/how-to-guides/uvloop.rst +++ b/docs/source/how-to-guides/uvloop.rst @@ -2,12 +2,17 @@ How to test with uvloop ======================= +Redefinig 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: Replace the default event loop policy in your *conftest.py:* .. code-block:: python - import asyncio - + import pytest import uvloop - asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) + + @pytest.fixture(scope="session") + def event_loop_policy(): + return uvloop.EventLoopPolicy() + +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. diff --git a/docs/source/reference/changelog.rst b/docs/source/reference/changelog.rst index 39721dc6..a3fab017 100644 --- a/docs/source/reference/changelog.rst +++ b/docs/source/reference/changelog.rst @@ -5,6 +5,7 @@ Changelog 0.23.0 (UNRELEASED) =================== - Removes pytest-trio from the test dependencies `#620 `_ +- Introduces the *event_loop_policy* fixture which allows testing with non-default or multiple event loops `#662 `_ 0.22.0 (2023-10-31) =================== diff --git a/docs/source/reference/fixtures/event_loop_policy_example.py b/docs/source/reference/fixtures/event_loop_policy_example.py new file mode 100644 index 00000000..cfd7ab96 --- /dev/null +++ b/docs/source/reference/fixtures/event_loop_policy_example.py @@ -0,0 +1,17 @@ +import asyncio + +import pytest + + +class CustomEventLoopPolicy(asyncio.DefaultEventLoopPolicy): + pass + + +@pytest.fixture(scope="module") +def event_loop_policy(request): + return CustomEventLoopPolicy() + + +@pytest.mark.asyncio(scope="module") +async def test_uses_custom_event_loop_policy(): + assert isinstance(asyncio.get_event_loop_policy(), CustomEventLoopPolicy) diff --git a/docs/source/reference/fixtures/event_loop_policy_parametrized_example.py b/docs/source/reference/fixtures/event_loop_policy_parametrized_example.py new file mode 100644 index 00000000..1560889b --- /dev/null +++ b/docs/source/reference/fixtures/event_loop_policy_parametrized_example.py @@ -0,0 +1,23 @@ +import asyncio +from asyncio import DefaultEventLoopPolicy + +import pytest + + +class CustomEventLoopPolicy(asyncio.DefaultEventLoopPolicy): + pass + + +@pytest.fixture( + params=( + DefaultEventLoopPolicy(), + CustomEventLoopPolicy(), + ), +) +def event_loop_policy(request): + return request.param + + +@pytest.mark.asyncio +async def test_uses_custom_event_loop_policy(): + assert isinstance(asyncio.get_event_loop_policy(), DefaultEventLoopPolicy) diff --git a/docs/source/reference/fixtures/index.rst b/docs/source/reference/fixtures/index.rst index c0bfd300..354077f5 100644 --- a/docs/source/reference/fixtures/index.rst +++ b/docs/source/reference/fixtures/index.rst @@ -22,6 +22,24 @@ If you need to change the type of the event loop, prefer setting a custom event If the ``pytest.mark.asyncio`` decorator is applied to a test function, the ``event_loop`` fixture will be requested automatically by the test function. +event_loop_policy +================= +Returns the event loop policy used to create asyncio event loops. +The default return value is *asyncio.get_event_loop_policy().* + +This fixture can be overridden when a different event loop policy should be used. + +.. include:: event_loop_policy_example.py + :code: python + +Multiple policies can be provided via fixture parameters. +The fixture is automatically applied to all pytest-asyncio tests. +Therefore, all tests managed by pytest-asyncio are run once for each fixture parameter. +The following example runs the test with different event loop policies. + +.. include:: event_loop_policy_parametrized_example.py + :code: python + unused_tcp_port =============== Finds and yields a single unused TCP port on the localhost interface. Useful for diff --git a/docs/source/reference/markers/class_scoped_loop_custom_policies_strict_mode_example.py b/docs/source/reference/markers/class_scoped_loop_custom_policies_strict_mode_example.py index 85ccc3a1..afb4cc8a 100644 --- a/docs/source/reference/markers/class_scoped_loop_custom_policies_strict_mode_example.py +++ b/docs/source/reference/markers/class_scoped_loop_custom_policies_strict_mode_example.py @@ -3,12 +3,16 @@ import pytest -@pytest.mark.asyncio_event_loop( - policy=[ +@pytest.fixture( + params=[ asyncio.DefaultEventLoopPolicy(), asyncio.DefaultEventLoopPolicy(), ] ) +def event_loop_policy(request): + return request.param + + class TestWithDifferentLoopPolicies: @pytest.mark.asyncio async def test_parametrized_loop(self): diff --git a/docs/source/reference/markers/class_scoped_loop_custom_policy_strict_mode_example.py b/docs/source/reference/markers/class_scoped_loop_custom_policy_strict_mode_example.py index b4525ca4..e5cc6238 100644 --- a/docs/source/reference/markers/class_scoped_loop_custom_policy_strict_mode_example.py +++ b/docs/source/reference/markers/class_scoped_loop_custom_policy_strict_mode_example.py @@ -7,7 +7,12 @@ class CustomEventLoopPolicy(asyncio.DefaultEventLoopPolicy): pass -@pytest.mark.asyncio_event_loop(policy=CustomEventLoopPolicy()) +@pytest.fixture(scope="class") +def event_loop_policy(request): + return CustomEventLoopPolicy() + + +@pytest.mark.asyncio_event_loop class TestUsesCustomEventLoopPolicy: @pytest.mark.asyncio async def test_uses_custom_event_loop_policy(self): diff --git a/pytest_asyncio/plugin.py b/pytest_asyncio/plugin.py index b39a47b8..a6554e22 100644 --- a/pytest_asyncio/plugin.py +++ b/pytest_asyncio/plugin.py @@ -6,6 +6,7 @@ import inspect import socket import warnings +from asyncio import AbstractEventLoopPolicy from textwrap import dedent from typing import ( Any, @@ -553,12 +554,6 @@ def pytest_collectstart(collector: pytest.Collector): for mark in marks: if not mark.name == "asyncio_event_loop": continue - event_loop_policy = mark.kwargs.get("policy", asyncio.get_event_loop_policy()) - policy_params = ( - event_loop_policy - if isinstance(event_loop_policy, Iterable) - else (event_loop_policy,) - ) # There seem to be issues when a fixture is shadowed by another fixture # and both differ in their params. @@ -573,14 +568,12 @@ def pytest_collectstart(collector: pytest.Collector): @pytest.fixture( scope="class" if isinstance(collector, pytest.Class) else "module", name=event_loop_fixture_id, - params=policy_params, - ids=tuple(type(policy).__name__ for policy in policy_params), ) def scoped_event_loop( *args, # Function needs to accept "cls" when collected by pytest.Class - request, + event_loop_policy, ) -> Iterator[asyncio.AbstractEventLoop]: - new_loop_policy = request.param + new_loop_policy = event_loop_policy old_loop_policy = asyncio.get_event_loop_policy() old_loop = asyncio.get_event_loop() asyncio.set_event_loop_policy(new_loop_policy) @@ -675,6 +668,7 @@ def pytest_fixture_setup( _add_finalizers( fixturedef, _close_event_loop, + _restore_event_loop_policy(asyncio.get_event_loop_policy()), _provide_clean_event_loop, ) outcome = yield @@ -749,6 +743,23 @@ def _close_event_loop() -> None: loop.close() +def _restore_event_loop_policy(previous_policy) -> Callable[[], None]: + def _restore_policy(): + # Close any event loop associated with the old loop policy + # to avoid ResourceWarnings in the _provide_clean_event_loop finalizer + try: + with warnings.catch_warnings(): + warnings.simplefilter("ignore", DeprecationWarning) + loop = previous_policy.get_event_loop() + except RuntimeError: + loop = None + if loop: + loop.close() + asyncio.set_event_loop_policy(previous_policy) + + return _restore_policy + + def _provide_clean_event_loop() -> None: # At this point, the event loop for the current thread is closed. # When a user calls asyncio.get_event_loop(), they will get a closed loop. @@ -856,6 +867,8 @@ def pytest_runtest_setup(item: pytest.Item) -> None: @pytest.fixture def event_loop(request: FixtureRequest) -> Iterator[asyncio.AbstractEventLoop]: """Create an instance of the default event loop for each test case.""" + new_loop_policy = request.getfixturevalue(event_loop_policy.__name__) + asyncio.set_event_loop_policy(new_loop_policy) loop = asyncio.get_event_loop_policy().new_event_loop() # Add a magic value to the event loop, so pytest-asyncio can determine if the # event_loop fixture was overridden. Other implementations of event_loop don't @@ -867,6 +880,12 @@ def event_loop(request: FixtureRequest) -> Iterator[asyncio.AbstractEventLoop]: loop.close() +@pytest.fixture(scope="session", autouse=True) +def event_loop_policy() -> AbstractEventLoopPolicy: + """Return an instance of the policy used to create asyncio event loops.""" + return asyncio.get_event_loop_policy() + + def _unused_port(socket_type: int) -> int: """Find an unused localhost port from 1024-65535 and return it.""" with contextlib.closing(socket.socket(type=socket_type)) as sock: diff --git a/tests/markers/test_class_marker.py b/tests/markers/test_class_marker.py index f8cf4ca0..e06a34d8 100644 --- a/tests/markers/test_class_marker.py +++ b/tests/markers/test_class_marker.py @@ -140,8 +140,12 @@ def test_asyncio_event_loop_mark_allows_specifying_the_loop_policy( class CustomEventLoopPolicy(asyncio.DefaultEventLoopPolicy): pass - @pytest.mark.asyncio_event_loop(policy=CustomEventLoopPolicy()) - class TestUsesCustomEventLoopPolicy: + @pytest.mark.asyncio_event_loop + class TestUsesCustomEventLoop: + + @pytest.fixture(scope="class") + def event_loop_policy(self): + return CustomEventLoopPolicy() @pytest.mark.asyncio async def test_uses_custom_event_loop_policy(self): @@ -173,15 +177,18 @@ def test_asyncio_event_loop_mark_allows_specifying_multiple_loop_policies( import pytest - @pytest.mark.asyncio_event_loop( - policy=[ + @pytest.fixture( + params=[ asyncio.DefaultEventLoopPolicy(), asyncio.DefaultEventLoopPolicy(), ] ) + def event_loop_policy(request): + return request.param + class TestWithDifferentLoopPolicies: @pytest.mark.asyncio - async def test_parametrized_loop(self): + async def test_parametrized_loop(self, request): pass """ ) diff --git a/tests/markers/test_function_scope.py b/tests/markers/test_function_scope.py new file mode 100644 index 00000000..be45e5dd --- /dev/null +++ b/tests/markers/test_function_scope.py @@ -0,0 +1,147 @@ +from textwrap import dedent + +from pytest import Pytester + + +def test_asyncio_mark_provides_function_scoped_loop_strict_mode(pytester: Pytester): + pytester.makepyfile( + dedent( + """\ + import asyncio + import pytest + + pytestmark = pytest.mark.asyncio + + loop: asyncio.AbstractEventLoop + + async def test_remember_loop(): + global loop + loop = asyncio.get_running_loop() + + async def test_does_not_run_in_same_loop(): + global loop + assert asyncio.get_running_loop() is not loop + """ + ) + ) + result = pytester.runpytest("--asyncio-mode=strict") + result.assert_outcomes(passed=2) + + +def test_function_scope_supports_explicit_event_loop_fixture_request( + pytester: Pytester, +): + pytester.makepyfile( + dedent( + """\ + import pytest + + pytestmark = pytest.mark.asyncio + + async def test_remember_loop(event_loop): + pass + """ + ) + ) + result = pytester.runpytest("--asyncio-mode=strict") + result.assert_outcomes(passed=1, warnings=1) + result.stdout.fnmatch_lines( + '*is asynchronous and explicitly requests the "event_loop" fixture*' + ) + + +def test_asyncio_mark_respects_the_loop_policy( + pytester: Pytester, +): + pytester.makepyfile( + dedent( + """\ + import asyncio + import pytest + + pytestmark = pytest.mark.asyncio + + class CustomEventLoopPolicy(asyncio.DefaultEventLoopPolicy): + pass + + @pytest.fixture(scope="function") + def event_loop_policy(): + return CustomEventLoopPolicy() + + async def test_uses_custom_event_loop_policy(): + assert isinstance( + asyncio.get_event_loop_policy(), + CustomEventLoopPolicy, + ) + """ + ), + ) + result = pytester.runpytest("--asyncio-mode=strict") + result.assert_outcomes(passed=1) + + +def test_asyncio_mark_respects_parametrized_loop_policies( + pytester: Pytester, +): + pytester.makepyfile( + dedent( + """\ + import asyncio + + import pytest + + pytestmark = pytest.mark.asyncio + + class CustomEventLoopPolicy(asyncio.DefaultEventLoopPolicy): + pass + + @pytest.fixture( + scope="module", + params=[ + CustomEventLoopPolicy(), + CustomEventLoopPolicy(), + ], + ) + def event_loop_policy(request): + return request.param + + async def test_parametrized_loop(): + assert isinstance( + asyncio.get_event_loop_policy(), + CustomEventLoopPolicy, + ) + """ + ) + ) + result = pytester.runpytest_subprocess("--asyncio-mode=strict") + result.assert_outcomes(passed=2) + + +def test_asyncio_mark_provides_function_scoped_loop_to_fixtures( + pytester: Pytester, +): + pytester.makepyfile( + dedent( + """\ + import asyncio + + import pytest + import pytest_asyncio + + pytestmark = pytest.mark.asyncio + + loop: asyncio.AbstractEventLoop + + @pytest_asyncio.fixture + async def my_fixture(): + global loop + loop = asyncio.get_running_loop() + + async def test_runs_is_same_loop_as_fixture(my_fixture): + global loop + assert asyncio.get_running_loop() is loop + """ + ) + ) + result = pytester.runpytest_subprocess("--asyncio-mode=strict") + result.assert_outcomes(passed=1) diff --git a/tests/markers/test_module_marker.py b/tests/markers/test_module_marker.py index f6cd8762..882f51af 100644 --- a/tests/markers/test_module_marker.py +++ b/tests/markers/test_module_marker.py @@ -157,7 +157,11 @@ class CustomEventLoopPolicy(asyncio.DefaultEventLoopPolicy): from .custom_policy import CustomEventLoopPolicy - pytestmark = pytest.mark.asyncio_event_loop(policy=CustomEventLoopPolicy()) + pytestmark = pytest.mark.asyncio_event_loop + + @pytest.fixture(scope="module") + def event_loop_policy(): + return CustomEventLoopPolicy() @pytest.mark.asyncio async def test_uses_custom_event_loop_policy(): @@ -178,7 +182,7 @@ async def test_uses_custom_event_loop_policy(): async def test_does_not_use_custom_event_loop_policy(): assert not isinstance( asyncio.get_event_loop_policy(), - CustomEventLoopPolicy, + CustomEventLoopPolicy, ) """ ), @@ -197,12 +201,17 @@ def test_asyncio_event_loop_mark_allows_specifying_multiple_loop_policies( import pytest - pytestmark = pytest.mark.asyncio_event_loop( - policy=[ + pytestmark = pytest.mark.asyncio_event_loop + + @pytest.fixture( + scope="module", + params=[ asyncio.DefaultEventLoopPolicy(), asyncio.DefaultEventLoopPolicy(), - ] + ], ) + def event_loop_policy(request): + return request.param @pytest.mark.asyncio async def test_parametrized_loop(): From 4c5e66049890a792ed48fe5b44a5668745d141a5 Mon Sep 17 00:00:00 2001 From: Michael Seifert Date: Wed, 8 Nov 2023 13:02:17 +0100 Subject: [PATCH 097/151] [docs] Added how-to guide for testing with multiple loops. Signed-off-by: Michael Seifert --- docs/source/how-to-guides/index.rst | 1 + docs/source/how-to-guides/multiple_loops.rst | 10 ++++++++ .../how-to-guides/multiple_loops_example.py | 24 +++++++++++++++++++ 3 files changed, 35 insertions(+) create mode 100644 docs/source/how-to-guides/multiple_loops.rst create mode 100644 docs/source/how-to-guides/multiple_loops_example.py diff --git a/docs/source/how-to-guides/index.rst b/docs/source/how-to-guides/index.rst index 922fac91..5bcb3be7 100644 --- a/docs/source/how-to-guides/index.rst +++ b/docs/source/how-to-guides/index.rst @@ -5,6 +5,7 @@ How-To Guides .. toctree:: :hidden: + multiple_loops uvloop This section of the documentation provides code snippets and recipes to accomplish specific tasks with pytest-asyncio. diff --git a/docs/source/how-to-guides/multiple_loops.rst b/docs/source/how-to-guides/multiple_loops.rst new file mode 100644 index 00000000..3453c49f --- /dev/null +++ b/docs/source/how-to-guides/multiple_loops.rst @@ -0,0 +1,10 @@ +====================================== +How to test with different event loops +====================================== + +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: + +.. include:: multiple_loops_example.py + :code: python + +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. diff --git a/docs/source/how-to-guides/multiple_loops_example.py b/docs/source/how-to-guides/multiple_loops_example.py new file mode 100644 index 00000000..a4c7a01c --- /dev/null +++ b/docs/source/how-to-guides/multiple_loops_example.py @@ -0,0 +1,24 @@ +import asyncio +from asyncio import DefaultEventLoopPolicy + +import pytest + + +class CustomEventLoopPolicy(DefaultEventLoopPolicy): + pass + + +@pytest.fixture( + scope="session", + params=( + CustomEventLoopPolicy(), + CustomEventLoopPolicy(), + ), +) +def event_loop_policy(request): + return request.param + + +@pytest.mark.asyncio +async def test_uses_custom_event_loop_policy(): + assert isinstance(asyncio.get_event_loop_policy(), CustomEventLoopPolicy) From a7b1c39be72e976547618a4e543123aafce1f73b Mon Sep 17 00:00:00 2001 From: Michael Seifert Date: Sun, 5 Nov 2023 14:55:03 +0100 Subject: [PATCH 098/151] [feat!] Replaced the asyncio_event_loop marker with an optional "scope" kwarg to the asyncio mark. Signed-off-by: Michael Seifert --- docs/source/reference/changelog.rst | 9 +- docs/source/reference/fixtures/index.rst | 6 +- .../class_scoped_loop_auto_mode_example.py | 14 -- ..._loop_custom_policy_strict_mode_example.py | 19 -- .../class_scoped_loop_strict_mode_example.py | 4 +- ...d_loop_with_fixture_strict_mode_example.py | 3 +- ...ped_loop_pytestmark_strict_mode_example.py | 10 + ...unction_scoped_loop_strict_mode_example.py | 8 + docs/source/reference/markers/index.rst | 58 ++---- ...module_scoped_loop_strict_mode_example.py} | 2 +- .../pytestmark_asyncio_strict_mode_example.py | 11 - pytest_asyncio/plugin.py | 197 ++++++++++-------- ...st_class_marker.py => test_class_scope.py} | 74 ++++--- ..._module_marker.py => test_module_scope.py} | 58 +----- tox.ini | 12 +- 15 files changed, 209 insertions(+), 276 deletions(-) delete mode 100644 docs/source/reference/markers/class_scoped_loop_auto_mode_example.py delete mode 100644 docs/source/reference/markers/class_scoped_loop_custom_policy_strict_mode_example.py create mode 100644 docs/source/reference/markers/function_scoped_loop_pytestmark_strict_mode_example.py create mode 100644 docs/source/reference/markers/function_scoped_loop_strict_mode_example.py rename docs/source/reference/markers/{module_scoped_loop_auto_mode_example.py => module_scoped_loop_strict_mode_example.py} (88%) delete mode 100644 docs/source/reference/markers/pytestmark_asyncio_strict_mode_example.py rename tests/markers/{test_class_marker.py => test_class_scope.py} (80%) rename tests/markers/{test_module_marker.py => test_module_scope.py} (75%) diff --git a/docs/source/reference/changelog.rst b/docs/source/reference/changelog.rst index a3fab017..e8ec016c 100644 --- a/docs/source/reference/changelog.rst +++ b/docs/source/reference/changelog.rst @@ -4,11 +4,18 @@ Changelog 0.23.0 (UNRELEASED) =================== -- Removes pytest-trio from the test dependencies `#620 `_ +This release is backwards-compatible with v0.21. +Changes are non-breaking, unless you upgrade from v0.22. + +- BREAKING: The *asyncio_event_loop* mark has been removed. Class-scoped and module-scoped event loops can be requested + via the *scope* keyword argument to the _asyncio_ mark. - Introduces the *event_loop_policy* fixture which allows testing with non-default or multiple event loops `#662 `_ +- Removes pytest-trio from the test dependencies `#620 `_ 0.22.0 (2023-10-31) =================== +This release has been yanked from PyPI due to fundamental issues with the _asyncio_event_loop_ mark. + - Class-scoped and module-scoped event loops can be requested via the _asyncio_event_loop_ mark. `#620 `_ - Deprecate redefinition of the `event_loop` fixture. `#587 `_ diff --git a/docs/source/reference/fixtures/index.rst b/docs/source/reference/fixtures/index.rst index 354077f5..7b8dc818 100644 --- a/docs/source/reference/fixtures/index.rst +++ b/docs/source/reference/fixtures/index.rst @@ -6,8 +6,8 @@ event_loop ========== Creates a new asyncio event loop based on the current event loop policy. The new loop is available as the return value of this fixture for synchronous functions, or via `asyncio.get_running_loop `__ for asynchronous functions. -The event loop is closed when the fixture scope ends. The fixture scope defaults -to ``function`` scope. +The event loop is closed when the fixture scope ends. +The fixture scope defaults to ``function`` scope. .. include:: event_loop_example.py :code: python @@ -15,8 +15,6 @@ to ``function`` scope. Note that, when using the ``event_loop`` fixture, you need to interact with the event loop using methods like ``event_loop.run_until_complete``. If you want to *await* code inside your test function, you need to write a coroutine and use it as a test function. The `asyncio <#pytest-mark-asyncio>`__ marker is used to mark coroutines that should be treated as test functions. -If your tests require an asyncio event loop with class or module scope, apply the `asyncio_event_loop mark <./markers.html/#pytest-mark-asyncio-event-loop>`__ to the respective class or module. - If you need to change the type of the event loop, prefer setting a custom event loop policy over redefining the ``event_loop`` fixture. If the ``pytest.mark.asyncio`` decorator is applied to a test function, the ``event_loop`` diff --git a/docs/source/reference/markers/class_scoped_loop_auto_mode_example.py b/docs/source/reference/markers/class_scoped_loop_auto_mode_example.py deleted file mode 100644 index a839e571..00000000 --- a/docs/source/reference/markers/class_scoped_loop_auto_mode_example.py +++ /dev/null @@ -1,14 +0,0 @@ -import asyncio - -import pytest - - -@pytest.mark.asyncio_event_loop -class TestClassScopedLoop: - loop: asyncio.AbstractEventLoop - - async def test_remember_loop(self): - TestClassScopedLoop.loop = asyncio.get_running_loop() - - async def test_this_runs_in_same_loop(self): - assert asyncio.get_running_loop() is TestClassScopedLoop.loop diff --git a/docs/source/reference/markers/class_scoped_loop_custom_policy_strict_mode_example.py b/docs/source/reference/markers/class_scoped_loop_custom_policy_strict_mode_example.py deleted file mode 100644 index e5cc6238..00000000 --- a/docs/source/reference/markers/class_scoped_loop_custom_policy_strict_mode_example.py +++ /dev/null @@ -1,19 +0,0 @@ -import asyncio - -import pytest - - -class CustomEventLoopPolicy(asyncio.DefaultEventLoopPolicy): - pass - - -@pytest.fixture(scope="class") -def event_loop_policy(request): - return CustomEventLoopPolicy() - - -@pytest.mark.asyncio_event_loop -class TestUsesCustomEventLoopPolicy: - @pytest.mark.asyncio - async def test_uses_custom_event_loop_policy(self): - assert isinstance(asyncio.get_event_loop_policy(), CustomEventLoopPolicy) diff --git a/docs/source/reference/markers/class_scoped_loop_strict_mode_example.py b/docs/source/reference/markers/class_scoped_loop_strict_mode_example.py index c33b34b8..38b5689c 100644 --- a/docs/source/reference/markers/class_scoped_loop_strict_mode_example.py +++ b/docs/source/reference/markers/class_scoped_loop_strict_mode_example.py @@ -3,14 +3,12 @@ import pytest -@pytest.mark.asyncio_event_loop +@pytest.mark.asyncio(scope="class") class TestClassScopedLoop: loop: asyncio.AbstractEventLoop - @pytest.mark.asyncio async def test_remember_loop(self): TestClassScopedLoop.loop = asyncio.get_running_loop() - @pytest.mark.asyncio async def test_this_runs_in_same_loop(self): assert asyncio.get_running_loop() is TestClassScopedLoop.loop diff --git a/docs/source/reference/markers/class_scoped_loop_with_fixture_strict_mode_example.py b/docs/source/reference/markers/class_scoped_loop_with_fixture_strict_mode_example.py index c70a4bc6..f912dec9 100644 --- a/docs/source/reference/markers/class_scoped_loop_with_fixture_strict_mode_example.py +++ b/docs/source/reference/markers/class_scoped_loop_with_fixture_strict_mode_example.py @@ -5,7 +5,7 @@ import pytest_asyncio -@pytest.mark.asyncio_event_loop +@pytest.mark.asyncio(scope="class") class TestClassScopedLoop: loop: asyncio.AbstractEventLoop @@ -13,6 +13,5 @@ class TestClassScopedLoop: async def my_fixture(self): TestClassScopedLoop.loop = asyncio.get_running_loop() - @pytest.mark.asyncio async def test_runs_is_same_loop_as_fixture(self, my_fixture): assert asyncio.get_running_loop() is TestClassScopedLoop.loop diff --git a/docs/source/reference/markers/function_scoped_loop_pytestmark_strict_mode_example.py b/docs/source/reference/markers/function_scoped_loop_pytestmark_strict_mode_example.py new file mode 100644 index 00000000..f8e7e717 --- /dev/null +++ b/docs/source/reference/markers/function_scoped_loop_pytestmark_strict_mode_example.py @@ -0,0 +1,10 @@ +import asyncio + +import pytest + +# Marks all test coroutines in this module +pytestmark = pytest.mark.asyncio + + +async def test_runs_in_asyncio_event_loop(): + assert asyncio.get_running_loop() diff --git a/docs/source/reference/markers/function_scoped_loop_strict_mode_example.py b/docs/source/reference/markers/function_scoped_loop_strict_mode_example.py new file mode 100644 index 00000000..e30f73c5 --- /dev/null +++ b/docs/source/reference/markers/function_scoped_loop_strict_mode_example.py @@ -0,0 +1,8 @@ +import asyncio + +import pytest + + +@pytest.mark.asyncio +async def test_runs_in_asyncio_event_loop(): + assert asyncio.get_running_loop() diff --git a/docs/source/reference/markers/index.rst b/docs/source/reference/markers/index.rst index 6c3e5253..b625bbd7 100644 --- a/docs/source/reference/markers/index.rst +++ b/docs/source/reference/markers/index.rst @@ -4,62 +4,34 @@ Markers ``pytest.mark.asyncio`` ======================= -A coroutine or async generator with this marker will be treated as a test function by pytest. The marked function will be executed as an -asyncio task in the event loop provided by the ``event_loop`` fixture. +A coroutine or async generator with this marker is treated as a test function by pytest. +The marked function is executed as an asyncio task in the event loop provided by pytest-asyncio. -In order to make your test code a little more concise, the pytest |pytestmark|_ -feature can be used to mark entire modules or classes with this marker. -Only test coroutines will be affected (by default, coroutines prefixed by -``test_``), so, for example, fixtures are safe to define. - -.. include:: pytestmark_asyncio_strict_mode_example.py - :code: python - -In *auto* mode, the ``pytest.mark.asyncio`` marker can be omitted, the marker is added -automatically to *async* test functions. - - -``pytest.mark.asyncio_event_loop`` -================================== -Test classes or modules with this mark provide a class-scoped or module-scoped asyncio event loop. - -This functionality is orthogonal to the `asyncio` mark. -That means the presence of this mark does not imply that async test functions inside the class or module are collected by pytest-asyncio. -The collection happens automatically in `auto` mode. -However, if you're using strict mode, you still have to apply the `asyncio` mark to your async test functions. - -The following code example uses the `asyncio_event_loop` mark to provide a shared event loop for all tests in `TestClassScopedLoop`: - -.. include:: class_scoped_loop_strict_mode_example.py +.. include:: function_scoped_loop_strict_mode_example.py :code: python -In *auto* mode, the ``pytest.mark.asyncio`` marker can be omitted: +Multiple async tests in a single class or module can be marked using |pytestmark|_. -.. include:: class_scoped_loop_auto_mode_example.py +.. include:: function_scoped_loop_pytestmark_strict_mode_example.py :code: python -Similarly, a module-scoped loop is provided when adding the `asyncio_event_loop` mark to the module: - -.. include:: module_scoped_loop_auto_mode_example.py - :code: python +The ``pytest.mark.asyncio`` marker can be omitted entirely in *auto* mode, where the *asyncio* marker is added automatically to *async* test functions. -The `asyncio_event_loop` mark supports an optional `policy` keyword argument to set the asyncio event loop policy. +By default, each test runs in it's own asyncio event loop. +Multiple tests can share the same event loop by providing a *scope* keyword argument to the *asyncio* mark. +The following code example provides a shared event loop for all tests in `TestClassScopedLoop`: -.. include:: class_scoped_loop_custom_policy_strict_mode_example.py +.. include:: class_scoped_loop_strict_mode_example.py :code: python +Requesting class scope for tests that are not part of a class will give a *UsageError.* +Similar to class-scoped event loops, a module-scoped loop is provided when setting the asyncio mark's scope to *module:* -The ``policy`` keyword argument may also take an iterable of event loop policies. This causes tests under by the `asyncio_event_loop` mark to be parametrized with different policies: - -.. include:: class_scoped_loop_custom_policies_strict_mode_example.py +.. include:: module_scoped_loop_strict_mode_example.py :code: python -If no explicit policy is provided, the mark will use the loop policy returned by ``asyncio.get_event_loop_policy()``. - -Fixtures and tests sharing the same `asyncio_event_loop` mark are executed in the same event loop: - -.. include:: class_scoped_loop_with_fixture_strict_mode_example.py - :code: python +Requesting class scope with the test being part of a class will give a *UsageError*. +The supported scopes are *class*, and *module.* .. |pytestmark| replace:: ``pytestmark`` .. _pytestmark: http://doc.pytest.org/en/latest/example/markers.html#marking-whole-classes-or-modules diff --git a/docs/source/reference/markers/module_scoped_loop_auto_mode_example.py b/docs/source/reference/markers/module_scoped_loop_strict_mode_example.py similarity index 88% rename from docs/source/reference/markers/module_scoped_loop_auto_mode_example.py rename to docs/source/reference/markers/module_scoped_loop_strict_mode_example.py index e38bdeff..221d554e 100644 --- a/docs/source/reference/markers/module_scoped_loop_auto_mode_example.py +++ b/docs/source/reference/markers/module_scoped_loop_strict_mode_example.py @@ -2,7 +2,7 @@ import pytest -pytestmark = pytest.mark.asyncio_event_loop +pytestmark = pytest.mark.asyncio(scope="module") loop: asyncio.AbstractEventLoop diff --git a/docs/source/reference/markers/pytestmark_asyncio_strict_mode_example.py b/docs/source/reference/markers/pytestmark_asyncio_strict_mode_example.py deleted file mode 100644 index f1465728..00000000 --- a/docs/source/reference/markers/pytestmark_asyncio_strict_mode_example.py +++ /dev/null @@ -1,11 +0,0 @@ -import asyncio - -import pytest - -# All test coroutines will be treated as marked. -pytestmark = pytest.mark.asyncio - - -async def test_example(): - """No marker!""" - await asyncio.sleep(0) diff --git a/pytest_asyncio/plugin.py b/pytest_asyncio/plugin.py index a6554e22..8c91c3e3 100644 --- a/pytest_asyncio/plugin.py +++ b/pytest_asyncio/plugin.py @@ -26,14 +26,15 @@ ) import pytest -from _pytest.mark.structures import get_unpacked_marks from pytest import ( + Class, Collector, Config, FixtureRequest, Function, Item, Metafunc, + Module, Parser, PytestCollectionWarning, PytestDeprecationWarning, @@ -185,12 +186,6 @@ def pytest_configure(config: Config) -> None: "mark the test as a coroutine, it will be " "run using an asyncio event loop", ) - config.addinivalue_line( - "markers", - "asyncio_event_loop: " - "Provides an asyncio event loop in the scope of the marked test " - "class or module", - ) @pytest.hookimpl(tryfirst=True) @@ -207,11 +202,13 @@ def _preprocess_async_fixtures( config = collector.config asyncio_mode = _get_asyncio_mode(config) fixturemanager = config.pluginmanager.get_plugin("funcmanage") - event_loop_fixture_id = "event_loop" - for node, mark in collector.iter_markers_with_node("asyncio_event_loop"): - event_loop_fixture_id = node.stash.get(_event_loop_fixture_id, None) - if event_loop_fixture_id: - break + marker = collector.get_closest_marker("asyncio") + scope = marker.kwargs.get("scope", "function") if marker else "function" + if scope == "function": + event_loop_fixture_id = "event_loop" + else: + event_loop_node = _retrieve_scope_root(collector, scope) + event_loop_fixture_id = event_loop_node.stash.get(_event_loop_fixture_id, None) for fixtures in fixturemanager._arg2fixturedefs.values(): for fixturedef in fixtures: func = fixturedef.func @@ -548,48 +545,40 @@ def pytest_pycollect_makeitem_convert_async_functions_to_subclass( def pytest_collectstart(collector: pytest.Collector): if not isinstance(collector, (pytest.Class, pytest.Module)): return - # pytest.Collector.own_markers is empty at this point, - # so we rely on _pytest.mark.structures.get_unpacked_marks - marks = get_unpacked_marks(collector.obj) - for mark in marks: - if not mark.name == "asyncio_event_loop": - continue - - # There seem to be issues when a fixture is shadowed by another fixture - # and both differ in their params. - # https://github.com/pytest-dev/pytest/issues/2043 - # https://github.com/pytest-dev/pytest/issues/11350 - # As such, we assign a unique name for each event_loop fixture. - # The fixture name is stored in the collector's Stash, so it can - # be injected when setting up the test - event_loop_fixture_id = f"{collector.nodeid}::" - collector.stash[_event_loop_fixture_id] = event_loop_fixture_id - - @pytest.fixture( - scope="class" if isinstance(collector, pytest.Class) else "module", - name=event_loop_fixture_id, - ) - def scoped_event_loop( - *args, # Function needs to accept "cls" when collected by pytest.Class - event_loop_policy, - ) -> Iterator[asyncio.AbstractEventLoop]: - new_loop_policy = event_loop_policy - old_loop_policy = asyncio.get_event_loop_policy() - old_loop = asyncio.get_event_loop() - asyncio.set_event_loop_policy(new_loop_policy) - loop = asyncio.new_event_loop() - asyncio.set_event_loop(loop) - yield loop - loop.close() - asyncio.set_event_loop_policy(old_loop_policy) - asyncio.set_event_loop(old_loop) + # There seem to be issues when a fixture is shadowed by another fixture + # and both differ in their params. + # https://github.com/pytest-dev/pytest/issues/2043 + # https://github.com/pytest-dev/pytest/issues/11350 + # As such, we assign a unique name for each event_loop fixture. + # The fixture name is stored in the collector's Stash, so it can + # be injected when setting up the test + event_loop_fixture_id = f"{collector.nodeid}::" + collector.stash[_event_loop_fixture_id] = event_loop_fixture_id + + @pytest.fixture( + scope="class" if isinstance(collector, pytest.Class) else "module", + name=event_loop_fixture_id, + ) + def scoped_event_loop( + *args, # Function needs to accept "cls" when collected by pytest.Class + event_loop_policy, + ) -> Iterator[asyncio.AbstractEventLoop]: + new_loop_policy = event_loop_policy + old_loop_policy = asyncio.get_event_loop_policy() + old_loop = asyncio.get_event_loop() + asyncio.set_event_loop_policy(new_loop_policy) + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + yield loop + loop.close() + asyncio.set_event_loop_policy(old_loop_policy) + asyncio.set_event_loop(old_loop) - # @pytest.fixture does not register the fixture anywhere, so pytest doesn't - # know it exists. We work around this by attaching the fixture function to the - # collected Python class, where it will be picked up by pytest.Class.collect() - # or pytest.Module.collect(), respectively - collector.obj.__pytest_asyncio_scoped_event_loop = scoped_event_loop - break + # @pytest.fixture does not register the fixture anywhere, so pytest doesn't + # know it exists. We work around this by attaching the fixture function to the + # collected Python class, where it will be picked up by pytest.Class.collect() + # or pytest.Module.collect(), respectively + collector.obj.__pytest_asyncio_scoped_event_loop = scoped_event_loop def pytest_collection_modifyitems( @@ -608,7 +597,9 @@ def pytest_collection_modifyitems( if _get_asyncio_mode(config) != Mode.AUTO: return for item in items: - if isinstance(item, PytestAsyncioFunction): + if isinstance(item, PytestAsyncioFunction) and not item.get_closest_marker( + "asyncio" + ): item.add_marker("asyncio") @@ -618,40 +609,47 @@ def pytest_collection_modifyitems( %s:%d Replacing the event_loop fixture with a custom implementation is deprecated and will lead to errors in the future. - If you want to request an asyncio event loop with a class or module scope, - please attach the asyncio_event_loop mark to the respective class or module. + If you want to request an asyncio event loop with a scope other than function + scope, use the "scope" argument to the asyncio mark when marking the tests. + If you want to return different types of event loops, use the event_loop_policy + fixture. """ ) @pytest.hookimpl(tryfirst=True) def pytest_generate_tests(metafunc: Metafunc) -> None: - for event_loop_provider_node, _ in metafunc.definition.iter_markers_with_node( - "asyncio_event_loop" - ): - event_loop_fixture_id = event_loop_provider_node.stash.get( - _event_loop_fixture_id, None - ) - if event_loop_fixture_id: - # This specific fixture name may already be in metafunc.argnames, if this - # test indirectly depends on the fixture. For example, this is the case - # when the test depends on an async fixture, both of which share the same - # asyncio_event_loop mark. - if event_loop_fixture_id in metafunc.fixturenames: - continue - fixturemanager = metafunc.config.pluginmanager.get_plugin("funcmanage") - if "event_loop" in metafunc.fixturenames: - raise MultipleEventLoopsRequestedError( - _MULTIPLE_LOOPS_REQUESTED_ERROR - % (metafunc.definition.nodeid, event_loop_provider_node.nodeid), - ) - # Add the scoped event loop fixture to Metafunc's list of fixture names and - # fixturedefs and leave the actual parametrization to pytest - metafunc.fixturenames.insert(0, event_loop_fixture_id) - metafunc._arg2fixturedefs[ - event_loop_fixture_id - ] = fixturemanager._arg2fixturedefs[event_loop_fixture_id] - break + marker = metafunc.definition.get_closest_marker("asyncio") + if not marker: + return + scope = marker.kwargs.get("scope", "function") + if scope == "function": + return + event_loop_node = _retrieve_scope_root(metafunc.definition, scope) + event_loop_fixture_id = event_loop_node.stash.get(_event_loop_fixture_id, None) + + if event_loop_fixture_id: + # This specific fixture name may already be in metafunc.argnames, if this + # test indirectly depends on the fixture. For example, this is the case + # when the test depends on an async fixture, both of which share the same + # event loop fixture mark. + if event_loop_fixture_id in metafunc.fixturenames: + return + fixturemanager = metafunc.config.pluginmanager.get_plugin("funcmanage") + if "event_loop" in metafunc.fixturenames: + raise MultipleEventLoopsRequestedError( + _MULTIPLE_LOOPS_REQUESTED_ERROR.format( + test_name=metafunc.definition.nodeid, + scope=scope, + scoped_loop_node=event_loop_node.nodeid, + ), + ) + # Add the scoped event loop fixture to Metafunc's list of fixture names and + # fixturedefs and leave the actual parametrization to pytest + metafunc.fixturenames.insert(0, event_loop_fixture_id) + metafunc._arg2fixturedefs[ + event_loop_fixture_id + ] = fixturemanager._arg2fixturedefs[event_loop_fixture_id] @pytest.hookimpl(hookwrapper=True) @@ -831,11 +829,11 @@ def inner(*args, **kwargs): _MULTIPLE_LOOPS_REQUESTED_ERROR = dedent( """\ Multiple asyncio event loops with different scopes have been requested - by %s. The test explicitly requests the event_loop fixture, while another - event loop is provided by %s. + by {test_name}. The test explicitly requests the event_loop fixture, while + another event loop with {scope} scope is provided by {scoped_loop_node}. Remove "event_loop" from the requested fixture in your test to run the test - in a larger-scoped event loop or remove the "asyncio_event_loop" mark to run - the test in a function-scoped event loop. + in a {scope}-scoped event loop or remove the scope argument from the "asyncio" + mark to run the test in a function-scoped event loop. """ ) @@ -844,11 +842,12 @@ def pytest_runtest_setup(item: pytest.Item) -> None: marker = item.get_closest_marker("asyncio") if marker is None: return - event_loop_fixture_id = "event_loop" - for node, mark in item.iter_markers_with_node("asyncio_event_loop"): - event_loop_fixture_id = node.stash.get(_event_loop_fixture_id, None) - if event_loop_fixture_id: - break + scope = marker.kwargs.get("scope", "function") + if scope != "function": + parent_node = _retrieve_scope_root(item, scope) + event_loop_fixture_id = parent_node.stash[_event_loop_fixture_id] + else: + event_loop_fixture_id = "event_loop" fixturenames = item.fixturenames # type: ignore[attr-defined] # inject an event loop fixture for all async tests if "event_loop" in fixturenames: @@ -864,6 +863,22 @@ def pytest_runtest_setup(item: pytest.Item) -> None: ) +def _retrieve_scope_root(item: Union[Collector, Item], scope: str) -> Collector: + node_type_by_scope = { + "class": Class, + "module": Module, + } + scope_root_type = node_type_by_scope[scope] + for node in reversed(item.listchain()): + if isinstance(node, scope_root_type): + return node + error_message = ( + f"{item.name} is marked to be run in an event loop with scope {scope}, " + f"but is not part of any {scope}." + ) + raise pytest.UsageError(error_message) + + @pytest.fixture def event_loop(request: FixtureRequest) -> Iterator[asyncio.AbstractEventLoop]: """Create an instance of the default event loop for each test case.""" diff --git a/tests/markers/test_class_marker.py b/tests/markers/test_class_scope.py similarity index 80% rename from tests/markers/test_class_marker.py rename to tests/markers/test_class_scope.py index e06a34d8..33e5d2db 100644 --- a/tests/markers/test_class_marker.py +++ b/tests/markers/test_class_scope.py @@ -26,7 +26,7 @@ def sample_fixture(): return None -def test_asyncio_event_loop_mark_provides_class_scoped_loop_strict_mode( +def test_asyncio_mark_provides_class_scoped_loop_when_applied_to_functions( pytester: pytest.Pytester, ): pytester.makepyfile( @@ -35,15 +35,14 @@ def test_asyncio_event_loop_mark_provides_class_scoped_loop_strict_mode( import asyncio import pytest - @pytest.mark.asyncio_event_loop class TestClassScopedLoop: loop: asyncio.AbstractEventLoop - @pytest.mark.asyncio + @pytest.mark.asyncio(scope="class") async def test_remember_loop(self): TestClassScopedLoop.loop = asyncio.get_running_loop() - @pytest.mark.asyncio + @pytest.mark.asyncio(scope="class") async def test_this_runs_in_same_loop(self): assert asyncio.get_running_loop() is TestClassScopedLoop.loop """ @@ -53,7 +52,7 @@ async def test_this_runs_in_same_loop(self): result.assert_outcomes(passed=2) -def test_asyncio_event_loop_mark_provides_class_scoped_loop_auto_mode( +def test_asyncio_mark_provides_class_scoped_loop_when_applied_to_class( pytester: pytest.Pytester, ): pytester.makepyfile( @@ -62,7 +61,7 @@ def test_asyncio_event_loop_mark_provides_class_scoped_loop_auto_mode( import asyncio import pytest - @pytest.mark.asyncio_event_loop + @pytest.mark.asyncio(scope="class") class TestClassScopedLoop: loop: asyncio.AbstractEventLoop @@ -74,61 +73,59 @@ async def test_this_runs_in_same_loop(self): """ ) ) - result = pytester.runpytest("--asyncio-mode=auto") + result = pytester.runpytest("--asyncio-mode=strict") result.assert_outcomes(passed=2) -def test_asyncio_event_loop_mark_is_inherited_to_subclasses(pytester: pytest.Pytester): +def test_asyncio_mark_raises_when_class_scoped_is_request_without_class( + pytester: pytest.Pytester, +): pytester.makepyfile( dedent( """\ import asyncio import pytest - @pytest.mark.asyncio_event_loop - class TestSuperClassWithMark: + @pytest.mark.asyncio(scope="class") + async def test_has_no_surrounding_class(): pass - - class TestWithoutMark(TestSuperClassWithMark): - loop: asyncio.AbstractEventLoop - - @pytest.mark.asyncio - async def test_remember_loop(self): - TestWithoutMark.loop = asyncio.get_running_loop() - - @pytest.mark.asyncio - async def test_this_runs_in_same_loop(self): - assert asyncio.get_running_loop() is TestWithoutMark.loop """ ) ) result = pytester.runpytest("--asyncio-mode=strict") - result.assert_outcomes(passed=2) + result.assert_outcomes(errors=1) + result.stdout.fnmatch_lines( + "*is marked to be run in an event loop with scope*", + ) -def test_raise_when_event_loop_fixture_is_requested_in_addition_to_scoped_loop( - pytester: pytest.Pytester, -): +def test_asyncio_mark_is_inherited_to_subclasses(pytester: pytest.Pytester): pytester.makepyfile( dedent( """\ import asyncio import pytest - @pytest.mark.asyncio_event_loop - class TestClassScopedLoop: - @pytest.mark.asyncio - async def test_remember_loop(self, event_loop): - pass + @pytest.mark.asyncio(scope="class") + class TestSuperClassWithMark: + pass + + class TestWithoutMark(TestSuperClassWithMark): + loop: asyncio.AbstractEventLoop + + async def test_remember_loop(self): + TestWithoutMark.loop = asyncio.get_running_loop() + + async def test_this_runs_in_same_loop(self): + assert asyncio.get_running_loop() is TestWithoutMark.loop """ ) ) result = pytester.runpytest("--asyncio-mode=strict") - result.assert_outcomes(errors=1) - result.stdout.fnmatch_lines("*MultipleEventLoopsRequestedError: *") + result.assert_outcomes(passed=2) -def test_asyncio_event_loop_mark_allows_specifying_the_loop_policy( +def test_asyncio_mark_respects_the_loop_policy( pytester: pytest.Pytester, ): pytester.makepyfile( @@ -140,9 +137,7 @@ def test_asyncio_event_loop_mark_allows_specifying_the_loop_policy( class CustomEventLoopPolicy(asyncio.DefaultEventLoopPolicy): pass - @pytest.mark.asyncio_event_loop class TestUsesCustomEventLoop: - @pytest.fixture(scope="class") def event_loop_policy(self): return CustomEventLoopPolicy() @@ -167,7 +162,7 @@ async def test_does_not_use_custom_event_loop_policy(): result.assert_outcomes(passed=2) -def test_asyncio_event_loop_mark_allows_specifying_multiple_loop_policies( +def test_asyncio_mark_respects_parametrized_loop_policies( pytester: pytest.Pytester, ): pytester.makepyfile( @@ -178,6 +173,7 @@ def test_asyncio_event_loop_mark_allows_specifying_multiple_loop_policies( import pytest @pytest.fixture( + scope="class", params=[ asyncio.DefaultEventLoopPolicy(), asyncio.DefaultEventLoopPolicy(), @@ -186,8 +182,8 @@ def test_asyncio_event_loop_mark_allows_specifying_multiple_loop_policies( def event_loop_policy(request): return request.param + @pytest.mark.asyncio(scope="class") class TestWithDifferentLoopPolicies: - @pytest.mark.asyncio async def test_parametrized_loop(self, request): pass """ @@ -197,7 +193,7 @@ async def test_parametrized_loop(self, request): result.assert_outcomes(passed=2) -def test_asyncio_event_loop_mark_provides_class_scoped_loop_to_fixtures( +def test_asyncio_mark_provides_class_scoped_loop_to_fixtures( pytester: pytest.Pytester, ): pytester.makepyfile( @@ -208,7 +204,7 @@ def test_asyncio_event_loop_mark_provides_class_scoped_loop_to_fixtures( import pytest import pytest_asyncio - @pytest.mark.asyncio_event_loop + @pytest.mark.asyncio(scope="class") class TestClassScopedLoop: loop: asyncio.AbstractEventLoop diff --git a/tests/markers/test_module_marker.py b/tests/markers/test_module_scope.py similarity index 75% rename from tests/markers/test_module_marker.py rename to tests/markers/test_module_scope.py index 882f51af..1cd8ac65 100644 --- a/tests/markers/test_module_marker.py +++ b/tests/markers/test_module_scope.py @@ -59,22 +59,19 @@ def test_asyncio_mark_provides_module_scoped_loop_strict_mode(pytester: Pytester import asyncio import pytest - pytestmark = pytest.mark.asyncio_event_loop + pytestmark = pytest.mark.asyncio(scope="module") loop: asyncio.AbstractEventLoop - @pytest.mark.asyncio async def test_remember_loop(): global loop loop = asyncio.get_running_loop() - @pytest.mark.asyncio async def test_this_runs_in_same_loop(): global loop assert asyncio.get_running_loop() is loop class TestClassA: - @pytest.mark.asyncio async def test_this_runs_in_same_loop(self): global loop assert asyncio.get_running_loop() is loop @@ -85,36 +82,6 @@ async def test_this_runs_in_same_loop(self): result.assert_outcomes(passed=3) -def test_asyncio_mark_provides_class_scoped_loop_auto_mode(pytester: Pytester): - pytester.makepyfile( - dedent( - """\ - import asyncio - import pytest - - pytestmark = pytest.mark.asyncio_event_loop - - loop: asyncio.AbstractEventLoop - - async def test_remember_loop(): - global loop - loop = asyncio.get_running_loop() - - async def test_this_runs_in_same_loop(): - global loop - assert asyncio.get_running_loop() is loop - - class TestClassA: - async def test_this_runs_in_same_loop(self): - global loop - assert asyncio.get_running_loop() is loop - """ - ) - ) - result = pytester.runpytest("--asyncio-mode=auto") - result.assert_outcomes(passed=3) - - def test_raise_when_event_loop_fixture_is_requested_in_addition_to_scoped_loop( pytester: Pytester, ): @@ -124,9 +91,8 @@ def test_raise_when_event_loop_fixture_is_requested_in_addition_to_scoped_loop( import asyncio import pytest - pytestmark = pytest.mark.asyncio_event_loop + pytestmark = pytest.mark.asyncio(scope="module") - @pytest.mark.asyncio async def test_remember_loop(event_loop): pass """ @@ -137,7 +103,7 @@ async def test_remember_loop(event_loop): result.stdout.fnmatch_lines("*MultipleEventLoopsRequestedError: *") -def test_asyncio_event_loop_mark_allows_specifying_the_loop_policy( +def test_asyncio_mark_respects_the_loop_policy( pytester: Pytester, ): pytester.makepyfile( @@ -157,13 +123,12 @@ class CustomEventLoopPolicy(asyncio.DefaultEventLoopPolicy): from .custom_policy import CustomEventLoopPolicy - pytestmark = pytest.mark.asyncio_event_loop + pytestmark = pytest.mark.asyncio(scope="module") @pytest.fixture(scope="module") def event_loop_policy(): return CustomEventLoopPolicy() - @pytest.mark.asyncio async def test_uses_custom_event_loop_policy(): assert isinstance( asyncio.get_event_loop_policy(), @@ -178,7 +143,8 @@ async def test_uses_custom_event_loop_policy(): from .custom_policy import CustomEventLoopPolicy - @pytest.mark.asyncio + pytestmark = pytest.mark.asyncio(scope="module") + async def test_does_not_use_custom_event_loop_policy(): assert not isinstance( asyncio.get_event_loop_policy(), @@ -191,7 +157,7 @@ async def test_does_not_use_custom_event_loop_policy(): result.assert_outcomes(passed=2) -def test_asyncio_event_loop_mark_allows_specifying_multiple_loop_policies( +def test_asyncio_mark_respects_parametrized_loop_policies( pytester: Pytester, ): pytester.makepyfile( @@ -201,7 +167,7 @@ def test_asyncio_event_loop_mark_allows_specifying_multiple_loop_policies( import pytest - pytestmark = pytest.mark.asyncio_event_loop + pytestmark = pytest.mark.asyncio(scope="module") @pytest.fixture( scope="module", @@ -213,7 +179,6 @@ def test_asyncio_event_loop_mark_allows_specifying_multiple_loop_policies( def event_loop_policy(request): return request.param - @pytest.mark.asyncio async def test_parametrized_loop(): pass """ @@ -223,7 +188,7 @@ async def test_parametrized_loop(): result.assert_outcomes(passed=2) -def test_asyncio_event_loop_mark_provides_module_scoped_loop_to_fixtures( +def test_asyncio_mark_provides_module_scoped_loop_to_fixtures( pytester: Pytester, ): pytester.makepyfile( @@ -234,16 +199,15 @@ def test_asyncio_event_loop_mark_provides_module_scoped_loop_to_fixtures( import pytest import pytest_asyncio - pytestmark = pytest.mark.asyncio_event_loop + pytestmark = pytest.mark.asyncio(scope="module") loop: asyncio.AbstractEventLoop - @pytest_asyncio.fixture + @pytest_asyncio.fixture(scope="module") async def my_fixture(): global loop loop = asyncio.get_running_loop() - @pytest.mark.asyncio async def test_runs_is_same_loop_as_fixture(my_fixture): global loop assert asyncio.get_running_loop() is loop diff --git a/tox.ini b/tox.ini index 5acc30e2..7bab7350 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] minversion = 3.14.0 -envlist = py38, py39, py310, py311, py312, pytest-min +envlist = py38, py39, py310, py311, py312, pytest-min, docs isolated_build = true passenv = CI @@ -25,6 +25,16 @@ commands = make test allowlist_externals = make +[testenv:docs] +extras = docs +deps = + --requirement dependencies/docs/requirements.txt + --constraint dependencies/docs/constraints.txt +change_dir = docs +commands = make html +allowlist_externals = + make + [gh-actions] python = 3.8: py38, pytest-min From 7c043504aeed2d320996efebc3d66128ebfdc2b0 Mon Sep 17 00:00:00 2001 From: Michael Seifert Date: Sun, 5 Nov 2023 16:37:39 +0100 Subject: [PATCH 099/151] [feat] Add support for package-scoped loops. Signed-off-by: Michael Seifert --- docs/source/reference/changelog.rst | 3 +- docs/source/reference/markers/index.rst | 12 +- pytest_asyncio/plugin.py | 29 ++- tests/markers/test_package_scope.py | 225 ++++++++++++++++++++++++ 4 files changed, 261 insertions(+), 8 deletions(-) create mode 100644 tests/markers/test_package_scope.py diff --git a/docs/source/reference/changelog.rst b/docs/source/reference/changelog.rst index e8ec016c..10c274b2 100644 --- a/docs/source/reference/changelog.rst +++ b/docs/source/reference/changelog.rst @@ -7,8 +7,7 @@ Changelog This release is backwards-compatible with v0.21. Changes are non-breaking, unless you upgrade from v0.22. -- BREAKING: The *asyncio_event_loop* mark has been removed. Class-scoped and module-scoped event loops can be requested - via the *scope* keyword argument to the _asyncio_ mark. +- BREAKING: The *asyncio_event_loop* mark has been removed. Event loops with class, module and package scope can be requested via the *scope* keyword argument to the _asyncio_ mark. - Introduces the *event_loop_policy* fixture which allows testing with non-default or multiple event loops `#662 `_ - Removes pytest-trio from the test dependencies `#620 `_ diff --git a/docs/source/reference/markers/index.rst b/docs/source/reference/markers/index.rst index b625bbd7..83bcbbc0 100644 --- a/docs/source/reference/markers/index.rst +++ b/docs/source/reference/markers/index.rst @@ -19,19 +19,23 @@ The ``pytest.mark.asyncio`` marker can be omitted entirely in *auto* mode, where By default, each test runs in it's own asyncio event loop. Multiple tests can share the same event loop by providing a *scope* keyword argument to the *asyncio* mark. +The supported scopes are *class,* and *module,* and *package*. The following code example provides a shared event loop for all tests in `TestClassScopedLoop`: .. include:: class_scoped_loop_strict_mode_example.py :code: python -Requesting class scope for tests that are not part of a class will give a *UsageError.* -Similar to class-scoped event loops, a module-scoped loop is provided when setting the asyncio mark's scope to *module:* +Requesting class scope with the test being part of a class will give a *UsageError*. +Similar to class-scoped event loops, a module-scoped loop is provided when setting mark's scope to *module:* .. include:: module_scoped_loop_strict_mode_example.py :code: python -Requesting class scope with the test being part of a class will give a *UsageError*. -The supported scopes are *class*, and *module.* +Package-scoped loops only work with `regular Python packages. `__ +That means they require an *__init__.py* to be present. +Package-scoped loops do not work in `namespace packages. `__ +Subpackages do not share the loop with their parent package. + .. |pytestmark| replace:: ``pytestmark`` .. _pytestmark: http://doc.pytest.org/en/latest/example/markers.html#marking-whole-classes-or-modules diff --git a/pytest_asyncio/plugin.py b/pytest_asyncio/plugin.py index 8c91c3e3..85b6b260 100644 --- a/pytest_asyncio/plugin.py +++ b/pytest_asyncio/plugin.py @@ -5,6 +5,7 @@ import functools import inspect import socket +import sys import warnings from asyncio import AbstractEventLoopPolicy from textwrap import dedent @@ -35,6 +36,7 @@ Item, Metafunc, Module, + Package, Parser, PytestCollectionWarning, PytestDeprecationWarning, @@ -539,11 +541,16 @@ def pytest_pycollect_makeitem_convert_async_functions_to_subclass( _event_loop_fixture_id = StashKey[str] +_fixture_scope_by_collector_type = { + Class: "class", + Module: "module", + Package: "package", +} @pytest.hookimpl def pytest_collectstart(collector: pytest.Collector): - if not isinstance(collector, (pytest.Class, pytest.Module)): + if not isinstance(collector, (Class, Module, Package)): return # There seem to be issues when a fixture is shadowed by another fixture # and both differ in their params. @@ -556,7 +563,7 @@ def pytest_collectstart(collector: pytest.Collector): collector.stash[_event_loop_fixture_id] = event_loop_fixture_id @pytest.fixture( - scope="class" if isinstance(collector, pytest.Class) else "module", + scope=_fixture_scope_by_collector_type[type(collector)], name=event_loop_fixture_id, ) def scoped_event_loop( @@ -579,6 +586,23 @@ def scoped_event_loop( # collected Python class, where it will be picked up by pytest.Class.collect() # or pytest.Module.collect(), respectively collector.obj.__pytest_asyncio_scoped_event_loop = scoped_event_loop + # When collector is a package, collector.obj is the package's __init__.py. + # pytest doesn't seem to collect fixtures in __init__.py. + # Using parsefactories to collect fixtures in __init__.py their baseid will end + # with "__init__.py", thus limiting the scope of the fixture to the init module. + # Therefore, we tell the pluginmanager explicitly to collect the fixtures + # in the init module, but strip "__init__.py" from the baseid + # Possibly related to https://github.com/pytest-dev/pytest/issues/4085 + if isinstance(collector, Package): + fixturemanager = collector.config.pluginmanager.get_plugin("funcmanage") + package_node_id = _removesuffix(collector.nodeid, "__init__.py") + fixturemanager.parsefactories(collector.obj, nodeid=package_node_id) + + +def _removesuffix(s: str, suffix: str) -> str: + if sys.version_info < (3, 9): + return s[: -len(suffix)] + return s.removesuffix(suffix) def pytest_collection_modifyitems( @@ -867,6 +891,7 @@ def _retrieve_scope_root(item: Union[Collector, Item], scope: str) -> Collector: node_type_by_scope = { "class": Class, "module": Module, + "package": Package, } scope_root_type = node_type_by_scope[scope] for node in reversed(item.listchain()): diff --git a/tests/markers/test_package_scope.py b/tests/markers/test_package_scope.py new file mode 100644 index 00000000..fde2e836 --- /dev/null +++ b/tests/markers/test_package_scope.py @@ -0,0 +1,225 @@ +from textwrap import dedent + +from pytest import Pytester + + +def test_asyncio_mark_provides_package_scoped_loop_strict_mode(pytester: Pytester): + package_name = pytester.path.name + subpackage_name = "subpkg" + pytester.makepyfile( + __init__="", + shared_module=dedent( + """\ + import asyncio + + loop: asyncio.AbstractEventLoop = None + """ + ), + test_module_one=dedent( + f"""\ + import asyncio + import pytest + + from {package_name} import shared_module + + @pytest.mark.asyncio(scope="package") + async def test_remember_loop(): + shared_module.loop = asyncio.get_running_loop() + """ + ), + test_module_two=dedent( + f"""\ + import asyncio + import pytest + + from {package_name} import shared_module + + pytestmark = pytest.mark.asyncio(scope="package") + + async def test_this_runs_in_same_loop(): + assert asyncio.get_running_loop() is shared_module.loop + + class TestClassA: + async def test_this_runs_in_same_loop(self): + assert asyncio.get_running_loop() is shared_module.loop + """ + ), + ) + subpkg = pytester.mkpydir(subpackage_name) + subpkg.joinpath("test_subpkg.py").write_text( + dedent( + f"""\ + import asyncio + import pytest + + from {package_name} import shared_module + + pytestmark = pytest.mark.asyncio(scope="package") + + async def test_subpackage_runs_in_different_loop(): + assert asyncio.get_running_loop() is not shared_module.loop + """ + ) + ) + result = pytester.runpytest("--asyncio-mode=strict") + result.assert_outcomes(passed=4) + + +def test_raise_when_event_loop_fixture_is_requested_in_addition_to_scoped_loop( + pytester: Pytester, +): + pytester.makepyfile( + __init__="", + test_raises=dedent( + """\ + import asyncio + import pytest + + @pytest.mark.asyncio(scope="package") + async def test_remember_loop(event_loop): + pass + """ + ), + ) + result = pytester.runpytest("--asyncio-mode=strict") + result.assert_outcomes(errors=1) + result.stdout.fnmatch_lines("*MultipleEventLoopsRequestedError: *") + + +def test_asyncio_mark_respects_the_loop_policy( + pytester: Pytester, +): + pytester.makepyfile( + __init__="", + conftest=dedent( + """\ + import pytest + + from .custom_policy import CustomEventLoopPolicy + + @pytest.fixture(scope="package") + def event_loop_policy(): + return CustomEventLoopPolicy() + """ + ), + custom_policy=dedent( + """\ + import asyncio + + class CustomEventLoopPolicy(asyncio.DefaultEventLoopPolicy): + pass + """ + ), + test_uses_custom_policy=dedent( + """\ + import asyncio + import pytest + + from .custom_policy import CustomEventLoopPolicy + + pytestmark = pytest.mark.asyncio(scope="package") + + async def test_uses_custom_event_loop_policy(): + assert isinstance( + asyncio.get_event_loop_policy(), + CustomEventLoopPolicy, + ) + """ + ), + test_also_uses_custom_policy=dedent( + """\ + import asyncio + import pytest + + from .custom_policy import CustomEventLoopPolicy + + pytestmark = pytest.mark.asyncio(scope="package") + + async def test_also_uses_custom_event_loop_policy(): + assert isinstance( + asyncio.get_event_loop_policy(), + CustomEventLoopPolicy, + ) + """ + ), + ) + result = pytester.runpytest("--asyncio-mode=strict") + result.assert_outcomes(passed=2) + + +def test_asyncio_mark_respects_parametrized_loop_policies( + pytester: Pytester, +): + pytester.makepyfile( + __init__="", + test_parametrization=dedent( + """\ + import asyncio + + import pytest + + pytestmark = pytest.mark.asyncio(scope="package") + + @pytest.fixture( + scope="package", + params=[ + asyncio.DefaultEventLoopPolicy(), + asyncio.DefaultEventLoopPolicy(), + ], + ) + def event_loop_policy(request): + return request.param + + async def test_parametrized_loop(): + pass + """ + ), + ) + result = pytester.runpytest_subprocess("--asyncio-mode=strict") + result.assert_outcomes(passed=2) + + +def test_asyncio_mark_provides_package_scoped_loop_to_fixtures( + pytester: Pytester, +): + package_name = pytester.path.name + pytester.makepyfile( + __init__="", + conftest=dedent( + f"""\ + import asyncio + + import pytest_asyncio + + from {package_name} import shared_module + + @pytest_asyncio.fixture(scope="package") + async def my_fixture(): + shared_module.loop = asyncio.get_running_loop() + """ + ), + shared_module=dedent( + """\ + import asyncio + + loop: asyncio.AbstractEventLoop = None + """ + ), + test_fixture_runs_in_scoped_loop=dedent( + f"""\ + import asyncio + + import pytest + import pytest_asyncio + + from {package_name} import shared_module + + pytestmark = pytest.mark.asyncio(scope="package") + + async def test_runs_in_same_loop_as_fixture(my_fixture): + assert asyncio.get_running_loop() is shared_module.loop + """ + ), + ) + result = pytester.runpytest_subprocess("--asyncio-mode=strict") + result.assert_outcomes(passed=1) From 38150468dcee929ab6c12e287145b2daf23e3aa1 Mon Sep 17 00:00:00 2001 From: Michael Seifert Date: Wed, 8 Nov 2023 10:39:28 +0100 Subject: [PATCH 100/151] [docs] Link to auto mode concept from marker reference. Signed-off-by: Michael Seifert --- docs/source/reference/markers/index.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/source/reference/markers/index.rst b/docs/source/reference/markers/index.rst index 83bcbbc0..8cf2b279 100644 --- a/docs/source/reference/markers/index.rst +++ b/docs/source/reference/markers/index.rst @@ -15,7 +15,7 @@ Multiple async tests in a single class or module can be marked using |pytestmark .. include:: function_scoped_loop_pytestmark_strict_mode_example.py :code: python -The ``pytest.mark.asyncio`` marker can be omitted entirely in *auto* mode, where the *asyncio* marker is added automatically to *async* test functions. +The ``pytest.mark.asyncio`` marker can be omitted entirely in |auto mode|_ where the *asyncio* marker is added automatically to *async* test functions. By default, each test runs in it's own asyncio event loop. Multiple tests can share the same event loop by providing a *scope* keyword argument to the *asyncio* mark. @@ -37,5 +37,7 @@ Package-scoped loops do not work in `namespace packages. Date: Wed, 8 Nov 2023 11:35:57 +0100 Subject: [PATCH 101/151] [feat] Add support for session-scoped event loops. Signed-off-by: Michael Seifert --- docs/source/reference/changelog.rst | 2 +- docs/source/reference/markers/index.rst | 1 + pytest_asyncio/plugin.py | 27 +++ tests/markers/test_session_scope.py | 229 ++++++++++++++++++++++++ 4 files changed, 258 insertions(+), 1 deletion(-) create mode 100644 tests/markers/test_session_scope.py diff --git a/docs/source/reference/changelog.rst b/docs/source/reference/changelog.rst index 10c274b2..d902ff06 100644 --- a/docs/source/reference/changelog.rst +++ b/docs/source/reference/changelog.rst @@ -7,7 +7,7 @@ Changelog This release is backwards-compatible with v0.21. Changes are non-breaking, unless you upgrade from v0.22. -- BREAKING: The *asyncio_event_loop* mark has been removed. Event loops with class, module and package scope can be requested via the *scope* keyword argument to the _asyncio_ mark. +- BREAKING: The *asyncio_event_loop* mark has been removed. Event loops with class, module, package, and session scopes can be requested via the *scope* keyword argument to the _asyncio_ mark. - Introduces the *event_loop_policy* fixture which allows testing with non-default or multiple event loops `#662 `_ - Removes pytest-trio from the test dependencies `#620 `_ diff --git a/docs/source/reference/markers/index.rst b/docs/source/reference/markers/index.rst index 8cf2b279..a875b90d 100644 --- a/docs/source/reference/markers/index.rst +++ b/docs/source/reference/markers/index.rst @@ -36,6 +36,7 @@ That means they require an *__init__.py* to be present. Package-scoped loops do not work in `namespace packages. `__ Subpackages do not share the loop with their parent package. +Tests marked with *session* scope share the same event loop, even if the tests exist in different packages. .. |auto mode| replace:: *auto mode* .. _auto mode: ../../concepts.html#auto-mode diff --git a/pytest_asyncio/plugin.py b/pytest_asyncio/plugin.py index 85b6b260..942ad4de 100644 --- a/pytest_asyncio/plugin.py +++ b/pytest_asyncio/plugin.py @@ -545,11 +545,21 @@ def pytest_pycollect_makeitem_convert_async_functions_to_subclass( Class: "class", Module: "module", Package: "package", + Session: "session", } @pytest.hookimpl def pytest_collectstart(collector: pytest.Collector): + # Session is not a PyCollector type, so it doesn't have a corresponding + # "obj" attribute to attach a dynamic fixture function to. + # However, there's only one session per pytest run, so there's no need to + # create the fixture dynamically. We can simply define a session-scoped + # event loop fixture once in the plugin code. + if isinstance(collector, Session): + event_loop_fixture_id = _session_event_loop.__name__ + collector.stash[_event_loop_fixture_id] = event_loop_fixture_id + return if not isinstance(collector, (Class, Module, Package)): return # There seem to be issues when a fixture is shadowed by another fixture @@ -892,6 +902,7 @@ def _retrieve_scope_root(item: Union[Collector, Item], scope: str) -> Collector: "class": Class, "module": Module, "package": Package, + "session": Session, } scope_root_type = node_type_by_scope[scope] for node in reversed(item.listchain()): @@ -920,6 +931,22 @@ def event_loop(request: FixtureRequest) -> Iterator[asyncio.AbstractEventLoop]: loop.close() +@pytest.fixture(scope="session") +def _session_event_loop( + request: FixtureRequest, event_loop_policy: AbstractEventLoopPolicy +) -> Iterator[asyncio.AbstractEventLoop]: + new_loop_policy = event_loop_policy + old_loop_policy = asyncio.get_event_loop_policy() + old_loop = asyncio.get_event_loop() + asyncio.set_event_loop_policy(new_loop_policy) + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + yield loop + loop.close() + asyncio.set_event_loop_policy(old_loop_policy) + asyncio.set_event_loop(old_loop) + + @pytest.fixture(scope="session", autouse=True) def event_loop_policy() -> AbstractEventLoopPolicy: """Return an instance of the policy used to create asyncio event loops.""" diff --git a/tests/markers/test_session_scope.py b/tests/markers/test_session_scope.py new file mode 100644 index 00000000..1242cfee --- /dev/null +++ b/tests/markers/test_session_scope.py @@ -0,0 +1,229 @@ +from textwrap import dedent + +from pytest import Pytester + + +def test_asyncio_mark_provides_session_scoped_loop_strict_mode(pytester: Pytester): + package_name = pytester.path.name + pytester.makepyfile( + __init__="", + shared_module=dedent( + """\ + import asyncio + + loop: asyncio.AbstractEventLoop = None + """ + ), + test_module_one=dedent( + f"""\ + import asyncio + import pytest + + from {package_name} import shared_module + + @pytest.mark.asyncio(scope="session") + async def test_remember_loop(): + shared_module.loop = asyncio.get_running_loop() + """ + ), + test_module_two=dedent( + f"""\ + import asyncio + import pytest + + from {package_name} import shared_module + + pytestmark = pytest.mark.asyncio(scope="session") + + async def test_this_runs_in_same_loop(): + assert asyncio.get_running_loop() is shared_module.loop + + class TestClassA: + async def test_this_runs_in_same_loop(self): + assert asyncio.get_running_loop() is shared_module.loop + """ + ), + ) + subpackage_name = "subpkg" + subpkg = pytester.mkpydir(subpackage_name) + subpkg.joinpath("test_subpkg.py").write_text( + dedent( + f"""\ + import asyncio + import pytest + + from {package_name} import shared_module + + pytestmark = pytest.mark.asyncio(scope="session") + + async def test_subpackage_runs_in_same_loop(): + assert asyncio.get_running_loop() is shared_module.loop + """ + ) + ) + result = pytester.runpytest("--asyncio-mode=strict") + result.assert_outcomes(passed=4) + + +def test_raise_when_event_loop_fixture_is_requested_in_addition_to_scoped_loop( + pytester: Pytester, +): + pytester.makepyfile( + __init__="", + test_raises=dedent( + """\ + import asyncio + import pytest + + @pytest.mark.asyncio(scope="session") + async def test_remember_loop(event_loop): + pass + """ + ), + ) + result = pytester.runpytest("--asyncio-mode=strict") + result.assert_outcomes(errors=1) + result.stdout.fnmatch_lines("*MultipleEventLoopsRequestedError: *") + + +def test_asyncio_mark_respects_the_loop_policy( + pytester: Pytester, +): + pytester.makepyfile( + __init__="", + conftest=dedent( + """\ + import pytest + + from .custom_policy import CustomEventLoopPolicy + + @pytest.fixture(scope="session") + def event_loop_policy(): + return CustomEventLoopPolicy() + """ + ), + custom_policy=dedent( + """\ + import asyncio + + class CustomEventLoopPolicy(asyncio.DefaultEventLoopPolicy): + pass + """ + ), + test_uses_custom_policy=dedent( + """\ + import asyncio + import pytest + + from .custom_policy import CustomEventLoopPolicy + + pytestmark = pytest.mark.asyncio(scope="session") + + async def test_uses_custom_event_loop_policy(): + assert isinstance( + asyncio.get_event_loop_policy(), + CustomEventLoopPolicy, + ) + """ + ), + test_also_uses_custom_policy=dedent( + """\ + import asyncio + import pytest + + from .custom_policy import CustomEventLoopPolicy + + pytestmark = pytest.mark.asyncio(scope="session") + + async def test_also_uses_custom_event_loop_policy(): + assert isinstance( + asyncio.get_event_loop_policy(), + CustomEventLoopPolicy, + ) + """ + ), + ) + result = pytester.runpytest("--asyncio-mode=strict") + result.assert_outcomes(passed=2) + + +def test_asyncio_mark_respects_parametrized_loop_policies( + pytester: Pytester, +): + pytester.makepyfile( + __init__="", + test_parametrization=dedent( + """\ + import asyncio + + import pytest + + pytestmark = pytest.mark.asyncio(scope="session") + + @pytest.fixture( + scope="session", + params=[ + asyncio.DefaultEventLoopPolicy(), + asyncio.DefaultEventLoopPolicy(), + ], + ) + def event_loop_policy(request): + return request.param + + async def test_parametrized_loop(): + pass + """ + ), + ) + result = pytester.runpytest_subprocess("--asyncio-mode=strict") + result.assert_outcomes(passed=2) + + +def test_asyncio_mark_provides_session_scoped_loop_to_fixtures( + pytester: Pytester, +): + package_name = pytester.path.name + pytester.makepyfile( + __init__="", + conftest=dedent( + f"""\ + import asyncio + + import pytest_asyncio + + from {package_name} import shared_module + + @pytest_asyncio.fixture(scope="session") + async def my_fixture(): + shared_module.loop = asyncio.get_running_loop() + """ + ), + shared_module=dedent( + """\ + import asyncio + + loop: asyncio.AbstractEventLoop = None + """ + ), + ) + subpackage_name = "subpkg" + subpkg = pytester.mkpydir(subpackage_name) + subpkg.joinpath("test_subpkg.py").write_text( + dedent( + f"""\ + import asyncio + + import pytest + import pytest_asyncio + + from {package_name} import shared_module + + pytestmark = pytest.mark.asyncio(scope="session") + + async def test_runs_in_same_loop_as_fixture(my_fixture): + assert asyncio.get_running_loop() is shared_module.loop + """ + ) + ) + result = pytester.runpytest_subprocess("--asyncio-mode=strict") + result.assert_outcomes(passed=1) From 8704ec5cdc768a07616427561874e9fee7bdc15c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Nov 2023 18:14:55 +0000 Subject: [PATCH 102/151] Build(deps): Bump urllib3 from 2.0.7 to 2.1.0 in /dependencies/docs Bumps [urllib3](https://github.com/urllib3/urllib3) from 2.0.7 to 2.1.0. - [Release notes](https://github.com/urllib3/urllib3/releases) - [Changelog](https://github.com/urllib3/urllib3/blob/main/CHANGES.rst) - [Commits](https://github.com/urllib3/urllib3/compare/2.0.7...2.1.0) --- updated-dependencies: - dependency-name: urllib3 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- dependencies/docs/constraints.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies/docs/constraints.txt b/dependencies/docs/constraints.txt index 21439273..3ee57aed 100644 --- a/dependencies/docs/constraints.txt +++ b/dependencies/docs/constraints.txt @@ -20,4 +20,4 @@ sphinxcontrib-jquery==4.1 sphinxcontrib-jsmath==1.0.1 sphinxcontrib-qthelp==1.0.6 sphinxcontrib-serializinghtml==1.1.9 -urllib3==2.0.7 +urllib3==2.1.0 From e46369d1b55dc95bd5435e2cb0ec0c644646c8ec Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Nov 2023 18:41:43 +0000 Subject: [PATCH 103/151] Build(deps): Bump hypothesis in /dependencies/default Bumps [hypothesis](https://github.com/HypothesisWorks/hypothesis) from 6.88.3 to 6.88.4. - [Release notes](https://github.com/HypothesisWorks/hypothesis/releases) - [Commits](https://github.com/HypothesisWorks/hypothesis/compare/hypothesis-python-6.88.3...hypothesis-python-6.88.4) --- updated-dependencies: - dependency-name: hypothesis dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- dependencies/default/constraints.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies/default/constraints.txt b/dependencies/default/constraints.txt index f8fb873f..e393d482 100644 --- a/dependencies/default/constraints.txt +++ b/dependencies/default/constraints.txt @@ -2,7 +2,7 @@ attrs==23.1.0 coverage==7.3.2 exceptiongroup==1.1.3 flaky==3.7.0 -hypothesis==6.88.3 +hypothesis==6.88.4 iniconfig==2.0.0 mypy==1.6.1 mypy-extensions==1.0.0 From 50a2339924bdcdc6a11b4fd3ec7da59dd9c5b6c3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Nov 2023 18:41:50 +0000 Subject: [PATCH 104/151] Build(deps): Bump mypy from 1.6.1 to 1.7.0 in /dependencies/default Bumps [mypy](https://github.com/python/mypy) from 1.6.1 to 1.7.0. - [Changelog](https://github.com/python/mypy/blob/master/CHANGELOG.md) - [Commits](https://github.com/python/mypy/compare/v1.6.1...v1.7.0) --- updated-dependencies: - dependency-name: mypy dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- dependencies/default/constraints.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies/default/constraints.txt b/dependencies/default/constraints.txt index e393d482..8baa17ad 100644 --- a/dependencies/default/constraints.txt +++ b/dependencies/default/constraints.txt @@ -4,7 +4,7 @@ exceptiongroup==1.1.3 flaky==3.7.0 hypothesis==6.88.4 iniconfig==2.0.0 -mypy==1.6.1 +mypy==1.7.0 mypy-extensions==1.0.0 packaging==23.2 pluggy==1.3.0 From 502bd9046e73eda2c1c3693ede37ad5b40bcc7f6 Mon Sep 17 00:00:00 2001 From: Michael Seifert Date: Wed, 15 Nov 2023 13:43:06 +0100 Subject: [PATCH 105/151] [test] Add explcitit warning filter to tests that expect warnings. This avoids failing tests when running the tests with "python -m pytest" and installing a warning filter on the interpreter level. This can be useful for running tests in development mode, i.e. "python -Xdev -m pytest". Signed-off-by: Michael Seifert --- tests/markers/test_function_scope.py | 2 +- tests/markers/test_module_scope.py | 7 +++++-- tests/test_event_loop_fixture_finalizer.py | 7 +++++-- tests/test_event_loop_fixture_override_deprecation.py | 6 +++--- tests/test_explicit_event_loop_fixture_request.py | 10 +++++----- 5 files changed, 19 insertions(+), 13 deletions(-) diff --git a/tests/markers/test_function_scope.py b/tests/markers/test_function_scope.py index be45e5dd..df2c3e47 100644 --- a/tests/markers/test_function_scope.py +++ b/tests/markers/test_function_scope.py @@ -43,7 +43,7 @@ async def test_remember_loop(event_loop): """ ) ) - result = pytester.runpytest("--asyncio-mode=strict") + result = pytester.runpytest("--asyncio-mode=strict", "-W default") result.assert_outcomes(passed=1, warnings=1) result.stdout.fnmatch_lines( '*is asynchronous and explicitly requests the "event_loop" fixture*' diff --git a/tests/markers/test_module_scope.py b/tests/markers/test_module_scope.py index 1cd8ac65..1034af83 100644 --- a/tests/markers/test_module_scope.py +++ b/tests/markers/test_module_scope.py @@ -48,8 +48,11 @@ def sample_fixture(): """ ) ) - result = pytester.runpytest("--asyncio-mode=strict") - result.assert_outcomes(passed=2) + result = pytester.runpytest("--asyncio-mode=strict", "-W default") + result.assert_outcomes(passed=2, warnings=2) + result.stdout.fnmatch_lines( + '*is asynchronous and explicitly requests the "event_loop" fixture*' + ) def test_asyncio_mark_provides_module_scoped_loop_strict_mode(pytester: Pytester): diff --git a/tests/test_event_loop_fixture_finalizer.py b/tests/test_event_loop_fixture_finalizer.py index 5f8f9574..eabb54a3 100644 --- a/tests/test_event_loop_fixture_finalizer.py +++ b/tests/test_event_loop_fixture_finalizer.py @@ -84,8 +84,11 @@ async def test_async_with_explicit_fixture_request(event_loop): """ ) ) - result = pytester.runpytest("--asyncio-mode=strict") - result.assert_outcomes(passed=1) + result = pytester.runpytest("--asyncio-mode=strict", "-W default") + result.assert_outcomes(passed=1, warnings=1) + result.stdout.fnmatch_lines( + '*is asynchronous and explicitly requests the "event_loop" fixture*' + ) def test_event_loop_fixture_finalizer_raises_warning_when_fixture_leaves_loop_unclosed( diff --git a/tests/test_event_loop_fixture_override_deprecation.py b/tests/test_event_loop_fixture_override_deprecation.py index 3484ef76..45afc542 100644 --- a/tests/test_event_loop_fixture_override_deprecation.py +++ b/tests/test_event_loop_fixture_override_deprecation.py @@ -22,7 +22,7 @@ async def test_emits_warning(): """ ) ) - result = pytester.runpytest("--asyncio-mode=strict") + result = pytester.runpytest("--asyncio-mode=strict", "-W default") result.assert_outcomes(passed=1, warnings=1) result.stdout.fnmatch_lines( ["*event_loop fixture provided by pytest-asyncio has been redefined*"] @@ -50,7 +50,7 @@ async def test_emits_warning_when_requested_explicitly(event_loop): """ ) ) - result = pytester.runpytest("--asyncio-mode=strict") + result = pytester.runpytest("--asyncio-mode=strict", "-W default") result.assert_outcomes(passed=1, warnings=2) result.stdout.fnmatch_lines( ["*event_loop fixture provided by pytest-asyncio has been redefined*"] @@ -107,5 +107,5 @@ def test_emits_warning(uses_event_loop): """ ) ) - result = pytester.runpytest("--asyncio-mode=strict") + result = pytester.runpytest("--asyncio-mode=strict", "-W default") result.assert_outcomes(passed=1, warnings=1) diff --git a/tests/test_explicit_event_loop_fixture_request.py b/tests/test_explicit_event_loop_fixture_request.py index 8c4b732c..4cac85f7 100644 --- a/tests/test_explicit_event_loop_fixture_request.py +++ b/tests/test_explicit_event_loop_fixture_request.py @@ -17,7 +17,7 @@ async def test_coroutine_emits_warning(event_loop): """ ) ) - result = pytester.runpytest("--asyncio-mode=strict") + result = pytester.runpytest("--asyncio-mode=strict", "-W default") result.assert_outcomes(passed=1, warnings=1) result.stdout.fnmatch_lines( ['*is asynchronous and explicitly requests the "event_loop" fixture*'] @@ -39,7 +39,7 @@ async def test_coroutine_emits_warning(self, event_loop): """ ) ) - result = pytester.runpytest("--asyncio-mode=strict") + result = pytester.runpytest("--asyncio-mode=strict", "-W default") result.assert_outcomes(passed=1, warnings=1) result.stdout.fnmatch_lines( ['*is asynchronous and explicitly requests the "event_loop" fixture*'] @@ -62,7 +62,7 @@ async def test_coroutine_emits_warning(event_loop): """ ) ) - result = pytester.runpytest("--asyncio-mode=strict") + result = pytester.runpytest("--asyncio-mode=strict", "-W default") result.assert_outcomes(passed=1, warnings=1) result.stdout.fnmatch_lines( ['*is asynchronous and explicitly requests the "event_loop" fixture*'] @@ -88,7 +88,7 @@ async def test_uses_fixture(emits_warning): """ ) ) - result = pytester.runpytest("--asyncio-mode=strict") + result = pytester.runpytest("--asyncio-mode=strict", "-W default") result.assert_outcomes(passed=1, warnings=1) result.stdout.fnmatch_lines( ['*is asynchronous and explicitly requests the "event_loop" fixture*'] @@ -114,7 +114,7 @@ async def test_uses_fixture(emits_warning): """ ) ) - result = pytester.runpytest("--asyncio-mode=strict") + result = pytester.runpytest("--asyncio-mode=strict", "-W default") result.assert_outcomes(passed=1, warnings=1) result.stdout.fnmatch_lines( ['*is asynchronous and explicitly requests the "event_loop" fixture*'] From 7a589a724fd465692abdc4db81e06d38fa916360 Mon Sep 17 00:00:00 2001 From: Michael Seifert Date: Wed, 15 Nov 2023 13:53:48 +0100 Subject: [PATCH 106/151] [test] Added missing warning assertion to hypothesis test. Signed-off-by: Michael Seifert --- tests/hypothesis/test_base.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/tests/hypothesis/test_base.py b/tests/hypothesis/test_base.py index aef20d79..299c18fa 100644 --- a/tests/hypothesis/test_base.py +++ b/tests/hypothesis/test_base.py @@ -54,8 +54,14 @@ async def test_explicit_fixture_request(event_loop, n): """ ) ) - result = pytester.runpytest("--asyncio-mode=strict") - result.assert_outcomes(passed=1) + result = pytester.runpytest("--asyncio-mode=strict", "-W default") + result.assert_outcomes(passed=1, warnings=2) + result.stdout.fnmatch_lines( + [ + '*is asynchronous and explicitly requests the "event_loop" fixture*', + "*event_loop fixture provided by pytest-asyncio has been redefined*", + ] + ) def test_async_auto_marked(pytester: Pytester): From fe12dcb7c87265a5d89e3b23e328d615b29ecf3d Mon Sep 17 00:00:00 2001 From: Michael Seifert Date: Wed, 15 Nov 2023 13:54:22 +0100 Subject: [PATCH 107/151] [refactor] Use Pytester for one of the Hypothesis tests. Signed-off-by: Michael Seifert --- tests/hypothesis/test_base.py | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/tests/hypothesis/test_base.py b/tests/hypothesis/test_base.py index 299c18fa..c2a7ea6a 100644 --- a/tests/hypothesis/test_base.py +++ b/tests/hypothesis/test_base.py @@ -8,10 +8,22 @@ from pytest import Pytester -@given(st.integers()) -@pytest.mark.asyncio -async def test_mark_inner(n): - assert isinstance(n, int) +def test_hypothesis_given_decorator_before_asyncio_mark(pytester: Pytester): + pytester.makepyfile( + dedent( + """\ + import pytest + from hypothesis import given, strategies as st + + @given(st.integers()) + @pytest.mark.asyncio + async def test_mark_inner(n): + assert isinstance(n, int) + """ + ) + ) + result = pytester.runpytest("--asyncio-mode=strict", "-W default") + result.assert_outcomes(passed=1) @pytest.mark.asyncio From 810c9d7a2e9a87e9e00aee39c52325e59c0780f0 Mon Sep 17 00:00:00 2001 From: Michael Seifert Date: Wed, 15 Nov 2023 13:55:24 +0100 Subject: [PATCH 108/151] [fix] Fixes a bug that caused tests to run in the wrong event loop when requesting larger-scoped fixtures in a narrower-scoped test. Previously, pytest-asyncio relied on marks applied to pytest Collectors (e.g. classes and modules) to determine the loop scope. This logic is no longer applicable for fixtures, because pytest-asyncio now relies on the asyncio mark applied to tests. As a result, fixtures were looking for an "asyncio" mark on surrounding collectors to no avail and defaulted to choosing a function-scoped loop. This patch chooses the loop scope based on the fixture scope. Signed-off-by: Michael Seifert --- ...d_loop_with_fixture_strict_mode_example.py | 2 +- pytest_asyncio/plugin.py | 38 ++++-- .../test_async_fixtures_with_finalizer.py | 4 +- tests/markers/test_class_scope.py | 31 +++++ tests/markers/test_module_scope.py | 61 +++++++++ tests/markers/test_package_scope.py | 91 +++++++++++++ tests/markers/test_session_scope.py | 121 ++++++++++++++++++ 7 files changed, 336 insertions(+), 12 deletions(-) diff --git a/docs/source/reference/markers/class_scoped_loop_with_fixture_strict_mode_example.py b/docs/source/reference/markers/class_scoped_loop_with_fixture_strict_mode_example.py index f912dec9..538f1bd2 100644 --- a/docs/source/reference/markers/class_scoped_loop_with_fixture_strict_mode_example.py +++ b/docs/source/reference/markers/class_scoped_loop_with_fixture_strict_mode_example.py @@ -9,7 +9,7 @@ class TestClassScopedLoop: loop: asyncio.AbstractEventLoop - @pytest_asyncio.fixture + @pytest_asyncio.fixture(scope="class") async def my_fixture(self): TestClassScopedLoop.loop = asyncio.get_running_loop() diff --git a/pytest_asyncio/plugin.py b/pytest_asyncio/plugin.py index 942ad4de..dfbd9958 100644 --- a/pytest_asyncio/plugin.py +++ b/pytest_asyncio/plugin.py @@ -204,13 +204,6 @@ def _preprocess_async_fixtures( config = collector.config asyncio_mode = _get_asyncio_mode(config) fixturemanager = config.pluginmanager.get_plugin("funcmanage") - marker = collector.get_closest_marker("asyncio") - scope = marker.kwargs.get("scope", "function") if marker else "function" - if scope == "function": - event_loop_fixture_id = "event_loop" - else: - event_loop_node = _retrieve_scope_root(collector, scope) - event_loop_fixture_id = event_loop_node.stash.get(_event_loop_fixture_id, None) for fixtures in fixturemanager._arg2fixturedefs.values(): for fixturedef in fixtures: func = fixturedef.func @@ -222,6 +215,14 @@ def _preprocess_async_fixtures( # Ignore async fixtures without explicit asyncio mark in strict mode # This applies to pytest_trio fixtures, for example continue + scope = fixturedef.scope + if scope == "function": + event_loop_fixture_id = "event_loop" + else: + event_loop_node = _retrieve_scope_root(collector, scope) + event_loop_fixture_id = event_loop_node.stash.get( + _event_loop_fixture_id, None + ) _make_asyncio_fixture_function(func) function_signature = inspect.signature(func) if "event_loop" in function_signature.parameters: @@ -589,6 +590,12 @@ def scoped_event_loop( yield loop loop.close() asyncio.set_event_loop_policy(old_loop_policy) + # When a test uses both a scoped event loop and the event_loop fixture, + # the "_provide_clean_event_loop" finalizer of the event_loop fixture + # will already have installed a fresh event loop, in order to shield + # subsequent tests from side-effects. We close this loop before restoring + # the old loop to avoid ResourceWarnings. + asyncio.get_event_loop().close() asyncio.set_event_loop(old_loop) # @pytest.fixture does not register the fixture anywhere, so pytest doesn't @@ -680,7 +687,9 @@ def pytest_generate_tests(metafunc: Metafunc) -> None: ) # Add the scoped event loop fixture to Metafunc's list of fixture names and # fixturedefs and leave the actual parametrization to pytest - metafunc.fixturenames.insert(0, event_loop_fixture_id) + # The fixture needs to be appended to avoid messing up the fixture evaluation + # order + metafunc.fixturenames.append(event_loop_fixture_id) metafunc._arg2fixturedefs[ event_loop_fixture_id ] = fixturemanager._arg2fixturedefs[event_loop_fixture_id] @@ -885,8 +894,13 @@ def pytest_runtest_setup(item: pytest.Item) -> None: fixturenames = item.fixturenames # type: ignore[attr-defined] # inject an event loop fixture for all async tests if "event_loop" in fixturenames: + # Move the "event_loop" fixture to the beginning of the fixture evaluation + # closure for backwards compatibility fixturenames.remove("event_loop") - fixturenames.insert(0, event_loop_fixture_id) + fixturenames.insert(0, "event_loop") + else: + if event_loop_fixture_id not in fixturenames: + fixturenames.append(event_loop_fixture_id) obj = getattr(item, "obj", None) if not getattr(obj, "hypothesis", False) and getattr( obj, "is_hypothesis_test", False @@ -944,6 +958,12 @@ def _session_event_loop( yield loop loop.close() asyncio.set_event_loop_policy(old_loop_policy) + # When a test uses both a scoped event loop and the event_loop fixture, + # the "_provide_clean_event_loop" finalizer of the event_loop fixture + # will already have installed a fresh event loop, in order to shield + # subsequent tests from side-effects. We close this loop before restoring + # the old loop to avoid ResourceWarnings. + asyncio.get_event_loop().close() asyncio.set_event_loop(old_loop) diff --git a/tests/async_fixtures/test_async_fixtures_with_finalizer.py b/tests/async_fixtures/test_async_fixtures_with_finalizer.py index aa2ce3d7..699ac49d 100644 --- a/tests/async_fixtures/test_async_fixtures_with_finalizer.py +++ b/tests/async_fixtures/test_async_fixtures_with_finalizer.py @@ -4,13 +4,13 @@ import pytest -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="module") async def test_module_with_event_loop_finalizer(port_with_event_loop_finalizer): await asyncio.sleep(0.01) assert port_with_event_loop_finalizer -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="module") async def test_module_with_get_event_loop_finalizer(port_with_get_event_loop_finalizer): await asyncio.sleep(0.01) assert port_with_get_event_loop_finalizer diff --git a/tests/markers/test_class_scope.py b/tests/markers/test_class_scope.py index 33e5d2db..1f664774 100644 --- a/tests/markers/test_class_scope.py +++ b/tests/markers/test_class_scope.py @@ -220,3 +220,34 @@ async def test_runs_is_same_loop_as_fixture(self, my_fixture): ) result = pytester.runpytest_subprocess("--asyncio-mode=strict") result.assert_outcomes(passed=1) + + +def test_asyncio_mark_allows_combining_class_scoped_fixture_with_function_scoped_test( + pytester: pytest.Pytester, +): + pytester.makepyfile( + dedent( + """\ + import asyncio + + import pytest + import pytest_asyncio + + loop: asyncio.AbstractEventLoop + + class TestMixedScopes: + @pytest_asyncio.fixture(scope="class") + async def async_fixture(self): + global loop + loop = asyncio.get_running_loop() + + @pytest.mark.asyncio(scope="function") + async def test_runs_in_different_loop_as_fixture(self, async_fixture): + global loop + assert asyncio.get_running_loop() is not loop + + """ + ), + ) + result = pytester.runpytest("--asyncio-mode=strict") + result.assert_outcomes(passed=1) diff --git a/tests/markers/test_module_scope.py b/tests/markers/test_module_scope.py index 1034af83..b778c9a9 100644 --- a/tests/markers/test_module_scope.py +++ b/tests/markers/test_module_scope.py @@ -219,3 +219,64 @@ async def test_runs_is_same_loop_as_fixture(my_fixture): ) result = pytester.runpytest_subprocess("--asyncio-mode=strict") result.assert_outcomes(passed=1) + + +def test_asyncio_mark_allows_combining_module_scoped_fixture_with_class_scoped_test( + pytester: Pytester, +): + pytester.makepyfile( + dedent( + """\ + import asyncio + + import pytest + import pytest_asyncio + + loop: asyncio.AbstractEventLoop + + @pytest_asyncio.fixture(scope="module") + async def async_fixture(): + global loop + loop = asyncio.get_running_loop() + + @pytest.mark.asyncio(scope="class") + class TestMixedScopes: + async def test_runs_in_different_loop_as_fixture(self, async_fixture): + global loop + assert asyncio.get_running_loop() is not loop + + """ + ), + ) + result = pytester.runpytest("--asyncio-mode=strict") + result.assert_outcomes(passed=1) + + +def test_asyncio_mark_allows_combining_module_scoped_fixture_with_function_scoped_test( + pytester: Pytester, +): + pytester.makepyfile( + __init__="", + test_mixed_scopes=dedent( + """\ + import asyncio + + import pytest + import pytest_asyncio + + loop: asyncio.AbstractEventLoop + + @pytest_asyncio.fixture(scope="module") + async def async_fixture(): + global loop + loop = asyncio.get_running_loop() + + @pytest.mark.asyncio(scope="function") + async def test_runs_in_different_loop_as_fixture(async_fixture): + global loop + assert asyncio.get_running_loop() is not loop + """ + ), + ) + result = pytester.runpytest("--asyncio-mode=strict") + result.assert_outcomes(passed=1) diff --git a/tests/markers/test_package_scope.py b/tests/markers/test_package_scope.py index fde2e836..3d898c8d 100644 --- a/tests/markers/test_package_scope.py +++ b/tests/markers/test_package_scope.py @@ -223,3 +223,94 @@ async def test_runs_in_same_loop_as_fixture(my_fixture): ) result = pytester.runpytest_subprocess("--asyncio-mode=strict") result.assert_outcomes(passed=1) + + +def test_asyncio_mark_allows_combining_package_scoped_fixture_with_module_scoped_test( + pytester: Pytester, +): + pytester.makepyfile( + __init__="", + test_mixed_scopes=dedent( + """\ + import asyncio + + import pytest + import pytest_asyncio + + loop: asyncio.AbstractEventLoop + + @pytest_asyncio.fixture(scope="package") + async def async_fixture(): + global loop + loop = asyncio.get_running_loop() + + @pytest.mark.asyncio(scope="module") + async def test_runs_in_different_loop_as_fixture(async_fixture): + global loop + assert asyncio.get_running_loop() is not loop + """ + ), + ) + result = pytester.runpytest("--asyncio-mode=strict") + result.assert_outcomes(passed=1) + + +def test_asyncio_mark_allows_combining_package_scoped_fixture_with_class_scoped_test( + pytester: Pytester, +): + pytester.makepyfile( + __init__="", + test_mixed_scopes=dedent( + """\ + import asyncio + + import pytest + import pytest_asyncio + + loop: asyncio.AbstractEventLoop + + @pytest_asyncio.fixture(scope="package") + async def async_fixture(): + global loop + loop = asyncio.get_running_loop() + + @pytest.mark.asyncio(scope="class") + class TestMixedScopes: + async def test_runs_in_different_loop_as_fixture(self, async_fixture): + global loop + assert asyncio.get_running_loop() is not loop + """ + ), + ) + result = pytester.runpytest("--asyncio-mode=strict") + result.assert_outcomes(passed=1) + + +def test_asyncio_mark_allows_combining_package_scoped_fixture_with_function_scoped_test( + pytester: Pytester, +): + pytester.makepyfile( + __init__="", + test_mixed_scopes=dedent( + """\ + import asyncio + + import pytest + import pytest_asyncio + + loop: asyncio.AbstractEventLoop + + @pytest_asyncio.fixture(scope="package") + async def async_fixture(): + global loop + loop = asyncio.get_running_loop() + + @pytest.mark.asyncio + async def test_runs_in_different_loop_as_fixture(async_fixture): + global loop + assert asyncio.get_running_loop() is not loop + """ + ), + ) + result = pytester.runpytest("--asyncio-mode=strict") + result.assert_outcomes(passed=1) diff --git a/tests/markers/test_session_scope.py b/tests/markers/test_session_scope.py index 1242cfee..a9a8b7a8 100644 --- a/tests/markers/test_session_scope.py +++ b/tests/markers/test_session_scope.py @@ -227,3 +227,124 @@ async def test_runs_in_same_loop_as_fixture(my_fixture): ) result = pytester.runpytest_subprocess("--asyncio-mode=strict") result.assert_outcomes(passed=1) + + +def test_asyncio_mark_allows_combining_session_scoped_fixture_with_package_scoped_test( + pytester: Pytester, +): + pytester.makepyfile( + __init__="", + test_mixed_scopes=dedent( + """\ + import asyncio + + import pytest + import pytest_asyncio + + loop: asyncio.AbstractEventLoop + + @pytest_asyncio.fixture(scope="session") + async def async_fixture(): + global loop + loop = asyncio.get_running_loop() + + @pytest.mark.asyncio(scope="package") + async def test_runs_in_different_loop_as_fixture(async_fixture): + global loop + assert asyncio.get_running_loop() is not loop + """ + ), + ) + result = pytester.runpytest("--asyncio-mode=strict") + result.assert_outcomes(passed=1) + + +def test_asyncio_mark_allows_combining_session_scoped_fixture_with_module_scoped_test( + pytester: Pytester, +): + pytester.makepyfile( + __init__="", + test_mixed_scopes=dedent( + """\ + import asyncio + + import pytest + import pytest_asyncio + + loop: asyncio.AbstractEventLoop + + @pytest_asyncio.fixture(scope="session") + async def async_fixture(): + global loop + loop = asyncio.get_running_loop() + + @pytest.mark.asyncio(scope="module") + async def test_runs_in_different_loop_as_fixture(async_fixture): + global loop + assert asyncio.get_running_loop() is not loop + """ + ), + ) + result = pytester.runpytest("--asyncio-mode=strict") + result.assert_outcomes(passed=1) + + +def test_asyncio_mark_allows_combining_session_scoped_fixture_with_class_scoped_test( + pytester: Pytester, +): + pytester.makepyfile( + __init__="", + test_mixed_scopes=dedent( + """\ + import asyncio + + import pytest + import pytest_asyncio + + loop: asyncio.AbstractEventLoop + + @pytest_asyncio.fixture(scope="session") + async def async_fixture(): + global loop + loop = asyncio.get_running_loop() + + @pytest.mark.asyncio(scope="class") + class TestMixedScopes: + async def test_runs_in_different_loop_as_fixture(self, async_fixture): + global loop + assert asyncio.get_running_loop() is not loop + """ + ), + ) + result = pytester.runpytest("--asyncio-mode=strict") + result.assert_outcomes(passed=1) + + +def test_asyncio_mark_allows_combining_session_scoped_fixture_with_function_scoped_test( + pytester: Pytester, +): + pytester.makepyfile( + __init__="", + test_mixed_scopes=dedent( + """\ + import asyncio + + import pytest + import pytest_asyncio + + loop: asyncio.AbstractEventLoop + + @pytest_asyncio.fixture(scope="session") + async def async_fixture(): + global loop + loop = asyncio.get_running_loop() + + @pytest.mark.asyncio + async def test_runs_in_different_loop_as_fixture(async_fixture): + global loop + assert asyncio.get_running_loop() is not loop + """ + ), + ) + result = pytester.runpytest("--asyncio-mode=strict") + result.assert_outcomes(passed=1) From 340d27064fad3e47b37a4d16c067cbbee5cade88 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 20 Nov 2023 18:24:40 +0000 Subject: [PATCH 109/151] Build(deps): Bump pygments from 2.16.1 to 2.17.1 in /dependencies/docs Bumps [pygments](https://github.com/pygments/pygments) from 2.16.1 to 2.17.1. - [Release notes](https://github.com/pygments/pygments/releases) - [Changelog](https://github.com/pygments/pygments/blob/master/CHANGES) - [Commits](https://github.com/pygments/pygments/compare/2.16.1...2.17.1) --- updated-dependencies: - dependency-name: pygments dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- dependencies/docs/constraints.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies/docs/constraints.txt b/dependencies/docs/constraints.txt index 3ee57aed..1ed5ce10 100644 --- a/dependencies/docs/constraints.txt +++ b/dependencies/docs/constraints.txt @@ -8,7 +8,7 @@ imagesize==1.4.1 Jinja2==3.1.2 MarkupSafe==2.1.3 packaging==23.2 -Pygments==2.16.1 +Pygments==2.17.1 requests==2.31.0 snowballstemmer==2.2.0 Sphinx==7.2.6 From 6f3a2d925cfda9fa7ff4c4dc939862847c7fe041 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 20 Nov 2023 18:24:44 +0000 Subject: [PATCH 110/151] Build(deps): Bump certifi in /dependencies/docs Bumps [certifi](https://github.com/certifi/python-certifi) from 2023.7.22 to 2023.11.17. - [Commits](https://github.com/certifi/python-certifi/compare/2023.07.22...2023.11.17) --- updated-dependencies: - dependency-name: certifi dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- dependencies/docs/constraints.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies/docs/constraints.txt b/dependencies/docs/constraints.txt index 1ed5ce10..d11494a0 100644 --- a/dependencies/docs/constraints.txt +++ b/dependencies/docs/constraints.txt @@ -1,6 +1,6 @@ alabaster==0.7.13 Babel==2.13.1 -certifi==2023.7.22 +certifi==2023.11.17 charset-normalizer==3.3.2 docutils==0.18.1 idna==3.4 From 01f5fecca5921aaec52fc0ced036d24b356f3955 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 20 Nov 2023 18:35:27 +0000 Subject: [PATCH 111/151] Build(deps): Bump hypothesis in /dependencies/default Bumps [hypothesis](https://github.com/HypothesisWorks/hypothesis) from 6.88.4 to 6.90.0. - [Release notes](https://github.com/HypothesisWorks/hypothesis/releases) - [Commits](https://github.com/HypothesisWorks/hypothesis/compare/hypothesis-python-6.88.4...hypothesis-python-6.90.0) --- updated-dependencies: - dependency-name: hypothesis dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- dependencies/default/constraints.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies/default/constraints.txt b/dependencies/default/constraints.txt index 8baa17ad..92272c7d 100644 --- a/dependencies/default/constraints.txt +++ b/dependencies/default/constraints.txt @@ -2,7 +2,7 @@ attrs==23.1.0 coverage==7.3.2 exceptiongroup==1.1.3 flaky==3.7.0 -hypothesis==6.88.4 +hypothesis==6.90.0 iniconfig==2.0.0 mypy==1.7.0 mypy-extensions==1.0.0 From 144277b257972f3283bb13812d5059e2c3fa268f Mon Sep 17 00:00:00 2001 From: Michael Seifert Date: Wed, 22 Nov 2023 11:24:14 +0100 Subject: [PATCH 112/151] [fix] Fixes a bug that caused internal pytest errors during test collection with doctests. Pytest-asyncio attaches an event loop of a particular scope to each collector during pytest_collectstart. The implementation did not account for the fact that collectors may have specialized subclasses, such as DoctestModule. This caused a KeyError during a dictionary access, leading to an internal pytest error in the collection phase. This patch changes the implementation of pytest_collectstart to account for subclasses of collectors. Signed-off-by: Michael Seifert --- pytest_asyncio/plugin.py | 18 +++++++++++++----- tests/test_doctest.py | 19 +++++++++++++++++++ 2 files changed, 32 insertions(+), 5 deletions(-) create mode 100644 tests/test_doctest.py diff --git a/pytest_asyncio/plugin.py b/pytest_asyncio/plugin.py index dfbd9958..6e7a3e67 100644 --- a/pytest_asyncio/plugin.py +++ b/pytest_asyncio/plugin.py @@ -544,25 +544,33 @@ def pytest_pycollect_makeitem_convert_async_functions_to_subclass( _event_loop_fixture_id = StashKey[str] _fixture_scope_by_collector_type = { Class: "class", - Module: "module", + # Package is a subclass of module and the dict is used in isinstance checks + # Therefore, the order matters and Package needs to appear before Module Package: "package", + Module: "module", Session: "session", } @pytest.hookimpl def pytest_collectstart(collector: pytest.Collector): + try: + collector_scope = next( + scope + for cls, scope in _fixture_scope_by_collector_type.items() + if isinstance(collector, cls) + ) + except StopIteration: + return # Session is not a PyCollector type, so it doesn't have a corresponding # "obj" attribute to attach a dynamic fixture function to. # However, there's only one session per pytest run, so there's no need to # create the fixture dynamically. We can simply define a session-scoped # event loop fixture once in the plugin code. - if isinstance(collector, Session): + if collector_scope == "session": event_loop_fixture_id = _session_event_loop.__name__ collector.stash[_event_loop_fixture_id] = event_loop_fixture_id return - if not isinstance(collector, (Class, Module, Package)): - return # There seem to be issues when a fixture is shadowed by another fixture # and both differ in their params. # https://github.com/pytest-dev/pytest/issues/2043 @@ -574,7 +582,7 @@ def pytest_collectstart(collector: pytest.Collector): collector.stash[_event_loop_fixture_id] = event_loop_fixture_id @pytest.fixture( - scope=_fixture_scope_by_collector_type[type(collector)], + scope=collector_scope, name=event_loop_fixture_id, ) def scoped_event_loop( diff --git a/tests/test_doctest.py b/tests/test_doctest.py new file mode 100644 index 00000000..6c26e645 --- /dev/null +++ b/tests/test_doctest.py @@ -0,0 +1,19 @@ +from textwrap import dedent + +from pytest import Pytester + + +def test_plugin_does_not_interfere_with_doctest_collection(pytester: Pytester): + pytester.makepyfile( + dedent( + '''\ + def any_function(): + """ + >>> 42 + 42 + """ + ''' + ), + ) + result = pytester.runpytest("--asyncio-mode=strict", "--doctest-modules") + result.assert_outcomes(passed=1) From 77972a5dc2afd60777ff04dd13c1ef5887433e31 Mon Sep 17 00:00:00 2001 From: Michael Seifert Date: Sun, 19 Nov 2023 11:16:53 +0100 Subject: [PATCH 113/151] [refactor] Moved tests related to tcp and udp port fixtures to a separate module Signed-off-by: Michael Seifert --- tests/test_port_factories.py | 197 +++++++++++++++++++++++++++++++++++ tests/test_simple.py | 160 ---------------------------- 2 files changed, 197 insertions(+), 160 deletions(-) create mode 100644 tests/test_port_factories.py diff --git a/tests/test_port_factories.py b/tests/test_port_factories.py new file mode 100644 index 00000000..cbbd47b4 --- /dev/null +++ b/tests/test_port_factories.py @@ -0,0 +1,197 @@ +from textwrap import dedent + +from pytest import Pytester + +import pytest_asyncio.plugin + + +def test_unused_tcp_port_selects_unused_port(pytester: Pytester): + pytester.makepyfile( + dedent( + """\ + import asyncio + + import pytest + + @pytest.mark.asyncio + async def test_unused_port_fixture(unused_tcp_port): + async def closer(_, writer): + writer.close() + + server1 = await asyncio.start_server( + closer, host="localhost", port=unused_tcp_port + ) + + with pytest.raises(IOError): + await asyncio.start_server( + closer, host="localhost", port=unused_tcp_port + ) + + server1.close() + await server1.wait_closed() + """ + ) + ) + + +def test_unused_udp_port_selects_unused_port(pytester: Pytester): + pytester.makepyfile( + dedent( + """\ + @pytest.mark.asyncio + async def test_unused_udp_port_fixture(unused_udp_port): + class Closer: + def connection_made(self, transport): + pass + + def connection_lost(self, *arg, **kwd): + pass + + event_loop = asyncio.get_running_loop() + transport1, _ = await event_loop.create_datagram_endpoint( + Closer, + local_addr=("127.0.0.1", unused_udp_port), + reuse_port=False, + ) + + with pytest.raises(IOError): + await event_loop.create_datagram_endpoint( + Closer, + local_addr=("127.0.0.1", unused_udp_port), + reuse_port=False, + ) + + transport1.abort() + """ + ) + ) + + +def test_unused_tcp_port_factory_selects_unused_port(pytester: Pytester): + pytester.makepyfile( + dedent( + """\ + @pytest.mark.asyncio + async def test_unused_port_factory_fixture(unused_tcp_port_factory): + async def closer(_, writer): + writer.close() + + port1, port2, port3 = ( + unused_tcp_port_factory(), + unused_tcp_port_factory(), + unused_tcp_port_factory(), + ) + + server1 = await asyncio.start_server( + closer, host="localhost", port=port1 + ) + server2 = await asyncio.start_server( + closer, host="localhost", port=port2 + ) + server3 = await asyncio.start_server( + closer, host="localhost", port=port3 + ) + + for port in port1, port2, port3: + with pytest.raises(IOError): + await asyncio.start_server(closer, host="localhost", port=port) + + server1.close() + await server1.wait_closed() + server2.close() + await server2.wait_closed() + server3.close() + await server3.wait_closed() + """ + ) + ) + + +def test_unused_udp_port_factory_selects_unused_port(pytester: Pytester): + pytester.makepyfile( + dedent( + """\ + @pytest.mark.asyncio + async def test_unused_udp_port_factory_fixture(unused_udp_port_factory): + class Closer: + def connection_made(self, transport): + pass + + def connection_lost(self, *arg, **kwd): + pass + + port1, port2, port3 = ( + unused_udp_port_factory(), + unused_udp_port_factory(), + unused_udp_port_factory(), + ) + + event_loop = asyncio.get_running_loop() + transport1, _ = await event_loop.create_datagram_endpoint( + Closer, + local_addr=("127.0.0.1", port1), + reuse_port=False, + ) + transport2, _ = await event_loop.create_datagram_endpoint( + Closer, + local_addr=("127.0.0.1", port2), + reuse_port=False, + ) + transport3, _ = await event_loop.create_datagram_endpoint( + Closer, + local_addr=("127.0.0.1", port3), + reuse_port=False, + ) + + for port in port1, port2, port3: + with pytest.raises(IOError): + await event_loop.create_datagram_endpoint( + Closer, + local_addr=("127.0.0.1", port), + reuse_port=False, + ) + + transport1.abort() + transport2.abort() + transport3.abort() + """ + ) + ) + + +def test_unused_port_factory_duplicate(unused_tcp_port_factory, monkeypatch): + """Test correct avoidance of duplicate ports.""" + counter = 0 + + def mock_unused_tcp_port(_ignored): + """Force some duplicate ports.""" + nonlocal counter + counter += 1 + if counter < 5: + return 10000 + else: + return 10000 + counter + + monkeypatch.setattr(pytest_asyncio.plugin, "_unused_port", mock_unused_tcp_port) + + assert unused_tcp_port_factory() == 10000 + assert unused_tcp_port_factory() > 10000 + + +def test_unused_udp_port_factory_duplicate(unused_udp_port_factory, monkeypatch): + """Test correct avoidance of duplicate UDP ports.""" + counter = 0 + + def mock_unused_udp_port(_ignored): + """Force some duplicate ports.""" + nonlocal counter + counter += 1 + if counter < 5: + return 10000 + else: + return 10000 + counter + + monkeypatch.setattr(pytest_asyncio.plugin, "_unused_port", mock_unused_udp_port) + + assert unused_udp_port_factory() == 10000 + assert unused_udp_port_factory() > 10000 diff --git a/tests/test_simple.py b/tests/test_simple.py index b6020c69..c448de92 100644 --- a/tests/test_simple.py +++ b/tests/test_simple.py @@ -5,8 +5,6 @@ import pytest from pytest import Pytester -import pytest_asyncio.plugin - async def async_coro(): await asyncio.sleep(0) @@ -69,164 +67,6 @@ async def test_asyncio_marker_with_default_param(a_param=None): await asyncio.sleep(0) -@pytest.mark.asyncio -async def test_unused_port_fixture(unused_tcp_port): - """Test the unused TCP port fixture.""" - - async def closer(_, writer): - writer.close() - - server1 = await asyncio.start_server(closer, host="localhost", port=unused_tcp_port) - - with pytest.raises(IOError): - await asyncio.start_server(closer, host="localhost", port=unused_tcp_port) - - server1.close() - await server1.wait_closed() - - -@pytest.mark.asyncio -async def test_unused_udp_port_fixture(unused_udp_port): - """Test the unused TCP port fixture.""" - - class Closer: - def connection_made(self, transport): - pass - - def connection_lost(self, *arg, **kwd): - pass - - event_loop = asyncio.get_running_loop() - transport1, _ = await event_loop.create_datagram_endpoint( - Closer, - local_addr=("127.0.0.1", unused_udp_port), - reuse_port=False, - ) - - with pytest.raises(IOError): - await event_loop.create_datagram_endpoint( - Closer, - local_addr=("127.0.0.1", unused_udp_port), - reuse_port=False, - ) - - transport1.abort() - - -@pytest.mark.asyncio -async def test_unused_port_factory_fixture(unused_tcp_port_factory): - """Test the unused TCP port factory fixture.""" - - async def closer(_, writer): - writer.close() - - port1, port2, port3 = ( - unused_tcp_port_factory(), - unused_tcp_port_factory(), - unused_tcp_port_factory(), - ) - - server1 = await asyncio.start_server(closer, host="localhost", port=port1) - server2 = await asyncio.start_server(closer, host="localhost", port=port2) - server3 = await asyncio.start_server(closer, host="localhost", port=port3) - - for port in port1, port2, port3: - with pytest.raises(IOError): - await asyncio.start_server(closer, host="localhost", port=port) - - server1.close() - await server1.wait_closed() - server2.close() - await server2.wait_closed() - server3.close() - await server3.wait_closed() - - -@pytest.mark.asyncio -async def test_unused_udp_port_factory_fixture(unused_udp_port_factory): - """Test the unused UDP port factory fixture.""" - - class Closer: - def connection_made(self, transport): - pass - - def connection_lost(self, *arg, **kwd): - pass - - port1, port2, port3 = ( - unused_udp_port_factory(), - unused_udp_port_factory(), - unused_udp_port_factory(), - ) - - event_loop = asyncio.get_running_loop() - transport1, _ = await event_loop.create_datagram_endpoint( - Closer, - local_addr=("127.0.0.1", port1), - reuse_port=False, - ) - transport2, _ = await event_loop.create_datagram_endpoint( - Closer, - local_addr=("127.0.0.1", port2), - reuse_port=False, - ) - transport3, _ = await event_loop.create_datagram_endpoint( - Closer, - local_addr=("127.0.0.1", port3), - reuse_port=False, - ) - - for port in port1, port2, port3: - with pytest.raises(IOError): - await event_loop.create_datagram_endpoint( - Closer, - local_addr=("127.0.0.1", port), - reuse_port=False, - ) - - transport1.abort() - transport2.abort() - transport3.abort() - - -def test_unused_port_factory_duplicate(unused_tcp_port_factory, monkeypatch): - """Test correct avoidance of duplicate ports.""" - counter = 0 - - def mock_unused_tcp_port(_ignored): - """Force some duplicate ports.""" - nonlocal counter - counter += 1 - if counter < 5: - return 10000 - else: - return 10000 + counter - - monkeypatch.setattr(pytest_asyncio.plugin, "_unused_port", mock_unused_tcp_port) - - assert unused_tcp_port_factory() == 10000 - assert unused_tcp_port_factory() > 10000 - - -def test_unused_udp_port_factory_duplicate(unused_udp_port_factory, monkeypatch): - """Test correct avoidance of duplicate UDP ports.""" - counter = 0 - - def mock_unused_udp_port(_ignored): - """Force some duplicate ports.""" - nonlocal counter - counter += 1 - if counter < 5: - return 10000 - else: - return 10000 + counter - - monkeypatch.setattr(pytest_asyncio.plugin, "_unused_port", mock_unused_udp_port) - - assert unused_udp_port_factory() == 10000 - assert unused_udp_port_factory() > 10000 - - class TestMarkerInClassBasedTests: """Test that asyncio marked functions work for methods of test classes.""" From c42eb2ad7c467d655995083ed6b2511c688aaf0a Mon Sep 17 00:00:00 2001 From: Michael Seifert Date: Wed, 22 Nov 2023 10:22:51 +0100 Subject: [PATCH 114/151] [refactor] Extracted a context manager that sets a temporary event loop policy for the scope of the context. Signed-off-by: Michael Seifert --- pytest_asyncio/plugin.py | 58 +++++++++++++++++++--------------------- 1 file changed, 28 insertions(+), 30 deletions(-) diff --git a/pytest_asyncio/plugin.py b/pytest_asyncio/plugin.py index 6e7a3e67..9dee882e 100644 --- a/pytest_asyncio/plugin.py +++ b/pytest_asyncio/plugin.py @@ -590,21 +590,11 @@ def scoped_event_loop( event_loop_policy, ) -> Iterator[asyncio.AbstractEventLoop]: new_loop_policy = event_loop_policy - old_loop_policy = asyncio.get_event_loop_policy() - old_loop = asyncio.get_event_loop() - asyncio.set_event_loop_policy(new_loop_policy) - loop = asyncio.new_event_loop() - asyncio.set_event_loop(loop) - yield loop - loop.close() - asyncio.set_event_loop_policy(old_loop_policy) - # When a test uses both a scoped event loop and the event_loop fixture, - # the "_provide_clean_event_loop" finalizer of the event_loop fixture - # will already have installed a fresh event loop, in order to shield - # subsequent tests from side-effects. We close this loop before restoring - # the old loop to avoid ResourceWarnings. - asyncio.get_event_loop().close() - asyncio.set_event_loop(old_loop) + with _temporary_event_loop_policy(new_loop_policy): + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + yield loop + loop.close() # @pytest.fixture does not register the fixture anywhere, so pytest doesn't # know it exists. We work around this by attaching the fixture function to the @@ -630,6 +620,24 @@ def _removesuffix(s: str, suffix: str) -> str: return s.removesuffix(suffix) +@contextlib.contextmanager +def _temporary_event_loop_policy(policy: AbstractEventLoopPolicy) -> Iterator[None]: + old_loop_policy = asyncio.get_event_loop_policy() + old_loop = asyncio.get_event_loop() + asyncio.set_event_loop_policy(policy) + try: + yield + finally: + asyncio.set_event_loop_policy(old_loop_policy) + # When a test uses both a scoped event loop and the event_loop fixture, + # the "_provide_clean_event_loop" finalizer of the event_loop fixture + # will already have installed a fresh event loop, in order to shield + # subsequent tests from side-effects. We close this loop before restoring + # the old loop to avoid ResourceWarnings. + asyncio.get_event_loop().close() + asyncio.set_event_loop(old_loop) + + def pytest_collection_modifyitems( session: Session, config: Config, items: List[Item] ) -> None: @@ -958,21 +966,11 @@ def _session_event_loop( request: FixtureRequest, event_loop_policy: AbstractEventLoopPolicy ) -> Iterator[asyncio.AbstractEventLoop]: new_loop_policy = event_loop_policy - old_loop_policy = asyncio.get_event_loop_policy() - old_loop = asyncio.get_event_loop() - asyncio.set_event_loop_policy(new_loop_policy) - loop = asyncio.new_event_loop() - asyncio.set_event_loop(loop) - yield loop - loop.close() - asyncio.set_event_loop_policy(old_loop_policy) - # When a test uses both a scoped event loop and the event_loop fixture, - # the "_provide_clean_event_loop" finalizer of the event_loop fixture - # will already have installed a fresh event loop, in order to shield - # subsequent tests from side-effects. We close this loop before restoring - # the old loop to avoid ResourceWarnings. - asyncio.get_event_loop().close() - asyncio.set_event_loop(old_loop) + with _temporary_event_loop_policy(new_loop_policy): + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + yield loop + loop.close() @pytest.fixture(scope="session", autouse=True) From fac9092af9fab5625cff0dbecee7865b099f5309 Mon Sep 17 00:00:00 2001 From: Michael Seifert Date: Wed, 22 Nov 2023 10:54:42 +0100 Subject: [PATCH 115/151] [fix] Fixes a bug related to the ordering of the "event_loop" fixture in connection with parametrized tests. The fixture evaluation order changed for parametrizations of the same test. The reason is probably the fact that `event_loop` was inserted at position 0 in the pytest fixture closure for the current test. Since the synchronization wrapper for async tests uses the currently installed event loop rather than an explicit reference as of commit 36b226936e17232535e88ca34f9707cdf211776b, we can drop the insertion of the event_loop fixture as the first fixture to be evaluated. This patch also addresses an issue that caused RuntimeErrors when the event loop was set to None in a fixture that is requested by an async test. This can occur due to the use of asyncio.run, for example. Signed-off-by: Michael Seifert --- pytest_asyncio/plugin.py | 21 ++++++++--------- tests/markers/test_class_scope.py | 35 ++++++++++++++++++++++++++++ tests/markers/test_function_scope.py | 34 +++++++++++++++++++++++++++ tests/markers/test_module_scope.py | 34 +++++++++++++++++++++++++++ tests/markers/test_package_scope.py | 35 ++++++++++++++++++++++++++++ tests/markers/test_session_scope.py | 34 +++++++++++++++++++++++++++ 6 files changed, 182 insertions(+), 11 deletions(-) diff --git a/pytest_asyncio/plugin.py b/pytest_asyncio/plugin.py index 9dee882e..4f9ed217 100644 --- a/pytest_asyncio/plugin.py +++ b/pytest_asyncio/plugin.py @@ -623,7 +623,10 @@ def _removesuffix(s: str, suffix: str) -> str: @contextlib.contextmanager def _temporary_event_loop_policy(policy: AbstractEventLoopPolicy) -> Iterator[None]: old_loop_policy = asyncio.get_event_loop_policy() - old_loop = asyncio.get_event_loop() + try: + old_loop = asyncio.get_event_loop() + except RuntimeError: + old_loop = None asyncio.set_event_loop_policy(policy) try: yield @@ -634,7 +637,10 @@ def _temporary_event_loop_policy(policy: AbstractEventLoopPolicy) -> Iterator[No # will already have installed a fresh event loop, in order to shield # subsequent tests from side-effects. We close this loop before restoring # the old loop to avoid ResourceWarnings. - asyncio.get_event_loop().close() + try: + asyncio.get_event_loop().close() + except RuntimeError: + pass asyncio.set_event_loop(old_loop) @@ -908,15 +914,8 @@ def pytest_runtest_setup(item: pytest.Item) -> None: else: event_loop_fixture_id = "event_loop" fixturenames = item.fixturenames # type: ignore[attr-defined] - # inject an event loop fixture for all async tests - if "event_loop" in fixturenames: - # Move the "event_loop" fixture to the beginning of the fixture evaluation - # closure for backwards compatibility - fixturenames.remove("event_loop") - fixturenames.insert(0, "event_loop") - else: - if event_loop_fixture_id not in fixturenames: - fixturenames.append(event_loop_fixture_id) + if event_loop_fixture_id not in fixturenames: + fixturenames.append(event_loop_fixture_id) obj = getattr(item, "obj", None) if not getattr(obj, "hypothesis", False) and getattr( obj, "is_hypothesis_test", False diff --git a/tests/markers/test_class_scope.py b/tests/markers/test_class_scope.py index 1f664774..fa2fe81e 100644 --- a/tests/markers/test_class_scope.py +++ b/tests/markers/test_class_scope.py @@ -251,3 +251,38 @@ async def test_runs_in_different_loop_as_fixture(self, async_fixture): ) result = pytester.runpytest("--asyncio-mode=strict") result.assert_outcomes(passed=1) + + +def test_asyncio_mark_handles_missing_event_loop_triggered_by_fixture( + pytester: pytest.Pytester, +): + pytester.makepyfile( + dedent( + """\ + import pytest + import asyncio + + class TestClass: + @pytest.fixture(scope="class") + def sets_event_loop_to_none(self): + # asyncio.run() creates a new event loop without closing the + # existing one. For any test, but the first one, this leads to + # a ResourceWarning when the discarded loop is destroyed by the + # garbage collector. We close the current loop to avoid this. + try: + asyncio.get_event_loop().close() + except RuntimeError: + pass + return asyncio.run(asyncio.sleep(0)) + # asyncio.run() sets the current event loop to None when finished + + @pytest.mark.asyncio(scope="class") + # parametrization may impact fixture ordering + @pytest.mark.parametrize("n", (0, 1)) + async def test_does_not_fail(self, sets_event_loop_to_none, n): + pass + """ + ) + ) + result = pytester.runpytest("--asyncio-mode=strict") + result.assert_outcomes(passed=2) diff --git a/tests/markers/test_function_scope.py b/tests/markers/test_function_scope.py index df2c3e47..25ff609f 100644 --- a/tests/markers/test_function_scope.py +++ b/tests/markers/test_function_scope.py @@ -145,3 +145,37 @@ async def test_runs_is_same_loop_as_fixture(my_fixture): ) result = pytester.runpytest_subprocess("--asyncio-mode=strict") result.assert_outcomes(passed=1) + + +def test_asyncio_mark_handles_missing_event_loop_triggered_by_fixture( + pytester: Pytester, +): + pytester.makepyfile( + dedent( + """\ + import pytest + import asyncio + + @pytest.fixture + def sets_event_loop_to_none(): + # asyncio.run() creates a new event loop without closing the existing + # one. For any test, but the first one, this leads to a ResourceWarning + # when the discarded loop is destroyed by the garbage collector. + # We close the current loop to avoid this + try: + asyncio.get_event_loop().close() + except RuntimeError: + pass + return asyncio.run(asyncio.sleep(0)) + # asyncio.run() sets the current event loop to None when finished + + @pytest.mark.asyncio + # parametrization may impact fixture ordering + @pytest.mark.parametrize("n", (0, 1)) + async def test_does_not_fail(sets_event_loop_to_none, n): + pass + """ + ) + ) + result = pytester.runpytest("--asyncio-mode=strict") + result.assert_outcomes(passed=2) diff --git a/tests/markers/test_module_scope.py b/tests/markers/test_module_scope.py index b778c9a9..94547e40 100644 --- a/tests/markers/test_module_scope.py +++ b/tests/markers/test_module_scope.py @@ -280,3 +280,37 @@ async def test_runs_in_different_loop_as_fixture(async_fixture): ) result = pytester.runpytest("--asyncio-mode=strict") result.assert_outcomes(passed=1) + + +def test_asyncio_mark_handles_missing_event_loop_triggered_by_fixture( + pytester: Pytester, +): + pytester.makepyfile( + dedent( + """\ + import pytest + import asyncio + + @pytest.fixture(scope="module") + def sets_event_loop_to_none(): + # asyncio.run() creates a new event loop without closing the existing + # one. For any test, but the first one, this leads to a ResourceWarning + # when the discarded loop is destroyed by the garbage collector. + # We close the current loop to avoid this + try: + asyncio.get_event_loop().close() + except RuntimeError: + pass + return asyncio.run(asyncio.sleep(0)) + # asyncio.run() sets the current event loop to None when finished + + @pytest.mark.asyncio(scope="module") + # parametrization may impact fixture ordering + @pytest.mark.parametrize("n", (0, 1)) + async def test_does_not_fail(sets_event_loop_to_none, n): + pass + """ + ) + ) + result = pytester.runpytest("--asyncio-mode=strict") + result.assert_outcomes(passed=2) diff --git a/tests/markers/test_package_scope.py b/tests/markers/test_package_scope.py index 3d898c8d..1dc8a5c9 100644 --- a/tests/markers/test_package_scope.py +++ b/tests/markers/test_package_scope.py @@ -314,3 +314,38 @@ async def test_runs_in_different_loop_as_fixture(async_fixture): ) result = pytester.runpytest("--asyncio-mode=strict") result.assert_outcomes(passed=1) + + +def test_asyncio_mark_handles_missing_event_loop_triggered_by_fixture( + pytester: Pytester, +): + pytester.makepyfile( + __init__="", + test_loop_is_none=dedent( + """\ + import pytest + import asyncio + + @pytest.fixture(scope="package") + def sets_event_loop_to_none(): + # asyncio.run() creates a new event loop without closing the existing + # one. For any test, but the first one, this leads to a ResourceWarning + # when the discarded loop is destroyed by the garbage collector. + # We close the current loop to avoid this + try: + asyncio.get_event_loop().close() + except RuntimeError: + pass + return asyncio.run(asyncio.sleep(0)) + # asyncio.run() sets the current event loop to None when finished + + @pytest.mark.asyncio(scope="package") + # parametrization may impact fixture ordering + @pytest.mark.parametrize("n", (0, 1)) + async def test_does_not_fail(sets_event_loop_to_none, n): + pass + """ + ), + ) + result = pytester.runpytest("--asyncio-mode=strict") + result.assert_outcomes(passed=2) diff --git a/tests/markers/test_session_scope.py b/tests/markers/test_session_scope.py index a9a8b7a8..ac70d01d 100644 --- a/tests/markers/test_session_scope.py +++ b/tests/markers/test_session_scope.py @@ -348,3 +348,37 @@ async def test_runs_in_different_loop_as_fixture(async_fixture): ) result = pytester.runpytest("--asyncio-mode=strict") result.assert_outcomes(passed=1) + + +def test_asyncio_mark_handles_missing_event_loop_triggered_by_fixture( + pytester: Pytester, +): + pytester.makepyfile( + dedent( + """\ + import pytest + import asyncio + + @pytest.fixture(scope="session") + def sets_event_loop_to_none(): + # asyncio.run() creates a new event loop without closing the existing + # one. For any test, but the first one, this leads to a ResourceWarning + # when the discarded loop is destroyed by the garbage collector. + # We close the current loop to avoid this + try: + asyncio.get_event_loop().close() + except RuntimeError: + pass + return asyncio.run(asyncio.sleep(0)) + # asyncio.run() sets the current event loop to None when finished + + @pytest.mark.asyncio(scope="session") + # parametrization may impact fixture ordering + @pytest.mark.parametrize("n", (0, 1)) + async def test_does_not_fail(sets_event_loop_to_none, n): + pass + """ + ) + ) + result = pytester.runpytest("--asyncio-mode=strict") + result.assert_outcomes(passed=2) From 90d548a6d6b180f5cd0fb6c1c03313886b3c2185 Mon Sep 17 00:00:00 2001 From: Michael Seifert Date: Sun, 26 Nov 2023 11:44:28 +0100 Subject: [PATCH 116/151] [refactor] PytestAsyncioFunction.substitute returns the specialized subclass rather than the instance. Previously, PytestAsyncioFunction.substitute returned the Item instance unchanged, when no substitution occured. This change allows for different code branches based on whether the substitution happened or not. Signed-off-by: Michael Seifert --- pytest_asyncio/plugin.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/pytest_asyncio/plugin.py b/pytest_asyncio/plugin.py index 4f9ed217..80757327 100644 --- a/pytest_asyncio/plugin.py +++ b/pytest_asyncio/plugin.py @@ -21,6 +21,7 @@ Literal, Optional, Set, + Type, TypeVar, Union, overload, @@ -365,18 +366,19 @@ class PytestAsyncioFunction(Function): """Base class for all test functions managed by pytest-asyncio.""" @classmethod - def substitute(cls, item: Function, /) -> Function: + def item_subclass_for( + cls, item: Function, / + ) -> Union[Type["PytestAsyncioFunction"], None]: """ - Returns a PytestAsyncioFunction if there is an implementation that can handle - the specified function item. + Returns a subclass of PytestAsyncioFunction if there is a specialized subclass + for the specified function item. - If no implementation of PytestAsyncioFunction can handle the specified item, - the item is returned unchanged. + Return None if no specialized subclass exists for the specified item. """ for subclass in cls.__subclasses__(): if subclass._can_substitute(item): - return subclass._from_function(item) - return item + return subclass + return None @classmethod def _from_function(cls, function: Function, /) -> Function: @@ -535,7 +537,9 @@ def pytest_pycollect_makeitem_convert_async_functions_to_subclass( for node in node_iterator: updated_item = node if isinstance(node, Function): - updated_item = PytestAsyncioFunction.substitute(node) + specialized_item_class = PytestAsyncioFunction.item_subclass_for(node) + if specialized_item_class: + updated_item = specialized_item_class._from_function(node) updated_node_collection.append(updated_item) hook_result.force_result(updated_node_collection) From 92843918f90e208c32ea65623261179263bdd866 Mon Sep 17 00:00:00 2001 From: Michael Seifert Date: Sun, 26 Nov 2023 12:37:01 +0100 Subject: [PATCH 117/151] [fix] Fixes a bug in PytestAsyncioFunction._from_function that prevented own_markers from being transferred to the subclass instance. Signed-off-by: Michael Seifert --- pytest_asyncio/plugin.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pytest_asyncio/plugin.py b/pytest_asyncio/plugin.py index 80757327..fcf587d1 100644 --- a/pytest_asyncio/plugin.py +++ b/pytest_asyncio/plugin.py @@ -395,6 +395,7 @@ def _from_function(cls, function: Function, /) -> Function: keywords=function.keywords, originalname=function.originalname, ) + subclass_instance.own_markers.extend(function.own_markers) subclassed_function_signature = inspect.signature(subclass_instance.obj) if "event_loop" in subclassed_function_signature.parameters: subclass_instance.warn( From 900e593272448a0809a63d75eca5fc6c87aa85ac Mon Sep 17 00:00:00 2001 From: Michael Seifert Date: Sun, 26 Nov 2023 12:41:25 +0100 Subject: [PATCH 118/151] [refactor] Move automatic marking of async tests in auto mode from pytest_collection_modifyitems to pytest_pycollect_makeitem. This change causes PytestAsyncioFunction items to exclusively replace those pytest.Function items that are eligible as async tests. That means, once a test item has been substituted, an isinstance check is sufficient to determine if the item is an async test. Signed-off-by: Michael Seifert --- pytest_asyncio/plugin.py | 30 ++++++------------------------ 1 file changed, 6 insertions(+), 24 deletions(-) diff --git a/pytest_asyncio/plugin.py b/pytest_asyncio/plugin.py index fcf587d1..453b1242 100644 --- a/pytest_asyncio/plugin.py +++ b/pytest_asyncio/plugin.py @@ -540,9 +540,13 @@ def pytest_pycollect_makeitem_convert_async_functions_to_subclass( if isinstance(node, Function): specialized_item_class = PytestAsyncioFunction.item_subclass_for(node) if specialized_item_class: - updated_item = specialized_item_class._from_function(node) + if _get_asyncio_mode( + node.config + ) == Mode.AUTO and not node.get_closest_marker("asyncio"): + node.add_marker("asyncio") + if node.get_closest_marker("asyncio"): + updated_item = specialized_item_class._from_function(node) updated_node_collection.append(updated_item) - hook_result.force_result(updated_node_collection) @@ -649,28 +653,6 @@ def _temporary_event_loop_policy(policy: AbstractEventLoopPolicy) -> Iterator[No asyncio.set_event_loop(old_loop) -def pytest_collection_modifyitems( - session: Session, config: Config, items: List[Item] -) -> None: - """ - Marks collected async test items as `asyncio` tests. - - The mark is only applied in `AUTO` mode. It is applied to: - - - coroutines and async generators - - Hypothesis tests wrapping coroutines - - staticmethods wrapping coroutines - - """ - if _get_asyncio_mode(config) != Mode.AUTO: - return - for item in items: - if isinstance(item, PytestAsyncioFunction) and not item.get_closest_marker( - "asyncio" - ): - item.add_marker("asyncio") - - _REDEFINED_EVENT_LOOP_FIXTURE_WARNING = dedent( """\ The event_loop fixture provided by pytest-asyncio has been redefined in From b56d6c3d0bb8d04740f958522c4de6bd0390b393 Mon Sep 17 00:00:00 2001 From: Michael Seifert Date: Sun, 26 Nov 2023 12:48:48 +0100 Subject: [PATCH 119/151] [refactor] Removed redundant checks for asyncio marker in runtest implementation of PytestAsyncioFunction subclasses. Added assertion for existing asyncio marker in PytestAsyncioFunction._from_function. Signed-off-by: Michael Seifert --- pytest_asyncio/plugin.py | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/pytest_asyncio/plugin.py b/pytest_asyncio/plugin.py index 453b1242..eec7b1d6 100644 --- a/pytest_asyncio/plugin.py +++ b/pytest_asyncio/plugin.py @@ -386,6 +386,7 @@ def _from_function(cls, function: Function, /) -> Function: Instantiates this specific PytestAsyncioFunction type from the specified Function item. """ + assert function.get_closest_marker("asyncio") subclass_instance = cls.from_parent( function.parent, name=function.name, @@ -422,11 +423,10 @@ def _can_substitute(item: Function) -> bool: return asyncio.iscoroutinefunction(func) def runtest(self) -> None: - if self.get_closest_marker("asyncio"): - self.obj = wrap_in_sync( - # https://github.com/pytest-dev/pytest-asyncio/issues/596 - self.obj, # type: ignore[has-type] - ) + self.obj = wrap_in_sync( + # https://github.com/pytest-dev/pytest-asyncio/issues/596 + self.obj, # type: ignore[has-type] + ) super().runtest() @@ -466,11 +466,10 @@ def _can_substitute(item: Function) -> bool: ) def runtest(self) -> None: - if self.get_closest_marker("asyncio"): - self.obj = wrap_in_sync( - # https://github.com/pytest-dev/pytest-asyncio/issues/596 - self.obj, # type: ignore[has-type] - ) + self.obj = wrap_in_sync( + # https://github.com/pytest-dev/pytest-asyncio/issues/596 + self.obj, # type: ignore[has-type] + ) super().runtest() @@ -488,10 +487,9 @@ def _can_substitute(item: Function) -> bool: ) and asyncio.iscoroutinefunction(func.hypothesis.inner_test) def runtest(self) -> None: - if self.get_closest_marker("asyncio"): - self.obj.hypothesis.inner_test = wrap_in_sync( - self.obj.hypothesis.inner_test, - ) + self.obj.hypothesis.inner_test = wrap_in_sync( + self.obj.hypothesis.inner_test, + ) super().runtest() From 5fc341afcfb97f2adf67e33ce540d965d1521799 Mon Sep 17 00:00:00 2001 From: Michael Seifert Date: Sun, 26 Nov 2023 13:19:46 +0100 Subject: [PATCH 120/151] [feat] Added new function "is_async_test" which returns whether a pytest Item is an async test managed by pytest-asyncio. Signed-off-by: Michael Seifert --- docs/source/how-to-guides/index.rst | 1 + .../how-to-guides/test_item_is_async.rst | 7 ++ .../test_item_is_async_example.py | 7 ++ docs/source/reference/changelog.rst | 1 + docs/source/reference/functions.rst | 9 ++ docs/source/reference/index.rst | 1 + pytest_asyncio/__init__.py | 4 +- pytest_asyncio/plugin.py | 5 + tests/test_is_async_test.py | 105 ++++++++++++++++++ 9 files changed, 138 insertions(+), 2 deletions(-) create mode 100644 docs/source/how-to-guides/test_item_is_async.rst create mode 100644 docs/source/how-to-guides/test_item_is_async_example.py create mode 100644 docs/source/reference/functions.rst create mode 100644 tests/test_is_async_test.py diff --git a/docs/source/how-to-guides/index.rst b/docs/source/how-to-guides/index.rst index 5bcb3be7..71567aaf 100644 --- a/docs/source/how-to-guides/index.rst +++ b/docs/source/how-to-guides/index.rst @@ -7,5 +7,6 @@ How-To Guides multiple_loops uvloop + test_item_is_async This section of the documentation provides code snippets and recipes to accomplish specific tasks with pytest-asyncio. diff --git a/docs/source/how-to-guides/test_item_is_async.rst b/docs/source/how-to-guides/test_item_is_async.rst new file mode 100644 index 00000000..a9ea5d40 --- /dev/null +++ b/docs/source/how-to-guides/test_item_is_async.rst @@ -0,0 +1,7 @@ +======================================= +How to tell if a test function is async +======================================= +Use ``pytest_asyncio.is_async_item`` to determine if a test item is asynchronous and managed by pytest-asyncio. + +.. include:: test_item_is_async_example.py + :code: python diff --git a/docs/source/how-to-guides/test_item_is_async_example.py b/docs/source/how-to-guides/test_item_is_async_example.py new file mode 100644 index 00000000..31b44193 --- /dev/null +++ b/docs/source/how-to-guides/test_item_is_async_example.py @@ -0,0 +1,7 @@ +from pytest_asyncio import is_async_test + + +def pytest_collection_modifyitems(items): + for item in items: + if is_async_test(item): + pass diff --git a/docs/source/reference/changelog.rst b/docs/source/reference/changelog.rst index d902ff06..504c58f7 100644 --- a/docs/source/reference/changelog.rst +++ b/docs/source/reference/changelog.rst @@ -9,6 +9,7 @@ Changes are non-breaking, unless you upgrade from v0.22. - BREAKING: The *asyncio_event_loop* mark has been removed. Event loops with class, module, package, and session scopes can be requested via the *scope* keyword argument to the _asyncio_ mark. - Introduces the *event_loop_policy* fixture which allows testing with non-default or multiple event loops `#662 `_ +- Introduces ``pytest_asyncio.is_async_test`` which returns whether a test item is managed by pytest-asyncio `#376 `_ - Removes pytest-trio from the test dependencies `#620 `_ 0.22.0 (2023-10-31) diff --git a/docs/source/reference/functions.rst b/docs/source/reference/functions.rst new file mode 100644 index 00000000..fcd531c2 --- /dev/null +++ b/docs/source/reference/functions.rst @@ -0,0 +1,9 @@ +========= +Functions +========= + +is_async_test +============= +Returns whether a specific pytest Item is an asynchronous test managed by pytest-asyncio. + +This function is intended to be used in pytest hooks or by plugins that depend on pytest-asyncio. diff --git a/docs/source/reference/index.rst b/docs/source/reference/index.rst index 5fdc2724..b24c6e9c 100644 --- a/docs/source/reference/index.rst +++ b/docs/source/reference/index.rst @@ -7,6 +7,7 @@ Reference configuration fixtures/index + functions markers/index decorators/index changelog diff --git a/pytest_asyncio/__init__.py b/pytest_asyncio/__init__.py index 1bc2811d..95046981 100644 --- a/pytest_asyncio/__init__.py +++ b/pytest_asyncio/__init__.py @@ -1,5 +1,5 @@ """The main point for importing pytest-asyncio items.""" from ._version import version as __version__ # noqa -from .plugin import fixture +from .plugin import fixture, is_async_test -__all__ = ("fixture",) +__all__ = ("fixture", "is_async_test") diff --git a/pytest_asyncio/plugin.py b/pytest_asyncio/plugin.py index eec7b1d6..892d8237 100644 --- a/pytest_asyncio/plugin.py +++ b/pytest_asyncio/plugin.py @@ -963,6 +963,11 @@ def event_loop_policy() -> AbstractEventLoopPolicy: return asyncio.get_event_loop_policy() +def is_async_test(item: Item) -> bool: + """Returns whether a test item is a pytest-asyncio test""" + return isinstance(item, PytestAsyncioFunction) + + def _unused_port(socket_type: int) -> int: """Find an unused localhost port from 1024-65535 and return it.""" with contextlib.closing(socket.socket(type=socket_type)) as sock: diff --git a/tests/test_is_async_test.py b/tests/test_is_async_test.py new file mode 100644 index 00000000..512243b3 --- /dev/null +++ b/tests/test_is_async_test.py @@ -0,0 +1,105 @@ +from textwrap import dedent + +import pytest +from pytest import Pytester + + +def test_returns_false_for_sync_item(pytester: Pytester): + pytester.makepyfile( + dedent( + """\ + import pytest + import pytest_asyncio + + def test_sync(): + pass + + def pytest_collection_modifyitems(items): + async_tests = [ + item + for item in items + if pytest_asyncio.is_async_test(item) + ] + assert len(async_tests) == 0 + """ + ) + ) + result = pytester.runpytest("--asyncio-mode=strict") + result.assert_outcomes(passed=1) + + +def test_returns_true_for_marked_coroutine_item_in_strict_mode(pytester: Pytester): + pytester.makepyfile( + dedent( + """\ + import pytest + import pytest_asyncio + + @pytest.mark.asyncio + async def test_coro(): + pass + + def pytest_collection_modifyitems(items): + async_tests = [ + item + for item in items + if pytest_asyncio.is_async_test(item) + ] + assert len(async_tests) == 1 + """ + ) + ) + result = pytester.runpytest("--asyncio-mode=strict") + result.assert_outcomes(passed=1) + + +def test_returns_false_for_unmarked_coroutine_item_in_strict_mode(pytester: Pytester): + pytester.makepyfile( + dedent( + """\ + import pytest + import pytest_asyncio + + async def test_coro(): + pass + + def pytest_collection_modifyitems(items): + async_tests = [ + item + for item in items + if pytest_asyncio.is_async_test(item) + ] + assert len(async_tests) == 0 + """ + ) + ) + result = pytester.runpytest("--asyncio-mode=strict") + if pytest.version_tuple < (7, 2): + # Probably related to https://github.com/pytest-dev/pytest/pull/10012 + result.assert_outcomes(failed=1) + else: + result.assert_outcomes(skipped=1) + + +def test_returns_true_for_unmarked_coroutine_item_in_auto_mode(pytester: Pytester): + pytester.makepyfile( + dedent( + """\ + import pytest + import pytest_asyncio + + async def test_coro(): + pass + + def pytest_collection_modifyitems(items): + async_tests = [ + item + for item in items + if pytest_asyncio.is_async_test(item) + ] + assert len(async_tests) == 1 + """ + ) + ) + result = pytester.runpytest("--asyncio-mode=auto") + result.assert_outcomes(passed=1) From 0392f2cb1e16f59ee0993877ac2181e30a5d36e7 Mon Sep 17 00:00:00 2001 From: Michael Seifert Date: Sun, 26 Nov 2023 11:00:46 +0100 Subject: [PATCH 121/151] [docs] Reword section about asyncio event loops in "concepts". The section now explains the concept of "scoped loops". Signed-off-by: Michael Seifert --- docs/source/concepts.rst | 50 ++++++++++++++----- .../source/concepts_function_scope_example.py | 8 +++ docs/source/concepts_module_scope_example.py | 17 +++++++ 3 files changed, 62 insertions(+), 13 deletions(-) create mode 100644 docs/source/concepts_function_scope_example.py create mode 100644 docs/source/concepts_module_scope_example.py diff --git a/docs/source/concepts.rst b/docs/source/concepts.rst index e774791e..710c5365 100644 --- a/docs/source/concepts.rst +++ b/docs/source/concepts.rst @@ -4,19 +4,43 @@ Concepts asyncio event loops =================== -pytest-asyncio runs each test item in its own asyncio event loop. The loop can be accessed via ``asyncio.get_running_loop()``. - -.. code-block:: python - - async def test_runs_in_a_loop(): - assert asyncio.get_running_loop() - -Synchronous test functions can get access to an asyncio event loop via the `event_loop` fixture. - -.. code-block:: python - - def test_can_access_current_loop(event_loop): - assert event_loop +In order to understand how pytest-asyncio works, it helps to understand how pytest collectors work. +If you already know about pytest collectors, please :ref:`skip ahead `. +Otherwise, continue reading. +Let's assume we have a test suite with a file named *test_all_the_things.py* holding a single test, async or not: + +.. include:: concepts_function_scope_example.py + :code: python + +The file *test_all_the_things.py* is a Python module with a Python test function. +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. +This process is referred to as *test collection* by pytest. +In our particular example, pytest will find our test module and the test function. +We can visualize the collection result by running ``pytest --collect-only``:: + + + + +The example illustrates that the code of our test suite is hierarchical. +Pytest uses so called *collectors* for each level of the hierarchy. +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. +There's also a special *Session* collector at the root of the hierarchy. +You may notice that the individual levels resemble the possible `scopes of a pytest fixture. `__ + +.. _pytest-asyncio-event-loops: + +Pytest-asyncio provides one asyncio event loop for each pytest collector. +By default, each test runs in the event loop provided by the *Function* collector, i.e. tests use the loop with the narrowest scope. +This gives the highest level of isolation between tests. +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 *scope* keyword argument to the *asyncio* mark. +For example, the following two tests use the asyncio event loop provided by the *Module* collector: + +.. include:: concepts_module_scope_example.py + :code: python + +It's highly recommended for neighboring tests to use the same event loop scope. +For example, all tests in a class or module should use the same scope. +Assigning neighboring tests to different event loop scopes is discouraged as it can make test code hard to follow. Test discovery modes ==================== diff --git a/docs/source/concepts_function_scope_example.py b/docs/source/concepts_function_scope_example.py new file mode 100644 index 00000000..1506ecf7 --- /dev/null +++ b/docs/source/concepts_function_scope_example.py @@ -0,0 +1,8 @@ +import asyncio + +import pytest + + +@pytest.mark.asyncio +async def test_runs_in_a_loop(): + assert asyncio.get_running_loop() diff --git a/docs/source/concepts_module_scope_example.py b/docs/source/concepts_module_scope_example.py new file mode 100644 index 00000000..66972888 --- /dev/null +++ b/docs/source/concepts_module_scope_example.py @@ -0,0 +1,17 @@ +import asyncio + +import pytest + +loop: asyncio.AbstractEventLoop + + +@pytest.mark.asyncio(scope="module") +async def test_remember_loop(): + global loop + loop = asyncio.get_running_loop() + + +@pytest.mark.asyncio(scope="module") +async def test_runs_in_a_loop(): + global loop + assert asyncio.get_running_loop() is loop From 6ca920b47b7ea55129197ceda70a57169ab89b59 Mon Sep 17 00:00:00 2001 From: Michael Seifert Date: Sun, 26 Nov 2023 13:38:16 +0100 Subject: [PATCH 122/151] [docs] Added how-to guides for testing with different loop scopes. Signed-off-by: Michael Seifert --- .../how-to-guides/class_scoped_loop_example.py | 14 ++++++++++++++ docs/source/how-to-guides/index.rst | 4 ++++ .../how-to-guides/module_scoped_loop_example.py | 17 +++++++++++++++++ .../package_scoped_loop_example.py | 3 +++ .../run_class_tests_in_same_loop.rst | 8 ++++++++ .../run_module_tests_in_same_loop.rst | 8 ++++++++ .../run_package_tests_in_same_loop.rst | 11 +++++++++++ .../run_session_tests_in_same_loop.rst | 8 ++++++++ .../session_scoped_loop_example.py | 10 ++++++++++ 9 files changed, 83 insertions(+) create mode 100644 docs/source/how-to-guides/class_scoped_loop_example.py create mode 100644 docs/source/how-to-guides/module_scoped_loop_example.py create mode 100644 docs/source/how-to-guides/package_scoped_loop_example.py create mode 100644 docs/source/how-to-guides/run_class_tests_in_same_loop.rst create mode 100644 docs/source/how-to-guides/run_module_tests_in_same_loop.rst create mode 100644 docs/source/how-to-guides/run_package_tests_in_same_loop.rst create mode 100644 docs/source/how-to-guides/run_session_tests_in_same_loop.rst create mode 100644 docs/source/how-to-guides/session_scoped_loop_example.py diff --git a/docs/source/how-to-guides/class_scoped_loop_example.py b/docs/source/how-to-guides/class_scoped_loop_example.py new file mode 100644 index 00000000..5419a7ab --- /dev/null +++ b/docs/source/how-to-guides/class_scoped_loop_example.py @@ -0,0 +1,14 @@ +import asyncio + +import pytest + + +@pytest.mark.asyncio(scope="class") +class TestInOneEventLoopPerClass: + loop: asyncio.AbstractEventLoop + + async def test_remember_loop(self): + TestInOneEventLoopPerClass.loop = asyncio.get_running_loop() + + async def test_assert_same_loop(self): + assert asyncio.get_running_loop() is TestInOneEventLoopPerClass.loop diff --git a/docs/source/how-to-guides/index.rst b/docs/source/how-to-guides/index.rst index 71567aaf..a61ead50 100644 --- a/docs/source/how-to-guides/index.rst +++ b/docs/source/how-to-guides/index.rst @@ -5,6 +5,10 @@ How-To Guides .. toctree:: :hidden: + run_class_tests_in_same_loop + run_module_tests_in_same_loop + run_package_tests_in_same_loop + run_session_tests_in_same_loop multiple_loops uvloop test_item_is_async diff --git a/docs/source/how-to-guides/module_scoped_loop_example.py b/docs/source/how-to-guides/module_scoped_loop_example.py new file mode 100644 index 00000000..b4ef778c --- /dev/null +++ b/docs/source/how-to-guides/module_scoped_loop_example.py @@ -0,0 +1,17 @@ +import asyncio + +import pytest + +pytestmark = pytest.mark.asyncio(scope="module") + +loop: asyncio.AbstractEventLoop + + +async def test_remember_loop(): + global loop + loop = asyncio.get_running_loop() + + +async def test_assert_same_loop(): + global loop + assert asyncio.get_running_loop() is loop diff --git a/docs/source/how-to-guides/package_scoped_loop_example.py b/docs/source/how-to-guides/package_scoped_loop_example.py new file mode 100644 index 00000000..f48c33f1 --- /dev/null +++ b/docs/source/how-to-guides/package_scoped_loop_example.py @@ -0,0 +1,3 @@ +import pytest + +pytestmark = pytest.mark.asyncio(scope="package") diff --git a/docs/source/how-to-guides/run_class_tests_in_same_loop.rst b/docs/source/how-to-guides/run_class_tests_in_same_loop.rst new file mode 100644 index 00000000..a265899c --- /dev/null +++ b/docs/source/how-to-guides/run_class_tests_in_same_loop.rst @@ -0,0 +1,8 @@ +====================================================== +How to run all tests in a class in the same event loop +====================================================== +All tests can be run inside the same event loop by marking them with ``pytest.mark.asyncio(scope="class")``. +This is easily achieved by using the *asyncio* marker as a class decorator. + +.. include:: class_scoped_loop_example.py + :code: python diff --git a/docs/source/how-to-guides/run_module_tests_in_same_loop.rst b/docs/source/how-to-guides/run_module_tests_in_same_loop.rst new file mode 100644 index 00000000..e07eca2e --- /dev/null +++ b/docs/source/how-to-guides/run_module_tests_in_same_loop.rst @@ -0,0 +1,8 @@ +======================================================= +How to run all tests in a module in the same event loop +======================================================= +All tests can be run inside the same event loop by marking them with ``pytest.mark.asyncio(scope="module")``. +This is easily achieved by adding a `pytestmark` statement to your module. + +.. include:: module_scoped_loop_example.py + :code: python diff --git a/docs/source/how-to-guides/run_package_tests_in_same_loop.rst b/docs/source/how-to-guides/run_package_tests_in_same_loop.rst new file mode 100644 index 00000000..24326ed1 --- /dev/null +++ b/docs/source/how-to-guides/run_package_tests_in_same_loop.rst @@ -0,0 +1,11 @@ +======================================================== +How to run all tests in a package in the same event loop +======================================================== +All tests can be run inside the same event loop by marking them with ``pytest.mark.asyncio(scope="package")``. +Add the following code to the ``__init__.py`` of the test package: + +.. include:: package_scoped_loop_example.py + :code: python + +Note that this marker is not passed down to tests in subpackages. +Subpackages constitute their own, separate package. diff --git a/docs/source/how-to-guides/run_session_tests_in_same_loop.rst b/docs/source/how-to-guides/run_session_tests_in_same_loop.rst new file mode 100644 index 00000000..7b0da918 --- /dev/null +++ b/docs/source/how-to-guides/run_session_tests_in_same_loop.rst @@ -0,0 +1,8 @@ +========================================================== +How to run all tests in the session in the same event loop +========================================================== +All tests can be run inside the same event loop by marking them with ``pytest.mark.asyncio(scope="session")``. +The easiest way to mark all tests is via a ``pytest_collection_modifyitems`` hook in the ``conftest.py`` at the root folder of your test suite. + +.. include:: session_scoped_loop_example.py + :code: python diff --git a/docs/source/how-to-guides/session_scoped_loop_example.py b/docs/source/how-to-guides/session_scoped_loop_example.py new file mode 100644 index 00000000..e06ffeb5 --- /dev/null +++ b/docs/source/how-to-guides/session_scoped_loop_example.py @@ -0,0 +1,10 @@ +import pytest + +from pytest_asyncio import is_async_test + + +def pytest_collection_modifyitems(items): + pytest_asyncio_tests = (item for item in items if is_async_test(item)) + session_scope_marker = pytest.mark.asyncio(scope="session") + for async_test in pytest_asyncio_tests: + async_test.add_marker(session_scope_marker) From 86126f66a30b5c569e5944bdeb491329fa387068 Mon Sep 17 00:00:00 2001 From: touilleWoman Date: Thu, 30 Nov 2023 15:06:52 +0100 Subject: [PATCH 123/151] remove useless dependencies: flaky, mypy --- dependencies/default/constraints.txt | 3 -- dependencies/pytest-min/constraints.txt | 4 --- setup.cfg | 2 -- tests/test_flaky_integration.py | 43 ------------------------- 4 files changed, 52 deletions(-) delete mode 100644 tests/test_flaky_integration.py diff --git a/dependencies/default/constraints.txt b/dependencies/default/constraints.txt index 92272c7d..5876750d 100644 --- a/dependencies/default/constraints.txt +++ b/dependencies/default/constraints.txt @@ -1,11 +1,8 @@ attrs==23.1.0 coverage==7.3.2 exceptiongroup==1.1.3 -flaky==3.7.0 hypothesis==6.90.0 iniconfig==2.0.0 -mypy==1.7.0 -mypy-extensions==1.0.0 packaging==23.2 pluggy==1.3.0 pytest==7.4.3 diff --git a/dependencies/pytest-min/constraints.txt b/dependencies/pytest-min/constraints.txt index 133398b3..65e3addb 100644 --- a/dependencies/pytest-min/constraints.txt +++ b/dependencies/pytest-min/constraints.txt @@ -5,13 +5,10 @@ charset-normalizer==3.3.1 coverage==7.3.2 elementpath==4.1.5 exceptiongroup==1.1.3 -flaky==3.7.0 hypothesis==6.88.3 idna==3.4 iniconfig==2.0.0 mock==5.1.0 -mypy==1.6.1 -mypy-extensions==1.0.0 nose==1.3.7 packaging==23.2 pluggy==1.3.0 @@ -21,6 +18,5 @@ pytest==7.0.0 requests==2.31.0 sortedcontainers==2.4.0 tomli==2.0.1 -typing_extensions==4.8.0 urllib3==2.0.7 xmlschema==2.5.0 diff --git a/setup.cfg b/setup.cfg index fcd7477b..fdbaf625 100644 --- a/setup.cfg +++ b/setup.cfg @@ -46,8 +46,6 @@ install_requires = testing = coverage >= 6.2 hypothesis >= 5.7.1 - flaky >= 3.5.0 - mypy >= 0.931 docs = sphinx >= 5.3 sphinx-rtd-theme >= 1.0 diff --git a/tests/test_flaky_integration.py b/tests/test_flaky_integration.py deleted file mode 100644 index 54c9d2ea..00000000 --- a/tests/test_flaky_integration.py +++ /dev/null @@ -1,43 +0,0 @@ -"""Tests for the Flaky integration, which retries failed tests. -""" -from textwrap import dedent - - -def test_auto_mode_cmdline(testdir): - testdir.makepyfile( - dedent( - """\ - import asyncio - import flaky - import pytest - - _threshold = -1 - - @flaky.flaky(3, 2) - @pytest.mark.asyncio - async def test_asyncio_flaky_thing_that_fails_then_succeeds(): - global _threshold - await asyncio.sleep(0.1) - _threshold += 1 - assert _threshold != 1 - """ - ) - ) - # runpytest_subprocess() is required to don't pollute the output - # with flaky restart information - result = testdir.runpytest_subprocess("--asyncio-mode=strict") - result.assert_outcomes(passed=1) - result.stdout.fnmatch_lines( - [ - "===Flaky Test Report===", - "test_asyncio_flaky_thing_that_fails_then_succeeds passed 1 " - "out of the required 2 times. Running test again until it passes 2 times.", - "test_asyncio_flaky_thing_that_fails_then_succeeds failed " - "(1 runs remaining out of 3).", - " ", - " assert 1 != 1", - "test_asyncio_flaky_thing_that_fails_then_succeeds passed 2 " - "out of the required 2 times. Success!", - "===End Flaky Test Report===", - ] - ) From 3d012595d7ac4fb12c57bfe15c4ed9e32e4c9490 Mon Sep 17 00:00:00 2001 From: Michael Seifert Date: Sat, 2 Dec 2023 15:31:03 +0100 Subject: [PATCH 124/151] [docs] Mentioned removal of test dependencies in changelog. Signed-off-by: Michael Seifert --- docs/source/reference/changelog.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/reference/changelog.rst b/docs/source/reference/changelog.rst index 504c58f7..1c577af5 100644 --- a/docs/source/reference/changelog.rst +++ b/docs/source/reference/changelog.rst @@ -10,7 +10,7 @@ Changes are non-breaking, unless you upgrade from v0.22. - BREAKING: The *asyncio_event_loop* mark has been removed. Event loops with class, module, package, and session scopes can be requested via the *scope* keyword argument to the _asyncio_ mark. - Introduces the *event_loop_policy* fixture which allows testing with non-default or multiple event loops `#662 `_ - Introduces ``pytest_asyncio.is_async_test`` which returns whether a test item is managed by pytest-asyncio `#376 `_ -- Removes pytest-trio from the test dependencies `#620 `_ +- Removes and *pytest-trio,* *mypy,* and *flaky* from the test dependencies `#620 `_, `#674 `_, `#678 `_, 0.22.0 (2023-10-31) =================== From 9cab9906a3131a9467bf0dabc294961012fb6e2e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 29 Nov 2023 18:48:14 +0000 Subject: [PATCH 125/151] Build(deps): Bump pypa/gh-action-pypi-publish from 1.8.10 to 1.8.11 Bumps [pypa/gh-action-pypi-publish](https://github.com/pypa/gh-action-pypi-publish) from 1.8.10 to 1.8.11. - [Release notes](https://github.com/pypa/gh-action-pypi-publish/releases) - [Commits](https://github.com/pypa/gh-action-pypi-publish/compare/v1.8.10...v1.8.11) --- updated-dependencies: - dependency-name: pypa/gh-action-pypi-publish dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 17800cff..a83e27b8 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -146,7 +146,7 @@ jobs: run: | pandoc -s -o README.md README.rst - name: PyPI upload - uses: pypa/gh-action-pypi-publish@v1.8.10 + uses: pypa/gh-action-pypi-publish@v1.8.11 with: packages_dir: dist password: ${{ secrets.PYPI_API_TOKEN }} From 1c98ec39869df40ae407b62a52af027153538c26 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 27 Nov 2023 18:37:32 +0000 Subject: [PATCH 126/151] Build(deps): Bump deadsnakes/action from 3.0.1 to 3.1.0 Bumps [deadsnakes/action](https://github.com/deadsnakes/action) from 3.0.1 to 3.1.0. - [Release notes](https://github.com/deadsnakes/action/releases) - [Commits](https://github.com/deadsnakes/action/compare/v3.0.1...v3.1.0) --- updated-dependencies: - dependency-name: deadsnakes/action dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index a83e27b8..5b2cd769 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -69,7 +69,7 @@ jobs: if: "!endsWith(matrix.python-version, '-dev')" with: python-version: ${{ matrix.python-version }} - - uses: deadsnakes/action@v3.0.1 + - uses: deadsnakes/action@v3.1.0 if: endsWith(matrix.python-version, '-dev') with: python-version: ${{ matrix.python-version }} From ee23a658f4e591ae25ea547a824fd1ee7572ed57 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 27 Nov 2023 18:12:23 +0000 Subject: [PATCH 127/151] Build(deps): Bump idna from 3.4 to 3.6 in /dependencies/docs Bumps [idna](https://github.com/kjd/idna) from 3.4 to 3.6. - [Changelog](https://github.com/kjd/idna/blob/master/HISTORY.rst) - [Commits](https://github.com/kjd/idna/compare/v3.4...v3.6) --- updated-dependencies: - dependency-name: idna dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- dependencies/docs/constraints.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies/docs/constraints.txt b/dependencies/docs/constraints.txt index d11494a0..7945b9b9 100644 --- a/dependencies/docs/constraints.txt +++ b/dependencies/docs/constraints.txt @@ -3,7 +3,7 @@ Babel==2.13.1 certifi==2023.11.17 charset-normalizer==3.3.2 docutils==0.18.1 -idna==3.4 +idna==3.6 imagesize==1.4.1 Jinja2==3.1.2 MarkupSafe==2.1.3 From 540426d2316e68c8bbd3073a3dbcb40d97b35b9c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 27 Nov 2023 18:12:18 +0000 Subject: [PATCH 128/151] Build(deps): Bump pygments from 2.17.1 to 2.17.2 in /dependencies/docs Bumps [pygments](https://github.com/pygments/pygments) from 2.17.1 to 2.17.2. - [Release notes](https://github.com/pygments/pygments/releases) - [Changelog](https://github.com/pygments/pygments/blob/master/CHANGES) - [Commits](https://github.com/pygments/pygments/compare/2.17.1...2.17.2) --- updated-dependencies: - dependency-name: pygments dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- dependencies/docs/constraints.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies/docs/constraints.txt b/dependencies/docs/constraints.txt index 7945b9b9..d2450bbf 100644 --- a/dependencies/docs/constraints.txt +++ b/dependencies/docs/constraints.txt @@ -8,7 +8,7 @@ imagesize==1.4.1 Jinja2==3.1.2 MarkupSafe==2.1.3 packaging==23.2 -Pygments==2.17.1 +Pygments==2.17.2 requests==2.31.0 snowballstemmer==2.2.0 Sphinx==7.2.6 From 9a8b431ff1b2c994c7966b2b2d3684d1e79fa6c3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 2 Dec 2023 14:37:24 +0000 Subject: [PATCH 129/151] Build(deps): Bump exceptiongroup in /dependencies/default Bumps [exceptiongroup](https://github.com/agronholm/exceptiongroup) from 1.1.3 to 1.2.0. - [Release notes](https://github.com/agronholm/exceptiongroup/releases) - [Changelog](https://github.com/agronholm/exceptiongroup/blob/main/CHANGES.rst) - [Commits](https://github.com/agronholm/exceptiongroup/compare/1.1.3...1.2.0) --- updated-dependencies: - dependency-name: exceptiongroup dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- dependencies/default/constraints.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies/default/constraints.txt b/dependencies/default/constraints.txt index 5876750d..70844faa 100644 --- a/dependencies/default/constraints.txt +++ b/dependencies/default/constraints.txt @@ -1,6 +1,6 @@ attrs==23.1.0 coverage==7.3.2 -exceptiongroup==1.1.3 +exceptiongroup==1.2.0 hypothesis==6.90.0 iniconfig==2.0.0 packaging==23.2 From 533b886aab96df047e58377104ce90b0a27575ce Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 2 Dec 2023 14:44:37 +0000 Subject: [PATCH 130/151] Build(deps): Bump hypothesis in /dependencies/default Bumps [hypothesis](https://github.com/HypothesisWorks/hypothesis) from 6.90.0 to 6.91.0. - [Release notes](https://github.com/HypothesisWorks/hypothesis/releases) - [Commits](https://github.com/HypothesisWorks/hypothesis/compare/hypothesis-python-6.90.0...hypothesis-python-6.91.0) --- updated-dependencies: - dependency-name: hypothesis dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- dependencies/default/constraints.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies/default/constraints.txt b/dependencies/default/constraints.txt index 70844faa..5bde9955 100644 --- a/dependencies/default/constraints.txt +++ b/dependencies/default/constraints.txt @@ -1,7 +1,7 @@ attrs==23.1.0 coverage==7.3.2 exceptiongroup==1.2.0 -hypothesis==6.90.0 +hypothesis==6.91.0 iniconfig==2.0.0 packaging==23.2 pluggy==1.3.0 From 349c152d1149a9fb7011296292107a90eaa3ca91 Mon Sep 17 00:00:00 2001 From: Michael Seifert Date: Sat, 2 Dec 2023 16:04:52 +0100 Subject: [PATCH 131/151] [docs] Mention the #pytest-asyncio:matrix.org chat room in README and for getting community support. Signed-off-by: Michael Seifert --- README.rst | 3 +++ docs/source/support.rst | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 0682b744..f082798d 100644 --- a/README.rst +++ b/README.rst @@ -12,6 +12,9 @@ pytest-asyncio :alt: Supported Python versions .. image:: https://img.shields.io/badge/code%20style-black-000000.svg :target: https://github.com/ambv/black +.. image:: https://img.shields.io/badge/Matrix-%23pytest--asyncio-brightgreen + :alt: Matrix chat room: #pytest-asyncio + :target: https://matrix.to/#/#pytest-asyncio:matrix.org `pytest-asyncio `_ is a `pytest `_ plugin. It facilitates testing of code that uses the `asyncio `_ library. diff --git a/docs/source/support.rst b/docs/source/support.rst index 30981d94..f998bb35 100644 --- a/docs/source/support.rst +++ b/docs/source/support.rst @@ -18,4 +18,4 @@ If you require commercial support outside of the Tidelift subscription, reach ou Community support ================= -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 `discussions `__ to ask questions. +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. From 3c2691df55553598d1d72790fbf59d14d156cfac Mon Sep 17 00:00:00 2001 From: Michael Seifert Date: Sat, 2 Dec 2023 16:06:18 +0100 Subject: [PATCH 132/151] [docs] Remove badge which points out that the project uses Black as a code formatter. This information is not relevant for users. Signed-off-by: Michael Seifert --- README.rst | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.rst b/README.rst index f082798d..e056a880 100644 --- a/README.rst +++ b/README.rst @@ -10,8 +10,6 @@ pytest-asyncio .. image:: https://img.shields.io/pypi/pyversions/pytest-asyncio.svg :target: https://github.com/pytest-dev/pytest-asyncio :alt: Supported Python versions -.. image:: https://img.shields.io/badge/code%20style-black-000000.svg - :target: https://github.com/ambv/black .. image:: https://img.shields.io/badge/Matrix-%23pytest--asyncio-brightgreen :alt: Matrix chat room: #pytest-asyncio :target: https://matrix.to/#/#pytest-asyncio:matrix.org From 0b34e8e55bfe7037515df95db5f8f5c180be6699 Mon Sep 17 00:00:00 2001 From: Michael Seifert Date: Sun, 3 Dec 2023 10:22:12 +0100 Subject: [PATCH 133/151] [chore] Prepare release of v0.23.0 Signed-off-by: Michael Seifert --- docs/source/conf.py | 2 +- docs/source/reference/changelog.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index b85bc156..4bb6535d 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -9,7 +9,7 @@ project = "pytest-asyncio" copyright = "2023, pytest-asyncio contributors" author = "Tin Tvrtković" -release = "v0.22.0" +release = "v0.23.0" # -- General configuration --------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration diff --git a/docs/source/reference/changelog.rst b/docs/source/reference/changelog.rst index 1c577af5..103cea15 100644 --- a/docs/source/reference/changelog.rst +++ b/docs/source/reference/changelog.rst @@ -2,7 +2,7 @@ Changelog ========= -0.23.0 (UNRELEASED) +0.23.0 (2023-12-03) =================== This release is backwards-compatible with v0.21. Changes are non-breaking, unless you upgrade from v0.22. From 176d558ebbf5df4450c91ae21c6a009b73aac87c Mon Sep 17 00:00:00 2001 From: Michael Seifert Date: Tue, 31 Oct 2023 12:21:42 +0100 Subject: [PATCH 134/151] [refactor] Extracted test for pytest.skip into a separate test module. This prevents unrelated tests from aggregating in test_simple.py. Signed-off-by: Michael Seifert --- tests/test_pytest_skip.py | 38 ++++++++++++++++++++++++++++++++++++++ tests/test_simple.py | 35 ----------------------------------- 2 files changed, 38 insertions(+), 35 deletions(-) create mode 100644 tests/test_pytest_skip.py diff --git a/tests/test_pytest_skip.py b/tests/test_pytest_skip.py new file mode 100644 index 00000000..3c669cfb --- /dev/null +++ b/tests/test_pytest_skip.py @@ -0,0 +1,38 @@ +from textwrap import dedent + +from pytest import Pytester + + +def test_asyncio_marker_compatibility_with_skip(pytester: Pytester): + pytester.makepyfile( + dedent( + """\ + import pytest + + pytest_plugins = "pytest_asyncio" + + @pytest.mark.asyncio + async def test_no_warning_on_skip(): + pytest.skip("Test a skip error inside asyncio") + """ + ) + ) + result = pytester.runpytest("--asyncio-mode=strict") + result.assert_outcomes(skipped=1) + + +def test_asyncio_auto_mode_compatibility_with_skip(pytester: Pytester): + pytester.makepyfile( + dedent( + """\ + import pytest + + pytest_plugins = "pytest_asyncio" + + async def test_no_warning_on_skip(): + pytest.skip("Test a skip error inside asyncio") + """ + ) + ) + result = pytester.runpytest("--asyncio-mode=auto") + result.assert_outcomes(skipped=1) diff --git a/tests/test_simple.py b/tests/test_simple.py index c448de92..05c92694 100644 --- a/tests/test_simple.py +++ b/tests/test_simple.py @@ -100,41 +100,6 @@ async def test_event_loop_before_fixture(self, loop): assert await loop.run_in_executor(None, self.foo) == 1 -def test_asyncio_marker_compatibility_with_skip(pytester: Pytester): - pytester.makepyfile( - dedent( - """\ - import pytest - - pytest_plugins = "pytest_asyncio" - - @pytest.mark.asyncio - async def test_no_warning_on_skip(): - pytest.skip("Test a skip error inside asyncio") - """ - ) - ) - result = pytester.runpytest("--asyncio-mode=strict") - result.assert_outcomes(skipped=1) - - -def test_asyncio_auto_mode_compatibility_with_skip(pytester: Pytester): - pytester.makepyfile( - dedent( - """\ - import pytest - - pytest_plugins = "pytest_asyncio" - - async def test_no_warning_on_skip(): - pytest.skip("Test a skip error inside asyncio") - """ - ) - ) - result = pytester.runpytest("--asyncio-mode=auto") - result.assert_outcomes(skipped=1) - - def test_invalid_asyncio_mode(testdir): result = testdir.runpytest("-o", "asyncio_mode=True") result.stderr.no_fnmatch_line("INTERNALERROR> *") From a214c3e77149608d427ccab69140edb509c67697 Mon Sep 17 00:00:00 2001 From: Michael Seifert Date: Tue, 31 Oct 2023 13:41:49 +0100 Subject: [PATCH 135/151] [fix] Fixes a bug that caused an internal pytest error when using module-level skips. Signed-off-by: Michael Seifert --- docs/source/reference/changelog.rst | 5 +++ pytest_asyncio/plugin.py | 11 +++++- tests/test_pytest_skip.py | 56 +++++++++++++++++++++++++++-- 3 files changed, 69 insertions(+), 3 deletions(-) diff --git a/docs/source/reference/changelog.rst b/docs/source/reference/changelog.rst index 103cea15..79440f45 100644 --- a/docs/source/reference/changelog.rst +++ b/docs/source/reference/changelog.rst @@ -2,6 +2,11 @@ Changelog ========= +0.23.1 (2023-12-03) +=================== +- Fixes a bug that caused an internal pytest error when using module-level skips `#701 `_ + + 0.23.0 (2023-12-03) =================== This release is backwards-compatible with v0.21. diff --git a/pytest_asyncio/plugin.py b/pytest_asyncio/plugin.py index 892d8237..fb0ed226 100644 --- a/pytest_asyncio/plugin.py +++ b/pytest_asyncio/plugin.py @@ -28,6 +28,7 @@ ) import pytest +from _pytest.outcomes import OutcomeException from pytest import ( Class, Collector, @@ -607,7 +608,15 @@ def scoped_event_loop( # know it exists. We work around this by attaching the fixture function to the # collected Python class, where it will be picked up by pytest.Class.collect() # or pytest.Module.collect(), respectively - collector.obj.__pytest_asyncio_scoped_event_loop = scoped_event_loop + try: + collector.obj.__pytest_asyncio_scoped_event_loop = scoped_event_loop + except (OutcomeException, Collector.CollectError): + # Accessing Module.obj triggers a module import executing module-level + # statements. A module-level pytest.skip statement raises the "Skipped" + # OutcomeException or a Collector.CollectError, if the "allow_module_level" + # kwargs is missing. These cases are handled correctly when they happen inside + # Collector.collect(), but this hook runs before the actual collect call. + return # When collector is a package, collector.obj is the package's __init__.py. # pytest doesn't seem to collect fixtures in __init__.py. # Using parsefactories to collect fixtures in __init__.py their baseid will end diff --git a/tests/test_pytest_skip.py b/tests/test_pytest_skip.py index 3c669cfb..17d0befc 100644 --- a/tests/test_pytest_skip.py +++ b/tests/test_pytest_skip.py @@ -3,7 +3,7 @@ from pytest import Pytester -def test_asyncio_marker_compatibility_with_skip(pytester: Pytester): +def test_asyncio_strict_mode_skip(pytester: Pytester): pytester.makepyfile( dedent( """\ @@ -21,7 +21,7 @@ async def test_no_warning_on_skip(): result.assert_outcomes(skipped=1) -def test_asyncio_auto_mode_compatibility_with_skip(pytester: Pytester): +def test_asyncio_auto_mode_skip(pytester: Pytester): pytester.makepyfile( dedent( """\ @@ -36,3 +36,55 @@ async def test_no_warning_on_skip(): ) result = pytester.runpytest("--asyncio-mode=auto") result.assert_outcomes(skipped=1) + + +def test_asyncio_strict_mode_module_level_skip(pytester: Pytester): + pytester.makepyfile( + dedent( + """\ + import pytest + + pytest.skip("Skip all tests", allow_module_level=True) + + @pytest.mark.asyncio + async def test_is_skipped(): + pass + """ + ) + ) + result = pytester.runpytest("--asyncio-mode=strict") + result.assert_outcomes(skipped=1) + + +def test_asyncio_auto_mode_module_level_skip(pytester: Pytester): + pytester.makepyfile( + dedent( + """\ + import pytest + + pytest.skip("Skip all tests", allow_module_level=True) + + async def test_is_skipped(): + pass + """ + ) + ) + result = pytester.runpytest("--asyncio-mode=auto") + result.assert_outcomes(skipped=1) + + +def test_asyncio_auto_mode_wrong_skip_usage(pytester: Pytester): + pytester.makepyfile( + dedent( + """\ + import pytest + + pytest.skip("Skip all tests") + + async def test_is_skipped(): + pass + """ + ) + ) + result = pytester.runpytest("--asyncio-mode=auto") + result.assert_outcomes(errors=1) From b614c77dec6df7414fba78d2fbe7989c6ee16828 Mon Sep 17 00:00:00 2001 From: Michael Seifert Date: Mon, 4 Dec 2023 08:07:51 +0100 Subject: [PATCH 136/151] [fix] Fixes a bug that caused an internal pytest error when collecting .txt files. Signed-off-by: Michael Seifert --- docs/source/reference/changelog.rst | 5 +++++ pytest_asyncio/plugin.py | 6 +++++- tests/test_doctest.py | 20 ++++++++++++++++++++ 3 files changed, 30 insertions(+), 1 deletion(-) diff --git a/docs/source/reference/changelog.rst b/docs/source/reference/changelog.rst index 79440f45..c02a528a 100644 --- a/docs/source/reference/changelog.rst +++ b/docs/source/reference/changelog.rst @@ -2,6 +2,11 @@ Changelog ========= +0.23.2 (2023-12-04) +=================== +- Fixes a bug that caused an internal pytest error when collecting .txt files `#703 `_ + + 0.23.1 (2023-12-03) =================== - Fixes a bug that caused an internal pytest error when using module-level skips `#701 `_ diff --git a/pytest_asyncio/plugin.py b/pytest_asyncio/plugin.py index fb0ed226..934fb91c 100644 --- a/pytest_asyncio/plugin.py +++ b/pytest_asyncio/plugin.py @@ -609,7 +609,11 @@ def scoped_event_loop( # collected Python class, where it will be picked up by pytest.Class.collect() # or pytest.Module.collect(), respectively try: - collector.obj.__pytest_asyncio_scoped_event_loop = scoped_event_loop + pyobject = collector.obj + # If the collected module is a DoctestTextfile, collector.obj is None + if pyobject is None: + return + pyobject.__pytest_asyncio_scoped_event_loop = scoped_event_loop except (OutcomeException, Collector.CollectError): # Accessing Module.obj triggers a module import executing module-level # statements. A module-level pytest.skip statement raises the "Skipped" diff --git a/tests/test_doctest.py b/tests/test_doctest.py index 6c26e645..5b79619a 100644 --- a/tests/test_doctest.py +++ b/tests/test_doctest.py @@ -17,3 +17,23 @@ def any_function(): ) result = pytester.runpytest("--asyncio-mode=strict", "--doctest-modules") result.assert_outcomes(passed=1) + + +def test_plugin_does_not_interfere_with_doctest_textfile_collection(pytester: Pytester): + pytester.makefile(".txt", "") # collected as DoctestTextfile + pytester.makepyfile( + __init__="", + test_python_file=dedent( + """\ + import pytest + + pytest_plugins = "pytest_asyncio" + + @pytest.mark.asyncio + async def test_anything(): + pass + """ + ), + ) + result = pytester.runpytest("--asyncio-mode=strict") + result.assert_outcomes(passed=1) From 97b8eb24d148e4615d88a373127931227e63e60c Mon Sep 17 00:00:00 2001 From: Ujjwal Goel Date: Thu, 7 Dec 2023 01:33:10 -0500 Subject: [PATCH 137/151] [fix] Change `packages_dir` to `packages-dir` This should fix #699 and prevent the warning --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 5b2cd769..f3ebc963 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -148,7 +148,7 @@ jobs: - name: PyPI upload uses: pypa/gh-action-pypi-publish@v1.8.11 with: - packages_dir: dist + packages-dir: dist password: ${{ secrets.PYPI_API_TOKEN }} - name: GitHub Release uses: ncipollo/release-action@v1 From 9a7248aea4200c7409bed09b456dacf35eb7c558 Mon Sep 17 00:00:00 2001 From: Michael Seifert Date: Thu, 7 Dec 2023 09:22:18 +0100 Subject: [PATCH 138/151] [fix] Fixes a bug that caused event loops to be closed prematurely when using async generator fixtures with class scope or wider in a function-scoped test. The fixture setup of the "event_loop" fixture closes any existing loop. The existing loop could also belong to a pytest-asyncio scoped event loop fixture. This caused async generator fixtures using the scoped loop to raise a RuntimeError on teardown, because the scoped loop was closed before the fixture finalizer could not be run. In fact, everything after the async generation fixture's "yield" statement had no functioning event loop. The issue was addressed by adding a special attribute to the scoped event loops provided by pytest-asyncio. If this attribute is present, the setup code of the "event_loop" fixture will not close the loop. This allows keeping backwards compatibility for code that doesn't use scoped loops. It is assumed that the magic attribute can be removed after the deprecation period of event_loop_ fixture overrides. Signed-off-by: Michael Seifert --- docs/source/reference/changelog.rst | 5 +++++ pytest_asyncio/plugin.py | 5 ++++- tests/markers/test_module_scope.py | 30 ++++++++++++++++++++++++++++ tests/markers/test_session_scope.py | 31 +++++++++++++++++++++++++++++ 4 files changed, 70 insertions(+), 1 deletion(-) diff --git a/docs/source/reference/changelog.rst b/docs/source/reference/changelog.rst index c02a528a..7e063d24 100644 --- a/docs/source/reference/changelog.rst +++ b/docs/source/reference/changelog.rst @@ -2,6 +2,11 @@ Changelog ========= +0.23.3 (UNRELEASED) +=================== +- Fixes a bug that caused event loops to be closed prematurely when using async generator fixtures with class scope or wider in a function-scoped test `#708 `_ + + 0.23.2 (2023-12-04) =================== - Fixes a bug that caused an internal pytest error when collecting .txt files `#703 `_ diff --git a/pytest_asyncio/plugin.py b/pytest_asyncio/plugin.py index 934fb91c..0a92730c 100644 --- a/pytest_asyncio/plugin.py +++ b/pytest_asyncio/plugin.py @@ -600,6 +600,7 @@ def scoped_event_loop( new_loop_policy = event_loop_policy with _temporary_event_loop_policy(new_loop_policy): loop = asyncio.new_event_loop() + loop.__pytest_asyncio = True # type: ignore[attr-defined] asyncio.set_event_loop(loop) yield loop loop.close() @@ -749,7 +750,8 @@ def pytest_fixture_setup( with warnings.catch_warnings(): warnings.simplefilter("ignore", DeprecationWarning) old_loop = policy.get_event_loop() - if old_loop is not loop: + is_pytest_asyncio_loop = getattr(old_loop, "__pytest_asyncio", False) + if old_loop is not loop and not is_pytest_asyncio_loop: old_loop.close() except RuntimeError: # Either the current event loop has been set to None @@ -965,6 +967,7 @@ def _session_event_loop( new_loop_policy = event_loop_policy with _temporary_event_loop_policy(new_loop_policy): loop = asyncio.new_event_loop() + loop.__pytest_asyncio = True # type: ignore[attr-defined] asyncio.set_event_loop(loop) yield loop loop.close() diff --git a/tests/markers/test_module_scope.py b/tests/markers/test_module_scope.py index 94547e40..cf6b2f60 100644 --- a/tests/markers/test_module_scope.py +++ b/tests/markers/test_module_scope.py @@ -282,6 +282,36 @@ async def test_runs_in_different_loop_as_fixture(async_fixture): result.assert_outcomes(passed=1) +def test_allows_combining_module_scoped_asyncgen_fixture_with_function_scoped_test( + pytester: Pytester, +): + pytester.makepyfile( + dedent( + """\ + import asyncio + + import pytest + import pytest_asyncio + + loop: asyncio.AbstractEventLoop + + @pytest_asyncio.fixture(scope="module") + async def async_fixture(): + global loop + loop = asyncio.get_running_loop() + yield + + @pytest.mark.asyncio(scope="function") + async def test_runs_in_different_loop_as_fixture(async_fixture): + global loop + assert asyncio.get_running_loop() is not loop + """ + ), + ) + result = pytester.runpytest("--asyncio-mode=strict") + result.assert_outcomes(passed=1) + + def test_asyncio_mark_handles_missing_event_loop_triggered_by_fixture( pytester: Pytester, ): diff --git a/tests/markers/test_session_scope.py b/tests/markers/test_session_scope.py index ac70d01d..bd0baee5 100644 --- a/tests/markers/test_session_scope.py +++ b/tests/markers/test_session_scope.py @@ -350,6 +350,37 @@ async def test_runs_in_different_loop_as_fixture(async_fixture): result.assert_outcomes(passed=1) +def test_allows_combining_session_scoped_asyncgen_fixture_with_function_scoped_test( + pytester: Pytester, +): + pytester.makepyfile( + __init__="", + test_mixed_scopes=dedent( + """\ + import asyncio + + import pytest + import pytest_asyncio + + loop: asyncio.AbstractEventLoop + + @pytest_asyncio.fixture(scope="session") + async def async_fixture(): + global loop + loop = asyncio.get_running_loop() + yield + + @pytest.mark.asyncio + async def test_runs_in_different_loop_as_fixture(async_fixture): + global loop + assert asyncio.get_running_loop() is not loop + """ + ), + ) + result = pytester.runpytest("--asyncio-mode=strict") + result.assert_outcomes(passed=1) + + def test_asyncio_mark_handles_missing_event_loop_triggered_by_fixture( pytester: Pytester, ): From e936d6ff1f33d69f4926e739ceed8a74f5039ad3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 6 Dec 2023 18:20:52 +0000 Subject: [PATCH 139/151] Build(deps): Bump actions/setup-python from 4 to 5 Bumps [actions/setup-python](https://github.com/actions/setup-python) from 4 to 5. - [Release notes](https://github.com/actions/setup-python/releases) - [Commits](https://github.com/actions/setup-python/compare/v4...v5) --- updated-dependencies: - dependency-name: actions/setup-python dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/main.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index f3ebc963..6aeaaa83 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -24,7 +24,7 @@ jobs: - uses: actions/checkout@v4 with: fetch-depth: 0 - - uses: actions/setup-python@v4 + - uses: actions/setup-python@v5 with: python-version: ${{ env.PYTHON_LATEST }} - name: Install GitHub matcher for ActionLint checker @@ -65,7 +65,7 @@ jobs: steps: - uses: actions/checkout@v4 - - uses: actions/setup-python@v4 + - uses: actions/setup-python@v5 if: "!endsWith(matrix.python-version, '-dev')" with: python-version: ${{ matrix.python-version }} @@ -100,7 +100,7 @@ jobs: with: jobs: ${{ toJSON(needs) }} - uses: actions/checkout@v4 - - uses: actions/setup-python@v4 + - uses: actions/setup-python@v5 with: python-version: ${{ env.PYTHON_LATEST }} - name: Install Coverage.py From e1415c14887216c82203f88fa50a59da325c6cd8 Mon Sep 17 00:00:00 2001 From: Michael Seifert Date: Thu, 7 Dec 2023 09:30:58 +0100 Subject: [PATCH 140/151] [refactor] Renamed test_pytest_skip.py to test_skips.py. Signed-off-by: Michael Seifert --- tests/{test_pytest_skip.py => test_skips.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/{test_pytest_skip.py => test_skips.py} (100%) diff --git a/tests/test_pytest_skip.py b/tests/test_skips.py similarity index 100% rename from tests/test_pytest_skip.py rename to tests/test_skips.py From edb9ae0f740f2c11f716e646be750bed082c3bb6 Mon Sep 17 00:00:00 2001 From: Michael Seifert Date: Thu, 7 Dec 2023 09:34:51 +0100 Subject: [PATCH 141/151] [fix] Fixes a bug that caused an internal pytest error when using unittest.SkipTest in a module. Signed-off-by: Michael Seifert --- docs/source/reference/changelog.rst | 1 + pytest_asyncio/plugin.py | 5 +++++ tests/test_skips.py | 17 +++++++++++++++++ 3 files changed, 23 insertions(+) diff --git a/docs/source/reference/changelog.rst b/docs/source/reference/changelog.rst index 7e063d24..f06157e4 100644 --- a/docs/source/reference/changelog.rst +++ b/docs/source/reference/changelog.rst @@ -5,6 +5,7 @@ Changelog 0.23.3 (UNRELEASED) =================== - Fixes a bug that caused event loops to be closed prematurely when using async generator fixtures with class scope or wider in a function-scoped test `#708 `_ +- Fixes a bug that caused an internal pytest error when using unittest.SkipTest in a module `#711 `_ 0.23.2 (2023-12-04) diff --git a/pytest_asyncio/plugin.py b/pytest_asyncio/plugin.py index 0a92730c..4af34fec 100644 --- a/pytest_asyncio/plugin.py +++ b/pytest_asyncio/plugin.py @@ -26,6 +26,7 @@ Union, overload, ) +from unittest import SkipTest import pytest from _pytest.outcomes import OutcomeException @@ -622,6 +623,10 @@ def scoped_event_loop( # kwargs is missing. These cases are handled correctly when they happen inside # Collector.collect(), but this hook runs before the actual collect call. return + except SkipTest: + # Users may also have a unittest suite that they run with pytest. + # Therefore, we need to handle SkipTest to avoid breaking test collection. + return # When collector is a package, collector.obj is the package's __init__.py. # pytest doesn't seem to collect fixtures in __init__.py. # Using parsefactories to collect fixtures in __init__.py their baseid will end diff --git a/tests/test_skips.py b/tests/test_skips.py index 17d0befc..abd9dd70 100644 --- a/tests/test_skips.py +++ b/tests/test_skips.py @@ -88,3 +88,20 @@ async def test_is_skipped(): ) result = pytester.runpytest("--asyncio-mode=auto") result.assert_outcomes(errors=1) + + +def test_unittest_skiptest_compatibility(pytester: Pytester): + pytester.makepyfile( + dedent( + """\ + from unittest import SkipTest + + raise SkipTest("Skip all tests") + + async def test_is_skipped(): + pass + """ + ) + ) + result = pytester.runpytest("--asyncio-mode=auto") + result.assert_outcomes(skipped=1) From 28e91f00cd59d8aca364e6041f898cde8239b4e6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 18 Dec 2023 18:47:37 +0000 Subject: [PATCH 142/151] Build(deps): Bump hypothesis in /dependencies/default Bumps [hypothesis](https://github.com/HypothesisWorks/hypothesis) from 6.91.0 to 6.92.1. - [Release notes](https://github.com/HypothesisWorks/hypothesis/releases) - [Commits](https://github.com/HypothesisWorks/hypothesis/compare/hypothesis-python-6.91.0...hypothesis-python-6.92.1) --- updated-dependencies: - dependency-name: hypothesis dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- dependencies/default/constraints.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies/default/constraints.txt b/dependencies/default/constraints.txt index 5bde9955..5fc3e1d8 100644 --- a/dependencies/default/constraints.txt +++ b/dependencies/default/constraints.txt @@ -1,7 +1,7 @@ attrs==23.1.0 coverage==7.3.2 exceptiongroup==1.2.0 -hypothesis==6.91.0 +hypothesis==6.92.1 iniconfig==2.0.0 packaging==23.2 pluggy==1.3.0 From 3a15f3039c2b0101b73af651e8b9c667b1a51434 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 18 Dec 2023 18:47:18 +0000 Subject: [PATCH 143/151] Build(deps): Bump coverage from 7.3.2 to 7.3.3 in /dependencies/default Bumps [coverage](https://github.com/nedbat/coveragepy) from 7.3.2 to 7.3.3. - [Release notes](https://github.com/nedbat/coveragepy/releases) - [Changelog](https://github.com/nedbat/coveragepy/blob/master/CHANGES.rst) - [Commits](https://github.com/nedbat/coveragepy/compare/7.3.2...7.3.3) --- updated-dependencies: - dependency-name: coverage dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- dependencies/default/constraints.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies/default/constraints.txt b/dependencies/default/constraints.txt index 5fc3e1d8..b7f6024c 100644 --- a/dependencies/default/constraints.txt +++ b/dependencies/default/constraints.txt @@ -1,5 +1,5 @@ attrs==23.1.0 -coverage==7.3.2 +coverage==7.3.3 exceptiongroup==1.2.0 hypothesis==6.92.1 iniconfig==2.0.0 From 0166a7e55fd5ac31afdaffd7dd54e77003f6cc30 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Dec 2023 18:15:41 +0000 Subject: [PATCH 144/151] Build(deps): Bump typing-extensions in /dependencies/default Bumps [typing-extensions](https://github.com/python/typing_extensions) from 4.8.0 to 4.9.0. - [Release notes](https://github.com/python/typing_extensions/releases) - [Changelog](https://github.com/python/typing_extensions/blob/main/CHANGELOG.md) - [Commits](https://github.com/python/typing_extensions/compare/4.8.0...4.9.0) --- updated-dependencies: - dependency-name: typing-extensions dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- dependencies/default/constraints.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies/default/constraints.txt b/dependencies/default/constraints.txt index b7f6024c..cc9d5b7a 100644 --- a/dependencies/default/constraints.txt +++ b/dependencies/default/constraints.txt @@ -8,4 +8,4 @@ pluggy==1.3.0 pytest==7.4.3 sortedcontainers==2.4.0 tomli==2.0.1 -typing_extensions==4.8.0 +typing_extensions==4.9.0 From 650ec5875dcefc4eb4c1b2b0ba792aa643cd0823 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 18 Dec 2023 18:59:18 +0000 Subject: [PATCH 145/151] Build(deps): Bump babel from 2.13.1 to 2.14.0 in /dependencies/docs Bumps [babel](https://github.com/python-babel/babel) from 2.13.1 to 2.14.0. - [Release notes](https://github.com/python-babel/babel/releases) - [Changelog](https://github.com/python-babel/babel/blob/master/CHANGES.rst) - [Commits](https://github.com/python-babel/babel/compare/v2.13.1...v2.14.0) --- updated-dependencies: - dependency-name: babel dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- dependencies/docs/constraints.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies/docs/constraints.txt b/dependencies/docs/constraints.txt index d2450bbf..ce51ba9a 100644 --- a/dependencies/docs/constraints.txt +++ b/dependencies/docs/constraints.txt @@ -1,5 +1,5 @@ alabaster==0.7.13 -Babel==2.13.1 +Babel==2.14.0 certifi==2023.11.17 charset-normalizer==3.3.2 docutils==0.18.1 From 38d5c7eed0d5193752043631aabde287d0627127 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Dec 2023 19:06:29 +0000 Subject: [PATCH 146/151] Build(deps): Bump sphinx-rtd-theme in /dependencies/docs Bumps [sphinx-rtd-theme](https://github.com/readthedocs/sphinx_rtd_theme) from 1.3.0 to 2.0.0. - [Changelog](https://github.com/readthedocs/sphinx_rtd_theme/blob/master/docs/changelog.rst) - [Commits](https://github.com/readthedocs/sphinx_rtd_theme/compare/1.3.0...2.0.0) --- updated-dependencies: - dependency-name: sphinx-rtd-theme dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- dependencies/docs/constraints.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies/docs/constraints.txt b/dependencies/docs/constraints.txt index ce51ba9a..c8938488 100644 --- a/dependencies/docs/constraints.txt +++ b/dependencies/docs/constraints.txt @@ -12,7 +12,7 @@ Pygments==2.17.2 requests==2.31.0 snowballstemmer==2.2.0 Sphinx==7.2.6 -sphinx-rtd-theme==1.3.0 +sphinx-rtd-theme==2.0.0 sphinxcontrib-applehelp==1.0.7 sphinxcontrib-devhelp==1.0.5 sphinxcontrib-htmlhelp==2.0.4 From 31c7e6f9acda156a7aabf23d18e88c23f5f897a4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 25 Dec 2023 18:57:21 +0000 Subject: [PATCH 147/151] Build(deps): Bump coverage from 7.3.3 to 7.3.4 in /dependencies/default Bumps [coverage](https://github.com/nedbat/coveragepy) from 7.3.3 to 7.3.4. - [Release notes](https://github.com/nedbat/coveragepy/releases) - [Changelog](https://github.com/nedbat/coveragepy/blob/master/CHANGES.rst) - [Commits](https://github.com/nedbat/coveragepy/compare/7.3.3...7.3.4) --- updated-dependencies: - dependency-name: coverage dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- dependencies/default/constraints.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies/default/constraints.txt b/dependencies/default/constraints.txt index cc9d5b7a..56be8787 100644 --- a/dependencies/default/constraints.txt +++ b/dependencies/default/constraints.txt @@ -1,5 +1,5 @@ attrs==23.1.0 -coverage==7.3.3 +coverage==7.3.4 exceptiongroup==1.2.0 hypothesis==6.92.1 iniconfig==2.0.0 From 0c522bff1525e77ff75691ee7530c71fe63e2775 Mon Sep 17 00:00:00 2001 From: Michael Seifert Date: Wed, 20 Dec 2023 18:14:00 +0100 Subject: [PATCH 148/151] [fix] Fixes a bug that caused an internal pytest error when using ImportWarning in a module. The fix monkey patches the pytest.Module.collect to attach the scoped event loop fixture to the module, rather than directly accessing Module.obj. This allows dropping all error handling related to module imports that has been added, because pytest_collectstart isn't meant to deal with those errors. Signed-off-by: Michael Seifert --- docs/source/reference/changelog.rst | 1 + pytest_asyncio/plugin.py | 31 +++++++++++++++-------------- tests/test_import.py | 18 +++++++++++++++++ 3 files changed, 35 insertions(+), 15 deletions(-) create mode 100644 tests/test_import.py diff --git a/docs/source/reference/changelog.rst b/docs/source/reference/changelog.rst index f06157e4..834d65e5 100644 --- a/docs/source/reference/changelog.rst +++ b/docs/source/reference/changelog.rst @@ -6,6 +6,7 @@ Changelog =================== - Fixes a bug that caused event loops to be closed prematurely when using async generator fixtures with class scope or wider in a function-scoped test `#708 `_ - Fixes a bug that caused an internal pytest error when using unittest.SkipTest in a module `#711 `_ +- Fixes a bug that caused an internal pytest error when an ImportWarning is emitted in a module `#713 `_ 0.23.2 (2023-12-04) diff --git a/pytest_asyncio/plugin.py b/pytest_asyncio/plugin.py index 4af34fec..eb013f46 100644 --- a/pytest_asyncio/plugin.py +++ b/pytest_asyncio/plugin.py @@ -26,10 +26,8 @@ Union, overload, ) -from unittest import SkipTest import pytest -from _pytest.outcomes import OutcomeException from pytest import ( Class, Collector, @@ -608,25 +606,28 @@ def scoped_event_loop( # @pytest.fixture does not register the fixture anywhere, so pytest doesn't # know it exists. We work around this by attaching the fixture function to the - # collected Python class, where it will be picked up by pytest.Class.collect() + # collected Python object, where it will be picked up by pytest.Class.collect() # or pytest.Module.collect(), respectively - try: - pyobject = collector.obj - # If the collected module is a DoctestTextfile, collector.obj is None - if pyobject is None: - return - pyobject.__pytest_asyncio_scoped_event_loop = scoped_event_loop - except (OutcomeException, Collector.CollectError): + if type(collector) is Module: # Accessing Module.obj triggers a module import executing module-level # statements. A module-level pytest.skip statement raises the "Skipped" # OutcomeException or a Collector.CollectError, if the "allow_module_level" # kwargs is missing. These cases are handled correctly when they happen inside # Collector.collect(), but this hook runs before the actual collect call. - return - except SkipTest: - # Users may also have a unittest suite that they run with pytest. - # Therefore, we need to handle SkipTest to avoid breaking test collection. - return + # Therefore, we monkey patch Module.collect to add the scoped fixture to the + # module before it runs the actual collection. + def _patched_collect(): + collector.obj.__pytest_asyncio_scoped_event_loop = scoped_event_loop + return collector.__original_collect() + + collector.__original_collect = collector.collect + collector.collect = _patched_collect + else: + pyobject = collector.obj + # If the collected module is a DoctestTextfile, collector.obj is None + if pyobject is None: + return + pyobject.__pytest_asyncio_scoped_event_loop = scoped_event_loop # When collector is a package, collector.obj is the package's __init__.py. # pytest doesn't seem to collect fixtures in __init__.py. # Using parsefactories to collect fixtures in __init__.py their baseid will end diff --git a/tests/test_import.py b/tests/test_import.py new file mode 100644 index 00000000..77352150 --- /dev/null +++ b/tests/test_import.py @@ -0,0 +1,18 @@ +from textwrap import dedent + +from pytest import Pytester + + +def test_import_warning(pytester: Pytester): + pytester.makepyfile( + dedent( + """\ + raise ImportWarning() + + async def test_errors_out(): + pass + """ + ) + ) + result = pytester.runpytest("--asyncio-mode=auto") + result.assert_outcomes(errors=1) From e2cbb906c5124df131abe39c447c5486aae913be Mon Sep 17 00:00:00 2001 From: Michael Seifert Date: Sun, 24 Dec 2023 06:58:45 +0100 Subject: [PATCH 149/151] [docs] Mention correct issue in changelog. Signed-off-by: Michael Seifert --- docs/source/reference/changelog.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/reference/changelog.rst b/docs/source/reference/changelog.rst index 834d65e5..7aadd6a2 100644 --- a/docs/source/reference/changelog.rst +++ b/docs/source/reference/changelog.rst @@ -4,7 +4,7 @@ Changelog 0.23.3 (UNRELEASED) =================== -- Fixes a bug that caused event loops to be closed prematurely when using async generator fixtures with class scope or wider in a function-scoped test `#708 `_ +- Fixes a bug that caused event loops to be closed prematurely when using async generator fixtures with class scope or wider in a function-scoped test `#706 `_ - Fixes a bug that caused an internal pytest error when using unittest.SkipTest in a module `#711 `_ - Fixes a bug that caused an internal pytest error when an ImportWarning is emitted in a module `#713 `_ From 6a253e20fb174b6750075a6cfdd9409e4c6221f5 Mon Sep 17 00:00:00 2001 From: Michael Seifert Date: Sun, 24 Dec 2023 07:04:14 +0100 Subject: [PATCH 150/151] [docs] Shorten changelog by combining multiple issues. Signed-off-by: Michael Seifert --- docs/source/reference/changelog.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/source/reference/changelog.rst b/docs/source/reference/changelog.rst index 7aadd6a2..f00163c6 100644 --- a/docs/source/reference/changelog.rst +++ b/docs/source/reference/changelog.rst @@ -5,8 +5,8 @@ Changelog 0.23.3 (UNRELEASED) =================== - Fixes a bug that caused event loops to be closed prematurely when using async generator fixtures with class scope or wider in a function-scoped test `#706 `_ -- Fixes a bug that caused an internal pytest error when using unittest.SkipTest in a module `#711 `_ -- Fixes a bug that caused an internal pytest error when an ImportWarning is emitted in a module `#713 `_ +- Fixes various bugs that caused an internal pytest error during test collection `#711 `_ `#713 `_ `#719 `_ + 0.23.2 (2023-12-04) From 260b79185b198bad96a8fb2abc607a91ad8a5490 Mon Sep 17 00:00:00 2001 From: Michael Seifert Date: Mon, 1 Jan 2024 14:57:15 +0100 Subject: [PATCH 151/151] [docs] Prepare release of v0.23.3. Signed-off-by: Michael Seifert --- docs/source/reference/changelog.rst | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/source/reference/changelog.rst b/docs/source/reference/changelog.rst index f00163c6..b6f57af2 100644 --- a/docs/source/reference/changelog.rst +++ b/docs/source/reference/changelog.rst @@ -2,11 +2,14 @@ Changelog ========= -0.23.3 (UNRELEASED) +0.23.3 (2024-01-01) =================== - Fixes a bug that caused event loops to be closed prematurely when using async generator fixtures with class scope or wider in a function-scoped test `#706 `_ - Fixes various bugs that caused an internal pytest error during test collection `#711 `_ `#713 `_ `#719 `_ +Known issues +------------ +As of v0.23, pytest-asyncio attaches an asyncio event loop to each item of the test suite (i.e. session, packages, modules, classes, functions) and allows tests to be run in those loops when marked accordingly. Pytest-asyncio currently assumes that async fixture scope is correlated with the new event loop scope. This prevents fixtures from being evaluated independently from the event loop scope and breaks some existing test suites (see `#706`_). For example, a test suite may require all fixtures and tests to run in the same event loop, but have async fixtures that are set up and torn down for each module. If you're affected by this issue, please continue using the v0.21 release, until it is resolved. 0.23.2 (2023-12-04)