diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index 3d725a50..c79c0de8 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -2,13 +2,7 @@
version: 2
updates:
- package-ecosystem: pip
- directory: /dependencies/default
- schedule:
- interval: weekly
- open-pull-requests-limit: 10
- target-branch: main
-- package-ecosystem: pip
- directory: /dependencies/docs
+ directory: /
schedule:
interval: weekly
open-pull-requests-limit: 10
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 12348ae1..08fc7ebb 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -10,40 +10,32 @@ on:
merge_group:
workflow_dispatch:
+permissions: {}
+
env:
PYTHON_LATEST: 3.13
jobs:
- lint:
- name: Run linters
+ build:
+ name: Build package
runs-on: ubuntu-latest
outputs:
version: ${{ steps.version.outputs.version }}
prerelease: ${{ steps.version.outputs.prerelease }}
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v5
with:
fetch-depth: 0
- - uses: actions/setup-python@v5
+ persist-credentials: false
+ - uses: actions/setup-python@v6
with:
python-version: ${{ env.PYTHON_LATEST }}
- - name: Install GitHub matcher for ActionLint checker
- run: |
- echo "::add-matcher::.github/actionlint-matcher.json"
- - name: Install pre-commit
- run: python -m pip install pre-commit
- - name: Run pre-commit checks
- run: pre-commit run --all-files --show-diff-on-failure
- - name: Install check-wheel-content, and twine
- run: python -m pip install build check-wheel-contents twine
- - name: Build package
- run: python -m build
+ - name: Install tox
+ run: python -m pip install tox
+ - name: Build package and check distributions
+ run: tox run -e build
- name: List result
run: ls -l dist
- - name: Check wheel contents
- run: check-wheel-contents dist/*.whl
- - name: Check long_description
- run: python -m twine check dist/*
- name: Install pytest-asyncio
run: pip install .
- name: Get version info
@@ -55,23 +47,48 @@ jobs:
name: dist
path: dist
+ lint:
+ name: Run linters
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v5
+ with:
+ fetch-depth: 0
+ persist-credentials: false
+ - uses: actions/setup-python@v6
+ with:
+ python-version: ${{ env.PYTHON_LATEST }}
+ - name: Install GitHub matcher for ActionLint checker
+ run: |
+ echo "::add-matcher::.github/actionlint-matcher.json"
+ - name: Install pre-commit
+ run: python -m pip install pre-commit
+ - name: Run pre-commit checks
+ run: pre-commit run --all-files --show-diff-on-failure
+
test:
name: ${{ matrix.os }} - Python ${{ matrix.python-version }}
runs-on: ${{ matrix.os }}-latest
-
+ continue-on-error: ${{ !matrix.required }}
strategy:
matrix:
os: [ubuntu, windows]
python-version: ['3.9', '3.10', '3.11', '3.12', '3.13']
+ required: [true]
+ include:
+ - os: ubuntu
+ python-version: 3.14-dev
+ required: false
+ - os: windows
+ python-version: 3.14-dev
+ required: false
+
steps:
- - uses: actions/checkout@v4
- - uses: actions/setup-python@v5
- if: "!endsWith(matrix.python-version, '-dev')"
+ - uses: actions/checkout@v5
with:
- python-version: ${{ matrix.python-version }}
- - uses: deadsnakes/action@v3.2.0
- if: endsWith(matrix.python-version, '-dev')
+ persist-credentials: false
+ - uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
@@ -91,18 +108,26 @@ jobs:
path: coverage/coverage.*
if-no-files-found: error
+ lint-github-actions:
+ name: Lint GitHub Actions
+ permissions:
+ security-events: write
+ uses: zizmorcore/workflow/.github/workflows/reusable-zizmor.yml@3bb5e95068d0f44b6d2f3f7e91379bed1d2f96a8
+
check:
name: Check
if: always()
- needs: [lint, test]
+ needs: [build, lint, test]
runs-on: ubuntu-latest
steps:
- name: Decide whether the needed jobs succeeded or failed
- uses: re-actors/alls-green@release/v1
+ uses: re-actors/alls-green@05ac9388f0aebcb5727afa17fcccfecd6f8ec5fe # v1.2.2
with:
jobs: ${{ toJSON(needs) }}
- - uses: actions/checkout@v4
- - uses: actions/setup-python@v5
+ - uses: actions/checkout@v5
+ with:
+ persist-credentials: false
+ - uses: actions/setup-python@v6
with:
python-version: ${{ env.PYTHON_LATEST }}
- name: Install Coverage.py
@@ -110,7 +135,7 @@ jobs:
set -xe
python -m pip install --upgrade coverage[toml]
- name: Download coverage data for all test runs
- uses: actions/download-artifact@v4
+ uses: actions/download-artifact@v5
with:
pattern: coverage-*
path: coverage
@@ -120,47 +145,107 @@ jobs:
coverage combine
coverage xml
- name: Upload coverage report
- uses: codecov/codecov-action@v5
+ uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5.5.1
with:
files: coverage.xml
fail_ci_if_error: true
token: ${{ secrets.CODECOV_TOKEN }}
- deploy:
- name: Deploy
- environment: release
- # Run only on pushing a tag
- if: github.event_name == 'push' && contains(github.ref, 'refs/tags/')
- needs: [lint, check]
+ create-github-release:
+ name: Create GitHub release
+ needs: [build, lint, check]
runs-on: ubuntu-latest
+ permissions:
+ contents: write
steps:
+ - name: Checkout
+ uses: actions/checkout@v5
+ with:
+ fetch-depth: 0
+ persist-credentials: false
+ - name: Install Python
+ uses: actions/setup-python@v6
+ - name: Install towncrier
+ run: pip install towncrier==24.8.0
- name: Install pandoc
run: |
sudo apt-get install -y pandoc
- - name: Checkout
- uses: actions/checkout@v4
+ - name: Install pytest-asyncio
+ run: pip install .
+ - name: Compile Release Notes Draft
+ if: ${{ !contains(github.ref, 'refs/tags/') }}
+ run: towncrier build --draft --version "${version}" > release-notes.rst
+ env:
+ version: ${{ needs.build.outputs.version }}
+ - name: Extract release notes from Git tag
+ if: github.event_name == 'push' && contains(github.ref, 'refs/tags/')
+ run: |
+ set -e
+ git fetch --tags --force # see https://github.com/actions/checkout/issues/290
+ git for-each-ref "${GITHUB_REF}" --format='%(contents)' > release-notes.rst
+ # Strip signature from signed tags
+ sed -i -e "/-----BEGIN PGP SIGNATURE-----/,/-----END PGP SIGNATURE-----\n/d" \
+ -e "/-----BEGIN SSH SIGNATURE-----/,/-----END SSH SIGNATURE-----\n/d" release-notes.rst
+ - name: Convert Release Notes to Markdown
+ run: |
+ pandoc --wrap=preserve -o release-notes.md release-notes.rst
+ - name: Upload artifacts
+ uses: actions/upload-artifact@v4
+ with:
+ name: release-notes.md
+ path: release-notes.md
+ - name: Download distributions
+ uses: actions/download-artifact@v5
+ with:
+ name: dist
+ path: dist
+ - name: Create GitHub Release
+ if: github.event_name == 'push' && contains(github.ref, 'refs/tags/')
+ uses: ncipollo/release-action@b7eabc95ff50cbeeedec83973935c8f306dfcd0b # v1.20.0
+ with:
+ name: pytest-asyncio ${{ needs.build.outputs.version }}
+ artifacts: dist/*
+ bodyFile: release-notes.md
+ prerelease: ${{ needs.build.outputs.prerelease }}
+ token: ${{ secrets.GITHUB_TOKEN }}
+ allowUpdates: true
+ draft: true
+ skipIfReleaseExists: true
+
+ publish-test-pypi:
+ name: Publish packages to test.pypi.org
+ if: github.event_name == 'push' && github.ref == 'refs/heads/main'
+ needs: [build, lint, check, create-github-release]
+ runs-on: ubuntu-latest
+ permissions:
+ id-token: write
+ steps:
- name: Download distributions
- uses: actions/download-artifact@v4
+ uses: actions/download-artifact@v5
+ with:
+ name: dist
+ path: dist
+ - name: Upload to test.pypi.org
+ uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0
+ with:
+ repository-url: https://test.pypi.org/legacy/
+
+ publish-pypi:
+ name: Publish packages to pypi.org
+ environment: release
+ if: github.event_name == 'push' && contains(github.ref, 'refs/tags/')
+ needs: [build, lint, check, create-github-release]
+ runs-on: ubuntu-latest
+ permissions:
+ id-token: write
+ steps:
+ - name: Download distributions
+ uses: actions/download-artifact@v5
with:
name: dist
path: dist
- name: Collected dists
run: |
tree dist
- - name: Convert README.rst to Markdown
- run: |
- pandoc -s -o README.md README.rst
- name: PyPI upload
- uses: pypa/gh-action-pypi-publish@v1.12.3
- with:
- attestations: true
- packages-dir: dist
- password: ${{ secrets.PYPI_API_TOKEN }}
- - name: GitHub Release
- uses: ncipollo/release-action@v1
- with:
- name: pytest-asyncio ${{ needs.lint.outputs.version }}
- artifacts: dist/*
- bodyFile: README.md
- prerelease: ${{ needs.lint.outputs.prerelease }}
- token: ${{ secrets.GITHUB_TOKEN }}
+ uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0
diff --git a/.gitignore b/.gitignore
index 5a568761..076a7d00 100644
--- a/.gitignore
+++ b/.gitignore
@@ -62,7 +62,3 @@ target/
# pyenv
.python-version
-
-
-# generated by setuptools_scm
-pytest_asyncio/_version.py
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index c1943f60..7a0801aa 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,21 +1,32 @@
---
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
- rev: v5.0.0
+ rev: v6.0.0
hooks:
+ - id: trailing-whitespace
+ - id: end-of-file-fixer
- id: check-merge-conflict
exclude: rst$
+ - id: check-case-conflict
+ - id: check-json
+ - id: check-xml
+ - id: check-yaml
+ - id: debug-statements
- repo: https://github.com/astral-sh/ruff-pre-commit
- rev: v0.8.6
+ rev: v0.12.12
hooks:
- id: ruff
args: [--fix]
+- repo: https://github.com/asottile/pyupgrade
+ rev: v3.20.0
+ hooks:
+ - id: pyupgrade
- repo: https://github.com/asottile/yesqa
rev: v1.5.0
hooks:
- id: yesqa
- repo: https://github.com/Zac-HD/shed
- rev: 2024.10.1
+ rev: 2025.6.1
hooks:
- id: shed
args:
@@ -29,20 +40,8 @@ repos:
hooks:
- id: yamlfmt
args: [--mapping, '2', --sequence, '2', --offset, '0']
-- repo: https://github.com/pre-commit/pre-commit-hooks
- rev: v5.0.0
- hooks:
- - id: trailing-whitespace
- - id: end-of-file-fixer
- - id: fix-encoding-pragma
- args: [--remove]
- - id: check-case-conflict
- - id: check-json
- - id: check-xml
- - id: check-yaml
- - id: debug-statements
- repo: https://github.com/pre-commit/mirrors-mypy
- rev: v1.14.1
+ rev: v1.17.1
hooks:
- id: mypy
exclude: ^(docs|tests)/.*
@@ -53,7 +52,7 @@ repos:
hooks:
- id: python-use-type-annotations
- repo: https://github.com/rhysd/actionlint
- rev: v1.7.6
+ rev: v1.7.7
hooks:
- id: actionlint-docker
args:
@@ -65,15 +64,19 @@ repos:
- 'SC1004:'
stages: [manual]
- repo: https://github.com/sirosen/check-jsonschema
- rev: 0.30.0
+ rev: 0.33.3
hooks:
- id: check-github-actions
- repo: https://github.com/tox-dev/pyproject-fmt
- rev: v2.5.0
+ rev: v2.6.0
hooks:
- id: pyproject-fmt
# https://pyproject-fmt.readthedocs.io/en/latest/#calculating-max-supported-python-version
- additional_dependencies: [tox>=4.9]
+ additional_dependencies: [tox>=4.28]
+- repo: https://github.com/zizmorcore/zizmor-pre-commit
+ rev: v1.12.1
+ hooks:
+ - id: zizmor
ci:
skip:
- actionlint-docker
diff --git a/tests/loop_fixture_scope/__init__.py b/changelog.d/.gitkeep
similarity index 100%
rename from tests/loop_fixture_scope/__init__.py
rename to changelog.d/.gitkeep
diff --git a/constraints.txt b/constraints.txt
new file mode 100644
index 00000000..76cf9fbd
--- /dev/null
+++ b/constraints.txt
@@ -0,0 +1,66 @@
+alabaster==0.7.16
+annotated-types-0.7.0
+attrs==25.3.0
+babel==2.17.0
+backports.asyncio.runner==1.2.0
+backports.tarfile==1.2.0
+certifi==2025.8.3
+charset-normalizer==3.4.3
+check-wheel-contents==0.6.1
+cffi==1.17.1
+click==8.1.8
+coverage==7.10.6
+cryptography==45.0.7
+docutils==0.21.2
+exceptiongroup==1.3.0
+hypothesis==6.138.15
+iniconfig==2.1.0
+id==1.5.0
+idna==3.10
+imagesize==1.4.1
+importlib_metadata==8.7.0
+iniconfig==2.1.0
+jaraco.classes==3.4.0
+jaraco.context==6.0.1
+jaraco.functools==4.3.0
+jeepney==0.9.0
+Jinja2==3.1.6
+keyring==25.6.0
+markdown-it-py==3.0.0
+MarkupSafe==3.0.2
+mdurl==0.1.2
+more-itertools==10.8.0
+nh3==0.3.0
+packaging==25.0
+pluggy==1.6.0
+Pygments==2.19.2
+pycparser==2.22
+pydantic==2.11.7
+pydantic-core==2.33.2
+pytest==8.4.2
+readme-renderer==44.0
+requests==2.32.5
+requests-toolbelt==1.0.0
+rfc3986==2.0.0
+rich==14.1.0
+SecretStorage==3.3.3
+setuptools==80.9.0
+setuptools-scm==9.2.0
+snowballstemmer==3.0.1
+sortedcontainers==2.4.0
+Sphinx==8.0.2
+sphinx-rtd-theme==3.0.2
+sphinxcontrib-applehelp==2.0.0
+sphinxcontrib-devhelp==2.0.0
+sphinxcontrib-htmlhelp==2.1.0
+sphinxcontrib-jquery==4.1
+sphinxcontrib-jsmath==1.0.1
+sphinxcontrib-qthelp==2.0.0
+sphinxcontrib-serializinghtml==2.0.0
+tomli==2.2.1
+twine==6.2.0
+typing_extensions==4.15.0
+typing-inspection==0.4.1
+urllib3==2.5.0
+wheel-filename==1.4.2
+zipp==3.23.0
diff --git a/dependencies/default/constraints.txt b/dependencies/default/constraints.txt
deleted file mode 100644
index 816c4639..00000000
--- a/dependencies/default/constraints.txt
+++ /dev/null
@@ -1,11 +0,0 @@
-attrs==24.3.0
-coverage==7.6.10
-exceptiongroup==1.2.2
-hypothesis==6.123.4
-iniconfig==2.0.0
-packaging==24.2
-pluggy==1.5.0
-pytest==8.3.4
-sortedcontainers==2.4.0
-tomli==2.2.1
-typing_extensions==4.12.2
diff --git a/dependencies/default/requirements.txt b/dependencies/default/requirements.txt
deleted file mode 100644
index 42cfc8d3..00000000
--- a/dependencies/default/requirements.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-# Always adjust install_requires in setup.cfg and pytest-min-requirements.txt
-# when changing runtime dependencies
-pytest >= 8.2,<9
diff --git a/dependencies/docs/constraints.txt b/dependencies/docs/constraints.txt
index 607c1632..8d2df620 100644
--- a/dependencies/docs/constraints.txt
+++ b/dependencies/docs/constraints.txt
@@ -1,16 +1,16 @@
alabaster==0.7.16
-Babel==2.16.0
-certifi==2024.12.14
-charset-normalizer==3.4.1
+Babel==2.17.0
+certifi==2025.8.3
+charset-normalizer==3.4.3
docutils==0.21.2
idna==3.10
imagesize==1.4.1
-Jinja2==3.1.5
+Jinja2==3.1.6
MarkupSafe==3.0.2
-packaging==24.2
-Pygments==2.19.1
-requests==2.32.3
-snowballstemmer==2.2.0
+packaging==25.0
+Pygments==2.19.2
+requests==2.32.5
+snowballstemmer==3.0.1
Sphinx==8.0.2
sphinx-rtd-theme==3.0.2
sphinxcontrib-applehelp==2.0.0
@@ -20,4 +20,4 @@ sphinxcontrib-jquery==4.1
sphinxcontrib-jsmath==1.0.1
sphinxcontrib-qthelp==2.0.0
sphinxcontrib-serializinghtml==2.0.0
-urllib3==2.3.0
+urllib3==2.5.0
diff --git a/dependencies/docs/requirements.txt b/dependencies/docs/requirements.txt
deleted file mode 100644
index 1bfd7f50..00000000
--- a/dependencies/docs/requirements.txt
+++ /dev/null
@@ -1,2 +0,0 @@
-sphinx >= 5.3
-sphinx-rtd-theme >= 1.0
diff --git a/docs/concepts.rst b/docs/concepts.rst
index be8b775b..591059a8 100644
--- a/docs/concepts.rst
+++ b/docs/concepts.rst
@@ -47,8 +47,12 @@ Assigning neighboring tests to different event loop scopes is discouraged as it
Test discovery modes
====================
-Pytest-asyncio provides two modes for test discovery, *strict* and *auto*.
+Pytest-asyncio provides two modes for test discovery, *strict* and *auto*. This can be set through Pytest's ``--asyncio-mode`` command line flag, or through the configuration file:
+.. code-block:: toml
+
+ [tool.pytest.ini_options]
+ asyncio_mode = "auto" # or "strict"
Strict mode
-----------
@@ -67,3 +71,15 @@ In *auto* mode pytest-asyncio automatically adds the *asyncio* marker to all asy
This mode is intended for projects that use *asyncio* as their only asynchronous programming library. Auto mode makes for the simplest test and fixture configuration and is the recommended default.
If you intend to support multiple asynchronous programming libraries, e.g. *asyncio* and *trio*, strict mode will be the preferred option.
+
+.. _concepts/concurrent_execution:
+
+Test execution and concurrency
+==============================
+
+pytest-asyncio runs async tests sequentially, just like how pytest runs synchronous tests. Each asynchronous test runs within its assigned event loop. For example, consider the following two tests:
+
+.. include:: concepts_concurrent_execution_example.py
+ :code: python
+
+This sequential execution is intentional and important for maintaining test isolation. Running tests concurrently could introduce race conditions and side effects where one test could interfere with another, making test results unreliable and difficult to debug.
diff --git a/docs/concepts_concurrent_execution_example.py b/docs/concepts_concurrent_execution_example.py
new file mode 100644
index 00000000..e573ca27
--- /dev/null
+++ b/docs/concepts_concurrent_execution_example.py
@@ -0,0 +1,16 @@
+import asyncio
+
+import pytest
+
+
+@pytest.mark.asyncio
+async def test_first():
+ await asyncio.sleep(2) # Takes 2 seconds
+
+
+@pytest.mark.asyncio
+async def test_second():
+ await asyncio.sleep(2) # Takes 2 seconds
+
+
+# Total execution time: ~4 seconds, not ~2 seconds
diff --git a/docs/how-to-guides/change_default_test_loop.rst b/docs/how-to-guides/change_default_test_loop.rst
new file mode 100644
index 00000000..c5b625d1
--- /dev/null
+++ b/docs/how-to-guides/change_default_test_loop.rst
@@ -0,0 +1,24 @@
+=======================================================
+How to change the default event loop scope of all tests
+=======================================================
+The :ref:`configuration/asyncio_default_test_loop_scope` configuration option sets the default event loop scope for asynchronous tests. The following code snippets configure all tests to run in a session-scoped loop by default:
+
+.. code-block:: ini
+ :caption: pytest.ini
+
+ [pytest]
+ asyncio_default_test_loop_scope = session
+
+.. code-block:: toml
+ :caption: pyproject.toml
+
+ [tool.pytest.ini_options]
+ asyncio_default_test_loop_scope = "session"
+
+.. code-block:: ini
+ :caption: setup.cfg
+
+ [tool:pytest]
+ asyncio_default_test_loop_scope = session
+
+Please refer to :ref:`configuration/asyncio_default_test_loop_scope` for other valid scopes.
diff --git a/docs/how-to-guides/index.rst b/docs/how-to-guides/index.rst
index 7b3c4f31..2dadc881 100644
--- a/docs/how-to-guides/index.rst
+++ b/docs/how-to-guides/index.rst
@@ -9,11 +9,12 @@ How-To Guides
migrate_from_0_23
change_fixture_loop
change_default_fixture_loop
+ change_default_test_loop
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
+ parametrize_with_asyncio
uvloop
test_item_is_async
diff --git a/docs/how-to-guides/multiple_loops_example.py b/docs/how-to-guides/multiple_loops_example.py
index a4c7a01c..2083e8b6 100644
--- a/docs/how-to-guides/multiple_loops_example.py
+++ b/docs/how-to-guides/multiple_loops_example.py
@@ -1,5 +1,9 @@
import asyncio
-from asyncio import DefaultEventLoopPolicy
+import warnings
+
+with warnings.catch_warnings():
+ warnings.simplefilter("ignore", DeprecationWarning)
+ from asyncio import DefaultEventLoopPolicy
import pytest
@@ -20,5 +24,6 @@ def event_loop_policy(request):
@pytest.mark.asyncio
+@pytest.mark.filterwarnings("ignore::DeprecationWarning")
async def test_uses_custom_event_loop_policy():
assert isinstance(asyncio.get_event_loop_policy(), CustomEventLoopPolicy)
diff --git a/docs/how-to-guides/parametrize_with_asyncio.rst b/docs/how-to-guides/parametrize_with_asyncio.rst
new file mode 100644
index 00000000..b965cb7d
--- /dev/null
+++ b/docs/how-to-guides/parametrize_with_asyncio.rst
@@ -0,0 +1,11 @@
+=====================================
+How to parametrize asynchronous tests
+=====================================
+
+The ``pytest.mark.parametrize`` marker works with asynchronous tests the same as with synchronous tests. You can apply both ``pytest.mark.asyncio`` and ``pytest.mark.parametrize`` to asynchronous test functions:
+
+.. include:: parametrize_with_asyncio_example.py
+ :code: python
+
+.. note::
+ Whilst asynchronous tests can be parametrized, each individual test case still runs sequentially, not concurrently. For more information about how pytest-asyncio executes tests, see :ref:`concepts/concurrent_execution`.
diff --git a/docs/how-to-guides/parametrize_with_asyncio_example.py b/docs/how-to-guides/parametrize_with_asyncio_example.py
new file mode 100644
index 00000000..54f181be
--- /dev/null
+++ b/docs/how-to-guides/parametrize_with_asyncio_example.py
@@ -0,0 +1,10 @@
+import asyncio
+
+import pytest
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize("value", [1, 2, 3])
+async def test_parametrized_async_function(value):
+ await asyncio.sleep(1)
+ assert value > 0
diff --git a/docs/how-to-guides/run_session_tests_in_same_loop.rst b/docs/how-to-guides/run_session_tests_in_same_loop.rst
deleted file mode 100644
index f166fea0..00000000
--- a/docs/how-to-guides/run_session_tests_in_same_loop.rst
+++ /dev/null
@@ -1,10 +0,0 @@
-==========================================================
-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(loop_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
-
-Note that this will also override *all* manually applied marks in *strict* mode.
diff --git a/docs/how-to-guides/session_scoped_loop_example.py b/docs/how-to-guides/session_scoped_loop_example.py
deleted file mode 100644
index 79cc8676..00000000
--- a/docs/how-to-guides/session_scoped_loop_example.py
+++ /dev/null
@@ -1,10 +0,0 @@
-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(loop_scope="session")
- for async_test in pytest_asyncio_tests:
- async_test.add_marker(session_scope_marker, append=False)
diff --git a/docs/how-to-guides/test_session_scoped_loop_example.py b/docs/how-to-guides/test_session_scoped_loop_example.py
deleted file mode 100644
index 3d642246..00000000
--- a/docs/how-to-guides/test_session_scoped_loop_example.py
+++ /dev/null
@@ -1,63 +0,0 @@
-from pathlib import Path
-from textwrap import dedent
-
-from pytest import Pytester
-
-
-def test_session_scoped_loop_configuration_works_in_auto_mode(
- pytester: Pytester,
-):
- session_wide_mark_conftest = (
- Path(__file__).parent / "session_scoped_loop_example.py"
- )
- pytester.makeconftest(session_wide_mark_conftest.read_text())
- pytester.makepyfile(
- dedent(
- """\
- import asyncio
-
- session_loop = None
-
- async def test_store_loop(request):
- global session_loop
- session_loop = asyncio.get_running_loop()
-
- async def test_compare_loop(request):
- global session_loop
- assert asyncio.get_running_loop() is session_loop
- """
- )
- )
- result = pytester.runpytest_subprocess("--asyncio-mode=auto")
- result.assert_outcomes(passed=2)
-
-
-def test_session_scoped_loop_configuration_works_in_strict_mode(
- pytester: Pytester,
-):
- session_wide_mark_conftest = (
- Path(__file__).parent / "session_scoped_loop_example.py"
- )
- pytester.makeconftest(session_wide_mark_conftest.read_text())
- pytester.makepyfile(
- dedent(
- """\
- import asyncio
- import pytest
-
- session_loop = None
-
- @pytest.mark.asyncio
- async def test_store_loop(request):
- global session_loop
- session_loop = asyncio.get_running_loop()
-
- @pytest.mark.asyncio
- async def test_compare_loop(request):
- global session_loop
- assert asyncio.get_running_loop() is session_loop
- """
- )
- )
- result = pytester.runpytest_subprocess("--asyncio-mode=strict")
- result.assert_outcomes(passed=2)
diff --git a/docs/how-to-guides/uvloop.rst b/docs/how-to-guides/uvloop.rst
index 889c0f9d..a796bea7 100644
--- a/docs/how-to-guides/uvloop.rst
+++ b/docs/how-to-guides/uvloop.rst
@@ -2,7 +2,7 @@
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:
+Redefining the *event_loop_policy* fixture will parametrize all async tests. The following example causes all async tests to run multiple times, once for each event loop in the fixture parameters:
Replace the default event loop policy in your *conftest.py:*
.. code-block:: python
diff --git a/docs/reference/changelog.rst b/docs/reference/changelog.rst
index a28c9fd3..f5f0b189 100644
--- a/docs/reference/changelog.rst
+++ b/docs/reference/changelog.rst
@@ -2,11 +2,116 @@
Changelog
=========
-0.25.2 (2025-01-08)
+All notable changes to this project will be documented in this file.
+
+The format is based on `Keep a Changelog `__, and this project adheres to `Semantic Versioning `__.
+
+This project uses `towncrier `__ for changlog management and the changes for the upcoming release can be found in https://github.com/pytest-dev/pytest-asyncio/tree/main/changelog.d/.
+
+.. towncrier release notes start
+
+`1.2.0 `_ - 2025-09-12
+===============================================================================
+
+Added
+-----
+
+- ``--asyncio-debug`` CLI option and ``asyncio_debug`` configuration option to enable asyncio debug mode for the default event loop. (`#980 `_)
+- A ``pytest.UsageError`` for invalid configuration values of ``asyncio_default_fixture_loop_scope`` and ``asyncio_default_test_loop_scope``. (`#1189 `_)
+- Compatibility with the `Pyright` type checker (`#731 `_)
+
+
+Fixed
+-----
+
+- ``RuntimeError: There is no current event loop in thread 'MainThread'`` when any test unsets the event loop (such as when using ``asyncio.run`` and ``asyncio.Runner``). (`#1177 `_)
+- Deprecation warning when decorating an asynchronous fixture with ``@pytest.fixture`` in `strict` mode. The warning message now refers to the correct package. (`#1198 `_)
+
+
+Notes for Downstream Packagers
+------------------------------
+
+- Bump the minimum required version of tox to v4.28. This change is only relevant if you use the ``tox.ini`` file provided by pytest-asyncio to run tests.
+- Extend dependency on typing-extensions>=4.12 from Python<3.10 to Python<3.13.
+
+
+`1.1.0 `_ - 2025-07-16
+===============================================================================
+
+Added
+-----
+
+- Propagation of ContextVars from async fixtures to other fixtures and tests on Python 3.10 and older (`#127 `_)
+- Cancellation of tasks when the `loop_scope` ends (`#200 `_)
+- Warning when the current event loop is closed by a test
+
+
+Fixed
+-----
+
+- Error about missing loop when calling functions requiring a loop in the `finally` clause of a task (`#878 `_)
+- An error that could cause duplicate warnings to be issued
+
+
+Notes for Downstream Packagers
+------------------------------
+
+- Added runtime dependency on `backports.asyncio.runner `__ for use with Python 3.10 and older
+
+
+`1.0.0 `_ - 2025-05-26
+===============================================================================
+
+Removed
+-------
+
+- The deprecated *event_loop* fixture. (`#1106 `_)
+
+
+Added
+-----
+
+- Prelimiary support for Python 3.14 (`#1025 `_)
+
+
+Changed
+-------
+
+- Scoped event loops (e.g. module-scoped loops) are created once rather than per scope (e.g. per module). This reduces the number of fixtures and speeds up collection time, especially for large test suites. (`#1107 `_)
+- The *loop_scope* argument to ``pytest.mark.asyncio`` no longer forces that a pytest Collector exists at the level of the specified scope. For example, a test function marked with ``pytest.mark.asyncio(loop_scope="class")`` no longer requires a class surrounding the test. This is consistent with the behavior of the *scope* argument to ``pytest_asyncio.fixture``. (`#1112 `_)
+
+
+Fixed
+-----
+
+- An error caused when using pytest's `--setup-plan` option. (`#630 `_)
+- Unsuppressed import errors with pytest option ``--doctest-ignore-import-errors`` (`#797 `_)
+- A "fixture not found" error in connection with package-scoped loops (`#1052 `_)
+
+
+Notes for Downstream Packagers
+------------------------------
+
+- Removed a test that had an ordering dependency on other tests. (`#1114 `_)
+
+
+0.26.0 (2025-03-25)
+===================
+- Adds configuration option that sets default event loop scope for all tests `#793 `_
+- Improved type annotations for ``pytest_asyncio.fixture`` `#1045 `_
+- Added ``typing-extensions`` as additional dependency for Python ``<3.10`` `#1045 `_
+
+
+0.25.3 (2025-01-28)
===================
+- Avoid errors in cleanup of async generators when event loop is already closed `#1040 `_
+
+0.25.2 (2025-01-08)
+===================
- Call ``loop.shutdown_asyncgens()`` before closing the event loop to ensure async generators are closed in the same manner as ``asyncio.run`` does `#1034 `_
+
0.25.1 (2025-01-02)
===================
- Fixes an issue that caused a broken event loop when a function-scoped test was executed in between two tests with wider loop scope `#950 `_
@@ -16,13 +121,12 @@ Changelog
0.25.0 (2024-12-13)
===================
-- Deprecated: Added warning when asyncio test requests async ``@pytest.fixture`` in strict mode. This will become an error in a future version of flake8-asyncio. `#979 `_
+- Deprecated: Added warning when asyncio test requests async ``@pytest.fixture`` in strict mode. This will become an error in a future version of pytest-asyncio. `#979 `_
- Updates the error message about `pytest.mark.asyncio`'s `scope` keyword argument to say `loop_scope` instead. `#1004 `_
- Verbose log displays correct parameter name: asyncio_default_fixture_loop_scope `#990 `_
- Propagates `contextvars` set in async fixtures to other fixtures and tests on Python 3.11 and above. `#1008 `_
-
0.24.0 (2024-08-22)
===================
- BREAKING: Updated minimum supported pytest version to v8.2.0
diff --git a/docs/reference/configuration.rst b/docs/reference/configuration.rst
index 35c67302..7fef0af4 100644
--- a/docs/reference/configuration.rst
+++ b/docs/reference/configuration.rst
@@ -8,6 +8,35 @@ asyncio_default_fixture_loop_scope
==================================
Determines the default event loop scope of asynchronous fixtures. When this configuration option is unset, it defaults to the fixture scope. In future versions of pytest-asyncio, the value will default to ``function`` when unset. Possible values are: ``function``, ``class``, ``module``, ``package``, ``session``
+.. _configuration/asyncio_default_test_loop_scope:
+
+asyncio_default_test_loop_scope
+===============================
+Determines the default event loop scope of asynchronous tests. When this configuration option is unset, it default to function scope. Possible values are: ``function``, ``class``, ``module``, ``package``, ``session``
+
+.. _configuration/asyncio_debug:
+
+asyncio_debug
+=============
+Enables `asyncio debug mode `_ for the default event loop used by asynchronous tests and fixtures.
+
+The debug mode can be set by the ``asyncio_debug`` configuration option in the `configuration file
+`_:
+
+.. code-block:: ini
+
+ # pytest.ini
+ [pytest]
+ asyncio_debug = true
+
+The value can also be set via the ``--asyncio-debug`` command-line option:
+
+.. code-block:: bash
+
+ $ pytest tests --asyncio-debug
+
+By default, asyncio debug mode is disabled.
+
asyncio_mode
============
The pytest-asyncio mode can be set by the ``asyncio_mode`` configuration option in the `configuration file
diff --git a/docs/reference/fixtures/event_loop_example.py b/docs/reference/fixtures/event_loop_example.py
deleted file mode 100644
index b5a82b62..00000000
--- a/docs/reference/fixtures/event_loop_example.py
+++ /dev/null
@@ -1,5 +0,0 @@
-import asyncio
-
-
-def test_event_loop_fixture(event_loop):
- event_loop.run_until_complete(asyncio.sleep(0))
diff --git a/docs/reference/fixtures/event_loop_policy_example.py b/docs/reference/fixtures/event_loop_policy_example.py
index 5fd87b73..e8642527 100644
--- a/docs/reference/fixtures/event_loop_policy_example.py
+++ b/docs/reference/fixtures/event_loop_policy_example.py
@@ -1,9 +1,14 @@
import asyncio
+import warnings
+
+with warnings.catch_warnings():
+ warnings.simplefilter("ignore", DeprecationWarning)
+ from asyncio import DefaultEventLoopPolicy
import pytest
-class CustomEventLoopPolicy(asyncio.DefaultEventLoopPolicy):
+class CustomEventLoopPolicy(DefaultEventLoopPolicy):
pass
@@ -13,5 +18,6 @@ def event_loop_policy(request):
@pytest.mark.asyncio(loop_scope="module")
+@pytest.mark.filterwarnings("ignore::DeprecationWarning")
async def test_uses_custom_event_loop_policy():
assert isinstance(asyncio.get_event_loop_policy(), CustomEventLoopPolicy)
diff --git a/docs/reference/fixtures/event_loop_policy_parametrized_example.py b/docs/reference/fixtures/event_loop_policy_parametrized_example.py
index 1560889b..19552d81 100644
--- a/docs/reference/fixtures/event_loop_policy_parametrized_example.py
+++ b/docs/reference/fixtures/event_loop_policy_parametrized_example.py
@@ -1,10 +1,14 @@
import asyncio
-from asyncio import DefaultEventLoopPolicy
+import warnings
+
+with warnings.catch_warnings():
+ warnings.simplefilter("ignore", DeprecationWarning)
+ from asyncio import DefaultEventLoopPolicy
import pytest
-class CustomEventLoopPolicy(asyncio.DefaultEventLoopPolicy):
+class CustomEventLoopPolicy(DefaultEventLoopPolicy):
pass
@@ -19,5 +23,6 @@ def event_loop_policy(request):
@pytest.mark.asyncio
+@pytest.mark.filterwarnings("ignore::DeprecationWarning")
async def test_uses_custom_event_loop_policy():
assert isinstance(asyncio.get_event_loop_policy(), DefaultEventLoopPolicy)
diff --git a/docs/reference/fixtures/index.rst b/docs/reference/fixtures/index.rst
index 04953783..3d151dcb 100644
--- a/docs/reference/fixtures/index.rst
+++ b/docs/reference/fixtures/index.rst
@@ -2,33 +2,6 @@
Fixtures
========
-event_loop
-==========
-*This fixture is deprecated.*
-
-*If you want to request an asyncio event loop with a scope other than function
-scope, use the "loop_scope" argument to* :ref:`reference/markers/asyncio` *when marking the tests.
-If you want to return different types of event loops, use the* :ref:`reference/fixtures/event_loop_policy`
-*fixture.*
-
-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.
-
-.. 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 :ref:`asyncio ` marker
-is used to mark coroutines that should be treated as test functions.
-
-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``
-fixture will be requested automatically by the test function.
-
-.. _reference/fixtures/event_loop_policy:
-
event_loop_policy
=================
Returns the event loop policy used to create asyncio event loops.
diff --git a/docs/reference/markers/class_scoped_loop_custom_policies_strict_mode_example.py b/docs/reference/markers/class_scoped_loop_custom_policies_strict_mode_example.py
index afb4cc8a..5bb26247 100644
--- a/docs/reference/markers/class_scoped_loop_custom_policies_strict_mode_example.py
+++ b/docs/reference/markers/class_scoped_loop_custom_policies_strict_mode_example.py
@@ -1,12 +1,16 @@
-import asyncio
+import warnings
+
+with warnings.catch_warnings():
+ warnings.simplefilter("ignore", DeprecationWarning)
+ from asyncio import DefaultEventLoopPolicy
import pytest
@pytest.fixture(
params=[
- asyncio.DefaultEventLoopPolicy(),
- asyncio.DefaultEventLoopPolicy(),
+ DefaultEventLoopPolicy(),
+ DefaultEventLoopPolicy(),
]
)
def event_loop_policy(request):
diff --git a/docs/reference/markers/index.rst b/docs/reference/markers/index.rst
index e7d700c9..7715077b 100644
--- a/docs/reference/markers/index.rst
+++ b/docs/reference/markers/index.rst
@@ -21,21 +21,17 @@ 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 *loop_scope* keyword argument to the *asyncio* mark.
-The supported scopes are *class,* and *module,* and *package*.
+The supported scopes are *function,* *class,* and *module,* *package,* and *session*.
The following code example provides a shared event loop for all tests in `TestClassScopedLoop`:
.. include:: class_scoped_loop_strict_mode_example.py
:code: python
-If you request class scope for a test that is not part of a class, it will result in 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
-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.
Tests marked with *session* scope share the same event loop, even if the tests exist in different packages.
diff --git a/pyproject.toml b/pyproject.toml
index b89c24da..46804245 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,10 +1,8 @@
[build-system]
build-backend = "setuptools.build_meta"
-
requires = [
- "setuptools>=51",
+ "setuptools>=77",
"setuptools-scm[toml]>=6.2",
- "wheel>=0.36",
]
[project]
@@ -12,23 +10,29 @@ name = "pytest-asyncio"
description = "Pytest support for asyncio"
readme.content-type = "text/x-rst"
readme.file = "README.rst"
-license.text = "Apache 2.0"
+license = "Apache-2.0"
+license-files = [
+ "LICENSE",
+]
+maintainers = [
+ { name = "Michael Seifert", email = "m.seifert@digitalernachschub.de" },
+]
authors = [
- { name = "Tin Tvrtković ", email = "tinchester@gmail.com" },
+ { name = "Tin Tvrtković", email = "tinchester@gmail.com" },
]
requires-python = ">=3.9"
classifiers = [
- "Development Status :: 4 - Beta",
+ "Development Status :: 5 - Production/Stable",
"Framework :: AsyncIO",
"Framework :: Pytest",
"Intended Audience :: Developers",
- "License :: OSI Approved :: Apache Software License",
"Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
+ "Programming Language :: Python :: 3.14",
"Topic :: Software Development :: Testing",
"Typing :: Typed",
]
@@ -37,7 +41,9 @@ dynamic = [
]
dependencies = [
+ "backports-asyncio-runner>=1.1,<2; python_version<'3.11'",
"pytest>=8.2,<9",
+ "typing-extensions>=4.12; python_version<'3.13'",
]
optional-dependencies.docs = [
"sphinx>=5.3",
@@ -59,12 +65,9 @@ packages = [
"pytest_asyncio",
]
include-package-data = true
-license-files = [
- "LICENSE",
-]
[tool.setuptools_scm]
-write_to = "pytest_asyncio/_version.py"
+local_scheme = "no-local-version"
[tool.ruff]
line-length = 88
@@ -108,6 +111,9 @@ lint.ignore = [
"D415", # First line should end with a period, question mark, or exclamation point
]
+[tool.pyproject-fmt]
+max_supported_python = "3.14"
+
[tool.pytest.ini_options]
python_files = [
"test_*.py",
@@ -123,7 +129,6 @@ asyncio_default_fixture_loop_scope = "function"
junit_family = "xunit2"
filterwarnings = [
"error",
- "ignore:The event_loop fixture provided by pytest-asyncio has been redefined.*:DeprecationWarning",
]
[tool.coverage.run]
@@ -132,10 +137,48 @@ source = [
]
branch = true
data_file = "coverage/coverage"
-omit = [
- "*/_version.py",
-]
parallel = true
[tool.coverage.report]
show_missing = true
+
+[tool.towncrier]
+directory = "changelog.d"
+filename = "docs/reference/changelog.rst"
+title_format = "`{version} `_ - {project_date}"
+issue_format = "`#{issue} `_"
+
+[[tool.towncrier.type]]
+directory = "security"
+name = "Security"
+showcontent = true
+
+[[tool.towncrier.type]]
+directory = "removed"
+name = "Removed"
+showcontent = true
+
+[[tool.towncrier.type]]
+directory = "deprecated"
+name = "Deprecated"
+showcontent = true
+
+[[tool.towncrier.type]]
+directory = "added"
+name = "Added"
+showcontent = true
+
+[[tool.towncrier.type]]
+directory = "changed"
+name = "Changed"
+showcontent = true
+
+[[tool.towncrier.type]]
+directory = "fixed"
+name = "Fixed"
+showcontent = true
+
+[[tool.towncrier.type]]
+directory = "downstream"
+name = "Notes for Downstream Packagers"
+showcontent = true
diff --git a/pytest_asyncio/__init__.py b/pytest_asyncio/__init__.py
index c25c1bf1..abd62e15 100644
--- a/pytest_asyncio/__init__.py
+++ b/pytest_asyncio/__init__.py
@@ -2,7 +2,10 @@
from __future__ import annotations
-from ._version import version as __version__ # noqa: F401
+from importlib.metadata import version
+
from .plugin import fixture, is_async_test
+__version__ = version(__name__)
+
__all__ = ("fixture", "is_async_test")
diff --git a/pytest_asyncio/plugin.py b/pytest_asyncio/plugin.py
index 2f028ae1..29252b3e 100644
--- a/pytest_asyncio/plugin.py
+++ b/pytest_asyncio/plugin.py
@@ -9,19 +9,19 @@
import functools
import inspect
import socket
+import sys
+import traceback
import warnings
from asyncio import AbstractEventLoop, AbstractEventLoopPolicy
from collections.abc import (
AsyncIterator,
Awaitable,
- Coroutine as AbstractCoroutine,
Generator,
Iterable,
Iterator,
- Mapping,
Sequence,
)
-from textwrap import dedent
+from types import AsyncGeneratorType, CoroutineType
from typing import (
Any,
Callable,
@@ -33,45 +33,45 @@
import pluggy
import pytest
+from _pytest.fixtures import resolve_fixture_function
+from _pytest.scope import Scope
from pytest import (
- Class,
- Collector,
Config,
FixtureDef,
FixtureRequest,
Function,
Item,
Mark,
- Metafunc,
- Module,
- Package,
+ MonkeyPatch,
Parser,
PytestCollectionWarning,
PytestDeprecationWarning,
PytestPluginManager,
- Session,
- StashKey,
)
-_ScopeName = Literal["session", "package", "module", "class", "function"]
-_T = TypeVar("_T")
+if sys.version_info >= (3, 10):
+ from typing import ParamSpec
+else:
+ from typing_extensions import ParamSpec
-SimpleFixtureFunction = TypeVar(
- "SimpleFixtureFunction", bound=Callable[..., Awaitable[object]]
-)
-FactoryFixtureFunction = TypeVar(
- "FactoryFixtureFunction", bound=Callable[..., AsyncIterator[object]]
-)
-FixtureFunction = Union[SimpleFixtureFunction, FactoryFixtureFunction]
-FixtureFunctionMarker = Callable[[FixtureFunction], FixtureFunction]
+if sys.version_info >= (3, 11):
+ from asyncio import Runner
+else:
+ from backports.asyncio.runner import Runner
+if sys.version_info >= (3, 13):
+ from typing import TypeIs
+else:
+ from typing_extensions import TypeIs
-class PytestAsyncioError(Exception):
- """Base class for exceptions raised by pytest-asyncio"""
+_ScopeName = Literal["session", "package", "module", "class", "function"]
+_R = TypeVar("_R", bound=Union[Awaitable[Any], AsyncIterator[Any]])
+_P = ParamSpec("_P")
+FixtureFunction = Callable[_P, _R]
-class MultipleEventLoopsRequestedError(PytestAsyncioError):
- """Raised when a test requests multiple asyncio event loops."""
+class PytestAsyncioError(Exception):
+ """Base class for exceptions raised by pytest-asyncio"""
class Mode(str, enum.Enum):
@@ -96,22 +96,41 @@ def pytest_addoption(parser: Parser, pluginmanager: PytestPluginManager) -> None
metavar="MODE",
help=ASYNCIO_MODE_HELP,
)
+ group.addoption(
+ "--asyncio-debug",
+ dest="asyncio_debug",
+ action="store_true",
+ default=None,
+ help="enable asyncio debug mode for the default event loop",
+ )
parser.addini(
"asyncio_mode",
help="default value for --asyncio-mode",
default="strict",
)
+ parser.addini(
+ "asyncio_debug",
+ help="enable asyncio debug mode for the default event loop",
+ type="bool",
+ default="false",
+ )
parser.addini(
"asyncio_default_fixture_loop_scope",
type="string",
help="default scope of the asyncio event loop used to execute async fixtures",
default=None,
)
+ parser.addini(
+ "asyncio_default_test_loop_scope",
+ type="string",
+ help="default scope of the asyncio event loop used to execute tests",
+ default="function",
+ )
@overload
def fixture(
- fixture_function: FixtureFunction,
+ fixture_function: FixtureFunction[_P, _R],
*,
scope: _ScopeName | Callable[[str, Config], _ScopeName] = ...,
loop_scope: _ScopeName | None = ...,
@@ -123,7 +142,7 @@ def fixture(
| None
) = ...,
name: str | None = ...,
-) -> FixtureFunction: ...
+) -> FixtureFunction[_P, _R]: ...
@overload
@@ -140,14 +159,17 @@ def fixture(
| None
) = ...,
name: str | None = None,
-) -> FixtureFunctionMarker: ...
+) -> Callable[[FixtureFunction[_P, _R]], FixtureFunction[_P, _R]]: ...
def fixture(
- fixture_function: FixtureFunction | None = None,
+ fixture_function: FixtureFunction[_P, _R] | None = None,
loop_scope: _ScopeName | None = None,
**kwargs: Any,
-) -> FixtureFunction | FixtureFunctionMarker:
+) -> (
+ FixtureFunction[_P, _R]
+ | Callable[[FixtureFunction[_P, _R]], FixtureFunction[_P, _R]]
+):
if fixture_function is not None:
_make_asyncio_fixture_function(fixture_function, loop_scope)
return pytest.fixture(fixture_function, **kwargs)
@@ -155,7 +177,7 @@ def fixture(
else:
@functools.wraps(fixture)
- def inner(fixture_function: FixtureFunction) -> FixtureFunction:
+ def inner(fixture_function: FixtureFunction[_P, _R]) -> FixtureFunction[_P, _R]:
return fixture(fixture_function, loop_scope=loop_scope, **kwargs)
return inner
@@ -191,6 +213,17 @@ def _get_asyncio_mode(config: Config) -> Mode:
) from e
+def _get_asyncio_debug(config: Config) -> bool:
+ val = config.getoption("asyncio_debug")
+ if val is None:
+ val = config.getini("asyncio_debug")
+
+ if isinstance(val, bool):
+ return val
+ else:
+ return val == "true"
+
+
_DEFAULT_FIXTURE_LOOP_SCOPE_UNSET = """\
The configuration option "asyncio_default_fixture_loop_scope" is unset.
The event loop scope for asynchronous fixtures will default to the fixture caching \
@@ -201,10 +234,25 @@ def _get_asyncio_mode(config: Config) -> Mode:
"""
+def _validate_scope(scope: str | None, option_name: str) -> None:
+ if scope is None:
+ return
+ valid_scopes = [s.value for s in Scope]
+ if scope not in valid_scopes:
+ raise pytest.UsageError(
+ f"{scope!r} is not a valid {option_name}. "
+ f"Valid scopes are: {', '.join(valid_scopes)}."
+ )
+
+
def pytest_configure(config: Config) -> None:
- default_loop_scope = config.getini("asyncio_default_fixture_loop_scope")
- if not default_loop_scope:
+ default_fixture_loop_scope = config.getini("asyncio_default_fixture_loop_scope")
+ _validate_scope(default_fixture_loop_scope, "asyncio_default_fixture_loop_scope")
+ if not default_fixture_loop_scope:
warnings.warn(PytestDeprecationWarning(_DEFAULT_FIXTURE_LOOP_SCOPE_UNSET))
+
+ default_test_loop_scope = config.getini("asyncio_default_test_loop_scope")
+ _validate_scope(default_test_loop_scope, "asyncio_default_test_loop_scope")
config.addinivalue_line(
"markers",
"asyncio: "
@@ -217,116 +265,57 @@ def pytest_configure(config: Config) -> None:
def pytest_report_header(config: Config) -> list[str]:
"""Add asyncio config to pytest header."""
mode = _get_asyncio_mode(config)
- default_loop_scope = config.getini("asyncio_default_fixture_loop_scope")
+ debug = _get_asyncio_debug(config)
+ default_fixture_loop_scope = config.getini("asyncio_default_fixture_loop_scope")
+ default_test_loop_scope = _get_default_test_loop_scope(config)
+ header = [
+ f"mode={mode}",
+ f"debug={debug}",
+ f"asyncio_default_fixture_loop_scope={default_fixture_loop_scope}",
+ f"asyncio_default_test_loop_scope={default_test_loop_scope}",
+ ]
return [
- f"asyncio: mode={mode}, asyncio_default_fixture_loop_scope={default_loop_scope}"
+ "asyncio: " + ", ".join(header),
]
-def _preprocess_async_fixtures(
- collector: Collector,
- processed_fixturedefs: set[FixtureDef],
-) -> None:
- config = collector.config
- default_loop_scope = config.getini("asyncio_default_fixture_loop_scope")
- asyncio_mode = _get_asyncio_mode(config)
- fixturemanager = config.pluginmanager.get_plugin("funcmanage")
- assert fixturemanager is not None
- for fixtures in fixturemanager._arg2fixturedefs.values():
- for fixturedef in fixtures:
- func = fixturedef.func
- if fixturedef in processed_fixturedefs or not _is_coroutine_or_asyncgen(
- func
- ):
- continue
- if asyncio_mode == Mode.STRICT and not _is_asyncio_fixture_function(func):
- # Ignore async fixtures without explicit asyncio mark in strict mode
- # This applies to pytest_trio fixtures, for example
- continue
- scope = (
- getattr(func, "_loop_scope", None)
- or default_loop_scope
- or fixturedef.scope
- )
- if scope == "function" and "event_loop" not in fixturedef.argnames:
- fixturedef.argnames += ("event_loop",)
- _make_asyncio_fixture_function(func, scope)
- 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."
- )
- )
- if "request" not in fixturedef.argnames:
- fixturedef.argnames += ("request",)
- _synchronize_async_fixture(fixturedef)
- assert _is_asyncio_fixture_function(fixturedef.func)
- processed_fixturedefs.add(fixturedef)
-
-
-def _synchronize_async_fixture(fixturedef: FixtureDef) -> None:
- """Wraps the fixture function of an async fixture in a synchronous function."""
+def _fixture_synchronizer(
+ fixturedef: FixtureDef, runner: Runner, request: FixtureRequest
+) -> Callable:
+ """Returns a synchronous function evaluating the specified fixture."""
+ fixture_function = resolve_fixture_function(fixturedef, request)
if inspect.isasyncgenfunction(fixturedef.func):
- _wrap_asyncgen_fixture(fixturedef)
+ return _wrap_asyncgen_fixture(fixture_function, runner, request) # type: ignore[arg-type]
elif inspect.iscoroutinefunction(fixturedef.func):
- _wrap_async_fixture(fixturedef)
+ return _wrap_async_fixture(fixture_function, runner, request) # type: ignore[arg-type]
+ else:
+ return fixturedef.func
-def _add_kwargs(
- func: Callable[..., Any],
- kwargs: dict[str, Any],
- event_loop: asyncio.AbstractEventLoop,
- request: FixtureRequest,
-) -> dict[str, Any]:
- sig = inspect.signature(func)
- ret = kwargs.copy()
- if "request" in sig.parameters:
- ret["request"] = request
- if "event_loop" in sig.parameters:
- ret["event_loop"] = event_loop
- return ret
-
-
-def _perhaps_rebind_fixture_func(func: _T, instance: Any | None) -> _T:
- if instance is not None:
- # The fixture needs to be bound to the actual request.instance
- # so it is bound to the same object as the test method.
- unbound, cls = func, None
- try:
- unbound, cls = func.__func__, type(func.__self__) # type: ignore
- except AttributeError:
- pass
- # Only if the fixture was bound before to an instance of
- # the same type.
- if cls is not None and isinstance(instance, cls):
- func = unbound.__get__(instance) # type: ignore
- return func
-
+AsyncGenFixtureParams = ParamSpec("AsyncGenFixtureParams")
+AsyncGenFixtureYieldType = TypeVar("AsyncGenFixtureYieldType")
-def _wrap_asyncgen_fixture(fixturedef: FixtureDef) -> None:
- fixture = fixturedef.func
- @functools.wraps(fixture)
- def _asyncgen_fixture_wrapper(request: FixtureRequest, **kwargs: Any):
- func = _perhaps_rebind_fixture_func(fixture, request.instance)
- event_loop_fixture_id = _get_event_loop_fixture_id_for_async_fixture(
- request, func
- )
- event_loop = request.getfixturevalue(event_loop_fixture_id)
- kwargs.pop(event_loop_fixture_id, None)
- gen_obj = func(**_add_kwargs(func, kwargs, event_loop, request))
+def _wrap_asyncgen_fixture(
+ fixture_function: Callable[
+ AsyncGenFixtureParams, AsyncGeneratorType[AsyncGenFixtureYieldType, Any]
+ ],
+ runner: Runner,
+ request: FixtureRequest,
+) -> Callable[AsyncGenFixtureParams, AsyncGenFixtureYieldType]:
+ @functools.wraps(fixture_function)
+ def _asyncgen_fixture_wrapper(
+ *args: AsyncGenFixtureParams.args,
+ **kwargs: AsyncGenFixtureParams.kwargs,
+ ):
+ gen_obj = fixture_function(*args, **kwargs)
async def setup():
- res = await gen_obj.__anext__() # type: ignore[union-attr]
+ res = await gen_obj.__anext__()
return res
context = contextvars.copy_context()
- setup_task = _create_task_in_context(event_loop, setup(), context)
- result = event_loop.run_until_complete(setup_task)
+ result = runner.run(setup(), context=context)
reset_contextvars = _apply_contextvar_changes(context)
@@ -335,7 +324,7 @@ def finalizer() -> None:
async def async_finalizer() -> None:
try:
- await gen_obj.__anext__() # type: ignore[union-attr]
+ await gen_obj.__anext__()
except StopAsyncIteration:
pass
else:
@@ -343,36 +332,38 @@ async def async_finalizer() -> None:
msg += "Yield only once."
raise ValueError(msg)
- task = _create_task_in_context(event_loop, async_finalizer(), context)
- event_loop.run_until_complete(task)
+ runner.run(async_finalizer(), context=context)
if reset_contextvars is not None:
reset_contextvars()
request.addfinalizer(finalizer)
return result
- fixturedef.func = _asyncgen_fixture_wrapper # type: ignore[misc]
+ return _asyncgen_fixture_wrapper
-def _wrap_async_fixture(fixturedef: FixtureDef) -> None:
- fixture = fixturedef.func
+AsyncFixtureParams = ParamSpec("AsyncFixtureParams")
+AsyncFixtureReturnType = TypeVar("AsyncFixtureReturnType")
- @functools.wraps(fixture)
- def _async_fixture_wrapper(request: FixtureRequest, **kwargs: Any):
- func = _perhaps_rebind_fixture_func(fixture, request.instance)
- event_loop_fixture_id = _get_event_loop_fixture_id_for_async_fixture(
- request, func
- )
- event_loop = request.getfixturevalue(event_loop_fixture_id)
- kwargs.pop(event_loop_fixture_id, None)
+def _wrap_async_fixture(
+ fixture_function: Callable[
+ AsyncFixtureParams, CoroutineType[Any, Any, AsyncFixtureReturnType]
+ ],
+ runner: Runner,
+ request: FixtureRequest,
+) -> Callable[AsyncFixtureParams, AsyncFixtureReturnType]:
+ @functools.wraps(fixture_function)
+ def _async_fixture_wrapper(
+ *args: AsyncFixtureParams.args,
+ **kwargs: AsyncFixtureParams.kwargs,
+ ):
async def setup():
- res = await func(**_add_kwargs(func, kwargs, event_loop, request))
+ res = await fixture_function(*args, **kwargs)
return res
context = contextvars.copy_context()
- setup_task = _create_task_in_context(event_loop, setup(), context)
- result = event_loop.run_until_complete(setup_task)
+ result = runner.run(setup(), context=context)
# Copy the context vars modified by the setup task into the current
# context, and (if needed) add a finalizer to reset them.
@@ -388,50 +379,7 @@ async def setup():
return result
- fixturedef.func = _async_fixture_wrapper # type: ignore[misc]
-
-
-def _get_event_loop_fixture_id_for_async_fixture(
- request: FixtureRequest, func: Any
-) -> str:
- default_loop_scope = request.config.getini("asyncio_default_fixture_loop_scope")
- loop_scope = (
- getattr(func, "_loop_scope", None) or default_loop_scope or request.scope
- )
- if loop_scope == "function":
- event_loop_fixture_id = "event_loop"
- else:
- event_loop_node = _retrieve_scope_root(request._pyfuncitem, loop_scope)
- event_loop_fixture_id = event_loop_node.stash.get(
- # Type ignored because of non-optimal mypy inference.
- _event_loop_fixture_id, # type: ignore[arg-type]
- "",
- )
- assert event_loop_fixture_id
- return event_loop_fixture_id
-
-
-def _create_task_in_context(
- loop: asyncio.AbstractEventLoop,
- coro: AbstractCoroutine[Any, Any, _T],
- context: contextvars.Context,
-) -> asyncio.Task[_T]:
- """
- Return an asyncio task that runs the coro in the specified context,
- if possible.
-
- This allows fixture setup and teardown to be run as separate asyncio tasks,
- while still being able to use context-manager idioms to maintain context
- variables and make those variables visible to test functions.
-
- This is only fully supported on Python 3.11 and newer, as it requires
- the API added for https://github.com/python/cpython/issues/91150.
- On earlier versions, the returned task will use the default context instead.
- """
- try:
- return loop.create_task(coro, context=context)
- except TypeError:
- return loop.create_task(coro)
+ return _async_fixture_wrapper
def _apply_contextvar_changes(
@@ -489,6 +437,7 @@ def _from_function(cls, function: Function, /) -> Function:
Function item.
"""
assert function.get_closest_marker("asyncio")
+ assert function.parent is not None
subclass_instance = cls.from_parent(
function.parent,
name=function.name,
@@ -500,15 +449,6 @@ def _from_function(cls, function: Function, /) -> Function:
)
subclass_instance.own_markers = function.own_markers
assert subclass_instance.own_markers == function.own_markers
- 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
@@ -516,6 +456,49 @@ def _can_substitute(item: Function) -> bool:
"""Returns whether the specified function can be replaced by this class"""
raise NotImplementedError()
+ def setup(self) -> None:
+ runner_fixture_id = f"_{self._loop_scope}_scoped_runner"
+ if runner_fixture_id not in self.fixturenames:
+ self.fixturenames.append(runner_fixture_id)
+ return super().setup()
+
+ def runtest(self) -> None:
+ runner_fixture_id = f"_{self._loop_scope}_scoped_runner"
+ runner = self._request.getfixturevalue(runner_fixture_id)
+ context = contextvars.copy_context()
+ synchronized_obj = _synchronize_coroutine(
+ getattr(*self._synchronization_target_attr), runner, context
+ )
+ with MonkeyPatch.context() as c:
+ c.setattr(*self._synchronization_target_attr, synchronized_obj)
+ super().runtest()
+
+ @functools.cached_property
+ def _loop_scope(self) -> _ScopeName:
+ """
+ Return the scope of the asyncio event loop this item is run in.
+
+ The effective scope is determined lazily. It is identical to to the
+ `loop_scope` value of the closest `asyncio` pytest marker. If no such
+ marker is present, the the loop scope is determined by the configuration
+ value of `asyncio_default_test_loop_scope`, instead.
+ """
+ marker = self.get_closest_marker("asyncio")
+ assert marker is not None
+ default_loop_scope = _get_default_test_loop_scope(self.config)
+ return _get_marked_loop_scope(marker, default_loop_scope)
+
+ @property
+ def _synchronization_target_attr(self) -> tuple[object, str]:
+ """
+ Return the coroutine that needs to be synchronized during the test run.
+
+ This method is inteded to be overwritten by subclasses when they need to apply
+ the coroutine synchronizer to a value that's different from self.obj
+ e.g. the AsyncHypothesisTest subclass.
+ """
+ return self, "obj"
+
class Coroutine(PytestAsyncioFunction):
"""Pytest item created by a coroutine"""
@@ -525,13 +508,6 @@ def _can_substitute(item: Function) -> bool:
func = item.obj
return inspect.iscoroutinefunction(func)
- def runtest(self) -> None:
- self.obj = wrap_in_sync(
- # https://github.com/pytest-dev/pytest-asyncio/issues/596
- self.obj, # type: ignore[has-type]
- )
- super().runtest()
-
class AsyncGenerator(PytestAsyncioFunction):
"""Pytest item created by an asynchronous generator"""
@@ -568,13 +544,6 @@ def _can_substitute(item: Function) -> bool:
func.__func__
)
- def runtest(self) -> None:
- self.obj = wrap_in_sync(
- # https://github.com/pytest-dev/pytest-asyncio/issues/596
- self.obj, # type: ignore[has-type]
- )
- super().runtest()
-
class AsyncHypothesisTest(PytestAsyncioFunction):
"""
@@ -582,6 +551,16 @@ class AsyncHypothesisTest(PytestAsyncioFunction):
@hypothesis.given.
"""
+ def setup(self) -> None:
+ if not getattr(self.obj, "hypothesis", False) and getattr(
+ self.obj, "is_hypothesis_test", False
+ ):
+ pytest.fail(
+ f"test function `{self!r}` is using Hypothesis, but pytest-asyncio "
+ "only works with Hypothesis 3.64.0 or later."
+ )
+ return super().setup()
+
@staticmethod
def _can_substitute(item: Function) -> bool:
func = item.obj
@@ -591,27 +570,9 @@ def _can_substitute(item: Function) -> bool:
and inspect.iscoroutinefunction(func.hypothesis.inner_test)
)
- def runtest(self) -> None:
- self.obj.hypothesis.inner_test = wrap_in_sync(
- self.obj.hypothesis.inner_test,
- )
- super().runtest()
-
-
-_HOLDER: set[FixtureDef] = set()
-
-
-# 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: pytest.Module | pytest.Class, name: str, obj: object
-) -> pytest.Item | pytest.Collector | list[pytest.Item | pytest.Collector] | None:
- """A pytest hook to collect asyncio coroutines."""
- if not collector.funcnamefilter(name):
- return None
- _preprocess_async_fixtures(collector, _HOLDER)
- return None
+ @property
+ def _synchronization_target_attr(self) -> tuple[object, str]:
+ return self.obj.hypothesis, "inner_test"
# The function name needs to start with "pytest_"
@@ -655,317 +616,31 @@ def pytest_pycollect_makeitem_convert_async_functions_to_subclass(
hook_result.force_result(updated_node_collection)
-_event_loop_fixture_id = StashKey[str]()
-_fixture_scope_by_collector_type: Mapping[type[pytest.Collector], _ScopeName] = {
- Class: "class",
- # 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",
-}
-
-# A stack used to push package-scoped loops during collection of a package
-# and pop those loops during collection of a Module
-__package_loop_stack: list[FixtureFunctionMarker | FixtureFunction] = []
-
-
-@pytest.hookimpl
-def pytest_collectstart(collector: pytest.Collector) -> None:
- 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 collector_scope == "session":
- event_loop_fixture_id = _session_event_loop.__name__
- collector.stash[_event_loop_fixture_id] = event_loop_fixture_id
- 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
- # 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=collector_scope,
- 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
- with (
- _temporary_event_loop_policy(new_loop_policy),
- _provide_event_loop() as loop,
- ):
- asyncio.set_event_loop(loop)
- yield 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 object, where it will be picked up by pytest.Class.collect()
- # or pytest.Module.collect(), respectively
- if type(collector) is Package:
- # Packages do not have a corresponding Python object. Therefore, the fixture
- # for the package-scoped event loop is added to a stack. When a module inside
- # the package is collected, the module will attach the fixture to its
- # Python object.
- __package_loop_stack.append(scoped_event_loop)
- elif isinstance(collector, 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.
- # Therefore, we monkey patch Module.collect to add the scoped fixture to the
- # module before it runs the actual collection.
- def _patched_collect():
- # If the collected module is a DoctestTextfile, collector.obj is None
- module = collector.obj
- if module is not None:
- module.__pytest_asyncio_scoped_event_loop = scoped_event_loop
- try:
- package_loop = __package_loop_stack.pop()
- module.__pytest_asyncio_package_scoped_event_loop = package_loop
- except IndexError:
- pass
- return collector.__original_collect()
-
- collector.__original_collect = collector.collect # type: ignore[attr-defined]
- collector.collect = _patched_collect # type: ignore[method-assign]
- elif isinstance(collector, Class):
- collector.obj.__pytest_asyncio_scoped_event_loop = scoped_event_loop
-
-
@contextlib.contextmanager
def _temporary_event_loop_policy(policy: AbstractEventLoopPolicy) -> Iterator[None]:
- old_loop_policy = asyncio.get_event_loop_policy()
+ old_loop_policy = _get_event_loop_policy()
try:
old_loop = _get_event_loop_no_warn()
except RuntimeError:
old_loop = None
- asyncio.set_event_loop_policy(policy)
+ _set_event_loop_policy(policy)
try:
yield
finally:
- # Try detecting user-created event loops that were left unclosed
- # at the end of a test.
- try:
- current_loop: AbstractEventLoop | None = _get_event_loop_no_warn()
- except RuntimeError:
- current_loop = None
- if current_loop is not None and not current_loop.is_closed():
- warnings.warn(
- _UNCLOSED_EVENT_LOOP_WARNING % current_loop,
- DeprecationWarning,
- )
- current_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.
- try:
- _get_event_loop_no_warn().close()
- except RuntimeError:
- pass
- asyncio.set_event_loop(old_loop)
-
-
-_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 scope other than function
- scope, use the "loop_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:
- marker = metafunc.definition.get_closest_marker("asyncio")
- if not marker:
- return
- scope = _get_marked_loop_scope(marker)
- 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")
- assert fixturemanager is not None
- 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
- # 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]
- )
-
-
-@pytest.hookimpl(hookwrapper=True)
-def pytest_fixture_setup(
- fixturedef: FixtureDef,
-) -> Generator[None, pluggy.Result, None]:
- """Adjust the event loop policy when an event loop is produced."""
- if fixturedef.argname == "event_loop":
- # 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
- # any specific fixture.
- # see https://github.com/pytest-dev/pytest/issues/5848
- _add_finalizers(
- fixturedef,
- _close_event_loop,
- _restore_event_loop_policy(asyncio.get_event_loop_policy()),
- _provide_clean_event_loop,
- )
- outcome = yield
- loop: asyncio.AbstractEventLoop = 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 _is_pytest_asyncio_loop(loop):
- _, 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:
- old_loop = _get_event_loop_no_warn(policy)
- if old_loop is not loop and not _is_pytest_asyncio_loop(old_loop):
- old_loop.close()
- except RuntimeError:
- # Either the current event loop has been set to None
- # or the loop policy doesn't specify to create new loops
- # or we're not in the main thread
- pass
- policy.set_event_loop(loop)
- return
-
- yield
-
-
-def _make_pytest_asyncio_loop(loop: AbstractEventLoop) -> AbstractEventLoop:
- loop.__pytest_asyncio = True # type: ignore[attr-defined]
- return loop
-
-
-def _is_pytest_asyncio_loop(loop: AbstractEventLoop) -> bool:
- return getattr(loop, "__pytest_asyncio", False)
-
-
-def _add_finalizers(fixturedef: FixtureDef, *finalizers: Callable[[], object]) -> None:
- """
- Registers the specified fixture finalizers in the fixture.
-
- Finalizers need to be specified in the exact order in which they should be invoked.
-
- :param fixturedef: Fixture definition which finalizers should be added to
- :param finalizers: Finalizers to be added
- """
- for finalizer in reversed(finalizers):
- fixturedef.addfinalizer(finalizer)
-
-
-_UNCLOSED_EVENT_LOOP_WARNING = dedent(
- """\
- pytest-asyncio detected an unclosed event loop when tearing down the event_loop
- fixture: %r
- pytest-asyncio will close the event loop for you, but future versions of the
- library will no longer do so. In order to ensure compatibility with future
- versions, please make sure that:
- 1. Any custom "event_loop" fixture properly closes the loop after yielding it
- 2. The scopes of your custom "event_loop" fixtures do not overlap
- 3. Your code does not modify the event loop in async fixtures or tests
- """
-)
+ _set_event_loop_policy(old_loop_policy)
+ _set_event_loop(old_loop)
-def _close_event_loop() -> None:
- policy = asyncio.get_event_loop_policy()
- try:
- loop = policy.get_event_loop()
- except RuntimeError:
- loop = None
- if loop is not None and not _is_pytest_asyncio_loop(loop):
- if not loop.is_closed():
- warnings.warn(
- _UNCLOSED_EVENT_LOOP_WARNING % loop,
- DeprecationWarning,
- )
- loop.close()
+def _get_event_loop_policy() -> AbstractEventLoopPolicy:
+ with warnings.catch_warnings():
+ warnings.simplefilter("ignore", DeprecationWarning)
+ return asyncio.get_event_loop_policy()
-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:
- loop = _get_event_loop_no_warn(previous_policy)
- except RuntimeError:
- loop = None
- if loop and not _is_pytest_asyncio_loop(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.
- # In order to avoid this side effect from pytest-asyncio, we need to replace
- # the current loop with a fresh one.
- # Note that we cannot set the loop to None, because get_event_loop only creates
- # a new loop, when set_event_loop has not been called.
- policy = asyncio.get_event_loop_policy()
- try:
- old_loop = _get_event_loop_no_warn(policy)
- except RuntimeError:
- old_loop = None
- if old_loop is not None and not _is_pytest_asyncio_loop(old_loop):
- new_loop = policy.new_event_loop()
- policy.set_event_loop(new_loop)
+def _set_event_loop_policy(policy: AbstractEventLoopPolicy) -> None:
+ with warnings.catch_warnings():
+ warnings.simplefilter("ignore", DeprecationWarning)
+ asyncio.set_event_loop_policy(policy)
def _get_event_loop_no_warn(
@@ -979,16 +654,17 @@ def _get_event_loop_no_warn(
return asyncio.get_event_loop()
+def _set_event_loop(loop: AbstractEventLoop | None) -> None:
+ with warnings.catch_warnings():
+ warnings.simplefilter("ignore", DeprecationWarning)
+ asyncio.set_event_loop(loop)
+
+
@pytest.hookimpl(tryfirst=True, hookwrapper=True)
def pytest_pyfunc_call(pyfuncitem: Function) -> object | None:
- """
- Pytest hook called before a test case is run.
-
- Wraps marked tests in a synchronous function
- where the wrapped test coroutine is executed in an event loop.
- """
+ """Pytest hook called before a test case is run."""
if pyfuncitem.get_closest_marker("asyncio") is not None:
- if isinstance(pyfuncitem, PytestAsyncioFunction):
+ if is_async_test(pyfuncitem):
asyncio_mode = _get_asyncio_mode(pyfuncitem.config)
for fixname, fixtures in pyfuncitem._fixtureinfo.name2fixturedefs.items():
# name2fixturedefs is a dict between fixture name and a list of matching
@@ -1007,7 +683,7 @@ def pytest_pyfunc_call(pyfuncitem: Function) -> object | None:
"You might want to use @pytest_asyncio.fixture or switch "
"to auto mode. "
"This will become an error in future versions of "
- "flake8-asyncio."
+ "pytest-asyncio."
),
stacklevel=1,
)
@@ -1029,72 +705,48 @@ def pytest_pyfunc_call(pyfuncitem: Function) -> object | None:
return None
-def wrap_in_sync(
- func: Callable[..., Awaitable[Any]],
+def _synchronize_coroutine(
+ func: Callable[..., CoroutineType],
+ runner: asyncio.Runner,
+ context: contextvars.Context,
):
"""
- Return a sync wrapper around an async function executing it in the
- current event loop.
+ Return a sync wrapper around a coroutine executing it in the
+ specified runner and context.
"""
- # if the function is already wrapped, we rewrap using the original one
- # not using __wrapped__ because the original function may already be
- # a wrapped one
- raw_func = getattr(func, "_raw_test_func", None)
- if raw_func is not None:
- func = raw_func
@functools.wraps(func)
def inner(*args, **kwargs):
coro = func(*args, **kwargs)
- _loop = _get_event_loop_no_warn()
- task = asyncio.ensure_future(coro, loop=_loop)
- try:
- _loop.run_until_complete(task)
- except BaseException:
- # run_until_complete doesn't get the result from exceptions
- # that are not subclasses of `Exception`. Consume all
- # exceptions to prevent asyncio's warning from logging.
- if task.done() and not task.cancelled():
- task.exception()
- raise
-
- inner._raw_test_func = func # type: ignore[attr-defined]
- return inner
+ runner.run(coro, context=context)
-
-_MULTIPLE_LOOPS_REQUESTED_ERROR = dedent(
- """\
- Multiple asyncio event loops with different scopes have been requested
- 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 {scope}-scoped event loop or remove the scope argument from the "asyncio"
- mark to run the test in a function-scoped event loop.
- """
-)
+ return inner
-def pytest_runtest_setup(item: pytest.Item) -> None:
- marker = item.get_closest_marker("asyncio")
- if marker is None:
- return
- scope = _get_marked_loop_scope(marker)
- 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]
- 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
- ):
- pytest.fail(
- f"test function `{item!r}` is using Hypothesis, but pytest-asyncio "
- "only works with Hypothesis 3.64.0 or later."
- )
+@pytest.hookimpl(wrapper=True)
+def pytest_fixture_setup(fixturedef: FixtureDef, request) -> object | None:
+ asyncio_mode = _get_asyncio_mode(request.config)
+ if not _is_asyncio_fixture_function(fixturedef.func):
+ if asyncio_mode == Mode.STRICT:
+ # Ignore async fixtures without explicit asyncio mark in strict mode
+ # This applies to pytest_trio fixtures, for example
+ return (yield)
+ if not _is_coroutine_or_asyncgen(fixturedef.func):
+ return (yield)
+ default_loop_scope = request.config.getini("asyncio_default_fixture_loop_scope")
+ loop_scope = (
+ getattr(fixturedef.func, "_loop_scope", None)
+ or default_loop_scope
+ or fixturedef.scope
+ )
+ runner_fixture_id = f"_{loop_scope}_scoped_runner"
+ runner = request.getfixturevalue(runner_fixture_id)
+ synchronizer = _fixture_synchronizer(fixturedef, runner, request)
+ _make_asyncio_fixture_function(synchronizer, loop_scope)
+ with MonkeyPatch.context() as c:
+ c.setattr(fixturedef, "func", synchronizer)
+ hook_result = yield
+ return hook_result
_DUPLICATE_LOOP_SCOPE_DEFINITION_ERROR = """\
@@ -1108,7 +760,9 @@ def pytest_runtest_setup(item: pytest.Item) -> None:
"""
-def _get_marked_loop_scope(asyncio_marker: Mark) -> _ScopeName:
+def _get_marked_loop_scope(
+ asyncio_marker: Mark, default_loop_scope: _ScopeName
+) -> _ScopeName:
assert asyncio_marker.name == "asyncio"
if asyncio_marker.args or (
asyncio_marker.kwargs and set(asyncio_marker.kwargs) - {"loop_scope", "scope"}
@@ -1119,74 +773,75 @@ def _get_marked_loop_scope(asyncio_marker: Mark) -> _ScopeName:
raise pytest.UsageError(_DUPLICATE_LOOP_SCOPE_DEFINITION_ERROR)
warnings.warn(PytestDeprecationWarning(_MARKER_SCOPE_KWARG_DEPRECATION_WARNING))
scope = asyncio_marker.kwargs.get("loop_scope") or asyncio_marker.kwargs.get(
- "scope", "function"
+ "scope"
)
+ if scope is None:
+ scope = default_loop_scope
assert scope in {"function", "class", "module", "package", "session"}
return scope
-def _retrieve_scope_root(item: Collector | Item, scope: str) -> Collector:
- node_type_by_scope = {
- "class": Class,
- "module": Module,
- "package": Package,
- "session": Session,
- }
- scope_root_type = node_type_by_scope[scope]
- for node in reversed(item.listchain()):
- if isinstance(node, scope_root_type):
- assert isinstance(node, pytest.Collector)
- 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)
+def _get_default_test_loop_scope(config: Config) -> Any:
+ return config.getini("asyncio_default_test_loop_scope")
-@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__)
- with _temporary_event_loop_policy(new_loop_policy), _provide_event_loop() as loop:
- yield loop
+_RUNNER_TEARDOWN_WARNING = """\
+An exception occurred during teardown of an asyncio.Runner. \
+The reason is likely that you closed the underlying event loop in a test, \
+which prevents the cleanup of asynchronous generators by the runner.
+This warning will become an error in future versions of pytest-asyncio. \
+Please ensure that your tests don't close the event loop. \
+Here is the traceback of the exception triggered during teardown:
+%s
+"""
-@contextlib.contextmanager
-def _provide_event_loop() -> Iterator[asyncio.AbstractEventLoop]:
- 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 = _make_pytest_asyncio_loop(loop)
- try:
- yield loop
- finally:
- try:
- loop.run_until_complete(loop.shutdown_asyncgens())
- finally:
- loop.close()
+def _create_scoped_runner_fixture(scope: _ScopeName) -> Callable:
+ @pytest.fixture(
+ scope=scope,
+ name=f"_{scope}_scoped_runner",
+ )
+ def _scoped_runner(
+ event_loop_policy,
+ request: FixtureRequest,
+ ) -> Iterator[Runner]:
+ new_loop_policy = event_loop_policy
+ debug_mode = _get_asyncio_debug(request.config)
+ with _temporary_event_loop_policy(new_loop_policy):
+ runner = Runner(debug=debug_mode).__enter__()
+ try:
+ yield runner
+ except Exception as e:
+ runner.__exit__(type(e), e, e.__traceback__)
+ else:
+ with warnings.catch_warnings():
+ warnings.filterwarnings(
+ "ignore", ".*BaseEventLoop.shutdown_asyncgens.*", RuntimeWarning
+ )
+ try:
+ runner.__exit__(None, None, None)
+ except RuntimeError:
+ warnings.warn(
+ _RUNNER_TEARDOWN_WARNING % traceback.format_exc(),
+ RuntimeWarning,
+ )
+ return _scoped_runner
-@pytest.fixture(scope="session")
-def _session_event_loop(
- request: FixtureRequest, event_loop_policy: AbstractEventLoopPolicy
-) -> Iterator[asyncio.AbstractEventLoop]:
- new_loop_policy = event_loop_policy
- with _temporary_event_loop_policy(new_loop_policy), _provide_event_loop() as loop:
- asyncio.set_event_loop(loop)
- yield loop
+
+for scope in Scope:
+ globals()[f"_{scope.value}_scoped_runner"] = _create_scoped_runner_fixture(
+ scope.value
+ )
@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()
+ return _get_event_loop_policy()
-def is_async_test(item: Item) -> bool:
+def is_async_test(item: Item) -> TypeIs[PytestAsyncioFunction]:
"""Returns whether a test item is a pytest-asyncio test"""
return isinstance(item, PytestAsyncioFunction)
diff --git a/setup.cfg b/setup.cfg
deleted file mode 100644
index f68f54b8..00000000
--- a/setup.cfg
+++ /dev/null
@@ -1,18 +0,0 @@
-[metadata]
-# Not everything is in in pyproject.toml because of this issue:
-; Traceback (most recent call last):
-; File "/tmp/build-env-rud8b5r6/lib/python3.12/site-packages/setuptools/config/expand.py", line 69, in __getattr__
-; return next(
-; ^^^^^
-;StopIteration
-;
-;The above exception was the direct cause of the following exception:
-;
-;Traceback (most recent call last):
-; File "/tmp/build-env-rud8b5r6/lib/python3.12/site-packages/setuptools/config/expand.py", line 183, in read_attr
-; return getattr(StaticModule(module_name, spec), attr_name)
-; ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-; File "/tmp/build-env-rud8b5r6/lib/python3.12/site-packages/setuptools/config/expand.py", line 75, in __getattr__
-; raise AttributeError(f"{self.name} has no attribute {attr}") from e
-;AttributeError: pytest_asyncio has no attribute __version__
-version = attr: pytest_asyncio.__version__
diff --git a/tests/async_fixtures/test_async_fixtures_contextvars.py b/tests/async_fixtures/test_async_fixtures_contextvars.py
index ff79e17e..e8634d0c 100644
--- a/tests/async_fixtures/test_async_fixtures_contextvars.py
+++ b/tests/async_fixtures/test_async_fixtures_contextvars.py
@@ -5,8 +5,8 @@
from __future__ import annotations
-import sys
from textwrap import dedent
+from typing import Literal
import pytest
from pytest import Pytester
@@ -56,11 +56,6 @@ async def test(check_var_fixture):
result.assert_outcomes(passed=1)
-@pytest.mark.xfail(
- sys.version_info < (3, 11),
- reason="requires asyncio Task context support",
- strict=True,
-)
def test_var_from_async_generator_propagates_to_sync(pytester: Pytester):
pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function")
pytester.makepyfile(
@@ -86,11 +81,6 @@ async def test(check_var_fixture):
result.assert_outcomes(passed=1)
-@pytest.mark.xfail(
- sys.version_info < (3, 11),
- reason="requires asyncio Task context support",
- strict=True,
-)
def test_var_from_async_fixture_propagates_to_sync(pytester: Pytester):
pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function")
pytester.makepyfile(
@@ -115,11 +105,6 @@ def test(check_var_fixture):
result.assert_outcomes(passed=1)
-@pytest.mark.xfail(
- sys.version_info < (3, 11),
- reason="requires asyncio Task context support",
- strict=True,
-)
def test_var_from_generator_reset_before_previous_fixture_cleanup(pytester: Pytester):
pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function")
pytester.makepyfile(
@@ -149,11 +134,6 @@ async def test(var_fixture):
result.assert_outcomes(passed=1)
-@pytest.mark.xfail(
- sys.version_info < (3, 11),
- reason="requires asyncio Task context support",
- strict=True,
-)
def test_var_from_fixture_reset_before_previous_fixture_cleanup(pytester: Pytester):
pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function")
pytester.makepyfile(
@@ -183,11 +163,6 @@ async def test(var_fixture):
result.assert_outcomes(passed=1)
-@pytest.mark.xfail(
- sys.version_info < (3, 11),
- reason="requires asyncio Task context support",
- strict=True,
-)
def test_var_previous_value_restored_after_fixture(pytester: Pytester):
pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function")
pytester.makepyfile(
@@ -216,11 +191,6 @@ async def test(var_fixture_2):
result.assert_outcomes(passed=1)
-@pytest.mark.xfail(
- sys.version_info < (3, 11),
- reason="requires asyncio Task context support",
- strict=True,
-)
def test_var_set_to_existing_value_ok(pytester: Pytester):
pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function")
pytester.makepyfile(
@@ -245,3 +215,56 @@ async def test(same_var_fixture):
)
result = pytester.runpytest("--asyncio-mode=strict")
result.assert_outcomes(passed=1)
+
+
+def test_no_isolation_against_context_changes_in_sync_tests(pytester: Pytester):
+ pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function")
+ pytester.makepyfile(
+ dedent(
+ """
+ import pytest
+ import pytest_asyncio
+ from contextvars import ContextVar
+
+ _context_var = ContextVar("my_var")
+
+ def test_sync():
+ _context_var.set("new_value")
+
+ @pytest.mark.asyncio
+ async def test_async():
+ assert _context_var.get() == "new_value"
+ """
+ )
+ )
+ result = pytester.runpytest("--asyncio-mode=strict")
+ result.assert_outcomes(passed=2)
+
+
+@pytest.mark.parametrize("loop_scope", ("function", "module"))
+def test_isolation_against_context_changes_in_async_tests(
+ pytester: Pytester, loop_scope: Literal["function", "module"]
+):
+ pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function")
+ pytester.makepyfile(
+ dedent(
+ f"""
+ import pytest
+ import pytest_asyncio
+ from contextvars import ContextVar
+
+ _context_var = ContextVar("my_var")
+
+ @pytest.mark.asyncio(loop_scope="{loop_scope}")
+ async def test_async_first():
+ _context_var.set("new_value")
+
+ @pytest.mark.asyncio(loop_scope="{loop_scope}")
+ async def test_async_second():
+ with pytest.raises(LookupError):
+ _context_var.get()
+ """
+ )
+ )
+ result = pytester.runpytest("--asyncio-mode=strict")
+ result.assert_outcomes(passed=2)
diff --git a/tests/async_fixtures/test_async_fixtures_scope.py b/tests/async_fixtures/test_async_fixtures_scope.py
deleted file mode 100644
index 7fbed781..00000000
--- a/tests/async_fixtures/test_async_fixtures_scope.py
+++ /dev/null
@@ -1,30 +0,0 @@
-"""
-We support module-scoped async fixtures, but only if the event loop is
-module-scoped too.
-"""
-
-from __future__ import annotations
-
-import asyncio
-
-import pytest
-
-
-@pytest.fixture(scope="module")
-def event_loop():
- """A module-scoped event loop."""
- loop = asyncio.new_event_loop()
- yield loop
- loop.close()
-
-
-@pytest.fixture(scope="module")
-async def async_fixture():
- await asyncio.sleep(0.1)
- return 1
-
-
-@pytest.mark.asyncio
-async def test_async_fixture_scope(async_fixture):
- assert async_fixture == 1
- await asyncio.sleep(0.1)
diff --git a/tests/async_fixtures/test_async_fixtures_with_finalizer.py b/tests/async_fixtures/test_async_fixtures_with_finalizer.py
deleted file mode 100644
index 199ecbca..00000000
--- a/tests/async_fixtures/test_async_fixtures_with_finalizer.py
+++ /dev/null
@@ -1,63 +0,0 @@
-from __future__ import annotations
-
-import asyncio
-import functools
-
-import pytest
-
-import pytest_asyncio
-
-
-@pytest.mark.asyncio(loop_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(loop_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
-
-
-@pytest.fixture(scope="module")
-def event_loop():
- """Change event_loop fixture to module level."""
- policy = asyncio.get_event_loop_policy()
- loop = policy.new_event_loop()
- yield loop
- loop.close()
-
-
-@pytest_asyncio.fixture(loop_scope="module", scope="module")
-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
-
- asyncio.run(port_afinalizer())
-
- worker = asyncio.ensure_future(asyncio.sleep(0.2))
- request.addfinalizer(functools.partial(port_finalizer, worker))
- return True
-
-
-@pytest_asyncio.fixture(loop_scope="module", scope="module")
-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
- # RuntimeError is raised if task is created on a different loop.
- # This can happen when pytest_fixture_setup
- # does not set up the loop correctly,
- # for example when policy.set_event_loop() is called with a wrong argument
- await finalizer
-
- current_loop = asyncio.get_event_loop_policy().get_event_loop()
- current_loop.run_until_complete(port_afinalizer())
-
- worker = asyncio.ensure_future(asyncio.sleep(0.2))
- request.addfinalizer(functools.partial(port_finalizer, worker))
- return True
diff --git a/tests/async_fixtures/test_async_gen_fixtures.py b/tests/async_fixtures/test_async_gen_fixtures.py
deleted file mode 100644
index ddc2f5be..00000000
--- a/tests/async_fixtures/test_async_gen_fixtures.py
+++ /dev/null
@@ -1,53 +0,0 @@
-from __future__ import annotations
-
-import unittest.mock
-
-import pytest
-
-START = object()
-END = object()
-RETVAL = object()
-
-
-@pytest.fixture(scope="module")
-def mock():
- return unittest.mock.Mock(return_value=RETVAL)
-
-
-@pytest.fixture
-async def async_gen_fixture(mock):
- try:
- yield mock(START)
- except Exception as e:
- mock(e)
- else:
- mock(END)
-
-
-@pytest.mark.asyncio
-async def test_async_gen_fixture(async_gen_fixture, mock):
- assert mock.called
- assert mock.call_args_list[-1] == unittest.mock.call(START)
- assert async_gen_fixture is RETVAL
-
-
-@pytest.mark.asyncio
-async def test_async_gen_fixture_finalized(mock):
- try:
- assert mock.called
- assert mock.call_args_list[-1] == unittest.mock.call(END)
- finally:
- mock.reset_mock()
-
-
-class TestAsyncGenFixtureMethod:
- is_same_instance = False
-
- @pytest.fixture(autouse=True)
- async def async_gen_fixture_method(self):
- self.is_same_instance = True
- yield None
-
- @pytest.mark.asyncio
- async def test_async_gen_fixture_method(self):
- assert self.is_same_instance
diff --git a/tests/async_fixtures/test_parametrized_loop.py b/tests/async_fixtures/test_parametrized_loop.py
deleted file mode 100644
index ca2cb5c7..00000000
--- a/tests/async_fixtures/test_parametrized_loop.py
+++ /dev/null
@@ -1,48 +0,0 @@
-from __future__ import annotations
-
-from textwrap import dedent
-
-from pytest import Pytester
-
-
-def test_event_loop_parametrization(pytester: Pytester):
- pytester.makepyfile(
- dedent(
- """\
- import asyncio
-
- import pytest
- import pytest_asyncio
-
- TESTS_COUNT = 0
-
-
- def teardown_module():
- # parametrized 2 * 2 times: 2 for 'event_loop' and 2 for 'fix'
- assert TESTS_COUNT == 4
-
-
- @pytest.fixture(scope="module", params=[1, 2])
- def event_loop(request):
- request.param
- loop = asyncio.new_event_loop()
- yield loop
- loop.close()
-
-
- @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)
diff --git a/tests/conftest.py b/tests/conftest.py
index 76e2026f..eecab735 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -1,34 +1,3 @@
from __future__ import annotations
-import asyncio
-
-import pytest
-
pytest_plugins = "pytester"
-
-
-@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
-
-
-@pytest.fixture(scope="session", name="factory_involving_factories")
-def factory_involving_factories_fixture(unused_tcp_port_factory):
- def factory():
- return unused_tcp_port_factory()
-
- return factory
diff --git a/tests/hypothesis/test_base.py b/tests/hypothesis/test_base.py
index 4b185f62..487b05fe 100644
--- a/tests/hypothesis/test_base.py
+++ b/tests/hypothesis/test_base.py
@@ -45,43 +45,6 @@ async def test_mark_and_parametrize(x, y):
assert y in (1, 2)
-def test_can_use_explicit_event_loop_fixture(pytester: Pytester):
- pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = module")
- pytester.makepyfile(
- dedent(
- """\
- import asyncio
- import pytest
- from hypothesis import given
- import hypothesis.strategies as st
-
- pytest_plugins = 'pytest_asyncio'
-
- @pytest.fixture(scope="module")
- def event_loop():
- loop = asyncio.get_event_loop_policy().new_event_loop()
- yield loop
- loop.close()
-
- @given(st.integers())
- @pytest.mark.asyncio
- async def test_explicit_fixture_request(event_loop, n):
- semaphore = asyncio.Semaphore(value=0)
- event_loop.call_soon(semaphore.release)
- await semaphore.acquire()
- """
- )
- )
- result = pytester.runpytest_subprocess("--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):
pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function")
pytester.makepyfile(
diff --git a/tests/loop_fixture_scope/conftest.py b/tests/loop_fixture_scope/conftest.py
deleted file mode 100644
index 4e8b06de..00000000
--- a/tests/loop_fixture_scope/conftest.py
+++ /dev/null
@@ -1,17 +0,0 @@
-from __future__ import annotations
-
-import asyncio
-
-import pytest
-
-
-class CustomSelectorLoop(asyncio.SelectorEventLoop):
- """A subclass with no overrides, just to test for presence."""
-
-
-@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()
diff --git a/tests/loop_fixture_scope/test_loop_fixture_scope.py b/tests/loop_fixture_scope/test_loop_fixture_scope.py
deleted file mode 100644
index eb1bae58..00000000
--- a/tests/loop_fixture_scope/test_loop_fixture_scope.py
+++ /dev/null
@@ -1,19 +0,0 @@
-"""Unit tests for overriding the event loop with a larger scoped one."""
-
-from __future__ import annotations
-
-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/markers/test_class_scope.py b/tests/markers/test_class_scope.py
index 4bddb4b8..e8732e86 100644
--- a/tests/markers/test_class_scope.py
+++ b/tests/markers/test_class_scope.py
@@ -82,29 +82,6 @@ async def test_this_runs_in_same_loop(self):
result.assert_outcomes(passed=2)
-def test_asyncio_mark_raises_when_class_scoped_is_request_without_class(
- pytester: pytest.Pytester,
-):
- pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function")
- pytester.makepyfile(
- dedent(
- """\
- import asyncio
- import pytest
-
- @pytest.mark.asyncio(loop_scope="class")
- async def test_has_no_surrounding_class():
- pass
- """
- )
- )
- result = pytester.runpytest("--asyncio-mode=strict")
- result.assert_outcomes(errors=1)
- result.stdout.fnmatch_lines(
- "*is marked to be run in an event loop with scope*",
- )
-
-
def test_asyncio_mark_is_inherited_to_subclasses(pytester: pytest.Pytester):
pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function")
pytester.makepyfile(
diff --git a/tests/markers/test_function_scope.py b/tests/markers/test_function_scope.py
index c17a6225..16d45da5 100644
--- a/tests/markers/test_function_scope.py
+++ b/tests/markers/test_function_scope.py
@@ -87,32 +87,9 @@ async def test_warns():
"""
)
)
- result = pytester.runpytest_subprocess("--asyncio-mode=strict")
- result.assert_outcomes(passed=1, warnings=2)
- result.stdout.fnmatch_lines("*DeprecationWarning*")
-
-
-def test_function_scope_supports_explicit_event_loop_fixture_request(
- pytester: Pytester,
-):
- pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function")
- pytester.makepyfile(
- dedent(
- """\
- import pytest
-
- pytestmark = pytest.mark.asyncio
-
- async def test_remember_loop(event_loop):
- pass
- """
- )
- )
- result = pytester.runpytest_subprocess("--asyncio-mode=strict", "-W default")
+ result = pytester.runpytest("--asyncio-mode=strict", "--assert=plain")
result.assert_outcomes(passed=1, warnings=1)
- result.stdout.fnmatch_lines(
- '*is asynchronous and explicitly requests the "event_loop" fixture*'
- )
+ result.stdout.fnmatch_lines("*DeprecationWarning*")
def test_asyncio_mark_respects_the_loop_policy(
@@ -180,7 +157,7 @@ async def test_parametrized_loop():
"""
)
)
- result = pytester.runpytest_subprocess("--asyncio-mode=strict")
+ result = pytester.runpytest("--asyncio-mode=strict")
result.assert_outcomes(passed=2)
@@ -211,7 +188,7 @@ async def test_runs_is_same_loop_as_fixture(my_fixture):
"""
)
)
- result = pytester.runpytest_subprocess("--asyncio-mode=strict")
+ result = pytester.runpytest("--asyncio-mode=strict")
result.assert_outcomes(passed=1)
@@ -265,7 +242,7 @@ async def test_anything():
"""
)
)
- result = pytester.runpytest_subprocess("--asyncio-mode=strict")
+ result = pytester.runpytest("--asyncio-mode=strict", "--assert=plain")
result.assert_outcomes(warnings=0, passed=1)
@@ -297,5 +274,5 @@ async def test_markers_not_duplicated(request):
"""
)
)
- result = pytester.runpytest_subprocess("--asyncio-mode=auto")
+ result = pytester.runpytest("--asyncio-mode=auto", "--assert=plain")
result.assert_outcomes(warnings=0, passed=1)
diff --git a/tests/markers/test_module_scope.py b/tests/markers/test_module_scope.py
index 7dbdbb7f..a050f503 100644
--- a/tests/markers/test_module_scope.py
+++ b/tests/markers/test_module_scope.py
@@ -5,59 +5,6 @@
from pytest import Pytester
-def test_asyncio_mark_works_on_module_level(pytester: Pytester):
- pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function")
- pytester.makepyfile(
- dedent(
- """\
- import asyncio
-
- import pytest
-
- pytestmark = pytest.mark.asyncio
-
-
- class TestPyTestMark:
- async def test_is_asyncio(self, event_loop, sample_fixture):
- assert asyncio.get_event_loop()
-
- counter = 1
-
- async def inc():
- nonlocal counter
- counter += 1
- await asyncio.sleep(0)
-
- await asyncio.ensure_future(inc())
- assert counter == 2
-
-
- 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)
-
- await asyncio.ensure_future(inc())
- assert counter == 2
-
-
- @pytest.fixture
- def sample_fixture():
- return None
- """
- )
- )
- result = pytester.runpytest_subprocess("--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):
pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function")
pytester.makepyfile(
@@ -89,28 +36,6 @@ async def test_this_runs_in_same_loop(self):
result.assert_outcomes(passed=3)
-def test_raise_when_event_loop_fixture_is_requested_in_addition_to_scoped_loop(
- pytester: Pytester,
-):
- pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function")
- pytester.makepyfile(
- dedent(
- """\
- import asyncio
- import pytest
-
- pytestmark = pytest.mark.asyncio(loop_scope="module")
-
- 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,
):
diff --git a/tests/markers/test_package_scope.py b/tests/markers/test_package_scope.py
index 204238a4..3e41459b 100644
--- a/tests/markers/test_package_scope.py
+++ b/tests/markers/test_package_scope.py
@@ -69,28 +69,6 @@ async def test_subpackage_runs_in_different_loop():
result.assert_outcomes(passed=4)
-def test_raise_when_event_loop_fixture_is_requested_in_addition_to_scoped_loop(
- pytester: Pytester,
-):
- pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function")
- pytester.makepyfile(
- __init__="",
- test_raises=dedent(
- """\
- import asyncio
- import pytest
-
- @pytest.mark.asyncio(loop_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,
):
@@ -361,23 +339,3 @@ async def test_does_not_fail(sets_event_loop_to_none, n):
)
result = pytester.runpytest("--asyncio-mode=strict")
result.assert_outcomes(passed=2)
-
-
-def test_standalone_test_does_not_trigger_warning_about_no_current_event_loop_being_set(
- pytester: Pytester,
-):
- pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function")
- pytester.makepyfile(
- __init__="",
- test_module=dedent(
- """\
- import pytest
-
- @pytest.mark.asyncio(loop_scope="package")
- async def test_anything():
- pass
- """
- ),
- )
- result = pytester.runpytest_subprocess("--asyncio-mode=strict")
- result.assert_outcomes(warnings=0, passed=1)
diff --git a/tests/markers/test_session_scope.py b/tests/markers/test_session_scope.py
index 70e191b2..2d3a4993 100644
--- a/tests/markers/test_session_scope.py
+++ b/tests/markers/test_session_scope.py
@@ -70,28 +70,6 @@ async def test_subpackage_runs_in_same_loop():
result.assert_outcomes(passed=4)
-def test_raise_when_event_loop_fixture_is_requested_in_addition_to_scoped_loop(
- pytester: Pytester,
-):
- pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function")
- pytester.makepyfile(
- __init__="",
- test_raises=dedent(
- """\
- import asyncio
- import pytest
-
- @pytest.mark.asyncio(loop_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,
):
diff --git a/tests/modes/test_strict_mode.py b/tests/modes/test_strict_mode.py
index 52cbb251..d7dc4ac6 100644
--- a/tests/modes/test_strict_mode.py
+++ b/tests/modes/test_strict_mode.py
@@ -2,7 +2,7 @@
from textwrap import dedent
-from pytest import Pytester
+from pytest import Pytester, version_tuple as pytest_version
def test_strict_mode_cmdline(pytester: Pytester):
@@ -95,7 +95,10 @@ async def test_anything():
)
)
result = pytester.runpytest_subprocess("--asyncio-mode=strict", "-W default")
- result.assert_outcomes(skipped=1, warnings=1)
+ if pytest_version >= (8, 4, 0):
+ result.assert_outcomes(failed=1, skipped=0, warnings=0)
+ else:
+ result.assert_outcomes(skipped=1, warnings=1)
result.stdout.fnmatch_lines(["*async def functions are not natively supported*"])
@@ -117,7 +120,11 @@ async def test_anything(any_fixture):
)
)
result = pytester.runpytest_subprocess("--asyncio-mode=strict", "-W default")
- result.assert_outcomes(skipped=1, warnings=2)
+
+ if pytest_version >= (8, 4, 0):
+ result.assert_outcomes(failed=1, skipped=0, warnings=2)
+ else:
+ result.assert_outcomes(skipped=1, warnings=2)
result.stdout.fnmatch_lines(
[
"*async def functions are not natively supported*",
@@ -149,7 +156,10 @@ async def test_anything(any_fixture):
)
)
result = pytester.runpytest_subprocess("--asyncio-mode=strict", "-W default")
- result.assert_outcomes(passed=1, failed=0, skipped=0, warnings=1)
+ if pytest_version >= (8, 4, 0):
+ result.assert_outcomes(passed=1, failed=0, skipped=0, warnings=2)
+ else:
+ result.assert_outcomes(passed=1, failed=0, skipped=0, warnings=1)
result.stdout.fnmatch_lines(
[
"*warnings summary*",
@@ -163,7 +173,7 @@ async def test_anything(any_fixture):
"@pytest.fixture 'any_fixture' in strict mode. "
"You might want to use @pytest_asyncio.fixture or switch to "
"auto mode. "
- "This will become an error in future versions of flake8-asyncio."
+ "This will become an error in future versions of pytest-asyncio."
),
],
)
@@ -193,7 +203,10 @@ async def test_anything(any_fixture):
)
)
result = pytester.runpytest_subprocess("--asyncio-mode=strict", "-W default")
- result.assert_outcomes(passed=1, warnings=1)
+ if pytest_version >= (8, 4, 0):
+ result.assert_outcomes(passed=1, warnings=2)
+ else:
+ result.assert_outcomes(passed=1, warnings=1)
result.stdout.fnmatch_lines(
[
"*warnings summary*",
@@ -207,7 +220,7 @@ async def test_anything(any_fixture):
"@pytest.fixture 'any_fixture' in strict mode. "
"You might want to use @pytest_asyncio.fixture or switch to "
"auto mode. "
- "This will become an error in future versions of flake8-asyncio."
+ "This will become an error in future versions of pytest-asyncio."
),
],
)
diff --git a/tests/test_asyncio_debug.py b/tests/test_asyncio_debug.py
new file mode 100644
index 00000000..b097b63c
--- /dev/null
+++ b/tests/test_asyncio_debug.py
@@ -0,0 +1,216 @@
+from __future__ import annotations
+
+from textwrap import dedent
+
+import pytest
+from pytest import Pytester
+
+
+def test_asyncio_debug_disabled_by_default(pytester: Pytester):
+ pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function")
+ pytester.makepyfile(
+ dedent(
+ """\
+ import asyncio
+ import pytest
+
+ pytest_plugins = "pytest_asyncio"
+
+ @pytest.mark.asyncio
+ async def test_debug_mode_disabled():
+ loop = asyncio.get_running_loop()
+ assert not loop.get_debug()
+ """
+ )
+ )
+ result = pytester.runpytest()
+ result.assert_outcomes(passed=1)
+
+
+def test_asyncio_debug_enabled_via_cli_option(pytester: Pytester):
+ pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function")
+ pytester.makepyfile(
+ dedent(
+ """\
+ import asyncio
+ import pytest
+
+ pytest_plugins = "pytest_asyncio"
+
+ @pytest.mark.asyncio
+ async def test_debug_mode_enabled():
+ loop = asyncio.get_running_loop()
+ assert loop.get_debug()
+ """
+ )
+ )
+ result = pytester.runpytest("--asyncio-debug")
+ result.assert_outcomes(passed=1)
+
+
+@pytest.mark.parametrize("config_value", ("true", "1"))
+def test_asyncio_debug_enabled_via_config_option(pytester: Pytester, config_value: str):
+ pytester.makeini(
+ dedent(
+ f"""\
+ [pytest]
+ asyncio_default_fixture_loop_scope = function
+ asyncio_debug = {config_value}
+ """
+ )
+ )
+ pytester.makepyfile(
+ dedent(
+ """\
+ import asyncio
+ import pytest
+
+ pytest_plugins = "pytest_asyncio"
+
+ @pytest.mark.asyncio
+ async def test_debug_mode_enabled():
+ loop = asyncio.get_running_loop()
+ assert loop.get_debug()
+ """
+ )
+ )
+ result = pytester.runpytest()
+ result.assert_outcomes(passed=1)
+
+
+@pytest.mark.parametrize("config_value", ("false", "0"))
+def test_asyncio_debug_disabled_via_config_option(
+ pytester: Pytester,
+ config_value: str,
+):
+ pytester.makeini(
+ dedent(
+ f"""\
+ [pytest]
+ asyncio_default_fixture_loop_scope = function
+ asyncio_debug = {config_value}
+ """
+ )
+ )
+ pytester.makepyfile(
+ dedent(
+ """\
+ import asyncio
+ import pytest
+
+ pytest_plugins = "pytest_asyncio"
+
+ @pytest.mark.asyncio
+ async def test_debug_mode_disabled():
+ loop = asyncio.get_running_loop()
+ assert not loop.get_debug()
+ """
+ )
+ )
+ result = pytester.runpytest()
+ result.assert_outcomes(passed=1)
+
+
+def test_asyncio_debug_cli_option_overrides_config(pytester: Pytester):
+ pytester.makeini(
+ "[pytest]\nasyncio_default_fixture_loop_scope = function\nasyncio_debug = false"
+ )
+ pytester.makepyfile(
+ dedent(
+ """\
+ import asyncio
+ import pytest
+
+ pytest_plugins = "pytest_asyncio"
+
+ @pytest.mark.asyncio
+ async def test_debug_mode_enabled():
+ loop = asyncio.get_running_loop()
+ assert loop.get_debug()
+ """
+ )
+ )
+ result = pytester.runpytest("--asyncio-debug")
+ result.assert_outcomes(passed=1)
+
+
+@pytest.mark.parametrize("loop_scope", ("function", "module", "session"))
+def test_asyncio_debug_with_different_loop_scopes(pytester: Pytester, loop_scope: str):
+ pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function")
+ pytester.makepyfile(
+ dedent(
+ f"""\
+ import asyncio
+ import pytest
+
+ pytest_plugins = "pytest_asyncio"
+
+ @pytest.mark.asyncio(loop_scope="{loop_scope}")
+ async def test_debug_mode_with_scope():
+ loop = asyncio.get_running_loop()
+ assert loop.get_debug()
+ """
+ )
+ )
+ result = pytester.runpytest("--asyncio-debug")
+ result.assert_outcomes(passed=1)
+
+
+def test_asyncio_debug_with_async_fixtures(pytester: Pytester):
+ pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function")
+ pytester.makepyfile(
+ dedent(
+ """\
+ import asyncio
+ import pytest
+ import pytest_asyncio
+
+ pytest_plugins = "pytest_asyncio"
+
+ @pytest_asyncio.fixture
+ async def async_fixture():
+ loop = asyncio.get_running_loop()
+ assert loop.get_debug()
+ return "fixture_value"
+
+ @pytest.mark.asyncio
+ async def test_debug_mode_with_fixture(async_fixture):
+ loop = asyncio.get_running_loop()
+ assert loop.get_debug()
+ assert async_fixture == "fixture_value"
+ """
+ )
+ )
+ result = pytester.runpytest("--asyncio-debug")
+ result.assert_outcomes(passed=1)
+
+
+def test_asyncio_debug_multiple_test_functions(pytester: Pytester):
+ pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function")
+ pytester.makepyfile(
+ dedent(
+ """\
+ import asyncio
+ import pytest
+
+ pytest_plugins = "pytest_asyncio"
+
+ @pytest.mark.asyncio
+ async def test_debug_first():
+ loop = asyncio.get_running_loop()
+ assert loop.get_debug()
+
+ @pytest.mark.asyncio
+ async def test_debug_second():
+ loop = asyncio.get_running_loop()
+ assert loop.get_debug()
+
+ @pytest.mark.asyncio
+ async def test_debug_third():
+ loop = asyncio.get_running_loop()
+ assert loop.get_debug()
+ """
+ )
+ )
+ result = pytester.runpytest("--asyncio-debug")
+ result.assert_outcomes(passed=3)
diff --git a/tests/test_asyncio_mark.py b/tests/test_asyncio_mark.py
index e22be989..81731adb 100644
--- a/tests/test_asyncio_mark.py
+++ b/tests/test_asyncio_mark.py
@@ -146,3 +146,80 @@ async def test_a():
result.stdout.fnmatch_lines(
["*Tests based on asynchronous generators are not supported*"]
)
+
+
+def test_asyncio_marker_fallbacks_to_configured_default_loop_scope_if_not_set(
+ pytester: Pytester,
+):
+ pytester.makeini(
+ dedent(
+ """\
+ [pytest]
+ asyncio_default_fixture_loop_scope = function
+ asyncio_default_test_loop_scope = session
+ """
+ )
+ )
+
+ pytester.makepyfile(
+ dedent(
+ """\
+ import asyncio
+ import pytest_asyncio
+ import pytest
+
+ loop: asyncio.AbstractEventLoop
+
+ @pytest_asyncio.fixture(loop_scope="session", scope="session")
+ async def session_loop_fixture():
+ global loop
+ loop = asyncio.get_running_loop()
+
+ async def test_a(session_loop_fixture):
+ global loop
+ assert asyncio.get_running_loop() is loop
+ """
+ )
+ )
+
+ result = pytester.runpytest("--asyncio-mode=auto")
+ result.assert_outcomes(passed=1)
+
+
+def test_asyncio_marker_uses_marker_loop_scope_even_if_config_is_set(
+ pytester: Pytester,
+):
+ pytester.makeini(
+ dedent(
+ """\
+ [pytest]
+ asyncio_default_fixture_loop_scope = function
+ asyncio_default_test_loop_scope = module
+ """
+ )
+ )
+
+ pytester.makepyfile(
+ dedent(
+ """\
+ import asyncio
+ import pytest_asyncio
+ import pytest
+
+ loop: asyncio.AbstractEventLoop
+
+ @pytest_asyncio.fixture(loop_scope="session", scope="session")
+ async def session_loop_fixture():
+ global loop
+ loop = asyncio.get_running_loop()
+
+ @pytest.mark.asyncio(loop_scope="session")
+ async def test_a(session_loop_fixture):
+ global loop
+ assert asyncio.get_running_loop() is loop
+ """
+ )
+ )
+
+ result = pytester.runpytest("--asyncio-mode=auto")
+ result.assert_outcomes(passed=1)
diff --git a/tests/test_dependent_fixtures.py b/tests/test_dependent_fixtures.py
deleted file mode 100644
index 2e53700a..00000000
--- a/tests/test_dependent_fixtures.py
+++ /dev/null
@@ -1,16 +0,0 @@
-from __future__ import annotations
-
-import asyncio
-
-import pytest
-
-
-@pytest.mark.asyncio
-async def test_dependent_fixture(dependent_fixture):
- """Test a dependent fixture."""
- await asyncio.sleep(0.1)
-
-
-@pytest.mark.asyncio
-async def test_factory_involving_factories(factory_involving_factories):
- factory_involving_factories()
diff --git a/tests/test_event_loop_fixture.py b/tests/test_event_loop_fixture.py
index 447d15d5..8b9ac634 100644
--- a/tests/test_event_loop_fixture.py
+++ b/tests/test_event_loop_fixture.py
@@ -80,3 +80,64 @@ async def generator_fn():
)
result = pytester.runpytest_subprocess("--asyncio-mode=strict", "-W", "default")
result.assert_outcomes(passed=1, warnings=0)
+
+
+def test_closing_event_loop_in_sync_fixture_teardown_raises_warning(
+ pytester: Pytester,
+):
+ pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function")
+ pytester.makepyfile(
+ dedent(
+ """\
+ import asyncio
+ import pytest
+ import pytest_asyncio
+ pytest_plugins = 'pytest_asyncio'
+
+ @pytest_asyncio.fixture
+ async def _event_loop():
+ return asyncio.get_running_loop()
+
+ @pytest.fixture
+ def close_event_loop(_event_loop):
+ yield
+ # fixture has its own cleanup code
+ _event_loop.close()
+
+ @pytest.mark.asyncio
+ async def test_something(close_event_loop):
+ await asyncio.sleep(0.01)
+ """
+ )
+ )
+ result = pytester.runpytest_subprocess("--asyncio-mode=strict")
+ result.assert_outcomes(passed=1, warnings=1)
+ result.stdout.fnmatch_lines(
+ ["*An exception occurred during teardown of an asyncio.Runner*"]
+ )
+
+
+def test_event_loop_fixture_asyncgen_error(
+ pytester: Pytester,
+):
+ pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function")
+ pytester.makepyfile(
+ dedent(
+ """\
+ import asyncio
+ import pytest
+
+ pytest_plugins = 'pytest_asyncio'
+
+ @pytest.mark.asyncio
+ async def test_something():
+ # mock shutdown_asyncgen failure
+ loop = asyncio.get_running_loop()
+ async def fail():
+ raise RuntimeError("mock error cleaning up...")
+ loop.shutdown_asyncgens = fail
+ """
+ )
+ )
+ result = pytester.runpytest_subprocess("--asyncio-mode=strict", "-W", "default")
+ result.assert_outcomes(passed=1, warnings=1)
diff --git a/tests/test_event_loop_fixture_finalizer.py b/tests/test_event_loop_fixture_finalizer.py
deleted file mode 100644
index 1e378643..00000000
--- a/tests/test_event_loop_fixture_finalizer.py
+++ /dev/null
@@ -1,149 +0,0 @@
-from __future__ import annotations
-
-from textwrap import dedent
-
-from pytest import Pytester
-
-
-def test_event_loop_fixture_finalizer_returns_fresh_loop_after_test(pytester: Pytester):
- pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function")
- pytester.makepyfile(
- dedent(
- """\
- import asyncio
-
- import pytest
-
- loop = asyncio.new_event_loop()
- asyncio.set_event_loop(loop)
-
- @pytest.mark.asyncio
- async def test_1():
- # This async test runs in its own event loop
- global loop
- running_loop = asyncio.get_event_loop_policy().get_event_loop()
- # Make sure this test case received a different loop
- assert running_loop is not loop
-
- def test_2():
- # Code outside of pytest-asyncio should not receive a "used" event loop
- current_loop = asyncio.get_event_loop_policy().get_event_loop()
- assert not current_loop.is_running()
- assert not current_loop.is_closed()
- """
- )
- )
- result = pytester.runpytest("--asyncio-mode=strict")
- result.assert_outcomes(passed=2)
-
-
-def test_event_loop_fixture_finalizer_handles_loop_set_to_none_sync(
- pytester: Pytester,
-):
- pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function")
- pytester.makepyfile(
- dedent(
- """\
- import asyncio
-
- def test_sync(event_loop):
- asyncio.get_event_loop_policy().set_event_loop(None)
- """
- )
- )
- result = pytester.runpytest("--asyncio-mode=strict")
- result.assert_outcomes(passed=1)
-
-
-def test_event_loop_fixture_finalizer_handles_loop_set_to_none_async_without_fixture(
- pytester: Pytester,
-):
- pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function")
- pytester.makepyfile(
- dedent(
- """\
- import asyncio
- import pytest
-
- @pytest.mark.asyncio
- async def test_async_without_explicit_fixture_request():
- asyncio.get_event_loop_policy().set_event_loop(None)
- """
- )
- )
- result = pytester.runpytest("--asyncio-mode=strict")
- result.assert_outcomes(passed=1)
-
-
-def test_event_loop_fixture_finalizer_handles_loop_set_to_none_async_with_fixture(
- pytester: Pytester,
-):
- pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function")
- pytester.makepyfile(
- dedent(
- """\
- import asyncio
- import pytest
-
- @pytest.mark.asyncio
- async def test_async_with_explicit_fixture_request(event_loop):
- asyncio.get_event_loop_policy().set_event_loop(None)
- """
- )
- )
- result = pytester.runpytest_subprocess("--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(
- pytester: Pytester,
-):
- pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function")
- pytester.makepyfile(
- dedent(
- """\
- import asyncio
- import pytest
-
- pytest_plugins = 'pytest_asyncio'
-
- @pytest.fixture
- def event_loop():
- loop = asyncio.get_event_loop_policy().new_event_loop()
- yield loop
-
- @pytest.mark.asyncio
- async def test_ends_with_unclosed_loop():
- pass
- """
- )
- )
- result = pytester.runpytest_subprocess("--asyncio-mode=strict", "-W", "default")
- result.assert_outcomes(passed=1, warnings=2)
- result.stdout.fnmatch_lines("*unclosed event loop*")
-
-
-def test_event_loop_fixture_finalizer_raises_warning_when_test_leaves_loop_unclosed(
- pytester: Pytester,
-):
- pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function")
- pytester.makepyfile(
- dedent(
- """\
- import asyncio
- import pytest
-
- pytest_plugins = 'pytest_asyncio'
-
- @pytest.mark.asyncio
- async def test_ends_with_unclosed_loop():
- asyncio.set_event_loop(asyncio.new_event_loop())
- """
- )
- )
- result = pytester.runpytest_subprocess("--asyncio-mode=strict", "-W", "default")
- result.assert_outcomes(passed=1, warnings=1)
- 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
deleted file mode 100644
index 04859ef7..00000000
--- a/tests/test_event_loop_fixture_override_deprecation.py
+++ /dev/null
@@ -1,113 +0,0 @@
-from __future__ import annotations
-
-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
- """
- )
- )
- result = pytester.runpytest_subprocess("--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*"]
- )
-
-
-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_requested_explicitly(event_loop):
- pass
- """
- )
- )
- result = pytester.runpytest_subprocess("--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*"]
- )
- 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(
- 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_subprocess("--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_subprocess("--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
deleted file mode 100644
index c685ad84..00000000
--- a/tests/test_explicit_event_loop_fixture_request.py
+++ /dev/null
@@ -1,168 +0,0 @@
-from __future__ import annotations
-
-from textwrap import dedent
-
-from pytest import Pytester
-
-
-def test_emit_warning_when_event_loop_is_explicitly_requested_in_coroutine(
- pytester: Pytester,
-):
- pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function")
- pytester.makepyfile(
- dedent(
- """\
- import pytest
-
- @pytest.mark.asyncio
- async def test_coroutine_emits_warning(event_loop):
- pass
- """
- )
- )
- result = pytester.runpytest_subprocess("--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_emit_warning_when_event_loop_is_explicitly_requested_in_coroutine_method(
- pytester: Pytester,
-):
- pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function")
- pytester.makepyfile(
- dedent(
- """\
- import pytest
-
- class TestEmitsWarning:
- @pytest.mark.asyncio
- async def test_coroutine_emits_warning(self, event_loop):
- pass
- """
- )
- )
- result = pytester.runpytest_subprocess("--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_emit_warning_when_event_loop_is_explicitly_requested_in_coroutine_staticmethod(
- pytester: Pytester,
-):
- pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function")
- pytester.makepyfile(
- dedent(
- """\
- import pytest
-
- class TestEmitsWarning:
- @staticmethod
- @pytest.mark.asyncio
- async def test_coroutine_emits_warning(event_loop):
- pass
- """
- )
- )
- result = pytester.runpytest_subprocess("--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_emit_warning_when_event_loop_is_explicitly_requested_in_coroutine_fixture(
- pytester: Pytester,
-):
- pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function")
- 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_subprocess("--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_emit_warning_when_event_loop_is_explicitly_requested_in_async_gen_fixture(
- pytester: Pytester,
-):
- pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function")
- 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_subprocess("--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_does_not_emit_warning_when_event_loop_is_explicitly_requested_in_sync_function(
- pytester: Pytester,
-):
- pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function")
- 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.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function")
- 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_fixture_loop_scopes.py b/tests/test_fixture_loop_scopes.py
index a9ce4b35..95e46818 100644
--- a/tests/test_fixture_loop_scopes.py
+++ b/tests/test_fixture_loop_scopes.py
@@ -136,3 +136,20 @@ async def test_runs_in_fixture_loop(fixture_loop):
)
result = pytester.runpytest_subprocess("--asyncio-mode=strict")
result.assert_outcomes(passed=1)
+
+
+def test_invalid_default_fixture_loop_scope_raises_error(pytester: Pytester):
+ pytester.makeini(
+ """\
+ [pytest]
+ asyncio_default_fixture_loop_scope = invalid_scope
+ """
+ )
+ result = pytester.runpytest()
+ result.stderr.fnmatch_lines(
+ [
+ "ERROR: 'invalid_scope' is not a valid "
+ "asyncio_default_fixture_loop_scope. Valid scopes are: "
+ "function, class, module, package, session."
+ ]
+ )
diff --git a/tests/test_multiloop.py b/tests/test_multiloop.py
deleted file mode 100644
index e6c852b9..00000000
--- a/tests/test_multiloop.py
+++ /dev/null
@@ -1,72 +0,0 @@
-from __future__ import annotations
-
-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, warnings=2)
diff --git a/tests/test_package.py b/tests/test_package.py
new file mode 100644
index 00000000..6f4f720d
--- /dev/null
+++ b/tests/test_package.py
@@ -0,0 +1,5 @@
+import pytest_asyncio
+
+
+def test_package_exposes_version():
+ assert pytest_asyncio.__version__
diff --git a/tests/test_set_event_loop.py b/tests/test_set_event_loop.py
new file mode 100644
index 00000000..20037b48
--- /dev/null
+++ b/tests/test_set_event_loop.py
@@ -0,0 +1,371 @@
+from __future__ import annotations
+
+import sys
+from textwrap import dedent
+
+import pytest
+from pytest import Pytester
+
+
+@pytest.mark.parametrize(
+ "test_loop_scope",
+ ("function", "module", "package", "session"),
+)
+@pytest.mark.parametrize(
+ "loop_breaking_action",
+ [
+ "asyncio.set_event_loop(None)",
+ "asyncio.run(asyncio.sleep(0))",
+ pytest.param(
+ "with asyncio.Runner(): pass",
+ marks=pytest.mark.skipif(
+ sys.version_info < (3, 11),
+ reason="asyncio.Runner requires Python 3.11+",
+ ),
+ ),
+ ],
+)
+def test_set_event_loop_none(
+ pytester: Pytester,
+ test_loop_scope: str,
+ loop_breaking_action: str,
+):
+ pytester.makeini(
+ dedent(
+ f"""\
+ [pytest]
+ asyncio_default_test_loop_scope = {test_loop_scope}
+ asyncio_default_fixture_loop_scope = function
+ """
+ )
+ )
+ pytester.makepyfile(
+ dedent(
+ f"""\
+ import asyncio
+ import pytest
+
+ pytest_plugins = "pytest_asyncio"
+
+ @pytest.mark.asyncio
+ async def test_before():
+ pass
+
+ def test_set_event_loop_none():
+ {loop_breaking_action}
+
+ @pytest.mark.asyncio
+ async def test_after():
+ pass
+ """
+ )
+ )
+ result = pytester.runpytest_subprocess()
+ result.assert_outcomes(passed=3)
+
+
+@pytest.mark.parametrize(
+ "loop_breaking_action",
+ [
+ "asyncio.set_event_loop(None)",
+ "asyncio.run(asyncio.sleep(0))",
+ pytest.param(
+ "with asyncio.Runner(): pass",
+ marks=pytest.mark.skipif(
+ sys.version_info < (3, 11),
+ reason="asyncio.Runner requires Python 3.11+",
+ ),
+ ),
+ ],
+)
+def test_set_event_loop_none_class(pytester: Pytester, loop_breaking_action: str):
+ pytester.makeini(
+ dedent(
+ """\
+ [pytest]
+ asyncio_default_test_loop_scope = class
+ asyncio_default_fixture_loop_scope = function
+ """
+ )
+ )
+ pytester.makepyfile(
+ dedent(
+ f"""\
+ import asyncio
+ import pytest
+
+ pytest_plugins = "pytest_asyncio"
+
+
+ class TestClass:
+ @pytest.mark.asyncio
+ async def test_before(self):
+ pass
+
+ def test_set_event_loop_none(self):
+ {loop_breaking_action}
+
+ @pytest.mark.asyncio
+ async def test_after(self):
+ pass
+ """
+ )
+ )
+ result = pytester.runpytest_subprocess()
+ result.assert_outcomes(passed=3)
+
+
+@pytest.mark.parametrize("test_loop_scope", ("module", "package", "session"))
+@pytest.mark.parametrize(
+ "loop_breaking_action",
+ [
+ "asyncio.set_event_loop(None)",
+ "asyncio.run(asyncio.sleep(0))",
+ pytest.param(
+ "with asyncio.Runner(): pass",
+ marks=pytest.mark.skipif(
+ sys.version_info < (3, 11),
+ reason="asyncio.Runner requires Python 3.11+",
+ ),
+ ),
+ ],
+)
+def test_original_shared_loop_is_reinstated_not_fresh_loop(
+ pytester: Pytester,
+ test_loop_scope: str,
+ loop_breaking_action: str,
+):
+ pytester.makeini(
+ dedent(
+ f"""\
+ [pytest]
+ asyncio_default_test_loop_scope = {test_loop_scope}
+ asyncio_default_fixture_loop_scope = function
+ """
+ )
+ )
+ pytester.makepyfile(
+ dedent(
+ f"""\
+ import asyncio
+ import pytest
+
+ pytest_plugins = "pytest_asyncio"
+
+ original_shared_loop: asyncio.AbstractEventLoop = None
+
+ @pytest.mark.asyncio
+ async def test_store_original_shared_loop():
+ global original_shared_loop
+ original_shared_loop = asyncio.get_running_loop()
+ original_shared_loop._custom_marker = "original_loop_marker"
+
+ def test_unset_event_loop():
+ {loop_breaking_action}
+
+ @pytest.mark.asyncio
+ async def test_verify_original_loop_reinstated():
+ global original_shared_loop
+ current_loop = asyncio.get_running_loop()
+ assert current_loop is original_shared_loop
+ assert hasattr(current_loop, '_custom_marker')
+ assert current_loop._custom_marker == "original_loop_marker"
+ """
+ )
+ )
+ result = pytester.runpytest_subprocess("--asyncio-mode=strict")
+ result.assert_outcomes(passed=3)
+
+
+@pytest.mark.parametrize("test_loop_scope", ("module", "package", "session"))
+@pytest.mark.parametrize(
+ "loop_breaking_action",
+ [
+ "asyncio.set_event_loop(None)",
+ "asyncio.run(asyncio.sleep(0))",
+ pytest.param(
+ "with asyncio.Runner(): pass",
+ marks=pytest.mark.skipif(
+ sys.version_info < (3, 11),
+ reason="asyncio.Runner requires Python 3.11+",
+ ),
+ ),
+ ],
+)
+def test_shared_loop_with_fixture_preservation(
+ pytester: Pytester,
+ test_loop_scope: str,
+ loop_breaking_action: str,
+):
+ pytester.makeini(
+ dedent(
+ f"""\
+ [pytest]
+ asyncio_default_test_loop_scope = {test_loop_scope}
+ asyncio_default_fixture_loop_scope = {test_loop_scope}
+ """
+ )
+ )
+ pytester.makepyfile(
+ dedent(
+ f"""\
+ import asyncio
+ import pytest
+ import pytest_asyncio
+
+ pytest_plugins = "pytest_asyncio"
+
+ fixture_loop: asyncio.AbstractEventLoop = None
+ long_running_task = None
+
+ @pytest_asyncio.fixture
+ async def webserver():
+ global fixture_loop, long_running_task
+ fixture_loop = asyncio.get_running_loop()
+
+ async def background_task():
+ while True:
+ await asyncio.sleep(1)
+
+ long_running_task = asyncio.create_task(background_task())
+ yield
+ long_running_task.cancel()
+
+
+ @pytest.mark.asyncio
+ async def test_before(webserver):
+ global fixture_loop, long_running_task
+ assert asyncio.get_running_loop() is fixture_loop
+ assert not long_running_task.done()
+
+
+ def test_set_event_loop_none():
+ {loop_breaking_action}
+
+
+ @pytest.mark.asyncio
+ async def test_after(webserver):
+ global fixture_loop, long_running_task
+ current_loop = asyncio.get_running_loop()
+ assert current_loop is fixture_loop
+ assert not long_running_task.done()
+ """
+ )
+ )
+ result = pytester.runpytest_subprocess("--asyncio-mode=strict")
+ result.assert_outcomes(passed=3)
+
+
+@pytest.mark.parametrize(
+ "first_scope,second_scope",
+ [
+ ("module", "session"),
+ ("session", "module"),
+ ("package", "session"),
+ ("session", "package"),
+ ("package", "module"),
+ ("module", "package"),
+ ],
+)
+@pytest.mark.parametrize(
+ "loop_breaking_action",
+ [
+ "asyncio.set_event_loop(None)",
+ "asyncio.run(asyncio.sleep(0))",
+ pytest.param(
+ "with asyncio.Runner(): pass",
+ marks=pytest.mark.skipif(
+ sys.version_info < (3, 11),
+ reason="asyncio.Runner requires Python 3.11+",
+ ),
+ ),
+ ],
+)
+def test_shared_loop_with_multiple_fixtures_preservation(
+ pytester: Pytester,
+ first_scope: str,
+ second_scope: str,
+ loop_breaking_action: str,
+):
+ pytester.makeini(
+ dedent(
+ """\
+ [pytest]
+ asyncio_default_test_loop_scope = session
+ asyncio_default_fixture_loop_scope = session
+ """
+ )
+ )
+ pytester.makepyfile(
+ dedent(
+ f"""\
+ import asyncio
+ import pytest
+ import pytest_asyncio
+
+ pytest_plugins = "pytest_asyncio"
+
+ first_fixture_loop: asyncio.AbstractEventLoop = None
+ second_fixture_loop: asyncio.AbstractEventLoop = None
+ first_long_running_task = None
+ second_long_running_task = None
+
+ @pytest_asyncio.fixture(scope="{first_scope}", loop_scope="{first_scope}")
+ async def first_webserver():
+ global first_fixture_loop, first_long_running_task
+ first_fixture_loop = asyncio.get_running_loop()
+
+ async def background_task():
+ while True:
+ await asyncio.sleep(0.1)
+
+ first_long_running_task = asyncio.create_task(background_task())
+ yield
+ first_long_running_task.cancel()
+
+ @pytest_asyncio.fixture(scope="{second_scope}", loop_scope="{second_scope}")
+ async def second_webserver():
+ global second_fixture_loop, second_long_running_task
+ second_fixture_loop = asyncio.get_running_loop()
+
+ async def background_task():
+ while True:
+ await asyncio.sleep(0.1)
+
+ second_long_running_task = asyncio.create_task(background_task())
+ yield
+ second_long_running_task.cancel()
+
+ @pytest.mark.asyncio(loop_scope="{first_scope}")
+ async def test_before_first(first_webserver):
+ global first_fixture_loop, first_long_running_task
+ assert asyncio.get_running_loop() is first_fixture_loop
+ assert not first_long_running_task.done()
+
+ @pytest.mark.asyncio(loop_scope="{second_scope}")
+ async def test_before_second(second_webserver):
+ global second_fixture_loop, second_long_running_task
+ assert asyncio.get_running_loop() is second_fixture_loop
+ assert not second_long_running_task.done()
+
+ def test_set_event_loop_none():
+ {loop_breaking_action}
+
+ @pytest.mark.asyncio(loop_scope="{first_scope}")
+ async def test_after_first(first_webserver):
+ global first_fixture_loop, first_long_running_task
+ current_loop = asyncio.get_running_loop()
+ assert current_loop is first_fixture_loop
+ assert not first_long_running_task.done()
+
+ @pytest.mark.asyncio(loop_scope="{second_scope}")
+ async def test_after_second(second_webserver):
+ global second_fixture_loop, second_long_running_task
+ current_loop = asyncio.get_running_loop()
+ assert current_loop is second_fixture_loop
+ assert not second_long_running_task.done()
+ """
+ )
+ )
+ result = pytester.runpytest_subprocess("--asyncio-mode=strict")
+ result.assert_outcomes(passed=5)
diff --git a/tests/test_simple.py b/tests/test_simple.py
index b8a34fb2..f92ef4e7 100644
--- a/tests/test_simple.py
+++ b/tests/test_simple.py
@@ -14,13 +14,6 @@ async def async_coro():
return "ok"
-def test_event_loop_fixture(event_loop):
- """Test the injection of the event_loop fixture."""
- assert event_loop
- ret = event_loop.run_until_complete(async_coro())
- assert ret == "ok"
-
-
@pytest.mark.asyncio
async def test_asyncio_marker():
"""Test the asyncio pytest marker."""
diff --git a/tests/test_subprocess.py b/tests/test_subprocess.py
index c32ba964..438f49f2 100644
--- a/tests/test_subprocess.py
+++ b/tests/test_subprocess.py
@@ -7,16 +7,6 @@
import pytest
-if sys.platform == "win32":
- # The default asyncio event loop implementation on Windows does not
- # support subprocesses. Subprocesses are available for Windows if a
- # ProactorEventLoop is used.
- @pytest.fixture()
- def event_loop():
- loop = asyncio.ProactorEventLoop()
- yield loop
- loop.close()
-
@pytest.mark.asyncio
async def test_subprocess():
diff --git a/tests/test_task_cleanup.py b/tests/test_task_cleanup.py
new file mode 100644
index 00000000..eb1f7d3c
--- /dev/null
+++ b/tests/test_task_cleanup.py
@@ -0,0 +1,30 @@
+from __future__ import annotations
+
+from textwrap import dedent
+
+from pytest import Pytester
+
+
+def test_task_is_cancelled_when_abandoned_by_test(pytester: Pytester):
+ pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function")
+ pytester.makepyfile(
+ dedent(
+ """\
+ import asyncio
+ import pytest
+
+ @pytest.mark.asyncio
+ async def test_create_task():
+ async def coroutine():
+ try:
+ while True:
+ await asyncio.sleep(0)
+ finally:
+ raise RuntimeError("The task should be cancelled at this point.")
+
+ asyncio.create_task(coroutine())
+ """
+ )
+ )
+ result = pytester.runpytest("--asyncio-mode=strict")
+ result.assert_outcomes(passed=1)
diff --git a/tox.ini b/tox.ini
index 9a0cf93b..b6121581 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,26 +1,41 @@
[tox]
-minversion = 4.9.0
-envlist = py39, py310, py311, py312, py313, pytest-min, docs
+minversion = 4.28.0
+envlist = build, py39, py310, py311, py312, py313, py314, pytest-min, docs, pyright
isolated_build = true
passenv =
CI
+[pkgenv]
+constraints = constraints.txt
+
[testenv]
+package = external
extras = testing
-install_command = python -m pip install \
- --requirement dependencies/default/requirements.txt \
- --constraint dependencies/default/constraints.txt \
- {opts} {packages}
+constraints = constraints.txt
commands = make test
allowlist_externals =
make
+[testenv:.pkg_external]
+deps = build
+package_glob = {toxinidir}{/}dist{/}*.whl
+commands =
+ python -c 'import shutil; shutil.rmtree("{toxinidir}{/}dist", ignore_errors=True)'
+ pyproject-build --outdir {toxinidir}{/}dist .
+
+[testenv:build]
+description = Check distribution files
+deps =
+ check-wheel-contents
+ twine
+commands =
+ check-wheel-contents {toxinidir}{/}dist
+ twine check {toxinidir}{/}dist{/}*
+
[testenv:pytest-min]
extras = testing
-install_command = python -m pip install \
- --requirement dependencies/pytest-min/requirements.txt \
- --constraint dependencies/pytest-min/constraints.txt \
- {opts} {packages}
+constraints = dependencies/pytest-min/constraints.txt
+deps = -r dependencies/pytest-min/requirements.txt
commands = make test
allowlist_externals =
make
@@ -29,9 +44,6 @@ allowlist_externals =
allowlist_externals =
git
extras = docs
-deps =
- --requirement dependencies/docs/requirements.txt
- --constraint dependencies/docs/constraints.txt
change_dir = docs
description = Build The Docs with {basepython}
commands =
@@ -69,11 +81,19 @@ passenv =
SSH_AUTH_SOCK
skip_install = false
+[testenv:pyright]
+deps =
+ pyright[nodejs]
+ pytest
+commands = pyright pytest_asyncio/
+skip_install = true
+
[gh-actions]
python =
- 3.9: py39, pytest-min
+ 3.9: py39, pytest-min, build
3.10: py310
3.11: py311
3.12: py312
- 3.13: py313
+ 3.13: py313, pyright
+ 3.14-dev: py314
pypy3: pypy3