diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index 9a663d8e..3d725a50 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -7,6 +7,12 @@ updates:
interval: weekly
open-pull-requests-limit: 10
target-branch: main
+- package-ecosystem: pip
+ directory: /dependencies/docs
+ schedule:
+ interval: weekly
+ open-pull-requests-limit: 10
+ target-branch: main
- package-ecosystem: github-actions
directory: /
schedule:
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 2b9e03af..6aeaaa83 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -7,10 +7,11 @@ on:
tags: [v*]
pull_request:
branches: [main]
+ merge_group:
workflow_dispatch:
env:
- PYTHON_LATEST: 3.11
+ PYTHON_LATEST: 3.12
jobs:
lint:
@@ -20,10 +21,10 @@ jobs:
version: ${{ steps.version.outputs.version }}
prerelease: ${{ steps.version.outputs.prerelease }}
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v4
with:
fetch-depth: 0
- - uses: actions/setup-python@v4
+ - uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_LATEST }}
- name: Install GitHub matcher for ActionLint checker
@@ -60,15 +61,15 @@ jobs:
strategy:
matrix:
- python-version: ['3.7', '3.8', '3.9', '3.10', '3.11', 3.12-dev]
+ python-version: ['3.8', '3.9', '3.10', '3.11', '3.12']
steps:
- - uses: actions/checkout@v3
- - uses: actions/setup-python@v4
+ - uses: actions/checkout@v4
+ - uses: actions/setup-python@v5
if: "!endsWith(matrix.python-version, '-dev')"
with:
python-version: ${{ matrix.python-version }}
- - uses: deadsnakes/action@v3.0.1
+ - uses: deadsnakes/action@v3.1.0
if: endsWith(matrix.python-version, '-dev')
with:
python-version: ${{ matrix.python-version }}
@@ -98,8 +99,8 @@ jobs:
uses: re-actors/alls-green@release/v1
with:
jobs: ${{ toJSON(needs) }}
- - uses: actions/checkout@v3
- - uses: actions/setup-python@v4
+ - uses: actions/checkout@v4
+ - uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_LATEST }}
- name: Install Coverage.py
@@ -132,7 +133,7 @@ jobs:
run: |
sudo apt-get install -y pandoc
- name: Checkout
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
- name: Download distributions
uses: actions/download-artifact@v3
with:
@@ -145,9 +146,9 @@ jobs:
run: |
pandoc -s -o README.md README.rst
- name: PyPI upload
- uses: pypa/gh-action-pypi-publish@v1.8.8
+ uses: pypa/gh-action-pypi-publish@v1.8.11
with:
- packages_dir: dist
+ packages-dir: dist
password: ${{ secrets.PYPI_API_TOKEN }}
- name: GitHub Release
uses: ncipollo/release-action@v1
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index fc81f2f5..4e5d2f8e 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -42,7 +42,7 @@ repos:
- id: mypy
exclude: ^(docs|tests)/.*
- repo: https://github.com/pycqa/flake8
- rev: 5.0.4
+ rev: 6.1.0
hooks:
- id: flake8
language_version: python3
diff --git a/.readthedocs.yaml b/.readthedocs.yaml
new file mode 100644
index 00000000..d825e855
--- /dev/null
+++ b/.readthedocs.yaml
@@ -0,0 +1,18 @@
+---
+# Read the Docs configuration file for Sphinx projects
+# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
+
+version: 2
+build:
+ os: ubuntu-22.04
+ tools:
+ python: '3.12'
+
+sphinx:
+ configuration: docs/source/conf.py
+ fail_on_warning: true
+
+python:
+ install:
+ - requirements: dependencies/default/constraints.txt
+ - requirements: dependencies/docs/constraints.txt
diff --git a/Makefile b/Makefile
index 8bc58c49..e1ef5d27 100644
--- a/Makefile
+++ b/Makefile
@@ -21,7 +21,7 @@ clean-test: ## remove test and coverage artifacts
rm -fr htmlcov/
test:
- coverage run --parallel-mode --omit */_version.py -m pytest tests
+ coverage run --parallel-mode --omit */_version.py -m pytest
install:
pip install -U pre-commit
diff --git a/README.rst b/README.rst
index 0682b744..e056a880 100644
--- a/README.rst
+++ b/README.rst
@@ -10,8 +10,9 @@ pytest-asyncio
.. image:: https://img.shields.io/pypi/pyversions/pytest-asyncio.svg
:target: https://github.com/pytest-dev/pytest-asyncio
:alt: Supported Python versions
-.. image:: https://img.shields.io/badge/code%20style-black-000000.svg
- :target: https://github.com/ambv/black
+.. image:: https://img.shields.io/badge/Matrix-%23pytest--asyncio-brightgreen
+ :alt: Matrix chat room: #pytest-asyncio
+ :target: https://matrix.to/#/#pytest-asyncio:matrix.org
`pytest-asyncio `_ is a `pytest `_ plugin. It facilitates testing of code that uses the `asyncio `_ library.
diff --git a/dependencies/default/constraints.txt b/dependencies/default/constraints.txt
index 04999c70..56be8787 100644
--- a/dependencies/default/constraints.txt
+++ b/dependencies/default/constraints.txt
@@ -1,24 +1,11 @@
-async-generator==1.10
attrs==23.1.0
-coverage==7.2.7
-exceptiongroup==1.1.2
-flaky==3.7.0
-hypothesis==6.79.4
-idna==3.4
-importlib-metadata==6.7.0
+coverage==7.3.4
+exceptiongroup==1.2.0
+hypothesis==6.92.1
iniconfig==2.0.0
-mypy==1.4.1
-mypy-extensions==1.0.0
-outcome==1.2.0
-packaging==23.1
-pluggy==1.2.0
-pyparsing==3.1.0
-pytest==7.4.0
-pytest-trio==0.8.0
-sniffio==1.3.0
+packaging==23.2
+pluggy==1.3.0
+pytest==7.4.3
sortedcontainers==2.4.0
tomli==2.0.1
-trio==0.22.1
-typed-ast==1.5.5
-typing_extensions==4.7.1
-zipp==3.15.0
+typing_extensions==4.9.0
diff --git a/dependencies/default/requirements.txt b/dependencies/default/requirements.txt
index a0009a85..0828607f 100644
--- a/dependencies/default/requirements.txt
+++ b/dependencies/default/requirements.txt
@@ -1,4 +1,3 @@
# Always adjust install_requires in setup.cfg and pytest-min-requirements.txt
# when changing runtime dependencies
pytest >= 7.0.0
-typing-extensions >= 3.7.2; python_version < "3.8"
diff --git a/dependencies/docs/constraints.txt b/dependencies/docs/constraints.txt
new file mode 100644
index 00000000..c8938488
--- /dev/null
+++ b/dependencies/docs/constraints.txt
@@ -0,0 +1,23 @@
+alabaster==0.7.13
+Babel==2.14.0
+certifi==2023.11.17
+charset-normalizer==3.3.2
+docutils==0.18.1
+idna==3.6
+imagesize==1.4.1
+Jinja2==3.1.2
+MarkupSafe==2.1.3
+packaging==23.2
+Pygments==2.17.2
+requests==2.31.0
+snowballstemmer==2.2.0
+Sphinx==7.2.6
+sphinx-rtd-theme==2.0.0
+sphinxcontrib-applehelp==1.0.7
+sphinxcontrib-devhelp==1.0.5
+sphinxcontrib-htmlhelp==2.0.4
+sphinxcontrib-jquery==4.1
+sphinxcontrib-jsmath==1.0.1
+sphinxcontrib-qthelp==1.0.6
+sphinxcontrib-serializinghtml==1.1.9
+urllib3==2.1.0
diff --git a/dependencies/docs/requirements.txt b/dependencies/docs/requirements.txt
new file mode 100644
index 00000000..1bfd7f50
--- /dev/null
+++ b/dependencies/docs/requirements.txt
@@ -0,0 +1,2 @@
+sphinx >= 5.3
+sphinx-rtd-theme >= 1.0
diff --git a/dependencies/pytest-min/constraints.txt b/dependencies/pytest-min/constraints.txt
index 1f82dbaf..65e3addb 100644
--- a/dependencies/pytest-min/constraints.txt
+++ b/dependencies/pytest-min/constraints.txt
@@ -1,22 +1,22 @@
-argcomplete==2.0.0
-attrs==22.1.0
-certifi==2022.9.24
-charset-normalizer==2.1.1
-elementpath==3.0.2
-exceptiongroup==1.0.0rc9
-hypothesis==6.56.3
+argcomplete==3.1.2
+attrs==23.1.0
+certifi==2023.7.22
+charset-normalizer==3.3.1
+coverage==7.3.2
+elementpath==4.1.5
+exceptiongroup==1.1.3
+hypothesis==6.88.3
idna==3.4
-iniconfig==1.1.1
-mock==4.0.3
+iniconfig==2.0.0
+mock==5.1.0
nose==1.3.7
-packaging==21.3
-pluggy==1.0.0
+packaging==23.2
+pluggy==1.3.0
py==1.11.0
-Pygments==2.13.0
-pyparsing==3.0.9
+Pygments==2.16.1
pytest==7.0.0
-requests==2.28.1
+requests==2.31.0
sortedcontainers==2.4.0
tomli==2.0.1
-urllib3==1.26.12
-xmlschema==2.1.1
+urllib3==2.0.7
+xmlschema==2.5.0
diff --git a/dependencies/pytest-min/requirements.txt b/dependencies/pytest-min/requirements.txt
index 4152d2f8..9fb33e96 100644
--- a/dependencies/pytest-min/requirements.txt
+++ b/dependencies/pytest-min/requirements.txt
@@ -1,4 +1,3 @@
# Always adjust install_requires in setup.cfg and requirements.txt
# when changing minimum version dependencies
pytest[testing] == 7.0.0
-typing-extensions >= 3.7.2; python_version < "3.8"
diff --git a/docs/source/concepts.rst b/docs/source/concepts.rst
index eb08bae6..710c5365 100644
--- a/docs/source/concepts.rst
+++ b/docs/source/concepts.rst
@@ -4,14 +4,43 @@ Concepts
asyncio event loops
===================
-pytest-asyncio runs each test item in its own asyncio event loop. The loop can be accessed via the ``event_loop`` fixture, which is automatically requested by all async tests.
+In order to understand how pytest-asyncio works, it helps to understand how pytest collectors work.
+If you already know about pytest collectors, please :ref:`skip ahead `.
+Otherwise, continue reading.
+Let's assume we have a test suite with a file named *test_all_the_things.py* holding a single test, async or not:
-.. code-block:: python
+.. include:: concepts_function_scope_example.py
+ :code: python
- async def test_provided_loop_is_running_loop(event_loop):
- assert event_loop is asyncio.get_running_loop()
+The file *test_all_the_things.py* is a Python module with a Python test function.
+When we run pytest, the test runner descends into Python packages, modules, and classes, in order to find all tests, regardless whether the tests will run or not.
+This process is referred to as *test collection* by pytest.
+In our particular example, pytest will find our test module and the test function.
+We can visualize the collection result by running ``pytest --collect-only``::
-You can think of `event_loop` as an autouse fixture for async tests.
+
+
+
+The example illustrates that the code of our test suite is hierarchical.
+Pytest uses so called *collectors* for each level of the hierarchy.
+Our contrived example test suite uses the *Module* and *Function* collectors, but real world test code may contain additional hierarchy levels via the *Package* or *Class* collectors.
+There's also a special *Session* collector at the root of the hierarchy.
+You may notice that the individual levels resemble the possible `scopes of a pytest fixture. `__
+
+.. _pytest-asyncio-event-loops:
+
+Pytest-asyncio provides one asyncio event loop for each pytest collector.
+By default, each test runs in the event loop provided by the *Function* collector, i.e. tests use the loop with the narrowest scope.
+This gives the highest level of isolation between tests.
+If two or more tests share a common ancestor collector, the tests can be configured to run in their ancestor's loop by passing the appropriate *scope* keyword argument to the *asyncio* mark.
+For example, the following two tests use the asyncio event loop provided by the *Module* collector:
+
+.. include:: concepts_module_scope_example.py
+ :code: python
+
+It's highly recommended for neighboring tests to use the same event loop scope.
+For example, all tests in a class or module should use the same scope.
+Assigning neighboring tests to different event loop scopes is discouraged as it can make test code hard to follow.
Test discovery modes
====================
diff --git a/docs/source/concepts_function_scope_example.py b/docs/source/concepts_function_scope_example.py
new file mode 100644
index 00000000..1506ecf7
--- /dev/null
+++ b/docs/source/concepts_function_scope_example.py
@@ -0,0 +1,8 @@
+import asyncio
+
+import pytest
+
+
+@pytest.mark.asyncio
+async def test_runs_in_a_loop():
+ assert asyncio.get_running_loop()
diff --git a/docs/source/concepts_module_scope_example.py b/docs/source/concepts_module_scope_example.py
new file mode 100644
index 00000000..66972888
--- /dev/null
+++ b/docs/source/concepts_module_scope_example.py
@@ -0,0 +1,17 @@
+import asyncio
+
+import pytest
+
+loop: asyncio.AbstractEventLoop
+
+
+@pytest.mark.asyncio(scope="module")
+async def test_remember_loop():
+ global loop
+ loop = asyncio.get_running_loop()
+
+
+@pytest.mark.asyncio(scope="module")
+async def test_runs_in_a_loop():
+ global loop
+ assert asyncio.get_running_loop() is loop
diff --git a/docs/source/conf.py b/docs/source/conf.py
index b61a6679..4bb6535d 100644
--- a/docs/source/conf.py
+++ b/docs/source/conf.py
@@ -7,9 +7,9 @@
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
project = "pytest-asyncio"
-copyright = "2022, pytest-asyncio contributors"
+copyright = "2023, pytest-asyncio contributors"
author = "Tin Tvrtković"
-release = "v0.20.1"
+release = "v0.23.0"
# -- General configuration ---------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
@@ -24,4 +24,4 @@
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
html_theme = "sphinx_rtd_theme"
-html_static_path = ["_static"]
+html_static_path = []
diff --git a/docs/source/how-to-guides/class_scoped_loop_example.py b/docs/source/how-to-guides/class_scoped_loop_example.py
new file mode 100644
index 00000000..5419a7ab
--- /dev/null
+++ b/docs/source/how-to-guides/class_scoped_loop_example.py
@@ -0,0 +1,14 @@
+import asyncio
+
+import pytest
+
+
+@pytest.mark.asyncio(scope="class")
+class TestInOneEventLoopPerClass:
+ loop: asyncio.AbstractEventLoop
+
+ async def test_remember_loop(self):
+ TestInOneEventLoopPerClass.loop = asyncio.get_running_loop()
+
+ async def test_assert_same_loop(self):
+ assert asyncio.get_running_loop() is TestInOneEventLoopPerClass.loop
diff --git a/docs/source/how-to-guides/index.rst b/docs/source/how-to-guides/index.rst
new file mode 100644
index 00000000..a61ead50
--- /dev/null
+++ b/docs/source/how-to-guides/index.rst
@@ -0,0 +1,16 @@
+=============
+How-To Guides
+=============
+
+.. toctree::
+ :hidden:
+
+ run_class_tests_in_same_loop
+ run_module_tests_in_same_loop
+ run_package_tests_in_same_loop
+ run_session_tests_in_same_loop
+ multiple_loops
+ uvloop
+ test_item_is_async
+
+This section of the documentation provides code snippets and recipes to accomplish specific tasks with pytest-asyncio.
diff --git a/docs/source/how-to-guides/module_scoped_loop_example.py b/docs/source/how-to-guides/module_scoped_loop_example.py
new file mode 100644
index 00000000..b4ef778c
--- /dev/null
+++ b/docs/source/how-to-guides/module_scoped_loop_example.py
@@ -0,0 +1,17 @@
+import asyncio
+
+import pytest
+
+pytestmark = pytest.mark.asyncio(scope="module")
+
+loop: asyncio.AbstractEventLoop
+
+
+async def test_remember_loop():
+ global loop
+ loop = asyncio.get_running_loop()
+
+
+async def test_assert_same_loop():
+ global loop
+ assert asyncio.get_running_loop() is loop
diff --git a/docs/source/how-to-guides/multiple_loops.rst b/docs/source/how-to-guides/multiple_loops.rst
new file mode 100644
index 00000000..3453c49f
--- /dev/null
+++ b/docs/source/how-to-guides/multiple_loops.rst
@@ -0,0 +1,10 @@
+======================================
+How to test with different event loops
+======================================
+
+Parametrizing the *event_loop_policy* fixture parametrizes all async tests. The following example causes all async tests to run multiple times, once for each event loop in the fixture parameters:
+
+.. include:: multiple_loops_example.py
+ :code: python
+
+You may choose to limit the scope of the fixture to *package,* *module,* or *class,* if you only want a subset of your tests to run with different event loops.
diff --git a/docs/source/how-to-guides/multiple_loops_example.py b/docs/source/how-to-guides/multiple_loops_example.py
new file mode 100644
index 00000000..a4c7a01c
--- /dev/null
+++ b/docs/source/how-to-guides/multiple_loops_example.py
@@ -0,0 +1,24 @@
+import asyncio
+from asyncio import DefaultEventLoopPolicy
+
+import pytest
+
+
+class CustomEventLoopPolicy(DefaultEventLoopPolicy):
+ pass
+
+
+@pytest.fixture(
+ scope="session",
+ params=(
+ CustomEventLoopPolicy(),
+ CustomEventLoopPolicy(),
+ ),
+)
+def event_loop_policy(request):
+ return request.param
+
+
+@pytest.mark.asyncio
+async def test_uses_custom_event_loop_policy():
+ assert isinstance(asyncio.get_event_loop_policy(), CustomEventLoopPolicy)
diff --git a/docs/source/how-to-guides/package_scoped_loop_example.py b/docs/source/how-to-guides/package_scoped_loop_example.py
new file mode 100644
index 00000000..f48c33f1
--- /dev/null
+++ b/docs/source/how-to-guides/package_scoped_loop_example.py
@@ -0,0 +1,3 @@
+import pytest
+
+pytestmark = pytest.mark.asyncio(scope="package")
diff --git a/docs/source/how-to-guides/run_class_tests_in_same_loop.rst b/docs/source/how-to-guides/run_class_tests_in_same_loop.rst
new file mode 100644
index 00000000..a265899c
--- /dev/null
+++ b/docs/source/how-to-guides/run_class_tests_in_same_loop.rst
@@ -0,0 +1,8 @@
+======================================================
+How to run all tests in a class in the same event loop
+======================================================
+All tests can be run inside the same event loop by marking them with ``pytest.mark.asyncio(scope="class")``.
+This is easily achieved by using the *asyncio* marker as a class decorator.
+
+.. include:: class_scoped_loop_example.py
+ :code: python
diff --git a/docs/source/how-to-guides/run_module_tests_in_same_loop.rst b/docs/source/how-to-guides/run_module_tests_in_same_loop.rst
new file mode 100644
index 00000000..e07eca2e
--- /dev/null
+++ b/docs/source/how-to-guides/run_module_tests_in_same_loop.rst
@@ -0,0 +1,8 @@
+=======================================================
+How to run all tests in a module in the same event loop
+=======================================================
+All tests can be run inside the same event loop by marking them with ``pytest.mark.asyncio(scope="module")``.
+This is easily achieved by adding a `pytestmark` statement to your module.
+
+.. include:: module_scoped_loop_example.py
+ :code: python
diff --git a/docs/source/how-to-guides/run_package_tests_in_same_loop.rst b/docs/source/how-to-guides/run_package_tests_in_same_loop.rst
new file mode 100644
index 00000000..24326ed1
--- /dev/null
+++ b/docs/source/how-to-guides/run_package_tests_in_same_loop.rst
@@ -0,0 +1,11 @@
+========================================================
+How to run all tests in a package in the same event loop
+========================================================
+All tests can be run inside the same event loop by marking them with ``pytest.mark.asyncio(scope="package")``.
+Add the following code to the ``__init__.py`` of the test package:
+
+.. include:: package_scoped_loop_example.py
+ :code: python
+
+Note that this marker is not passed down to tests in subpackages.
+Subpackages constitute their own, separate package.
diff --git a/docs/source/how-to-guides/run_session_tests_in_same_loop.rst b/docs/source/how-to-guides/run_session_tests_in_same_loop.rst
new file mode 100644
index 00000000..7b0da918
--- /dev/null
+++ b/docs/source/how-to-guides/run_session_tests_in_same_loop.rst
@@ -0,0 +1,8 @@
+==========================================================
+How to run all tests in the session in the same event loop
+==========================================================
+All tests can be run inside the same event loop by marking them with ``pytest.mark.asyncio(scope="session")``.
+The easiest way to mark all tests is via a ``pytest_collection_modifyitems`` hook in the ``conftest.py`` at the root folder of your test suite.
+
+.. include:: session_scoped_loop_example.py
+ :code: python
diff --git a/docs/source/how-to-guides/session_scoped_loop_example.py b/docs/source/how-to-guides/session_scoped_loop_example.py
new file mode 100644
index 00000000..e06ffeb5
--- /dev/null
+++ b/docs/source/how-to-guides/session_scoped_loop_example.py
@@ -0,0 +1,10 @@
+import pytest
+
+from pytest_asyncio import is_async_test
+
+
+def pytest_collection_modifyitems(items):
+ pytest_asyncio_tests = (item for item in items if is_async_test(item))
+ session_scope_marker = pytest.mark.asyncio(scope="session")
+ for async_test in pytest_asyncio_tests:
+ async_test.add_marker(session_scope_marker)
diff --git a/docs/source/how-to-guides/test_item_is_async.rst b/docs/source/how-to-guides/test_item_is_async.rst
new file mode 100644
index 00000000..a9ea5d40
--- /dev/null
+++ b/docs/source/how-to-guides/test_item_is_async.rst
@@ -0,0 +1,7 @@
+=======================================
+How to tell if a test function is async
+=======================================
+Use ``pytest_asyncio.is_async_item`` to determine if a test item is asynchronous and managed by pytest-asyncio.
+
+.. include:: test_item_is_async_example.py
+ :code: python
diff --git a/docs/source/how-to-guides/test_item_is_async_example.py b/docs/source/how-to-guides/test_item_is_async_example.py
new file mode 100644
index 00000000..31b44193
--- /dev/null
+++ b/docs/source/how-to-guides/test_item_is_async_example.py
@@ -0,0 +1,7 @@
+from pytest_asyncio import is_async_test
+
+
+def pytest_collection_modifyitems(items):
+ for item in items:
+ if is_async_test(item):
+ pass
diff --git a/docs/source/how-to-guides/uvloop.rst b/docs/source/how-to-guides/uvloop.rst
new file mode 100644
index 00000000..889c0f9d
--- /dev/null
+++ b/docs/source/how-to-guides/uvloop.rst
@@ -0,0 +1,18 @@
+=======================
+How to test with uvloop
+=======================
+
+Redefinig the *event_loop_policy* fixture will parametrize all async tests. The following example causes all async tests to run multiple times, once for each event loop in the fixture parameters:
+Replace the default event loop policy in your *conftest.py:*
+
+.. code-block:: python
+
+ import pytest
+ import uvloop
+
+
+ @pytest.fixture(scope="session")
+ def event_loop_policy():
+ return uvloop.EventLoopPolicy()
+
+You may choose to limit the scope of the fixture to *package,* *module,* or *class,* if you only want a subset of your tests to run with uvloop.
diff --git a/docs/source/index.rst b/docs/source/index.rst
index e6b33033..a5096c56 100644
--- a/docs/source/index.rst
+++ b/docs/source/index.rst
@@ -7,6 +7,7 @@ Welcome to pytest-asyncio!
:hidden:
concepts
+ how-to-guides/index
reference/index
support
diff --git a/docs/source/reference/changelog.rst b/docs/source/reference/changelog.rst
index f68a63c2..b6f57af2 100644
--- a/docs/source/reference/changelog.rst
+++ b/docs/source/reference/changelog.rst
@@ -2,12 +2,55 @@
Changelog
=========
-0.22.0 (UNRELEASED)
+0.23.3 (2024-01-01)
+===================
+- Fixes a bug that caused event loops to be closed prematurely when using async generator fixtures with class scope or wider in a function-scoped test `#706 `_
+- Fixes various bugs that caused an internal pytest error during test collection `#711 `_ `#713 `_ `#719 `_
+
+Known issues
+------------
+As of v0.23, pytest-asyncio attaches an asyncio event loop to each item of the test suite (i.e. session, packages, modules, classes, functions) and allows tests to be run in those loops when marked accordingly. Pytest-asyncio currently assumes that async fixture scope is correlated with the new event loop scope. This prevents fixtures from being evaluated independently from the event loop scope and breaks some existing test suites (see `#706`_). For example, a test suite may require all fixtures and tests to run in the same event loop, but have async fixtures that are set up and torn down for each module. If you're affected by this issue, please continue using the v0.21 release, until it is resolved.
+
+
+0.23.2 (2023-12-04)
+===================
+- Fixes a bug that caused an internal pytest error when collecting .txt files `#703 `_
+
+
+0.23.1 (2023-12-03)
+===================
+- Fixes a bug that caused an internal pytest error when using module-level skips `#701 `_
+
+
+0.23.0 (2023-12-03)
+===================
+This release is backwards-compatible with v0.21.
+Changes are non-breaking, unless you upgrade from v0.22.
+
+- BREAKING: The *asyncio_event_loop* mark has been removed. Event loops with class, module, package, and session scopes can be requested via the *scope* keyword argument to the _asyncio_ mark.
+- Introduces the *event_loop_policy* fixture which allows testing with non-default or multiple event loops `#662 `_
+- Introduces ``pytest_asyncio.is_async_test`` which returns whether a test item is managed by pytest-asyncio `#376 `_
+- Removes and *pytest-trio,* *mypy,* and *flaky* from the test dependencies `#620 `_, `#674 `_, `#678 `_,
+
+0.22.0 (2023-10-31)
+===================
+This release has been yanked from PyPI due to fundamental issues with the _asyncio_event_loop_ mark.
+
+- Class-scoped and module-scoped event loops can be requested
+ via the _asyncio_event_loop_ mark. `#620 `_
+- Deprecate redefinition of the `event_loop` fixture. `#587 `_
+ Users requiring a class-scoped or module-scoped asyncio event loop for their tests
+ should mark the corresponding class or module with `asyncio_event_loop`.
+- Test items based on asynchronous generators always exit with *xfail* status and emit a warning during the collection phase. This behavior is consistent with synchronous yield tests. `#642 `__
+- Remove support for Python 3.7
+- Declare support for Python 3.12
+
+0.21.1 (2023-07-12)
===================
- Output a proper error message when an invalid ``asyncio_mode`` is selected.
- Extend warning message about unclosed event loops with additional possible cause.
`#531 `_
-- Previously, some tests reported "skipped" or "xfailed" as a result. Now all tests report a "success" results.
+- Previously, some tests reported "skipped" or "xfailed" as a result. Now all tests report a "success" result.
0.21.0 (2023-03-19)
===================
diff --git a/docs/source/reference/decorators/fixture_strict_mode_example.py b/docs/source/reference/decorators/fixture_strict_mode_example.py
new file mode 100644
index 00000000..6442c103
--- /dev/null
+++ b/docs/source/reference/decorators/fixture_strict_mode_example.py
@@ -0,0 +1,14 @@
+import asyncio
+
+import pytest_asyncio
+
+
+@pytest_asyncio.fixture
+async def async_gen_fixture():
+ await asyncio.sleep(0.1)
+ yield "a value"
+
+
+@pytest_asyncio.fixture(scope="module")
+async def async_fixture():
+ return await asyncio.sleep(0.1)
diff --git a/docs/source/reference/decorators.rst b/docs/source/reference/decorators/index.rst
similarity index 66%
rename from docs/source/reference/decorators.rst
rename to docs/source/reference/decorators/index.rst
index 977ed6b8..5c96cf4b 100644
--- a/docs/source/reference/decorators.rst
+++ b/docs/source/reference/decorators/index.rst
@@ -3,20 +3,8 @@ Decorators
==========
Asynchronous fixtures are defined just like ordinary pytest fixtures, except they should be decorated with ``@pytest_asyncio.fixture``.
-.. code-block:: python3
-
- import pytest_asyncio
-
-
- @pytest_asyncio.fixture
- async def async_gen_fixture():
- await asyncio.sleep(0.1)
- yield "a value"
-
-
- @pytest_asyncio.fixture(scope="module")
- async def async_fixture():
- return await asyncio.sleep(0.1)
+.. include:: fixture_strict_mode_example.py
+ :code: python
All scopes are supported, but if you use a non-function scope you will need
to redefine the ``event_loop`` fixture to have the same or broader scope.
diff --git a/docs/source/reference/fixtures/event_loop_example.py b/docs/source/reference/fixtures/event_loop_example.py
new file mode 100644
index 00000000..b5a82b62
--- /dev/null
+++ b/docs/source/reference/fixtures/event_loop_example.py
@@ -0,0 +1,5 @@
+import asyncio
+
+
+def test_event_loop_fixture(event_loop):
+ event_loop.run_until_complete(asyncio.sleep(0))
diff --git a/docs/source/reference/fixtures/event_loop_policy_example.py b/docs/source/reference/fixtures/event_loop_policy_example.py
new file mode 100644
index 00000000..cfd7ab96
--- /dev/null
+++ b/docs/source/reference/fixtures/event_loop_policy_example.py
@@ -0,0 +1,17 @@
+import asyncio
+
+import pytest
+
+
+class CustomEventLoopPolicy(asyncio.DefaultEventLoopPolicy):
+ pass
+
+
+@pytest.fixture(scope="module")
+def event_loop_policy(request):
+ return CustomEventLoopPolicy()
+
+
+@pytest.mark.asyncio(scope="module")
+async def test_uses_custom_event_loop_policy():
+ assert isinstance(asyncio.get_event_loop_policy(), CustomEventLoopPolicy)
diff --git a/docs/source/reference/fixtures/event_loop_policy_parametrized_example.py b/docs/source/reference/fixtures/event_loop_policy_parametrized_example.py
new file mode 100644
index 00000000..1560889b
--- /dev/null
+++ b/docs/source/reference/fixtures/event_loop_policy_parametrized_example.py
@@ -0,0 +1,23 @@
+import asyncio
+from asyncio import DefaultEventLoopPolicy
+
+import pytest
+
+
+class CustomEventLoopPolicy(asyncio.DefaultEventLoopPolicy):
+ pass
+
+
+@pytest.fixture(
+ params=(
+ DefaultEventLoopPolicy(),
+ CustomEventLoopPolicy(),
+ ),
+)
+def event_loop_policy(request):
+ return request.param
+
+
+@pytest.mark.asyncio
+async def test_uses_custom_event_loop_policy():
+ assert isinstance(asyncio.get_event_loop_policy(), DefaultEventLoopPolicy)
diff --git a/docs/source/reference/fixtures.rst b/docs/source/reference/fixtures/index.rst
similarity index 53%
rename from docs/source/reference/fixtures.rst
rename to docs/source/reference/fixtures/index.rst
index adcc092d..7b8dc818 100644
--- a/docs/source/reference/fixtures.rst
+++ b/docs/source/reference/fixtures/index.rst
@@ -5,42 +5,38 @@ Fixtures
event_loop
==========
Creates a new asyncio event loop based on the current event loop policy. The new loop
-is available as the return value of this fixture or via `asyncio.get_running_loop `__.
-The event loop is closed when the fixture scope ends. The fixture scope defaults
-to ``function`` scope.
+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.
-.. code-block:: python
-
- def test_http_client(event_loop):
- url = "http://httpbin.org/get"
- resp = event_loop.run_until_complete(http_client(url))
- assert b"HTTP/1.1 200 OK" in resp
+.. include:: event_loop_example.py
+ :code: python
Note that, when using the ``event_loop`` fixture, you need to interact with the event loop using methods like ``event_loop.run_until_complete``. If you want to *await* code inside your test function, you need to write a coroutine and use it as a test function. The `asyncio <#pytest-mark-asyncio>`__ marker
is used to mark coroutines that should be treated as test functions.
-The ``event_loop`` fixture can be overridden in any of the standard pytest locations,
-e.g. directly in the test file, or in ``conftest.py``. This allows redefining the
-fixture scope, for example:
+If you need to change the type of the event loop, prefer setting a custom event loop policy over redefining the ``event_loop`` fixture.
-.. code-block:: python
+If the ``pytest.mark.asyncio`` decorator is applied to a test function, the ``event_loop``
+fixture will be requested automatically by the test function.
- @pytest.fixture(scope="module")
- def event_loop():
- policy = asyncio.get_event_loop_policy()
- loop = policy.new_event_loop()
- yield loop
- loop.close()
+event_loop_policy
+=================
+Returns the event loop policy used to create asyncio event loops.
+The default return value is *asyncio.get_event_loop_policy().*
-When defining multiple ``event_loop`` fixtures, you should ensure that their scopes don't overlap.
-Each of the fixtures replace the running event loop, potentially without proper clean up.
-This will emit a warning and likely lead to errors in your tests suite.
-You can manually check for overlapping ``event_loop`` fixtures by running pytest with the ``--setup-show`` option.
+This fixture can be overridden when a different event loop policy should be used.
-If you need to change the type of the event loop, prefer setting a custom event loop policy over redefining the ``event_loop`` fixture.
+.. include:: event_loop_policy_example.py
+ :code: python
-If the ``pytest.mark.asyncio`` decorator is applied to a test function, the ``event_loop``
-fixture will be requested automatically by the test function.
+Multiple policies can be provided via fixture parameters.
+The fixture is automatically applied to all pytest-asyncio tests.
+Therefore, all tests managed by pytest-asyncio are run once for each fixture parameter.
+The following example runs the test with different event loop policies.
+
+.. include:: event_loop_policy_parametrized_example.py
+ :code: python
unused_tcp_port
===============
diff --git a/docs/source/reference/functions.rst b/docs/source/reference/functions.rst
new file mode 100644
index 00000000..fcd531c2
--- /dev/null
+++ b/docs/source/reference/functions.rst
@@ -0,0 +1,9 @@
+=========
+Functions
+=========
+
+is_async_test
+=============
+Returns whether a specific pytest Item is an asynchronous test managed by pytest-asyncio.
+
+This function is intended to be used in pytest hooks or by plugins that depend on pytest-asyncio.
diff --git a/docs/source/reference/index.rst b/docs/source/reference/index.rst
index c07d0e19..b24c6e9c 100644
--- a/docs/source/reference/index.rst
+++ b/docs/source/reference/index.rst
@@ -6,9 +6,10 @@ Reference
:hidden:
configuration
- fixtures
- markers
- decorators
+ fixtures/index
+ functions
+ markers/index
+ decorators/index
changelog
This section of the documentation provides descriptions of the individual parts provided by pytest-asyncio.
diff --git a/docs/source/reference/markers.rst b/docs/source/reference/markers.rst
deleted file mode 100644
index eb89592c..00000000
--- a/docs/source/reference/markers.rst
+++ /dev/null
@@ -1,34 +0,0 @@
-=======
-Markers
-=======
-
-``pytest.mark.asyncio``
-=======================
-A coroutine or async generator with this marker will be treated as a test function by pytest. The marked function will be executed as an
-asyncio task in the event loop provided by the ``event_loop`` fixture.
-
-In order to make your test code a little more concise, the pytest |pytestmark|_
-feature can be used to mark entire modules or classes with this marker.
-Only test coroutines will be affected (by default, coroutines prefixed by
-``test_``), so, for example, fixtures are safe to define.
-
-.. code-block:: python
-
- import asyncio
-
- import pytest
-
- # All test coroutines will be treated as marked.
- pytestmark = pytest.mark.asyncio
-
-
- async def test_example(event_loop):
- """No marker!"""
- await asyncio.sleep(0, loop=event_loop)
-
-In *auto* mode, the ``pytest.mark.asyncio`` marker can be omitted, the marker is added
-automatically to *async* test functions.
-
-
-.. |pytestmark| replace:: ``pytestmark``
-.. _pytestmark: http://doc.pytest.org/en/latest/example/markers.html#marking-whole-classes-or-modules
diff --git a/docs/source/reference/markers/class_scoped_loop_custom_policies_strict_mode_example.py b/docs/source/reference/markers/class_scoped_loop_custom_policies_strict_mode_example.py
new file mode 100644
index 00000000..afb4cc8a
--- /dev/null
+++ b/docs/source/reference/markers/class_scoped_loop_custom_policies_strict_mode_example.py
@@ -0,0 +1,19 @@
+import asyncio
+
+import pytest
+
+
+@pytest.fixture(
+ params=[
+ asyncio.DefaultEventLoopPolicy(),
+ asyncio.DefaultEventLoopPolicy(),
+ ]
+)
+def event_loop_policy(request):
+ return request.param
+
+
+class TestWithDifferentLoopPolicies:
+ @pytest.mark.asyncio
+ async def test_parametrized_loop(self):
+ pass
diff --git a/docs/source/reference/markers/class_scoped_loop_strict_mode_example.py b/docs/source/reference/markers/class_scoped_loop_strict_mode_example.py
new file mode 100644
index 00000000..38b5689c
--- /dev/null
+++ b/docs/source/reference/markers/class_scoped_loop_strict_mode_example.py
@@ -0,0 +1,14 @@
+import asyncio
+
+import pytest
+
+
+@pytest.mark.asyncio(scope="class")
+class TestClassScopedLoop:
+ loop: asyncio.AbstractEventLoop
+
+ async def test_remember_loop(self):
+ TestClassScopedLoop.loop = asyncio.get_running_loop()
+
+ async def test_this_runs_in_same_loop(self):
+ assert asyncio.get_running_loop() is TestClassScopedLoop.loop
diff --git a/docs/source/reference/markers/class_scoped_loop_with_fixture_strict_mode_example.py b/docs/source/reference/markers/class_scoped_loop_with_fixture_strict_mode_example.py
new file mode 100644
index 00000000..538f1bd2
--- /dev/null
+++ b/docs/source/reference/markers/class_scoped_loop_with_fixture_strict_mode_example.py
@@ -0,0 +1,17 @@
+import asyncio
+
+import pytest
+
+import pytest_asyncio
+
+
+@pytest.mark.asyncio(scope="class")
+class TestClassScopedLoop:
+ loop: asyncio.AbstractEventLoop
+
+ @pytest_asyncio.fixture(scope="class")
+ async def my_fixture(self):
+ TestClassScopedLoop.loop = asyncio.get_running_loop()
+
+ async def test_runs_is_same_loop_as_fixture(self, my_fixture):
+ assert asyncio.get_running_loop() is TestClassScopedLoop.loop
diff --git a/docs/source/reference/markers/function_scoped_loop_pytestmark_strict_mode_example.py b/docs/source/reference/markers/function_scoped_loop_pytestmark_strict_mode_example.py
new file mode 100644
index 00000000..f8e7e717
--- /dev/null
+++ b/docs/source/reference/markers/function_scoped_loop_pytestmark_strict_mode_example.py
@@ -0,0 +1,10 @@
+import asyncio
+
+import pytest
+
+# Marks all test coroutines in this module
+pytestmark = pytest.mark.asyncio
+
+
+async def test_runs_in_asyncio_event_loop():
+ assert asyncio.get_running_loop()
diff --git a/docs/source/reference/markers/function_scoped_loop_strict_mode_example.py b/docs/source/reference/markers/function_scoped_loop_strict_mode_example.py
new file mode 100644
index 00000000..e30f73c5
--- /dev/null
+++ b/docs/source/reference/markers/function_scoped_loop_strict_mode_example.py
@@ -0,0 +1,8 @@
+import asyncio
+
+import pytest
+
+
+@pytest.mark.asyncio
+async def test_runs_in_asyncio_event_loop():
+ assert asyncio.get_running_loop()
diff --git a/docs/source/reference/markers/index.rst b/docs/source/reference/markers/index.rst
new file mode 100644
index 00000000..a875b90d
--- /dev/null
+++ b/docs/source/reference/markers/index.rst
@@ -0,0 +1,44 @@
+=======
+Markers
+=======
+
+``pytest.mark.asyncio``
+=======================
+A coroutine or async generator with this marker is treated as a test function by pytest.
+The marked function is executed as an asyncio task in the event loop provided by pytest-asyncio.
+
+.. include:: function_scoped_loop_strict_mode_example.py
+ :code: python
+
+Multiple async tests in a single class or module can be marked using |pytestmark|_.
+
+.. include:: function_scoped_loop_pytestmark_strict_mode_example.py
+ :code: python
+
+The ``pytest.mark.asyncio`` marker can be omitted entirely in |auto mode|_ where the *asyncio* marker is added automatically to *async* test functions.
+
+By default, each test runs in it's own asyncio event loop.
+Multiple tests can share the same event loop by providing a *scope* keyword argument to the *asyncio* mark.
+The supported scopes are *class,* and *module,* and *package*.
+The following code example provides a shared event loop for all tests in `TestClassScopedLoop`:
+
+.. include:: class_scoped_loop_strict_mode_example.py
+ :code: python
+
+Requesting class scope with the test being part of a class will give a *UsageError*.
+Similar to class-scoped event loops, a module-scoped loop is provided when setting mark's scope to *module:*
+
+.. include:: module_scoped_loop_strict_mode_example.py
+ :code: python
+
+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.
+
+.. |auto mode| replace:: *auto mode*
+.. _auto mode: ../../concepts.html#auto-mode
+.. |pytestmark| replace:: ``pytestmark``
+.. _pytestmark: http://doc.pytest.org/en/latest/example/markers.html#marking-whole-classes-or-modules
diff --git a/docs/source/reference/markers/module_scoped_loop_strict_mode_example.py b/docs/source/reference/markers/module_scoped_loop_strict_mode_example.py
new file mode 100644
index 00000000..221d554e
--- /dev/null
+++ b/docs/source/reference/markers/module_scoped_loop_strict_mode_example.py
@@ -0,0 +1,23 @@
+import asyncio
+
+import pytest
+
+pytestmark = pytest.mark.asyncio(scope="module")
+
+loop: asyncio.AbstractEventLoop
+
+
+async def test_remember_loop():
+ global loop
+ loop = asyncio.get_running_loop()
+
+
+async def test_this_runs_in_same_loop():
+ global loop
+ assert asyncio.get_running_loop() is loop
+
+
+class TestClassA:
+ async def test_this_runs_in_same_loop(self):
+ global loop
+ assert asyncio.get_running_loop() is loop
diff --git a/docs/source/support.rst b/docs/source/support.rst
index 30981d94..f998bb35 100644
--- a/docs/source/support.rst
+++ b/docs/source/support.rst
@@ -18,4 +18,4 @@ If you require commercial support outside of the Tidelift subscription, reach ou
Community support
=================
-The GitHub page of pytest-asyncio offers free community support on a best-effort basis. Please use the `issue tracker `__ to report bugs and the `discussions `__ to ask questions.
+The GitHub page of pytest-asyncio offers free community support on a best-effort basis. Please use the `issue tracker `__ to report bugs and the Matrix chat room `#pytest-asyncio:matrix.org `__ or `GitHub discussions `__ to ask questions.
diff --git a/pytest_asyncio/__init__.py b/pytest_asyncio/__init__.py
index 1bc2811d..95046981 100644
--- a/pytest_asyncio/__init__.py
+++ b/pytest_asyncio/__init__.py
@@ -1,5 +1,5 @@
"""The main point for importing pytest-asyncio items."""
from ._version import version as __version__ # noqa
-from .plugin import fixture
+from .plugin import fixture, is_async_test
-__all__ = ("fixture",)
+__all__ = ("fixture", "is_async_test")
diff --git a/pytest_asyncio/plugin.py b/pytest_asyncio/plugin.py
index 12669791..eb013f46 100644
--- a/pytest_asyncio/plugin.py
+++ b/pytest_asyncio/plugin.py
@@ -7,6 +7,7 @@
import socket
import sys
import warnings
+from asyncio import AbstractEventLoopPolicy
from textwrap import dedent
from typing import (
Any,
@@ -17,30 +18,34 @@
Iterable,
Iterator,
List,
+ Literal,
Optional,
Set,
+ Type,
TypeVar,
Union,
- cast,
overload,
)
import pytest
from pytest import (
+ Class,
+ Collector,
Config,
FixtureRequest,
Function,
Item,
+ Metafunc,
+ Module,
+ Package,
Parser,
+ PytestCollectionWarning,
+ PytestDeprecationWarning,
PytestPluginManager,
Session,
+ StashKey,
)
-if sys.version_info >= (3, 8):
- from typing import Literal
-else:
- from typing_extensions import Literal
-
_R = TypeVar("_R")
_ScopeName = Literal["session", "package", "module", "class", "function"]
@@ -60,6 +65,14 @@
SubRequest = Any
+class PytestAsyncioError(Exception):
+ """Base class for exceptions raised by pytest-asyncio"""
+
+
+class MultipleEventLoopsRequestedError(PytestAsyncioError):
+ """Raised when a test requests multiple asyncio event loops."""
+
+
class Mode(str, enum.Enum):
AUTO = "auto"
STRICT = "strict"
@@ -151,13 +164,8 @@ def _make_asyncio_fixture_function(obj: Any) -> None:
obj._force_asyncio_fixture = True
-def _is_coroutine(obj: Any) -> bool:
- """Check to see if an object is really an asyncio coroutine."""
- return asyncio.iscoroutinefunction(obj)
-
-
def _is_coroutine_or_asyncgen(obj: Any) -> bool:
- return _is_coroutine(obj) or inspect.isasyncgenfunction(obj)
+ return asyncio.iscoroutinefunction(obj) or inspect.isasyncgenfunction(obj)
def _get_asyncio_mode(config: Config) -> Mode:
@@ -191,9 +199,10 @@ def pytest_report_header(config: Config) -> List[str]:
def _preprocess_async_fixtures(
- config: Config,
+ collector: Collector,
processed_fixturedefs: Set[FixtureDef],
) -> None:
+ config = collector.config
asyncio_mode = _get_asyncio_mode(config)
fixturemanager = config.pluginmanager.get_plugin("funcmanage")
for fixtures in fixturemanager._arg2fixturedefs.values():
@@ -207,38 +216,61 @@ def _preprocess_async_fixtures(
# Ignore async fixtures without explicit asyncio mark in strict mode
# This applies to pytest_trio fixtures, for example
continue
+ scope = fixturedef.scope
+ if scope == "function":
+ event_loop_fixture_id = "event_loop"
+ else:
+ event_loop_node = _retrieve_scope_root(collector, scope)
+ event_loop_fixture_id = event_loop_node.stash.get(
+ _event_loop_fixture_id, None
+ )
_make_asyncio_fixture_function(func)
- _inject_fixture_argnames(fixturedef)
- _synchronize_async_fixture(fixturedef)
+ function_signature = inspect.signature(func)
+ if "event_loop" in function_signature.parameters:
+ warnings.warn(
+ PytestDeprecationWarning(
+ f"{func.__name__} is asynchronous and explicitly "
+ f'requests the "event_loop" fixture. Asynchronous fixtures and '
+ f'test functions should use "asyncio.get_running_loop()" '
+ f"instead."
+ )
+ )
+ _inject_fixture_argnames(fixturedef, event_loop_fixture_id)
+ _synchronize_async_fixture(fixturedef, event_loop_fixture_id)
assert _is_asyncio_fixture_function(fixturedef.func)
processed_fixturedefs.add(fixturedef)
-def _inject_fixture_argnames(fixturedef: FixtureDef) -> None:
+def _inject_fixture_argnames(
+ fixturedef: FixtureDef, event_loop_fixture_id: str
+) -> None:
"""
Ensures that `request` and `event_loop` are arguments of the specified fixture.
"""
to_add = []
- for name in ("request", "event_loop"):
+ for name in ("request", event_loop_fixture_id):
if name not in fixturedef.argnames:
to_add.append(name)
if to_add:
fixturedef.argnames += tuple(to_add)
-def _synchronize_async_fixture(fixturedef: FixtureDef) -> None:
+def _synchronize_async_fixture(
+ fixturedef: FixtureDef, event_loop_fixture_id: str
+) -> None:
"""
Wraps the fixture function of an async fixture in a synchronous function.
"""
if inspect.isasyncgenfunction(fixturedef.func):
- _wrap_asyncgen_fixture(fixturedef)
+ _wrap_asyncgen_fixture(fixturedef, event_loop_fixture_id)
elif inspect.iscoroutinefunction(fixturedef.func):
- _wrap_async_fixture(fixturedef)
+ _wrap_async_fixture(fixturedef, event_loop_fixture_id)
def _add_kwargs(
func: Callable[..., Any],
kwargs: Dict[str, Any],
+ event_loop_fixture_id: str,
event_loop: asyncio.AbstractEventLoop,
request: SubRequest,
) -> Dict[str, Any]:
@@ -246,8 +278,8 @@ def _add_kwargs(
ret = kwargs.copy()
if "request" in sig.parameters:
ret["request"] = request
- if "event_loop" in sig.parameters:
- ret["event_loop"] = event_loop
+ if event_loop_fixture_id in sig.parameters:
+ ret[event_loop_fixture_id] = event_loop
return ret
@@ -270,17 +302,18 @@ def _perhaps_rebind_fixture_func(
return func
-def _wrap_asyncgen_fixture(fixturedef: FixtureDef) -> None:
+def _wrap_asyncgen_fixture(fixturedef: FixtureDef, event_loop_fixture_id: str) -> None:
fixture = fixturedef.func
@functools.wraps(fixture)
- def _asyncgen_fixture_wrapper(
- event_loop: asyncio.AbstractEventLoop, request: SubRequest, **kwargs: Any
- ):
+ def _asyncgen_fixture_wrapper(request: SubRequest, **kwargs: Any):
func = _perhaps_rebind_fixture_func(
fixture, request.instance, fixturedef.unittest
)
- gen_obj = func(**_add_kwargs(func, kwargs, event_loop, request))
+ event_loop = kwargs.pop(event_loop_fixture_id)
+ gen_obj = func(
+ **_add_kwargs(func, kwargs, event_loop_fixture_id, event_loop, request)
+ )
async def setup():
res = await gen_obj.__anext__()
@@ -308,19 +341,20 @@ async def async_finalizer() -> None:
fixturedef.func = _asyncgen_fixture_wrapper
-def _wrap_async_fixture(fixturedef: FixtureDef) -> None:
+def _wrap_async_fixture(fixturedef: FixtureDef, event_loop_fixture_id: str) -> None:
fixture = fixturedef.func
@functools.wraps(fixture)
- def _async_fixture_wrapper(
- event_loop: asyncio.AbstractEventLoop, request: SubRequest, **kwargs: Any
- ):
+ def _async_fixture_wrapper(request: SubRequest, **kwargs: Any):
func = _perhaps_rebind_fixture_func(
fixture, request.instance, fixturedef.unittest
)
+ event_loop = kwargs.pop(event_loop_fixture_id)
async def setup():
- res = await func(**_add_kwargs(func, kwargs, event_loop, request))
+ res = await func(
+ **_add_kwargs(func, kwargs, event_loop_fixture_id, event_loop, request)
+ )
return res
return event_loop.run_until_complete(setup())
@@ -328,11 +362,144 @@ async def setup():
fixturedef.func = _async_fixture_wrapper
+class PytestAsyncioFunction(Function):
+ """Base class for all test functions managed by pytest-asyncio."""
+
+ @classmethod
+ def item_subclass_for(
+ cls, item: Function, /
+ ) -> Union[Type["PytestAsyncioFunction"], None]:
+ """
+ Returns a subclass of PytestAsyncioFunction if there is a specialized subclass
+ for the specified function item.
+
+ Return None if no specialized subclass exists for the specified item.
+ """
+ for subclass in cls.__subclasses__():
+ if subclass._can_substitute(item):
+ return subclass
+ return None
+
+ @classmethod
+ def _from_function(cls, function: Function, /) -> Function:
+ """
+ Instantiates this specific PytestAsyncioFunction type from the specified
+ Function item.
+ """
+ assert function.get_closest_marker("asyncio")
+ subclass_instance = cls.from_parent(
+ function.parent,
+ name=function.name,
+ callspec=getattr(function, "callspec", None),
+ callobj=function.obj,
+ fixtureinfo=function._fixtureinfo,
+ keywords=function.keywords,
+ originalname=function.originalname,
+ )
+ subclass_instance.own_markers.extend(function.own_markers)
+ subclassed_function_signature = inspect.signature(subclass_instance.obj)
+ if "event_loop" in subclassed_function_signature.parameters:
+ subclass_instance.warn(
+ PytestDeprecationWarning(
+ f"{subclass_instance.name} is asynchronous and explicitly "
+ f'requests the "event_loop" fixture. Asynchronous fixtures and '
+ f'test functions should use "asyncio.get_running_loop()" instead.'
+ )
+ )
+ return subclass_instance
+
+ @staticmethod
+ def _can_substitute(item: Function) -> bool:
+ """Returns whether the specified function can be replaced by this class"""
+ raise NotImplementedError()
+
+
+class Coroutine(PytestAsyncioFunction):
+ """Pytest item created by a coroutine"""
+
+ @staticmethod
+ def _can_substitute(item: Function) -> bool:
+ func = item.obj
+ return asyncio.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"""
+
+ @staticmethod
+ def _can_substitute(item: Function) -> bool:
+ func = item.obj
+ return inspect.isasyncgenfunction(func)
+
+ @classmethod
+ def _from_function(cls, function: Function, /) -> Function:
+ async_gen_item = super()._from_function(function)
+ unsupported_item_type_message = (
+ f"Tests based on asynchronous generators are not supported. "
+ f"{function.name} will be ignored."
+ )
+ async_gen_item.warn(PytestCollectionWarning(unsupported_item_type_message))
+ async_gen_item.add_marker(
+ pytest.mark.xfail(run=False, reason=unsupported_item_type_message)
+ )
+ return async_gen_item
+
+
+class AsyncStaticMethod(PytestAsyncioFunction):
+ """
+ Pytest item that is a coroutine or an asynchronous generator
+ decorated with staticmethod
+ """
+
+ @staticmethod
+ def _can_substitute(item: Function) -> bool:
+ func = item.obj
+ return isinstance(func, staticmethod) and _is_coroutine_or_asyncgen(
+ 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):
+ """
+ Pytest item that is coroutine or an asynchronous generator decorated by
+ @hypothesis.given.
+ """
+
+ @staticmethod
+ def _can_substitute(item: Function) -> bool:
+ func = item.obj
+ return getattr(
+ func, "is_hypothesis_test", False
+ ) and asyncio.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()
-@pytest.hookimpl(tryfirst=True)
-def pytest_pycollect_makeitem(
+# The function name needs to start with "pytest_"
+# see https://github.com/pytest-dev/pytest/issues/11307
+@pytest.hookimpl(specname="pytest_pycollect_makeitem", tryfirst=True)
+def pytest_pycollect_makeitem_preprocess_async_fixtures(
collector: Union[pytest.Module, pytest.Class], name: str, obj: object
) -> Union[
pytest.Item, pytest.Collector, List[Union[pytest.Item, pytest.Collector]], None
@@ -340,41 +507,219 @@ def pytest_pycollect_makeitem(
"""A pytest hook to collect asyncio coroutines."""
if not collector.funcnamefilter(name):
return None
- _preprocess_async_fixtures(collector.config, _HOLDER)
+ _preprocess_async_fixtures(collector, _HOLDER)
return None
-def pytest_collection_modifyitems(
- session: Session, config: Config, items: List[Item]
-) -> None:
+# The function name needs to start with "pytest_"
+# see https://github.com/pytest-dev/pytest/issues/11307
+@pytest.hookimpl(specname="pytest_pycollect_makeitem", hookwrapper=True)
+def pytest_pycollect_makeitem_convert_async_functions_to_subclass(
+ collector: Union[pytest.Module, pytest.Class], name: str, obj: object
+) -> Union[
+ pytest.Item, pytest.Collector, List[Union[pytest.Item, pytest.Collector]], None
+]:
"""
- Marks collected async test items as `asyncio` tests.
-
- The mark is only applied in `AUTO` mode. It is applied to:
+ Converts coroutines and async generators collected as pytest.Functions
+ to AsyncFunction items.
+ """
+ hook_result = yield
+ node_or_list_of_nodes = hook_result.get_result()
+ if not node_or_list_of_nodes:
+ return
+ try:
+ node_iterator = iter(node_or_list_of_nodes)
+ except TypeError:
+ # Treat single node as a single-element iterable
+ node_iterator = iter((node_or_list_of_nodes,))
+ updated_node_collection = []
+ for node in node_iterator:
+ updated_item = node
+ if isinstance(node, Function):
+ specialized_item_class = PytestAsyncioFunction.item_subclass_for(node)
+ if specialized_item_class:
+ if _get_asyncio_mode(
+ node.config
+ ) == Mode.AUTO and not node.get_closest_marker("asyncio"):
+ node.add_marker("asyncio")
+ if node.get_closest_marker("asyncio"):
+ updated_item = specialized_item_class._from_function(node)
+ updated_node_collection.append(updated_item)
+ hook_result.force_result(updated_node_collection)
+
+
+_event_loop_fixture_id = StashKey[str]
+_fixture_scope_by_collector_type = {
+ 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",
+}
+
+
+@pytest.hookimpl
+def pytest_collectstart(collector: pytest.Collector):
+ try:
+ collector_scope = next(
+ scope
+ for cls, scope in _fixture_scope_by_collector_type.items()
+ if isinstance(collector, cls)
+ )
+ except StopIteration:
+ return
+ # Session is not a PyCollector type, so it doesn't have a corresponding
+ # "obj" attribute to attach a dynamic fixture function to.
+ # However, there's only one session per pytest run, so there's no need to
+ # create the fixture dynamically. We can simply define a session-scoped
+ # event loop fixture once in the plugin code.
+ if 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):
+ loop = asyncio.new_event_loop()
+ loop.__pytest_asyncio = True # type: ignore[attr-defined]
+ asyncio.set_event_loop(loop)
+ yield loop
+ loop.close()
+
+ # @pytest.fixture does not register the fixture anywhere, so pytest doesn't
+ # know it exists. We work around this by attaching the fixture function to the
+ # collected Python object, where it will be picked up by pytest.Class.collect()
+ # or pytest.Module.collect(), respectively
+ if type(collector) is Module:
+ # Accessing Module.obj triggers a module import executing module-level
+ # statements. A module-level pytest.skip statement raises the "Skipped"
+ # OutcomeException or a Collector.CollectError, if the "allow_module_level"
+ # kwargs is missing. These cases are handled correctly when they happen inside
+ # Collector.collect(), but this hook runs before the actual collect call.
+ # Therefore, we monkey patch Module.collect to add the scoped fixture to the
+ # module before it runs the actual collection.
+ def _patched_collect():
+ collector.obj.__pytest_asyncio_scoped_event_loop = scoped_event_loop
+ return collector.__original_collect()
+
+ collector.__original_collect = collector.collect
+ collector.collect = _patched_collect
+ else:
+ pyobject = collector.obj
+ # If the collected module is a DoctestTextfile, collector.obj is None
+ if pyobject is None:
+ return
+ pyobject.__pytest_asyncio_scoped_event_loop = scoped_event_loop
+ # When collector is a package, collector.obj is the package's __init__.py.
+ # pytest doesn't seem to collect fixtures in __init__.py.
+ # Using parsefactories to collect fixtures in __init__.py their baseid will end
+ # with "__init__.py", thus limiting the scope of the fixture to the init module.
+ # Therefore, we tell the pluginmanager explicitly to collect the fixtures
+ # in the init module, but strip "__init__.py" from the baseid
+ # Possibly related to https://github.com/pytest-dev/pytest/issues/4085
+ if isinstance(collector, Package):
+ fixturemanager = collector.config.pluginmanager.get_plugin("funcmanage")
+ package_node_id = _removesuffix(collector.nodeid, "__init__.py")
+ fixturemanager.parsefactories(collector.obj, nodeid=package_node_id)
+
+
+def _removesuffix(s: str, suffix: str) -> str:
+ if sys.version_info < (3, 9):
+ return s[: -len(suffix)]
+ return s.removesuffix(suffix)
+
+
+@contextlib.contextmanager
+def _temporary_event_loop_policy(policy: AbstractEventLoopPolicy) -> Iterator[None]:
+ old_loop_policy = asyncio.get_event_loop_policy()
+ try:
+ old_loop = asyncio.get_event_loop()
+ except RuntimeError:
+ old_loop = None
+ asyncio.set_event_loop_policy(policy)
+ try:
+ yield
+ finally:
+ asyncio.set_event_loop_policy(old_loop_policy)
+ # When a test uses both a scoped event loop and the event_loop fixture,
+ # the "_provide_clean_event_loop" finalizer of the event_loop fixture
+ # will already have installed a fresh event loop, in order to shield
+ # subsequent tests from side-effects. We close this loop before restoring
+ # the old loop to avoid ResourceWarnings.
+ try:
+ asyncio.get_event_loop().close()
+ except RuntimeError:
+ pass
+ asyncio.set_event_loop(old_loop)
- - coroutines
- - staticmethods wrapping coroutines
- - Hypothesis tests wrapping coroutines
+_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 "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.
"""
- if _get_asyncio_mode(config) != Mode.AUTO:
- return
- function_items = (item for item in items if isinstance(item, Function))
- for function_item in function_items:
- function = function_item.obj
- if isinstance(function, staticmethod):
- # staticmethods need to be unwrapped.
- function = function.__func__
- if (
- _is_coroutine(function)
- or _is_hypothesis_test(function)
- and _hypothesis_test_wraps_coroutine(function)
- ):
- function_item.add_marker("asyncio")
+)
-def _hypothesis_test_wraps_coroutine(function: Any) -> bool:
- return _is_coroutine(function.hypothesis.inner_test)
+@pytest.hookimpl(tryfirst=True)
+def pytest_generate_tests(metafunc: Metafunc) -> None:
+ marker = metafunc.definition.get_closest_marker("asyncio")
+ if not marker:
+ return
+ scope = marker.kwargs.get("scope", "function")
+ if scope == "function":
+ return
+ event_loop_node = _retrieve_scope_root(metafunc.definition, scope)
+ event_loop_fixture_id = event_loop_node.stash.get(_event_loop_fixture_id, None)
+
+ if event_loop_fixture_id:
+ # This specific fixture name may already be in metafunc.argnames, if this
+ # test indirectly depends on the fixture. For example, this is the case
+ # when the test depends on an async fixture, both of which share the same
+ # event loop fixture mark.
+ if event_loop_fixture_id in metafunc.fixturenames:
+ return
+ fixturemanager = metafunc.config.pluginmanager.get_plugin("funcmanage")
+ if "event_loop" in metafunc.fixturenames:
+ raise MultipleEventLoopsRequestedError(
+ _MULTIPLE_LOOPS_REQUESTED_ERROR.format(
+ test_name=metafunc.definition.nodeid,
+ scope=scope,
+ scoped_loop_node=event_loop_node.nodeid,
+ ),
+ )
+ # Add the scoped event loop fixture to Metafunc's list of fixture names and
+ # fixturedefs and leave the actual parametrization to pytest
+ # 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)
@@ -391,16 +736,28 @@ def pytest_fixture_setup(
_add_finalizers(
fixturedef,
_close_event_loop,
+ _restore_event_loop_policy(asyncio.get_event_loop_policy()),
_provide_clean_event_loop,
)
outcome = yield
loop = outcome.get_result()
+ # Weird behavior was observed when checking for an attribute of FixtureDef.func
+ # Instead, we now check for a special attribute of the returned event loop
+ fixture_filename = inspect.getsourcefile(fixturedef.func)
+ if not getattr(loop, "__original_fixture_loop", False):
+ _, fixture_line_number = inspect.getsourcelines(fixturedef.func)
+ warnings.warn(
+ _REDEFINED_EVENT_LOOP_FIXTURE_WARNING
+ % (fixture_filename, fixture_line_number),
+ DeprecationWarning,
+ )
policy = asyncio.get_event_loop_policy()
try:
with warnings.catch_warnings():
warnings.simplefilter("ignore", DeprecationWarning)
old_loop = policy.get_event_loop()
- if old_loop is not loop:
+ is_pytest_asyncio_loop = getattr(old_loop, "__pytest_asyncio", False)
+ if old_loop is not loop and not is_pytest_asyncio_loop:
old_loop.close()
except RuntimeError:
# Either the current event loop has been set to None
@@ -455,6 +812,23 @@ def _close_event_loop() -> None:
loop.close()
+def _restore_event_loop_policy(previous_policy) -> Callable[[], None]:
+ def _restore_policy():
+ # Close any event loop associated with the old loop policy
+ # to avoid ResourceWarnings in the _provide_clean_event_loop finalizer
+ try:
+ with warnings.catch_warnings():
+ warnings.simplefilter("ignore", DeprecationWarning)
+ loop = previous_policy.get_event_loop()
+ except RuntimeError:
+ loop = None
+ if loop:
+ loop.close()
+ asyncio.set_event_loop_policy(previous_policy)
+
+ return _restore_policy
+
+
def _provide_clean_event_loop() -> None:
# At this point, the event loop for the current thread is closed.
# When a user calls asyncio.get_event_loop(), they will get a closed loop.
@@ -468,40 +842,31 @@ def _provide_clean_event_loop() -> None:
@pytest.hookimpl(tryfirst=True, hookwrapper=True)
-def pytest_pyfunc_call(pyfuncitem: pytest.Function) -> Optional[object]:
+def pytest_pyfunc_call(pyfuncitem: Function) -> Optional[object]:
"""
Pytest hook called before a test case is run.
Wraps marked tests in a synchronous function
where the wrapped test coroutine is executed in an event loop.
"""
- marker = pyfuncitem.get_closest_marker("asyncio")
- if marker is not None:
- funcargs: Dict[str, object] = pyfuncitem.funcargs # type: ignore[name-defined]
- loop = cast(asyncio.AbstractEventLoop, funcargs["event_loop"])
- if _is_hypothesis_test(pyfuncitem.obj):
- pyfuncitem.obj.hypothesis.inner_test = wrap_in_sync(
- pyfuncitem,
- pyfuncitem.obj.hypothesis.inner_test,
- _loop=loop,
- )
+ if pyfuncitem.get_closest_marker("asyncio") is not None:
+ if isinstance(pyfuncitem, PytestAsyncioFunction):
+ pass
else:
- pyfuncitem.obj = wrap_in_sync(
- pyfuncitem,
- pyfuncitem.obj,
- _loop=loop,
+ pyfuncitem.warn(
+ pytest.PytestWarning(
+ f"The test {pyfuncitem} is marked with '@pytest.mark.asyncio' "
+ "but it is not an async function. "
+ "Please remove the asyncio mark. "
+ "If the test is not marked explicitly, "
+ "check for global marks applied via 'pytestmark'."
+ )
)
yield
-def _is_hypothesis_test(function: Any) -> bool:
- return getattr(function, "is_hypothesis_test", False)
-
-
def wrap_in_sync(
- pyfuncitem: pytest.Function,
func: Callable[..., Awaitable[Any]],
- _loop: asyncio.AbstractEventLoop,
):
"""Return a sync wrapper around an async function executing it in the
current event loop."""
@@ -516,17 +881,7 @@ def wrap_in_sync(
@functools.wraps(func)
def inner(*args, **kwargs):
coro = func(*args, **kwargs)
- if not inspect.isawaitable(coro):
- pyfuncitem.warn(
- pytest.PytestWarning(
- f"The test {pyfuncitem} is marked with '@pytest.mark.asyncio' "
- "but it is not an async function. "
- "Please remove asyncio marker. "
- "If the test is not marked explicitly, "
- "check for global markers applied via 'pytestmark'."
- )
- )
- return
+ _loop = asyncio.get_event_loop()
task = asyncio.ensure_future(coro, loop=_loop)
try:
_loop.run_until_complete(task)
@@ -542,15 +897,31 @@ def inner(*args, **kwargs):
return inner
+_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.
+ """
+)
+
+
def pytest_runtest_setup(item: pytest.Item) -> None:
marker = item.get_closest_marker("asyncio")
if marker is None:
return
+ scope = marker.kwargs.get("scope", "function")
+ if scope != "function":
+ parent_node = _retrieve_scope_root(item, scope)
+ event_loop_fixture_id = parent_node.stash[_event_loop_fixture_id]
+ else:
+ event_loop_fixture_id = "event_loop"
fixturenames = item.fixturenames # type: ignore[attr-defined]
- # inject an event loop fixture for all async tests
- if "event_loop" in fixturenames:
- fixturenames.remove("event_loop")
- fixturenames.insert(0, "event_loop")
+ 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
@@ -561,14 +932,64 @@ def pytest_runtest_setup(item: pytest.Item) -> None:
)
+def _retrieve_scope_root(item: Union[Collector, Item], scope: str) -> Collector:
+ node_type_by_scope = {
+ "class": Class,
+ "module": Module,
+ "package": Package,
+ "session": Session,
+ }
+ scope_root_type = node_type_by_scope[scope]
+ for node in reversed(item.listchain()):
+ if isinstance(node, scope_root_type):
+ return node
+ error_message = (
+ f"{item.name} is marked to be run in an event loop with scope {scope}, "
+ f"but is not part of any {scope}."
+ )
+ raise pytest.UsageError(error_message)
+
+
@pytest.fixture
def event_loop(request: FixtureRequest) -> Iterator[asyncio.AbstractEventLoop]:
"""Create an instance of the default event loop for each test case."""
+ new_loop_policy = request.getfixturevalue(event_loop_policy.__name__)
+ asyncio.set_event_loop_policy(new_loop_policy)
loop = asyncio.get_event_loop_policy().new_event_loop()
+ # Add a magic value to the event loop, so pytest-asyncio can determine if the
+ # event_loop fixture was overridden. Other implementations of event_loop don't
+ # set this value.
+ # The magic value must be set as part of the function definition, because pytest
+ # seems to have multiple instances of the same FixtureDef or fixture function
+ loop.__original_fixture_loop = True # type: ignore[attr-defined]
yield loop
loop.close()
+@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):
+ loop = asyncio.new_event_loop()
+ loop.__pytest_asyncio = True # type: ignore[attr-defined]
+ asyncio.set_event_loop(loop)
+ yield loop
+ loop.close()
+
+
+@pytest.fixture(scope="session", autouse=True)
+def event_loop_policy() -> AbstractEventLoopPolicy:
+ """Return an instance of the policy used to create asyncio event loops."""
+ return asyncio.get_event_loop_policy()
+
+
+def is_async_test(item: Item) -> bool:
+ """Returns whether a test item is a pytest-asyncio test"""
+ return isinstance(item, PytestAsyncioFunction)
+
+
def _unused_port(socket_type: int) -> int:
"""Find an unused localhost port from 1024-65535 and return it."""
with contextlib.closing(socket.socket(type=socket_type)) as sock:
diff --git a/setup.cfg b/setup.cfg
index 3712ec16..fdbaf625 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -21,11 +21,11 @@ classifiers =
License :: OSI Approved :: Apache Software License
- Programming Language :: Python :: 3.7
Programming Language :: Python :: 3.8
Programming Language :: Python :: 3.9
Programming Language :: Python :: 3.10
Programming Language :: Python :: 3.11
+ Programming Language :: Python :: 3.12
Topic :: Software Development :: Testing
@@ -34,22 +34,18 @@ classifiers =
Typing :: Typed
[options]
-python_requires = >=3.7
+python_requires = >=3.8
packages = find:
include_package_data = True
# Always adjust requirements.txt and pytest-min-requirements.txt when changing runtime dependencies
install_requires =
pytest >= 7.0.0
- typing-extensions >= 3.7.2; python_version < "3.8"
[options.extras_require]
testing =
coverage >= 6.2
hypothesis >= 5.7.1
- flaky >= 3.5.0
- mypy >= 0.931
- pytest-trio >= 0.7.0
docs =
sphinx >= 5.3
sphinx-rtd-theme >= 1.0
@@ -66,12 +62,14 @@ branch = true
show_missing = true
[tool:pytest]
+python_files = test_*.py *_example.py
addopts = -rsx --tb=short
-testpaths = tests
+testpaths = docs/source tests
asyncio_mode = auto
junit_family=xunit2
filterwarnings =
error
+ ignore:The event_loop fixture provided by pytest-asyncio has been redefined.*:DeprecationWarning
[flake8]
max-line-length = 88
diff --git a/tests/async_fixtures/test_async_fixtures_with_finalizer.py b/tests/async_fixtures/test_async_fixtures_with_finalizer.py
index 2e72d5de..699ac49d 100644
--- a/tests/async_fixtures/test_async_fixtures_with_finalizer.py
+++ b/tests/async_fixtures/test_async_fixtures_with_finalizer.py
@@ -4,13 +4,13 @@
import pytest
-@pytest.mark.asyncio
+@pytest.mark.asyncio(scope="module")
async def test_module_with_event_loop_finalizer(port_with_event_loop_finalizer):
await asyncio.sleep(0.01)
assert port_with_event_loop_finalizer
-@pytest.mark.asyncio
+@pytest.mark.asyncio(scope="module")
async def test_module_with_get_event_loop_finalizer(port_with_get_event_loop_finalizer):
await asyncio.sleep(0.01)
assert port_with_get_event_loop_finalizer
@@ -26,14 +26,14 @@ def event_loop():
@pytest.fixture(scope="module")
-async def port_with_event_loop_finalizer(request, event_loop):
+async def port_with_event_loop_finalizer(request):
def port_finalizer(finalizer):
async def port_afinalizer():
# await task using loop provided by event_loop fixture
# RuntimeError is raised if task is created on a different loop
await finalizer
- event_loop.run_until_complete(port_afinalizer())
+ asyncio.get_event_loop().run_until_complete(port_afinalizer())
worker = asyncio.ensure_future(asyncio.sleep(0.2))
request.addfinalizer(functools.partial(port_finalizer, worker))
@@ -41,7 +41,7 @@ async def port_afinalizer():
@pytest.fixture(scope="module")
-async def port_with_get_event_loop_finalizer(request, event_loop):
+async def port_with_get_event_loop_finalizer(request):
def port_finalizer(finalizer):
async def port_afinalizer():
# await task using current loop retrieved from the event loop policy
diff --git a/tests/async_fixtures/test_nested.py b/tests/async_fixtures/test_nested.py
index e81e7824..da7ee3a1 100644
--- a/tests/async_fixtures/test_nested.py
+++ b/tests/async_fixtures/test_nested.py
@@ -12,7 +12,7 @@ async def async_inner_fixture():
@pytest.fixture()
-async def async_fixture_outer(async_inner_fixture, event_loop):
+async def async_fixture_outer(async_inner_fixture):
await asyncio.sleep(0.01)
print("outer start")
assert async_inner_fixture is True
diff --git a/tests/async_fixtures/test_parametrized_loop.py b/tests/async_fixtures/test_parametrized_loop.py
index 2fb8befa..2bdbe5e8 100644
--- a/tests/async_fixtures/test_parametrized_loop.py
+++ b/tests/async_fixtures/test_parametrized_loop.py
@@ -1,31 +1,46 @@
-import asyncio
+from textwrap import dedent
-import pytest
+from pytest import Pytester
-TESTS_COUNT = 0
+def test_event_loop_parametrization(pytester: Pytester):
+ pytester.makepyfile(
+ dedent(
+ """\
+ import asyncio
-def teardown_module():
- # parametrized 2 * 2 times: 2 for 'event_loop' and 2 for 'fix'
- assert TESTS_COUNT == 4
+ import pytest
+ import pytest_asyncio
+ TESTS_COUNT = 0
-@pytest.fixture(scope="module", params=[1, 2])
-def event_loop(request):
- request.param
- loop = asyncio.new_event_loop()
- yield loop
- loop.close()
+ def teardown_module():
+ # parametrized 2 * 2 times: 2 for 'event_loop' and 2 for 'fix'
+ assert TESTS_COUNT == 4
-@pytest.fixture(params=["a", "b"])
-async def fix(request):
- await asyncio.sleep(0)
- return request.param
+ @pytest.fixture(scope="module", params=[1, 2])
+ def event_loop(request):
+ request.param
+ loop = asyncio.new_event_loop()
+ yield loop
+ loop.close()
-@pytest.mark.asyncio
-async def test_parametrized_loop(fix):
- await asyncio.sleep(0)
- global TESTS_COUNT
- TESTS_COUNT += 1
+
+ @pytest_asyncio.fixture(params=["a", "b"])
+ async def fix(request):
+ await asyncio.sleep(0)
+ return request.param
+
+
+ @pytest.mark.asyncio
+ async def test_parametrized_loop(fix):
+ await asyncio.sleep(0)
+ global TESTS_COUNT
+ TESTS_COUNT += 1
+ """
+ )
+ )
+ result = pytester.runpytest_subprocess("--asyncio-mode=strict")
+ result.assert_outcomes(passed=4)
diff --git a/tests/hypothesis/test_base.py b/tests/hypothesis/test_base.py
index aef20d79..c2a7ea6a 100644
--- a/tests/hypothesis/test_base.py
+++ b/tests/hypothesis/test_base.py
@@ -8,10 +8,22 @@
from pytest import Pytester
-@given(st.integers())
-@pytest.mark.asyncio
-async def test_mark_inner(n):
- assert isinstance(n, int)
+def test_hypothesis_given_decorator_before_asyncio_mark(pytester: Pytester):
+ pytester.makepyfile(
+ dedent(
+ """\
+ import pytest
+ from hypothesis import given, strategies as st
+
+ @given(st.integers())
+ @pytest.mark.asyncio
+ async def test_mark_inner(n):
+ assert isinstance(n, int)
+ """
+ )
+ )
+ result = pytester.runpytest("--asyncio-mode=strict", "-W default")
+ result.assert_outcomes(passed=1)
@pytest.mark.asyncio
@@ -54,8 +66,14 @@ async def test_explicit_fixture_request(event_loop, n):
"""
)
)
- result = pytester.runpytest("--asyncio-mode=strict")
- result.assert_outcomes(passed=1)
+ result = pytester.runpytest("--asyncio-mode=strict", "-W default")
+ result.assert_outcomes(passed=1, warnings=2)
+ result.stdout.fnmatch_lines(
+ [
+ '*is asynchronous and explicitly requests the "event_loop" fixture*',
+ "*event_loop fixture provided by pytest-asyncio has been redefined*",
+ ]
+ )
def test_async_auto_marked(pytester: Pytester):
diff --git a/tests/hypothesis/test_inherited_test.py b/tests/hypothesis/test_inherited_test.py
deleted file mode 100644
index a7762264..00000000
--- a/tests/hypothesis/test_inherited_test.py
+++ /dev/null
@@ -1,20 +0,0 @@
-import hypothesis.strategies as st
-import pytest
-from hypothesis import given
-
-
-class BaseClass:
- @pytest.mark.asyncio
- @given(value=st.integers())
- async def test_hypothesis(self, value: int) -> None:
- pass
-
-
-class TestOne(BaseClass):
- """During the first execution the Hypothesis test
- is wrapped in a synchronous function."""
-
-
-class TestTwo(BaseClass):
- """Execute the test a second time to ensure that
- the test receives a fresh event loop."""
diff --git a/tests/loop_fixture_scope/conftest.py b/tests/loop_fixture_scope/conftest.py
index 223160c2..6b9a7649 100644
--- a/tests/loop_fixture_scope/conftest.py
+++ b/tests/loop_fixture_scope/conftest.py
@@ -7,11 +7,9 @@ class CustomSelectorLoop(asyncio.SelectorEventLoop):
"""A subclass with no overrides, just to test for presence."""
-loop = CustomSelectorLoop()
-
-
@pytest.fixture(scope="module")
def event_loop():
"""Create an instance of the default event loop for each test case."""
+ loop = CustomSelectorLoop()
yield loop
loop.close()
diff --git a/tests/markers/test_class_marker.py b/tests/markers/test_class_marker.py
deleted file mode 100644
index d46c3af7..00000000
--- a/tests/markers/test_class_marker.py
+++ /dev/null
@@ -1,25 +0,0 @@
-"""Test if pytestmark works when defined on a class."""
-import asyncio
-
-import pytest
-
-
-class TestPyTestMark:
- pytestmark = pytest.mark.asyncio
-
- 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
-
-
-@pytest.fixture
-def sample_fixture():
- return None
diff --git a/tests/markers/test_class_scope.py b/tests/markers/test_class_scope.py
new file mode 100644
index 00000000..fa2fe81e
--- /dev/null
+++ b/tests/markers/test_class_scope.py
@@ -0,0 +1,288 @@
+"""Test if pytestmark works when defined on a class."""
+import asyncio
+from textwrap import dedent
+
+import pytest
+
+
+class TestPyTestMark:
+ pytestmark = pytest.mark.asyncio
+
+ async def test_is_asyncio(self, 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
+
+
+def test_asyncio_mark_provides_class_scoped_loop_when_applied_to_functions(
+ pytester: pytest.Pytester,
+):
+ pytester.makepyfile(
+ dedent(
+ """\
+ import asyncio
+ import pytest
+
+ class TestClassScopedLoop:
+ loop: asyncio.AbstractEventLoop
+
+ @pytest.mark.asyncio(scope="class")
+ async def test_remember_loop(self):
+ TestClassScopedLoop.loop = asyncio.get_running_loop()
+
+ @pytest.mark.asyncio(scope="class")
+ async def test_this_runs_in_same_loop(self):
+ assert asyncio.get_running_loop() is TestClassScopedLoop.loop
+ """
+ )
+ )
+ result = pytester.runpytest("--asyncio-mode=strict")
+ result.assert_outcomes(passed=2)
+
+
+def test_asyncio_mark_provides_class_scoped_loop_when_applied_to_class(
+ pytester: pytest.Pytester,
+):
+ pytester.makepyfile(
+ dedent(
+ """\
+ import asyncio
+ import pytest
+
+ @pytest.mark.asyncio(scope="class")
+ class TestClassScopedLoop:
+ loop: asyncio.AbstractEventLoop
+
+ async def test_remember_loop(self):
+ TestClassScopedLoop.loop = asyncio.get_running_loop()
+
+ async def test_this_runs_in_same_loop(self):
+ assert asyncio.get_running_loop() is TestClassScopedLoop.loop
+ """
+ )
+ )
+ result = pytester.runpytest("--asyncio-mode=strict")
+ result.assert_outcomes(passed=2)
+
+
+def test_asyncio_mark_raises_when_class_scoped_is_request_without_class(
+ pytester: pytest.Pytester,
+):
+ pytester.makepyfile(
+ dedent(
+ """\
+ import asyncio
+ import pytest
+
+ @pytest.mark.asyncio(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.makepyfile(
+ dedent(
+ """\
+ import asyncio
+ import pytest
+
+ @pytest.mark.asyncio(scope="class")
+ class TestSuperClassWithMark:
+ pass
+
+ class TestWithoutMark(TestSuperClassWithMark):
+ loop: asyncio.AbstractEventLoop
+
+ async def test_remember_loop(self):
+ TestWithoutMark.loop = asyncio.get_running_loop()
+
+ async def test_this_runs_in_same_loop(self):
+ assert asyncio.get_running_loop() is TestWithoutMark.loop
+ """
+ )
+ )
+ result = pytester.runpytest("--asyncio-mode=strict")
+ result.assert_outcomes(passed=2)
+
+
+def test_asyncio_mark_respects_the_loop_policy(
+ pytester: pytest.Pytester,
+):
+ pytester.makepyfile(
+ dedent(
+ """\
+ import asyncio
+ import pytest
+
+ class CustomEventLoopPolicy(asyncio.DefaultEventLoopPolicy):
+ pass
+
+ class TestUsesCustomEventLoop:
+ @pytest.fixture(scope="class")
+ def event_loop_policy(self):
+ return CustomEventLoopPolicy()
+
+ @pytest.mark.asyncio
+ async def test_uses_custom_event_loop_policy(self):
+ assert isinstance(
+ asyncio.get_event_loop_policy(),
+ CustomEventLoopPolicy,
+ )
+
+ @pytest.mark.asyncio
+ async def test_does_not_use_custom_event_loop_policy():
+ assert not isinstance(
+ asyncio.get_event_loop_policy(),
+ CustomEventLoopPolicy,
+ )
+ """
+ )
+ )
+ result = pytester.runpytest("--asyncio-mode=strict")
+ result.assert_outcomes(passed=2)
+
+
+def test_asyncio_mark_respects_parametrized_loop_policies(
+ pytester: pytest.Pytester,
+):
+ pytester.makepyfile(
+ dedent(
+ """\
+ import asyncio
+
+ import pytest
+
+ @pytest.fixture(
+ scope="class",
+ params=[
+ asyncio.DefaultEventLoopPolicy(),
+ asyncio.DefaultEventLoopPolicy(),
+ ]
+ )
+ def event_loop_policy(request):
+ return request.param
+
+ @pytest.mark.asyncio(scope="class")
+ class TestWithDifferentLoopPolicies:
+ async def test_parametrized_loop(self, request):
+ pass
+ """
+ )
+ )
+ result = pytester.runpytest_subprocess("--asyncio-mode=strict")
+ result.assert_outcomes(passed=2)
+
+
+def test_asyncio_mark_provides_class_scoped_loop_to_fixtures(
+ pytester: pytest.Pytester,
+):
+ pytester.makepyfile(
+ dedent(
+ """\
+ import asyncio
+
+ import pytest
+ import pytest_asyncio
+
+ @pytest.mark.asyncio(scope="class")
+ class TestClassScopedLoop:
+ loop: asyncio.AbstractEventLoop
+
+ @pytest_asyncio.fixture
+ async def my_fixture(self):
+ TestClassScopedLoop.loop = asyncio.get_running_loop()
+
+ @pytest.mark.asyncio
+ async def test_runs_is_same_loop_as_fixture(self, my_fixture):
+ assert asyncio.get_running_loop() is TestClassScopedLoop.loop
+ """
+ )
+ )
+ result = pytester.runpytest_subprocess("--asyncio-mode=strict")
+ result.assert_outcomes(passed=1)
+
+
+def test_asyncio_mark_allows_combining_class_scoped_fixture_with_function_scoped_test(
+ pytester: pytest.Pytester,
+):
+ pytester.makepyfile(
+ dedent(
+ """\
+ import asyncio
+
+ import pytest
+ import pytest_asyncio
+
+ loop: asyncio.AbstractEventLoop
+
+ class TestMixedScopes:
+ @pytest_asyncio.fixture(scope="class")
+ async def async_fixture(self):
+ global loop
+ loop = asyncio.get_running_loop()
+
+ @pytest.mark.asyncio(scope="function")
+ async def test_runs_in_different_loop_as_fixture(self, async_fixture):
+ global loop
+ assert asyncio.get_running_loop() is not loop
+
+ """
+ ),
+ )
+ result = pytester.runpytest("--asyncio-mode=strict")
+ result.assert_outcomes(passed=1)
+
+
+def test_asyncio_mark_handles_missing_event_loop_triggered_by_fixture(
+ pytester: pytest.Pytester,
+):
+ pytester.makepyfile(
+ dedent(
+ """\
+ import pytest
+ import asyncio
+
+ class TestClass:
+ @pytest.fixture(scope="class")
+ def sets_event_loop_to_none(self):
+ # asyncio.run() creates a new event loop without closing the
+ # existing one. For any test, but the first one, this leads to
+ # a ResourceWarning when the discarded loop is destroyed by the
+ # garbage collector. We close the current loop to avoid this.
+ try:
+ asyncio.get_event_loop().close()
+ except RuntimeError:
+ pass
+ return asyncio.run(asyncio.sleep(0))
+ # asyncio.run() sets the current event loop to None when finished
+
+ @pytest.mark.asyncio(scope="class")
+ # parametrization may impact fixture ordering
+ @pytest.mark.parametrize("n", (0, 1))
+ async def test_does_not_fail(self, sets_event_loop_to_none, n):
+ pass
+ """
+ )
+ )
+ result = pytester.runpytest("--asyncio-mode=strict")
+ result.assert_outcomes(passed=2)
diff --git a/tests/markers/test_function_scope.py b/tests/markers/test_function_scope.py
new file mode 100644
index 00000000..25ff609f
--- /dev/null
+++ b/tests/markers/test_function_scope.py
@@ -0,0 +1,181 @@
+from textwrap import dedent
+
+from pytest import Pytester
+
+
+def test_asyncio_mark_provides_function_scoped_loop_strict_mode(pytester: Pytester):
+ pytester.makepyfile(
+ dedent(
+ """\
+ import asyncio
+ import pytest
+
+ pytestmark = pytest.mark.asyncio
+
+ loop: asyncio.AbstractEventLoop
+
+ async def test_remember_loop():
+ global loop
+ loop = asyncio.get_running_loop()
+
+ async def test_does_not_run_in_same_loop():
+ global loop
+ assert asyncio.get_running_loop() is not loop
+ """
+ )
+ )
+ result = pytester.runpytest("--asyncio-mode=strict")
+ result.assert_outcomes(passed=2)
+
+
+def test_function_scope_supports_explicit_event_loop_fixture_request(
+ pytester: Pytester,
+):
+ pytester.makepyfile(
+ dedent(
+ """\
+ import pytest
+
+ pytestmark = pytest.mark.asyncio
+
+ async def test_remember_loop(event_loop):
+ pass
+ """
+ )
+ )
+ result = pytester.runpytest("--asyncio-mode=strict", "-W default")
+ result.assert_outcomes(passed=1, warnings=1)
+ result.stdout.fnmatch_lines(
+ '*is asynchronous and explicitly requests the "event_loop" fixture*'
+ )
+
+
+def test_asyncio_mark_respects_the_loop_policy(
+ pytester: Pytester,
+):
+ pytester.makepyfile(
+ dedent(
+ """\
+ import asyncio
+ import pytest
+
+ pytestmark = pytest.mark.asyncio
+
+ class CustomEventLoopPolicy(asyncio.DefaultEventLoopPolicy):
+ pass
+
+ @pytest.fixture(scope="function")
+ def event_loop_policy():
+ return CustomEventLoopPolicy()
+
+ async def test_uses_custom_event_loop_policy():
+ assert isinstance(
+ asyncio.get_event_loop_policy(),
+ CustomEventLoopPolicy,
+ )
+ """
+ ),
+ )
+ result = pytester.runpytest("--asyncio-mode=strict")
+ result.assert_outcomes(passed=1)
+
+
+def test_asyncio_mark_respects_parametrized_loop_policies(
+ pytester: Pytester,
+):
+ pytester.makepyfile(
+ dedent(
+ """\
+ import asyncio
+
+ import pytest
+
+ pytestmark = pytest.mark.asyncio
+
+ class CustomEventLoopPolicy(asyncio.DefaultEventLoopPolicy):
+ pass
+
+ @pytest.fixture(
+ scope="module",
+ params=[
+ CustomEventLoopPolicy(),
+ CustomEventLoopPolicy(),
+ ],
+ )
+ def event_loop_policy(request):
+ return request.param
+
+ async def test_parametrized_loop():
+ assert isinstance(
+ asyncio.get_event_loop_policy(),
+ CustomEventLoopPolicy,
+ )
+ """
+ )
+ )
+ result = pytester.runpytest_subprocess("--asyncio-mode=strict")
+ result.assert_outcomes(passed=2)
+
+
+def test_asyncio_mark_provides_function_scoped_loop_to_fixtures(
+ pytester: Pytester,
+):
+ pytester.makepyfile(
+ dedent(
+ """\
+ import asyncio
+
+ import pytest
+ import pytest_asyncio
+
+ pytestmark = pytest.mark.asyncio
+
+ loop: asyncio.AbstractEventLoop
+
+ @pytest_asyncio.fixture
+ async def my_fixture():
+ global loop
+ loop = asyncio.get_running_loop()
+
+ async def test_runs_is_same_loop_as_fixture(my_fixture):
+ global loop
+ assert asyncio.get_running_loop() is loop
+ """
+ )
+ )
+ result = pytester.runpytest_subprocess("--asyncio-mode=strict")
+ result.assert_outcomes(passed=1)
+
+
+def test_asyncio_mark_handles_missing_event_loop_triggered_by_fixture(
+ pytester: Pytester,
+):
+ pytester.makepyfile(
+ dedent(
+ """\
+ import pytest
+ import asyncio
+
+ @pytest.fixture
+ def sets_event_loop_to_none():
+ # asyncio.run() creates a new event loop without closing the existing
+ # one. For any test, but the first one, this leads to a ResourceWarning
+ # when the discarded loop is destroyed by the garbage collector.
+ # We close the current loop to avoid this
+ try:
+ asyncio.get_event_loop().close()
+ except RuntimeError:
+ pass
+ return asyncio.run(asyncio.sleep(0))
+ # asyncio.run() sets the current event loop to None when finished
+
+ @pytest.mark.asyncio
+ # parametrization may impact fixture ordering
+ @pytest.mark.parametrize("n", (0, 1))
+ async def test_does_not_fail(sets_event_loop_to_none, n):
+ pass
+ """
+ )
+ )
+ result = pytester.runpytest("--asyncio-mode=strict")
+ result.assert_outcomes(passed=2)
diff --git a/tests/markers/test_module_marker.py b/tests/markers/test_module_marker.py
deleted file mode 100644
index 2f69dbc9..00000000
--- a/tests/markers/test_module_marker.py
+++ /dev/null
@@ -1,39 +0,0 @@
-"""Test if pytestmark works when defined in a module."""
-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
diff --git a/tests/markers/test_module_scope.py b/tests/markers/test_module_scope.py
new file mode 100644
index 00000000..cf6b2f60
--- /dev/null
+++ b/tests/markers/test_module_scope.py
@@ -0,0 +1,346 @@
+from textwrap import dedent
+
+from pytest import Pytester
+
+
+def test_asyncio_mark_works_on_module_level(pytester: Pytester):
+ 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("--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.makepyfile(
+ dedent(
+ """\
+ import asyncio
+ import pytest
+
+ pytestmark = pytest.mark.asyncio(scope="module")
+
+ loop: asyncio.AbstractEventLoop
+
+ async def test_remember_loop():
+ global loop
+ loop = asyncio.get_running_loop()
+
+ async def test_this_runs_in_same_loop():
+ global loop
+ assert asyncio.get_running_loop() is loop
+
+ class TestClassA:
+ async def test_this_runs_in_same_loop(self):
+ global loop
+ assert asyncio.get_running_loop() is loop
+ """
+ )
+ )
+ result = pytester.runpytest("--asyncio-mode=strict")
+ result.assert_outcomes(passed=3)
+
+
+def test_raise_when_event_loop_fixture_is_requested_in_addition_to_scoped_loop(
+ pytester: Pytester,
+):
+ pytester.makepyfile(
+ dedent(
+ """\
+ import asyncio
+ import pytest
+
+ pytestmark = pytest.mark.asyncio(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,
+):
+ pytester.makepyfile(
+ __init__="",
+ custom_policy=dedent(
+ """\
+ import asyncio
+
+ class CustomEventLoopPolicy(asyncio.DefaultEventLoopPolicy):
+ pass
+ """
+ ),
+ test_uses_custom_policy=dedent(
+ """\
+ import asyncio
+ import pytest
+
+ from .custom_policy import CustomEventLoopPolicy
+
+ pytestmark = pytest.mark.asyncio(scope="module")
+
+ @pytest.fixture(scope="module")
+ def event_loop_policy():
+ return CustomEventLoopPolicy()
+
+ async def test_uses_custom_event_loop_policy():
+ assert isinstance(
+ asyncio.get_event_loop_policy(),
+ CustomEventLoopPolicy,
+ )
+ """
+ ),
+ test_does_not_use_custom_policy=dedent(
+ """\
+ import asyncio
+ import pytest
+
+ from .custom_policy import CustomEventLoopPolicy
+
+ pytestmark = pytest.mark.asyncio(scope="module")
+
+ async def test_does_not_use_custom_event_loop_policy():
+ assert not isinstance(
+ asyncio.get_event_loop_policy(),
+ CustomEventLoopPolicy,
+ )
+ """
+ ),
+ )
+ result = pytester.runpytest("--asyncio-mode=strict")
+ result.assert_outcomes(passed=2)
+
+
+def test_asyncio_mark_respects_parametrized_loop_policies(
+ pytester: Pytester,
+):
+ pytester.makepyfile(
+ dedent(
+ """\
+ import asyncio
+
+ import pytest
+
+ pytestmark = pytest.mark.asyncio(scope="module")
+
+ @pytest.fixture(
+ scope="module",
+ params=[
+ asyncio.DefaultEventLoopPolicy(),
+ asyncio.DefaultEventLoopPolicy(),
+ ],
+ )
+ def event_loop_policy(request):
+ return request.param
+
+ async def test_parametrized_loop():
+ pass
+ """
+ )
+ )
+ result = pytester.runpytest_subprocess("--asyncio-mode=strict")
+ result.assert_outcomes(passed=2)
+
+
+def test_asyncio_mark_provides_module_scoped_loop_to_fixtures(
+ pytester: Pytester,
+):
+ pytester.makepyfile(
+ dedent(
+ """\
+ import asyncio
+
+ import pytest
+ import pytest_asyncio
+
+ pytestmark = pytest.mark.asyncio(scope="module")
+
+ loop: asyncio.AbstractEventLoop
+
+ @pytest_asyncio.fixture(scope="module")
+ async def my_fixture():
+ global loop
+ loop = asyncio.get_running_loop()
+
+ async def test_runs_is_same_loop_as_fixture(my_fixture):
+ global loop
+ assert asyncio.get_running_loop() is loop
+ """
+ )
+ )
+ result = pytester.runpytest_subprocess("--asyncio-mode=strict")
+ result.assert_outcomes(passed=1)
+
+
+def test_asyncio_mark_allows_combining_module_scoped_fixture_with_class_scoped_test(
+ pytester: Pytester,
+):
+ pytester.makepyfile(
+ dedent(
+ """\
+ import asyncio
+
+ import pytest
+ import pytest_asyncio
+
+ loop: asyncio.AbstractEventLoop
+
+ @pytest_asyncio.fixture(scope="module")
+ async def async_fixture():
+ global loop
+ loop = asyncio.get_running_loop()
+
+ @pytest.mark.asyncio(scope="class")
+ class TestMixedScopes:
+ async def test_runs_in_different_loop_as_fixture(self, async_fixture):
+ global loop
+ assert asyncio.get_running_loop() is not loop
+
+ """
+ ),
+ )
+ result = pytester.runpytest("--asyncio-mode=strict")
+ result.assert_outcomes(passed=1)
+
+
+def test_asyncio_mark_allows_combining_module_scoped_fixture_with_function_scoped_test(
+ pytester: Pytester,
+):
+ pytester.makepyfile(
+ __init__="",
+ test_mixed_scopes=dedent(
+ """\
+ import asyncio
+
+ import pytest
+ import pytest_asyncio
+
+ loop: asyncio.AbstractEventLoop
+
+ @pytest_asyncio.fixture(scope="module")
+ async def async_fixture():
+ global loop
+ loop = asyncio.get_running_loop()
+
+ @pytest.mark.asyncio(scope="function")
+ async def test_runs_in_different_loop_as_fixture(async_fixture):
+ global loop
+ assert asyncio.get_running_loop() is not loop
+ """
+ ),
+ )
+ result = pytester.runpytest("--asyncio-mode=strict")
+ result.assert_outcomes(passed=1)
+
+
+def test_allows_combining_module_scoped_asyncgen_fixture_with_function_scoped_test(
+ pytester: Pytester,
+):
+ pytester.makepyfile(
+ dedent(
+ """\
+ import asyncio
+
+ import pytest
+ import pytest_asyncio
+
+ loop: asyncio.AbstractEventLoop
+
+ @pytest_asyncio.fixture(scope="module")
+ async def async_fixture():
+ global loop
+ loop = asyncio.get_running_loop()
+ yield
+
+ @pytest.mark.asyncio(scope="function")
+ async def test_runs_in_different_loop_as_fixture(async_fixture):
+ global loop
+ assert asyncio.get_running_loop() is not loop
+ """
+ ),
+ )
+ result = pytester.runpytest("--asyncio-mode=strict")
+ result.assert_outcomes(passed=1)
+
+
+def test_asyncio_mark_handles_missing_event_loop_triggered_by_fixture(
+ pytester: Pytester,
+):
+ pytester.makepyfile(
+ dedent(
+ """\
+ import pytest
+ import asyncio
+
+ @pytest.fixture(scope="module")
+ def sets_event_loop_to_none():
+ # asyncio.run() creates a new event loop without closing the existing
+ # one. For any test, but the first one, this leads to a ResourceWarning
+ # when the discarded loop is destroyed by the garbage collector.
+ # We close the current loop to avoid this
+ try:
+ asyncio.get_event_loop().close()
+ except RuntimeError:
+ pass
+ return asyncio.run(asyncio.sleep(0))
+ # asyncio.run() sets the current event loop to None when finished
+
+ @pytest.mark.asyncio(scope="module")
+ # parametrization may impact fixture ordering
+ @pytest.mark.parametrize("n", (0, 1))
+ async def test_does_not_fail(sets_event_loop_to_none, n):
+ pass
+ """
+ )
+ )
+ result = pytester.runpytest("--asyncio-mode=strict")
+ result.assert_outcomes(passed=2)
diff --git a/tests/markers/test_package_scope.py b/tests/markers/test_package_scope.py
new file mode 100644
index 00000000..1dc8a5c9
--- /dev/null
+++ b/tests/markers/test_package_scope.py
@@ -0,0 +1,351 @@
+from textwrap import dedent
+
+from pytest import Pytester
+
+
+def test_asyncio_mark_provides_package_scoped_loop_strict_mode(pytester: Pytester):
+ package_name = pytester.path.name
+ subpackage_name = "subpkg"
+ pytester.makepyfile(
+ __init__="",
+ shared_module=dedent(
+ """\
+ import asyncio
+
+ loop: asyncio.AbstractEventLoop = None
+ """
+ ),
+ test_module_one=dedent(
+ f"""\
+ import asyncio
+ import pytest
+
+ from {package_name} import shared_module
+
+ @pytest.mark.asyncio(scope="package")
+ async def test_remember_loop():
+ shared_module.loop = asyncio.get_running_loop()
+ """
+ ),
+ test_module_two=dedent(
+ f"""\
+ import asyncio
+ import pytest
+
+ from {package_name} import shared_module
+
+ pytestmark = pytest.mark.asyncio(scope="package")
+
+ async def test_this_runs_in_same_loop():
+ assert asyncio.get_running_loop() is shared_module.loop
+
+ class TestClassA:
+ async def test_this_runs_in_same_loop(self):
+ assert asyncio.get_running_loop() is shared_module.loop
+ """
+ ),
+ )
+ subpkg = pytester.mkpydir(subpackage_name)
+ subpkg.joinpath("test_subpkg.py").write_text(
+ dedent(
+ f"""\
+ import asyncio
+ import pytest
+
+ from {package_name} import shared_module
+
+ pytestmark = pytest.mark.asyncio(scope="package")
+
+ async def test_subpackage_runs_in_different_loop():
+ assert asyncio.get_running_loop() is not shared_module.loop
+ """
+ )
+ )
+ result = pytester.runpytest("--asyncio-mode=strict")
+ result.assert_outcomes(passed=4)
+
+
+def test_raise_when_event_loop_fixture_is_requested_in_addition_to_scoped_loop(
+ pytester: Pytester,
+):
+ pytester.makepyfile(
+ __init__="",
+ test_raises=dedent(
+ """\
+ import asyncio
+ import pytest
+
+ @pytest.mark.asyncio(scope="package")
+ async def test_remember_loop(event_loop):
+ pass
+ """
+ ),
+ )
+ result = pytester.runpytest("--asyncio-mode=strict")
+ result.assert_outcomes(errors=1)
+ result.stdout.fnmatch_lines("*MultipleEventLoopsRequestedError: *")
+
+
+def test_asyncio_mark_respects_the_loop_policy(
+ pytester: Pytester,
+):
+ pytester.makepyfile(
+ __init__="",
+ conftest=dedent(
+ """\
+ import pytest
+
+ from .custom_policy import CustomEventLoopPolicy
+
+ @pytest.fixture(scope="package")
+ def event_loop_policy():
+ return CustomEventLoopPolicy()
+ """
+ ),
+ custom_policy=dedent(
+ """\
+ import asyncio
+
+ class CustomEventLoopPolicy(asyncio.DefaultEventLoopPolicy):
+ pass
+ """
+ ),
+ test_uses_custom_policy=dedent(
+ """\
+ import asyncio
+ import pytest
+
+ from .custom_policy import CustomEventLoopPolicy
+
+ pytestmark = pytest.mark.asyncio(scope="package")
+
+ async def test_uses_custom_event_loop_policy():
+ assert isinstance(
+ asyncio.get_event_loop_policy(),
+ CustomEventLoopPolicy,
+ )
+ """
+ ),
+ test_also_uses_custom_policy=dedent(
+ """\
+ import asyncio
+ import pytest
+
+ from .custom_policy import CustomEventLoopPolicy
+
+ pytestmark = pytest.mark.asyncio(scope="package")
+
+ async def test_also_uses_custom_event_loop_policy():
+ assert isinstance(
+ asyncio.get_event_loop_policy(),
+ CustomEventLoopPolicy,
+ )
+ """
+ ),
+ )
+ result = pytester.runpytest("--asyncio-mode=strict")
+ result.assert_outcomes(passed=2)
+
+
+def test_asyncio_mark_respects_parametrized_loop_policies(
+ pytester: Pytester,
+):
+ pytester.makepyfile(
+ __init__="",
+ test_parametrization=dedent(
+ """\
+ import asyncio
+
+ import pytest
+
+ pytestmark = pytest.mark.asyncio(scope="package")
+
+ @pytest.fixture(
+ scope="package",
+ params=[
+ asyncio.DefaultEventLoopPolicy(),
+ asyncio.DefaultEventLoopPolicy(),
+ ],
+ )
+ def event_loop_policy(request):
+ return request.param
+
+ async def test_parametrized_loop():
+ pass
+ """
+ ),
+ )
+ result = pytester.runpytest_subprocess("--asyncio-mode=strict")
+ result.assert_outcomes(passed=2)
+
+
+def test_asyncio_mark_provides_package_scoped_loop_to_fixtures(
+ pytester: Pytester,
+):
+ package_name = pytester.path.name
+ pytester.makepyfile(
+ __init__="",
+ conftest=dedent(
+ f"""\
+ import asyncio
+
+ import pytest_asyncio
+
+ from {package_name} import shared_module
+
+ @pytest_asyncio.fixture(scope="package")
+ async def my_fixture():
+ shared_module.loop = asyncio.get_running_loop()
+ """
+ ),
+ shared_module=dedent(
+ """\
+ import asyncio
+
+ loop: asyncio.AbstractEventLoop = None
+ """
+ ),
+ test_fixture_runs_in_scoped_loop=dedent(
+ f"""\
+ import asyncio
+
+ import pytest
+ import pytest_asyncio
+
+ from {package_name} import shared_module
+
+ pytestmark = pytest.mark.asyncio(scope="package")
+
+ async def test_runs_in_same_loop_as_fixture(my_fixture):
+ assert asyncio.get_running_loop() is shared_module.loop
+ """
+ ),
+ )
+ result = pytester.runpytest_subprocess("--asyncio-mode=strict")
+ result.assert_outcomes(passed=1)
+
+
+def test_asyncio_mark_allows_combining_package_scoped_fixture_with_module_scoped_test(
+ pytester: Pytester,
+):
+ pytester.makepyfile(
+ __init__="",
+ test_mixed_scopes=dedent(
+ """\
+ import asyncio
+
+ import pytest
+ import pytest_asyncio
+
+ loop: asyncio.AbstractEventLoop
+
+ @pytest_asyncio.fixture(scope="package")
+ async def async_fixture():
+ global loop
+ loop = asyncio.get_running_loop()
+
+ @pytest.mark.asyncio(scope="module")
+ async def test_runs_in_different_loop_as_fixture(async_fixture):
+ global loop
+ assert asyncio.get_running_loop() is not loop
+ """
+ ),
+ )
+ result = pytester.runpytest("--asyncio-mode=strict")
+ result.assert_outcomes(passed=1)
+
+
+def test_asyncio_mark_allows_combining_package_scoped_fixture_with_class_scoped_test(
+ pytester: Pytester,
+):
+ pytester.makepyfile(
+ __init__="",
+ test_mixed_scopes=dedent(
+ """\
+ import asyncio
+
+ import pytest
+ import pytest_asyncio
+
+ loop: asyncio.AbstractEventLoop
+
+ @pytest_asyncio.fixture(scope="package")
+ async def async_fixture():
+ global loop
+ loop = asyncio.get_running_loop()
+
+ @pytest.mark.asyncio(scope="class")
+ class TestMixedScopes:
+ async def test_runs_in_different_loop_as_fixture(self, async_fixture):
+ global loop
+ assert asyncio.get_running_loop() is not loop
+ """
+ ),
+ )
+ result = pytester.runpytest("--asyncio-mode=strict")
+ result.assert_outcomes(passed=1)
+
+
+def test_asyncio_mark_allows_combining_package_scoped_fixture_with_function_scoped_test(
+ pytester: Pytester,
+):
+ pytester.makepyfile(
+ __init__="",
+ test_mixed_scopes=dedent(
+ """\
+ import asyncio
+
+ import pytest
+ import pytest_asyncio
+
+ loop: asyncio.AbstractEventLoop
+
+ @pytest_asyncio.fixture(scope="package")
+ async def async_fixture():
+ global loop
+ loop = asyncio.get_running_loop()
+
+ @pytest.mark.asyncio
+ async def test_runs_in_different_loop_as_fixture(async_fixture):
+ global loop
+ assert asyncio.get_running_loop() is not loop
+ """
+ ),
+ )
+ result = pytester.runpytest("--asyncio-mode=strict")
+ result.assert_outcomes(passed=1)
+
+
+def test_asyncio_mark_handles_missing_event_loop_triggered_by_fixture(
+ pytester: Pytester,
+):
+ pytester.makepyfile(
+ __init__="",
+ test_loop_is_none=dedent(
+ """\
+ import pytest
+ import asyncio
+
+ @pytest.fixture(scope="package")
+ def sets_event_loop_to_none():
+ # asyncio.run() creates a new event loop without closing the existing
+ # one. For any test, but the first one, this leads to a ResourceWarning
+ # when the discarded loop is destroyed by the garbage collector.
+ # We close the current loop to avoid this
+ try:
+ asyncio.get_event_loop().close()
+ except RuntimeError:
+ pass
+ return asyncio.run(asyncio.sleep(0))
+ # asyncio.run() sets the current event loop to None when finished
+
+ @pytest.mark.asyncio(scope="package")
+ # parametrization may impact fixture ordering
+ @pytest.mark.parametrize("n", (0, 1))
+ async def test_does_not_fail(sets_event_loop_to_none, n):
+ pass
+ """
+ ),
+ )
+ result = pytester.runpytest("--asyncio-mode=strict")
+ result.assert_outcomes(passed=2)
diff --git a/tests/markers/test_session_scope.py b/tests/markers/test_session_scope.py
new file mode 100644
index 00000000..bd0baee5
--- /dev/null
+++ b/tests/markers/test_session_scope.py
@@ -0,0 +1,415 @@
+from textwrap import dedent
+
+from pytest import Pytester
+
+
+def test_asyncio_mark_provides_session_scoped_loop_strict_mode(pytester: Pytester):
+ package_name = pytester.path.name
+ pytester.makepyfile(
+ __init__="",
+ shared_module=dedent(
+ """\
+ import asyncio
+
+ loop: asyncio.AbstractEventLoop = None
+ """
+ ),
+ test_module_one=dedent(
+ f"""\
+ import asyncio
+ import pytest
+
+ from {package_name} import shared_module
+
+ @pytest.mark.asyncio(scope="session")
+ async def test_remember_loop():
+ shared_module.loop = asyncio.get_running_loop()
+ """
+ ),
+ test_module_two=dedent(
+ f"""\
+ import asyncio
+ import pytest
+
+ from {package_name} import shared_module
+
+ pytestmark = pytest.mark.asyncio(scope="session")
+
+ async def test_this_runs_in_same_loop():
+ assert asyncio.get_running_loop() is shared_module.loop
+
+ class TestClassA:
+ async def test_this_runs_in_same_loop(self):
+ assert asyncio.get_running_loop() is shared_module.loop
+ """
+ ),
+ )
+ subpackage_name = "subpkg"
+ subpkg = pytester.mkpydir(subpackage_name)
+ subpkg.joinpath("test_subpkg.py").write_text(
+ dedent(
+ f"""\
+ import asyncio
+ import pytest
+
+ from {package_name} import shared_module
+
+ pytestmark = pytest.mark.asyncio(scope="session")
+
+ async def test_subpackage_runs_in_same_loop():
+ assert asyncio.get_running_loop() is shared_module.loop
+ """
+ )
+ )
+ result = pytester.runpytest("--asyncio-mode=strict")
+ result.assert_outcomes(passed=4)
+
+
+def test_raise_when_event_loop_fixture_is_requested_in_addition_to_scoped_loop(
+ pytester: Pytester,
+):
+ pytester.makepyfile(
+ __init__="",
+ test_raises=dedent(
+ """\
+ import asyncio
+ import pytest
+
+ @pytest.mark.asyncio(scope="session")
+ async def test_remember_loop(event_loop):
+ pass
+ """
+ ),
+ )
+ result = pytester.runpytest("--asyncio-mode=strict")
+ result.assert_outcomes(errors=1)
+ result.stdout.fnmatch_lines("*MultipleEventLoopsRequestedError: *")
+
+
+def test_asyncio_mark_respects_the_loop_policy(
+ pytester: Pytester,
+):
+ pytester.makepyfile(
+ __init__="",
+ conftest=dedent(
+ """\
+ import pytest
+
+ from .custom_policy import CustomEventLoopPolicy
+
+ @pytest.fixture(scope="session")
+ def event_loop_policy():
+ return CustomEventLoopPolicy()
+ """
+ ),
+ custom_policy=dedent(
+ """\
+ import asyncio
+
+ class CustomEventLoopPolicy(asyncio.DefaultEventLoopPolicy):
+ pass
+ """
+ ),
+ test_uses_custom_policy=dedent(
+ """\
+ import asyncio
+ import pytest
+
+ from .custom_policy import CustomEventLoopPolicy
+
+ pytestmark = pytest.mark.asyncio(scope="session")
+
+ async def test_uses_custom_event_loop_policy():
+ assert isinstance(
+ asyncio.get_event_loop_policy(),
+ CustomEventLoopPolicy,
+ )
+ """
+ ),
+ test_also_uses_custom_policy=dedent(
+ """\
+ import asyncio
+ import pytest
+
+ from .custom_policy import CustomEventLoopPolicy
+
+ pytestmark = pytest.mark.asyncio(scope="session")
+
+ async def test_also_uses_custom_event_loop_policy():
+ assert isinstance(
+ asyncio.get_event_loop_policy(),
+ CustomEventLoopPolicy,
+ )
+ """
+ ),
+ )
+ result = pytester.runpytest("--asyncio-mode=strict")
+ result.assert_outcomes(passed=2)
+
+
+def test_asyncio_mark_respects_parametrized_loop_policies(
+ pytester: Pytester,
+):
+ pytester.makepyfile(
+ __init__="",
+ test_parametrization=dedent(
+ """\
+ import asyncio
+
+ import pytest
+
+ pytestmark = pytest.mark.asyncio(scope="session")
+
+ @pytest.fixture(
+ scope="session",
+ params=[
+ asyncio.DefaultEventLoopPolicy(),
+ asyncio.DefaultEventLoopPolicy(),
+ ],
+ )
+ def event_loop_policy(request):
+ return request.param
+
+ async def test_parametrized_loop():
+ pass
+ """
+ ),
+ )
+ result = pytester.runpytest_subprocess("--asyncio-mode=strict")
+ result.assert_outcomes(passed=2)
+
+
+def test_asyncio_mark_provides_session_scoped_loop_to_fixtures(
+ pytester: Pytester,
+):
+ package_name = pytester.path.name
+ pytester.makepyfile(
+ __init__="",
+ conftest=dedent(
+ f"""\
+ import asyncio
+
+ import pytest_asyncio
+
+ from {package_name} import shared_module
+
+ @pytest_asyncio.fixture(scope="session")
+ async def my_fixture():
+ shared_module.loop = asyncio.get_running_loop()
+ """
+ ),
+ shared_module=dedent(
+ """\
+ import asyncio
+
+ loop: asyncio.AbstractEventLoop = None
+ """
+ ),
+ )
+ subpackage_name = "subpkg"
+ subpkg = pytester.mkpydir(subpackage_name)
+ subpkg.joinpath("test_subpkg.py").write_text(
+ dedent(
+ f"""\
+ import asyncio
+
+ import pytest
+ import pytest_asyncio
+
+ from {package_name} import shared_module
+
+ pytestmark = pytest.mark.asyncio(scope="session")
+
+ async def test_runs_in_same_loop_as_fixture(my_fixture):
+ assert asyncio.get_running_loop() is shared_module.loop
+ """
+ )
+ )
+ result = pytester.runpytest_subprocess("--asyncio-mode=strict")
+ result.assert_outcomes(passed=1)
+
+
+def test_asyncio_mark_allows_combining_session_scoped_fixture_with_package_scoped_test(
+ pytester: Pytester,
+):
+ pytester.makepyfile(
+ __init__="",
+ test_mixed_scopes=dedent(
+ """\
+ import asyncio
+
+ import pytest
+ import pytest_asyncio
+
+ loop: asyncio.AbstractEventLoop
+
+ @pytest_asyncio.fixture(scope="session")
+ async def async_fixture():
+ global loop
+ loop = asyncio.get_running_loop()
+
+ @pytest.mark.asyncio(scope="package")
+ async def test_runs_in_different_loop_as_fixture(async_fixture):
+ global loop
+ assert asyncio.get_running_loop() is not loop
+ """
+ ),
+ )
+ result = pytester.runpytest("--asyncio-mode=strict")
+ result.assert_outcomes(passed=1)
+
+
+def test_asyncio_mark_allows_combining_session_scoped_fixture_with_module_scoped_test(
+ pytester: Pytester,
+):
+ pytester.makepyfile(
+ __init__="",
+ test_mixed_scopes=dedent(
+ """\
+ import asyncio
+
+ import pytest
+ import pytest_asyncio
+
+ loop: asyncio.AbstractEventLoop
+
+ @pytest_asyncio.fixture(scope="session")
+ async def async_fixture():
+ global loop
+ loop = asyncio.get_running_loop()
+
+ @pytest.mark.asyncio(scope="module")
+ async def test_runs_in_different_loop_as_fixture(async_fixture):
+ global loop
+ assert asyncio.get_running_loop() is not loop
+ """
+ ),
+ )
+ result = pytester.runpytest("--asyncio-mode=strict")
+ result.assert_outcomes(passed=1)
+
+
+def test_asyncio_mark_allows_combining_session_scoped_fixture_with_class_scoped_test(
+ pytester: Pytester,
+):
+ pytester.makepyfile(
+ __init__="",
+ test_mixed_scopes=dedent(
+ """\
+ import asyncio
+
+ import pytest
+ import pytest_asyncio
+
+ loop: asyncio.AbstractEventLoop
+
+ @pytest_asyncio.fixture(scope="session")
+ async def async_fixture():
+ global loop
+ loop = asyncio.get_running_loop()
+
+ @pytest.mark.asyncio(scope="class")
+ class TestMixedScopes:
+ async def test_runs_in_different_loop_as_fixture(self, async_fixture):
+ global loop
+ assert asyncio.get_running_loop() is not loop
+ """
+ ),
+ )
+ result = pytester.runpytest("--asyncio-mode=strict")
+ result.assert_outcomes(passed=1)
+
+
+def test_asyncio_mark_allows_combining_session_scoped_fixture_with_function_scoped_test(
+ pytester: Pytester,
+):
+ pytester.makepyfile(
+ __init__="",
+ test_mixed_scopes=dedent(
+ """\
+ import asyncio
+
+ import pytest
+ import pytest_asyncio
+
+ loop: asyncio.AbstractEventLoop
+
+ @pytest_asyncio.fixture(scope="session")
+ async def async_fixture():
+ global loop
+ loop = asyncio.get_running_loop()
+
+ @pytest.mark.asyncio
+ async def test_runs_in_different_loop_as_fixture(async_fixture):
+ global loop
+ assert asyncio.get_running_loop() is not loop
+ """
+ ),
+ )
+ result = pytester.runpytest("--asyncio-mode=strict")
+ result.assert_outcomes(passed=1)
+
+
+def test_allows_combining_session_scoped_asyncgen_fixture_with_function_scoped_test(
+ pytester: Pytester,
+):
+ pytester.makepyfile(
+ __init__="",
+ test_mixed_scopes=dedent(
+ """\
+ import asyncio
+
+ import pytest
+ import pytest_asyncio
+
+ loop: asyncio.AbstractEventLoop
+
+ @pytest_asyncio.fixture(scope="session")
+ async def async_fixture():
+ global loop
+ loop = asyncio.get_running_loop()
+ yield
+
+ @pytest.mark.asyncio
+ async def test_runs_in_different_loop_as_fixture(async_fixture):
+ global loop
+ assert asyncio.get_running_loop() is not loop
+ """
+ ),
+ )
+ result = pytester.runpytest("--asyncio-mode=strict")
+ result.assert_outcomes(passed=1)
+
+
+def test_asyncio_mark_handles_missing_event_loop_triggered_by_fixture(
+ pytester: Pytester,
+):
+ pytester.makepyfile(
+ dedent(
+ """\
+ import pytest
+ import asyncio
+
+ @pytest.fixture(scope="session")
+ def sets_event_loop_to_none():
+ # asyncio.run() creates a new event loop without closing the existing
+ # one. For any test, but the first one, this leads to a ResourceWarning
+ # when the discarded loop is destroyed by the garbage collector.
+ # We close the current loop to avoid this
+ try:
+ asyncio.get_event_loop().close()
+ except RuntimeError:
+ pass
+ return asyncio.run(asyncio.sleep(0))
+ # asyncio.run() sets the current event loop to None when finished
+
+ @pytest.mark.asyncio(scope="session")
+ # parametrization may impact fixture ordering
+ @pytest.mark.parametrize("n", (0, 1))
+ async def test_does_not_fail(sets_event_loop_to_none, n):
+ pass
+ """
+ )
+ )
+ result = pytester.runpytest("--asyncio-mode=strict")
+ result.assert_outcomes(passed=2)
diff --git a/tests/modes/test_strict_mode.py b/tests/modes/test_strict_mode.py
index 3b6487c7..3afc9f5b 100644
--- a/tests/modes/test_strict_mode.py
+++ b/tests/modes/test_strict_mode.py
@@ -66,3 +66,45 @@ async def test_a(self, fixture_a):
)
result = testdir.runpytest("--asyncio-mode=auto")
result.assert_outcomes(passed=1)
+
+
+def test_strict_mode_ignores_unmarked_coroutine(testdir):
+ testdir.makepyfile(
+ dedent(
+ """\
+ import pytest
+
+ async def test_anything():
+ pass
+ """
+ )
+ )
+ result = testdir.runpytest("--asyncio-mode=strict", "-W default")
+ result.assert_outcomes(skipped=1, warnings=1)
+ result.stdout.fnmatch_lines(["*async def functions are not natively supported*"])
+
+
+def test_strict_mode_ignores_unmarked_fixture(testdir):
+ testdir.makepyfile(
+ dedent(
+ """\
+ import pytest
+
+ # Not using pytest_asyncio.fixture
+ @pytest.fixture()
+ async def any_fixture():
+ raise RuntimeError()
+
+ async def test_anything(any_fixture):
+ pass
+ """
+ )
+ )
+ result = testdir.runpytest("--asyncio-mode=strict", "-W default")
+ result.assert_outcomes(skipped=1, warnings=2)
+ result.stdout.fnmatch_lines(
+ [
+ "*async def functions are not natively supported*",
+ "*coroutine 'any_fixture' was never awaited*",
+ ],
+ )
diff --git a/tests/multiloop/conftest.py b/tests/multiloop/conftest.py
deleted file mode 100644
index ebcb627a..00000000
--- a/tests/multiloop/conftest.py
+++ /dev/null
@@ -1,15 +0,0 @@
-import asyncio
-
-import pytest
-
-
-class CustomSelectorLoop(asyncio.SelectorEventLoop):
- """A subclass with no overrides, just to test for presence."""
-
-
-@pytest.fixture
-def event_loop():
- """Create an instance of the default event loop for each test case."""
- loop = CustomSelectorLoop()
- yield loop
- loop.close()
diff --git a/tests/multiloop/test_alternative_loops.py b/tests/multiloop/test_alternative_loops.py
deleted file mode 100644
index 5f66c967..00000000
--- a/tests/multiloop/test_alternative_loops.py
+++ /dev/null
@@ -1,16 +0,0 @@
-"""Unit tests for overriding the event loop."""
-import asyncio
-
-import pytest
-
-
-@pytest.mark.asyncio
-async def test_for_custom_loop():
- """This test should be executed using the custom loop."""
- await asyncio.sleep(0.01)
- assert type(asyncio.get_event_loop()).__name__ == "CustomSelectorLoop"
-
-
-@pytest.mark.asyncio
-async def test_dependent_fixture(dependent_fixture):
- await asyncio.sleep(0.1)
diff --git a/tests/test_asyncio_mark.py b/tests/test_asyncio_mark.py
new file mode 100644
index 00000000..b514cbcd
--- /dev/null
+++ b/tests/test_asyncio_mark.py
@@ -0,0 +1,146 @@
+from textwrap import dedent
+
+from pytest import Pytester
+
+
+def test_asyncio_mark_on_sync_function_emits_warning(pytester: Pytester):
+ pytester.makepyfile(
+ dedent(
+ """\
+ import pytest
+
+ @pytest.mark.asyncio
+ def test_a():
+ pass
+ """
+ )
+ )
+ result = pytester.runpytest("--asyncio-mode=strict", "-W default")
+ result.assert_outcomes(passed=1)
+ result.stdout.fnmatch_lines(
+ ["*is marked with '@pytest.mark.asyncio' but it is not an async function.*"]
+ )
+
+
+def test_asyncio_mark_on_async_generator_function_emits_warning_in_strict_mode(
+ pytester: Pytester,
+):
+ pytester.makepyfile(
+ dedent(
+ """\
+ import pytest
+
+ @pytest.mark.asyncio
+ async def test_a():
+ yield
+ """
+ )
+ )
+ result = pytester.runpytest("--asyncio-mode=strict", "-W default")
+ result.assert_outcomes(xfailed=1, warnings=1)
+ result.stdout.fnmatch_lines(
+ ["*Tests based on asynchronous generators are not supported*"]
+ )
+
+
+def test_asyncio_mark_on_async_generator_function_emits_warning_in_auto_mode(
+ pytester: Pytester,
+):
+ pytester.makepyfile(
+ dedent(
+ """\
+ async def test_a():
+ yield
+ """
+ )
+ )
+ result = pytester.runpytest("--asyncio-mode=auto", "-W default")
+ result.assert_outcomes(xfailed=1, warnings=1)
+ result.stdout.fnmatch_lines(
+ ["*Tests based on asynchronous generators are not supported*"]
+ )
+
+
+def test_asyncio_mark_on_async_generator_method_emits_warning_in_strict_mode(
+ pytester: Pytester,
+):
+ pytester.makepyfile(
+ dedent(
+ """\
+ import pytest
+
+ class TestAsyncGenerator:
+ @pytest.mark.asyncio
+ async def test_a(self):
+ yield
+ """
+ )
+ )
+ result = pytester.runpytest("--asyncio-mode=strict", "-W default")
+ result.assert_outcomes(xfailed=1, warnings=1)
+ result.stdout.fnmatch_lines(
+ ["*Tests based on asynchronous generators are not supported*"]
+ )
+
+
+def test_asyncio_mark_on_async_generator_method_emits_warning_in_auto_mode(
+ pytester: Pytester,
+):
+ pytester.makepyfile(
+ dedent(
+ """\
+ class TestAsyncGenerator:
+ @staticmethod
+ async def test_a():
+ yield
+ """
+ )
+ )
+ result = pytester.runpytest("--asyncio-mode=auto", "-W default")
+ result.assert_outcomes(xfailed=1, warnings=1)
+ result.stdout.fnmatch_lines(
+ ["*Tests based on asynchronous generators are not supported*"]
+ )
+
+
+def test_asyncio_mark_on_async_generator_staticmethod_emits_warning_in_strict_mode(
+ pytester: Pytester,
+):
+ pytester.makepyfile(
+ dedent(
+ """\
+ import pytest
+
+ class TestAsyncGenerator:
+ @staticmethod
+ @pytest.mark.asyncio
+ async def test_a():
+ yield
+ """
+ )
+ )
+ result = pytester.runpytest("--asyncio-mode=strict", "-W default")
+ result.assert_outcomes(xfailed=1, warnings=1)
+ result.stdout.fnmatch_lines(
+ ["*Tests based on asynchronous generators are not supported*"]
+ )
+
+
+def test_asyncio_mark_on_async_generator_staticmethod_emits_warning_in_auto_mode(
+ pytester: Pytester,
+):
+ pytester.makepyfile(
+ dedent(
+ """\
+ class TestAsyncGenerator:
+ @staticmethod
+ async def test_a():
+ yield
+ """
+ )
+ )
+ result = pytester.runpytest("--asyncio-mode=auto", "-W default")
+ result.assert_outcomes(xfailed=1, warnings=1)
+ result.stdout.fnmatch_lines(
+ ["*Tests based on asynchronous generators are not supported*"]
+ )
diff --git a/tests/test_doctest.py b/tests/test_doctest.py
new file mode 100644
index 00000000..5b79619a
--- /dev/null
+++ b/tests/test_doctest.py
@@ -0,0 +1,39 @@
+from textwrap import dedent
+
+from pytest import Pytester
+
+
+def test_plugin_does_not_interfere_with_doctest_collection(pytester: Pytester):
+ pytester.makepyfile(
+ dedent(
+ '''\
+ def any_function():
+ """
+ >>> 42
+ 42
+ """
+ '''
+ ),
+ )
+ result = pytester.runpytest("--asyncio-mode=strict", "--doctest-modules")
+ result.assert_outcomes(passed=1)
+
+
+def test_plugin_does_not_interfere_with_doctest_textfile_collection(pytester: Pytester):
+ pytester.makefile(".txt", "") # collected as DoctestTextfile
+ pytester.makepyfile(
+ __init__="",
+ test_python_file=dedent(
+ """\
+ import pytest
+
+ pytest_plugins = "pytest_asyncio"
+
+ @pytest.mark.asyncio
+ async def test_anything():
+ pass
+ """
+ ),
+ )
+ result = pytester.runpytest("--asyncio-mode=strict")
+ result.assert_outcomes(passed=1)
diff --git a/tests/test_event_loop_fixture_finalizer.py b/tests/test_event_loop_fixture_finalizer.py
index b676df2d..eabb54a3 100644
--- a/tests/test_event_loop_fixture_finalizer.py
+++ b/tests/test_event_loop_fixture_finalizer.py
@@ -84,8 +84,11 @@ async def test_async_with_explicit_fixture_request(event_loop):
"""
)
)
- result = pytester.runpytest("--asyncio-mode=strict")
- result.assert_outcomes(passed=1)
+ result = pytester.runpytest("--asyncio-mode=strict", "-W default")
+ result.assert_outcomes(passed=1, warnings=1)
+ result.stdout.fnmatch_lines(
+ '*is asynchronous and explicitly requests the "event_loop" fixture*'
+ )
def test_event_loop_fixture_finalizer_raises_warning_when_fixture_leaves_loop_unclosed(
@@ -111,7 +114,7 @@ async def test_ends_with_unclosed_loop():
)
)
result = pytester.runpytest("--asyncio-mode=strict", "-W", "default")
- result.assert_outcomes(passed=1, warnings=1)
+ result.assert_outcomes(passed=1, warnings=2)
result.stdout.fnmatch_lines("*unclosed event loop*")
diff --git a/tests/test_event_loop_fixture_override_deprecation.py b/tests/test_event_loop_fixture_override_deprecation.py
new file mode 100644
index 00000000..45afc542
--- /dev/null
+++ b/tests/test_event_loop_fixture_override_deprecation.py
@@ -0,0 +1,111 @@
+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("--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("--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("--asyncio-mode=strict")
+ result.assert_outcomes(passed=1, warnings=0)
+
+
+def test_emit_warning_when_redefined_event_loop_is_used_by_fixture(pytester: Pytester):
+ pytester.makepyfile(
+ dedent(
+ """\
+ import asyncio
+ import pytest
+ import pytest_asyncio
+
+ @pytest.fixture
+ def event_loop():
+ loop = asyncio.new_event_loop()
+ yield loop
+ loop.close()
+
+ @pytest_asyncio.fixture
+ async def uses_event_loop():
+ pass
+
+ def test_emits_warning(uses_event_loop):
+ pass
+ """
+ )
+ )
+ result = pytester.runpytest("--asyncio-mode=strict", "-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
new file mode 100644
index 00000000..4cac85f7
--- /dev/null
+++ b/tests/test_explicit_event_loop_fixture_request.py
@@ -0,0 +1,159 @@
+from textwrap import dedent
+
+from pytest import Pytester
+
+
+def test_emit_warning_when_event_loop_is_explicitly_requested_in_coroutine(
+ pytester: Pytester,
+):
+ pytester.makepyfile(
+ dedent(
+ """\
+ import pytest
+
+ @pytest.mark.asyncio
+ async def test_coroutine_emits_warning(event_loop):
+ pass
+ """
+ )
+ )
+ result = pytester.runpytest("--asyncio-mode=strict", "-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.makepyfile(
+ dedent(
+ """\
+ import pytest
+
+ class TestEmitsWarning:
+ @pytest.mark.asyncio
+ async def test_coroutine_emits_warning(self, event_loop):
+ pass
+ """
+ )
+ )
+ result = pytester.runpytest("--asyncio-mode=strict", "-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.makepyfile(
+ dedent(
+ """\
+ import pytest
+
+ class TestEmitsWarning:
+ @staticmethod
+ @pytest.mark.asyncio
+ async def test_coroutine_emits_warning(event_loop):
+ pass
+ """
+ )
+ )
+ result = pytester.runpytest("--asyncio-mode=strict", "-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.makepyfile(
+ dedent(
+ """\
+ import pytest
+ import pytest_asyncio
+
+ @pytest_asyncio.fixture
+ async def emits_warning(event_loop):
+ pass
+
+ @pytest.mark.asyncio
+ async def test_uses_fixture(emits_warning):
+ pass
+ """
+ )
+ )
+ result = pytester.runpytest("--asyncio-mode=strict", "-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.makepyfile(
+ dedent(
+ """\
+ import pytest
+ import pytest_asyncio
+
+ @pytest_asyncio.fixture
+ async def emits_warning(event_loop):
+ yield
+
+ @pytest.mark.asyncio
+ async def test_uses_fixture(emits_warning):
+ pass
+ """
+ )
+ )
+ result = pytester.runpytest("--asyncio-mode=strict", "-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.makepyfile(
+ dedent(
+ """\
+ import pytest
+
+ def test_uses_fixture(event_loop):
+ pass
+ """
+ )
+ )
+ result = pytester.runpytest("--asyncio-mode=strict")
+ result.assert_outcomes(passed=1)
+
+
+def test_does_not_emit_warning_when_event_loop_is_explicitly_requested_in_sync_fixture(
+ pytester: Pytester,
+):
+ pytester.makepyfile(
+ dedent(
+ """\
+ import pytest
+
+ @pytest.fixture
+ def any_fixture(event_loop):
+ pass
+
+ def test_uses_fixture(any_fixture):
+ pass
+ """
+ )
+ )
+ result = pytester.runpytest("--asyncio-mode=strict")
+ result.assert_outcomes(passed=1)
diff --git a/tests/test_flaky_integration.py b/tests/test_flaky_integration.py
deleted file mode 100644
index 54c9d2ea..00000000
--- a/tests/test_flaky_integration.py
+++ /dev/null
@@ -1,43 +0,0 @@
-"""Tests for the Flaky integration, which retries failed tests.
-"""
-from textwrap import dedent
-
-
-def test_auto_mode_cmdline(testdir):
- testdir.makepyfile(
- dedent(
- """\
- import asyncio
- import flaky
- import pytest
-
- _threshold = -1
-
- @flaky.flaky(3, 2)
- @pytest.mark.asyncio
- async def test_asyncio_flaky_thing_that_fails_then_succeeds():
- global _threshold
- await asyncio.sleep(0.1)
- _threshold += 1
- assert _threshold != 1
- """
- )
- )
- # runpytest_subprocess() is required to don't pollute the output
- # with flaky restart information
- result = testdir.runpytest_subprocess("--asyncio-mode=strict")
- result.assert_outcomes(passed=1)
- result.stdout.fnmatch_lines(
- [
- "===Flaky Test Report===",
- "test_asyncio_flaky_thing_that_fails_then_succeeds passed 1 "
- "out of the required 2 times. Running test again until it passes 2 times.",
- "test_asyncio_flaky_thing_that_fails_then_succeeds failed "
- "(1 runs remaining out of 3).",
- " ",
- " assert 1 != 1",
- "test_asyncio_flaky_thing_that_fails_then_succeeds passed 2 "
- "out of the required 2 times. Success!",
- "===End Flaky Test Report===",
- ]
- )
diff --git a/tests/test_import.py b/tests/test_import.py
new file mode 100644
index 00000000..77352150
--- /dev/null
+++ b/tests/test_import.py
@@ -0,0 +1,18 @@
+from textwrap import dedent
+
+from pytest import Pytester
+
+
+def test_import_warning(pytester: Pytester):
+ pytester.makepyfile(
+ dedent(
+ """\
+ raise ImportWarning()
+
+ async def test_errors_out():
+ pass
+ """
+ )
+ )
+ result = pytester.runpytest("--asyncio-mode=auto")
+ result.assert_outcomes(errors=1)
diff --git a/tests/test_is_async_test.py b/tests/test_is_async_test.py
new file mode 100644
index 00000000..512243b3
--- /dev/null
+++ b/tests/test_is_async_test.py
@@ -0,0 +1,105 @@
+from textwrap import dedent
+
+import pytest
+from pytest import Pytester
+
+
+def test_returns_false_for_sync_item(pytester: Pytester):
+ pytester.makepyfile(
+ dedent(
+ """\
+ import pytest
+ import pytest_asyncio
+
+ def test_sync():
+ pass
+
+ def pytest_collection_modifyitems(items):
+ async_tests = [
+ item
+ for item in items
+ if pytest_asyncio.is_async_test(item)
+ ]
+ assert len(async_tests) == 0
+ """
+ )
+ )
+ result = pytester.runpytest("--asyncio-mode=strict")
+ result.assert_outcomes(passed=1)
+
+
+def test_returns_true_for_marked_coroutine_item_in_strict_mode(pytester: Pytester):
+ pytester.makepyfile(
+ dedent(
+ """\
+ import pytest
+ import pytest_asyncio
+
+ @pytest.mark.asyncio
+ async def test_coro():
+ pass
+
+ def pytest_collection_modifyitems(items):
+ async_tests = [
+ item
+ for item in items
+ if pytest_asyncio.is_async_test(item)
+ ]
+ assert len(async_tests) == 1
+ """
+ )
+ )
+ result = pytester.runpytest("--asyncio-mode=strict")
+ result.assert_outcomes(passed=1)
+
+
+def test_returns_false_for_unmarked_coroutine_item_in_strict_mode(pytester: Pytester):
+ pytester.makepyfile(
+ dedent(
+ """\
+ import pytest
+ import pytest_asyncio
+
+ async def test_coro():
+ pass
+
+ def pytest_collection_modifyitems(items):
+ async_tests = [
+ item
+ for item in items
+ if pytest_asyncio.is_async_test(item)
+ ]
+ assert len(async_tests) == 0
+ """
+ )
+ )
+ result = pytester.runpytest("--asyncio-mode=strict")
+ if pytest.version_tuple < (7, 2):
+ # Probably related to https://github.com/pytest-dev/pytest/pull/10012
+ result.assert_outcomes(failed=1)
+ else:
+ result.assert_outcomes(skipped=1)
+
+
+def test_returns_true_for_unmarked_coroutine_item_in_auto_mode(pytester: Pytester):
+ pytester.makepyfile(
+ dedent(
+ """\
+ import pytest
+ import pytest_asyncio
+
+ async def test_coro():
+ pass
+
+ def pytest_collection_modifyitems(items):
+ async_tests = [
+ item
+ for item in items
+ if pytest_asyncio.is_async_test(item)
+ ]
+ assert len(async_tests) == 1
+ """
+ )
+ )
+ result = pytester.runpytest("--asyncio-mode=auto")
+ result.assert_outcomes(passed=1)
diff --git a/tests/test_multiloop.py b/tests/test_multiloop.py
new file mode 100644
index 00000000..c3713cc9
--- /dev/null
+++ b/tests/test_multiloop.py
@@ -0,0 +1,70 @@
+from textwrap import dedent
+
+from pytest import Pytester
+
+
+def test_event_loop_override(pytester: Pytester):
+ pytester.makeconftest(
+ dedent(
+ '''\
+ import asyncio
+
+ import pytest
+
+
+ @pytest.fixture
+ def dependent_fixture(event_loop):
+ """A fixture dependent on the event_loop fixture, doing some cleanup."""
+ counter = 0
+
+ async def just_a_sleep():
+ """Just sleep a little while."""
+ nonlocal event_loop
+ await asyncio.sleep(0.1)
+ nonlocal counter
+ counter += 1
+
+ event_loop.run_until_complete(just_a_sleep())
+ yield
+ event_loop.run_until_complete(just_a_sleep())
+
+ assert counter == 2
+
+
+ class CustomSelectorLoop(asyncio.SelectorEventLoop):
+ """A subclass with no overrides, just to test for presence."""
+
+
+ @pytest.fixture
+ def event_loop():
+ """Create an instance of the default event loop for each test case."""
+ loop = CustomSelectorLoop()
+ yield loop
+ loop.close()
+ '''
+ )
+ )
+ pytester.makepyfile(
+ dedent(
+ '''\
+ """Unit tests for overriding the event loop."""
+ import asyncio
+
+ import pytest
+
+
+ @pytest.mark.asyncio
+ async def test_for_custom_loop():
+ """This test should be executed using the custom loop."""
+ await asyncio.sleep(0.01)
+ assert type(asyncio.get_event_loop()).__name__ == "CustomSelectorLoop"
+
+
+ @pytest.mark.asyncio
+ async def test_dependent_fixture(dependent_fixture):
+ await asyncio.sleep(0.1)
+ '''
+ )
+ )
+ result = pytester.runpytest_subprocess("--asyncio-mode=strict")
+ result.assert_outcomes(passed=2, warnings=2)
diff --git a/tests/test_port_factories.py b/tests/test_port_factories.py
new file mode 100644
index 00000000..cbbd47b4
--- /dev/null
+++ b/tests/test_port_factories.py
@@ -0,0 +1,197 @@
+from textwrap import dedent
+
+from pytest import Pytester
+
+import pytest_asyncio.plugin
+
+
+def test_unused_tcp_port_selects_unused_port(pytester: Pytester):
+ pytester.makepyfile(
+ dedent(
+ """\
+ import asyncio
+
+ import pytest
+
+ @pytest.mark.asyncio
+ async def test_unused_port_fixture(unused_tcp_port):
+ async def closer(_, writer):
+ writer.close()
+
+ server1 = await asyncio.start_server(
+ closer, host="localhost", port=unused_tcp_port
+ )
+
+ with pytest.raises(IOError):
+ await asyncio.start_server(
+ closer, host="localhost", port=unused_tcp_port
+ )
+
+ server1.close()
+ await server1.wait_closed()
+ """
+ )
+ )
+
+
+def test_unused_udp_port_selects_unused_port(pytester: Pytester):
+ pytester.makepyfile(
+ dedent(
+ """\
+ @pytest.mark.asyncio
+ async def test_unused_udp_port_fixture(unused_udp_port):
+ class Closer:
+ def connection_made(self, transport):
+ pass
+
+ def connection_lost(self, *arg, **kwd):
+ pass
+
+ event_loop = asyncio.get_running_loop()
+ transport1, _ = await event_loop.create_datagram_endpoint(
+ Closer,
+ local_addr=("127.0.0.1", unused_udp_port),
+ reuse_port=False,
+ )
+
+ with pytest.raises(IOError):
+ await event_loop.create_datagram_endpoint(
+ Closer,
+ local_addr=("127.0.0.1", unused_udp_port),
+ reuse_port=False,
+ )
+
+ transport1.abort()
+ """
+ )
+ )
+
+
+def test_unused_tcp_port_factory_selects_unused_port(pytester: Pytester):
+ pytester.makepyfile(
+ dedent(
+ """\
+ @pytest.mark.asyncio
+ async def test_unused_port_factory_fixture(unused_tcp_port_factory):
+ async def closer(_, writer):
+ writer.close()
+
+ port1, port2, port3 = (
+ unused_tcp_port_factory(),
+ unused_tcp_port_factory(),
+ unused_tcp_port_factory(),
+ )
+
+ server1 = await asyncio.start_server(
+ closer, host="localhost", port=port1
+ )
+ server2 = await asyncio.start_server(
+ closer, host="localhost", port=port2
+ )
+ server3 = await asyncio.start_server(
+ closer, host="localhost", port=port3
+ )
+
+ for port in port1, port2, port3:
+ with pytest.raises(IOError):
+ await asyncio.start_server(closer, host="localhost", port=port)
+
+ server1.close()
+ await server1.wait_closed()
+ server2.close()
+ await server2.wait_closed()
+ server3.close()
+ await server3.wait_closed()
+ """
+ )
+ )
+
+
+def test_unused_udp_port_factory_selects_unused_port(pytester: Pytester):
+ pytester.makepyfile(
+ dedent(
+ """\
+ @pytest.mark.asyncio
+ async def test_unused_udp_port_factory_fixture(unused_udp_port_factory):
+ class Closer:
+ def connection_made(self, transport):
+ pass
+
+ def connection_lost(self, *arg, **kwd):
+ pass
+
+ port1, port2, port3 = (
+ unused_udp_port_factory(),
+ unused_udp_port_factory(),
+ unused_udp_port_factory(),
+ )
+
+ event_loop = asyncio.get_running_loop()
+ transport1, _ = await event_loop.create_datagram_endpoint(
+ Closer,
+ local_addr=("127.0.0.1", port1),
+ reuse_port=False,
+ )
+ transport2, _ = await event_loop.create_datagram_endpoint(
+ Closer,
+ local_addr=("127.0.0.1", port2),
+ reuse_port=False,
+ )
+ transport3, _ = await event_loop.create_datagram_endpoint(
+ Closer,
+ local_addr=("127.0.0.1", port3),
+ reuse_port=False,
+ )
+
+ for port in port1, port2, port3:
+ with pytest.raises(IOError):
+ await event_loop.create_datagram_endpoint(
+ Closer,
+ local_addr=("127.0.0.1", port),
+ reuse_port=False,
+ )
+
+ transport1.abort()
+ transport2.abort()
+ transport3.abort()
+ """
+ )
+ )
+
+
+def test_unused_port_factory_duplicate(unused_tcp_port_factory, monkeypatch):
+ """Test correct avoidance of duplicate ports."""
+ counter = 0
+
+ def mock_unused_tcp_port(_ignored):
+ """Force some duplicate ports."""
+ nonlocal counter
+ counter += 1
+ if counter < 5:
+ return 10000
+ else:
+ return 10000 + counter
+
+ monkeypatch.setattr(pytest_asyncio.plugin, "_unused_port", mock_unused_tcp_port)
+
+ assert unused_tcp_port_factory() == 10000
+ assert unused_tcp_port_factory() > 10000
+
+
+def test_unused_udp_port_factory_duplicate(unused_udp_port_factory, monkeypatch):
+ """Test correct avoidance of duplicate UDP ports."""
+ counter = 0
+
+ def mock_unused_udp_port(_ignored):
+ """Force some duplicate ports."""
+ nonlocal counter
+ counter += 1
+ if counter < 5:
+ return 10000
+ else:
+ return 10000 + counter
+
+ monkeypatch.setattr(pytest_asyncio.plugin, "_unused_port", mock_unused_udp_port)
+
+ assert unused_udp_port_factory() == 10000
+ assert unused_udp_port_factory() > 10000
diff --git a/tests/test_simple.py b/tests/test_simple.py
index 5e6a0d20..05c92694 100644
--- a/tests/test_simple.py
+++ b/tests/test_simple.py
@@ -5,8 +5,6 @@
import pytest
from pytest import Pytester
-import pytest_asyncio.plugin
-
async def async_coro():
await asyncio.sleep(0)
@@ -69,172 +67,9 @@ async def test_asyncio_marker_with_default_param(a_param=None):
await asyncio.sleep(0)
-@pytest.mark.asyncio
-async def test_unused_port_fixture(unused_tcp_port, event_loop):
- """Test the unused TCP port fixture."""
-
- async def closer(_, writer):
- writer.close()
-
- server1 = await asyncio.start_server(closer, host="localhost", port=unused_tcp_port)
-
- with pytest.raises(IOError):
- await asyncio.start_server(closer, host="localhost", port=unused_tcp_port)
-
- server1.close()
- await server1.wait_closed()
-
-
-@pytest.mark.asyncio
-async def test_unused_udp_port_fixture(unused_udp_port, event_loop):
- """Test the unused TCP port fixture."""
-
- class Closer:
- def connection_made(self, transport):
- pass
-
- def connection_lost(self, *arg, **kwd):
- pass
-
- transport1, _ = await event_loop.create_datagram_endpoint(
- Closer,
- local_addr=("127.0.0.1", unused_udp_port),
- reuse_port=False,
- )
-
- with pytest.raises(IOError):
- await event_loop.create_datagram_endpoint(
- Closer,
- local_addr=("127.0.0.1", unused_udp_port),
- reuse_port=False,
- )
-
- transport1.abort()
-
-
-@pytest.mark.asyncio
-async def test_unused_port_factory_fixture(unused_tcp_port_factory, event_loop):
- """Test the unused TCP port factory fixture."""
-
- async def closer(_, writer):
- writer.close()
-
- port1, port2, port3 = (
- unused_tcp_port_factory(),
- unused_tcp_port_factory(),
- unused_tcp_port_factory(),
- )
-
- server1 = await asyncio.start_server(closer, host="localhost", port=port1)
- server2 = await asyncio.start_server(closer, host="localhost", port=port2)
- server3 = await asyncio.start_server(closer, host="localhost", port=port3)
-
- for port in port1, port2, port3:
- with pytest.raises(IOError):
- await asyncio.start_server(closer, host="localhost", port=port)
-
- server1.close()
- await server1.wait_closed()
- server2.close()
- await server2.wait_closed()
- server3.close()
- await server3.wait_closed()
-
-
-@pytest.mark.asyncio
-async def test_unused_udp_port_factory_fixture(unused_udp_port_factory, event_loop):
- """Test the unused UDP port factory fixture."""
-
- class Closer:
- def connection_made(self, transport):
- pass
-
- def connection_lost(self, *arg, **kwd):
- pass
-
- port1, port2, port3 = (
- unused_udp_port_factory(),
- unused_udp_port_factory(),
- unused_udp_port_factory(),
- )
-
- transport1, _ = await event_loop.create_datagram_endpoint(
- Closer,
- local_addr=("127.0.0.1", port1),
- reuse_port=False,
- )
- transport2, _ = await event_loop.create_datagram_endpoint(
- Closer,
- local_addr=("127.0.0.1", port2),
- reuse_port=False,
- )
- transport3, _ = await event_loop.create_datagram_endpoint(
- Closer,
- local_addr=("127.0.0.1", port3),
- reuse_port=False,
- )
-
- for port in port1, port2, port3:
- with pytest.raises(IOError):
- await event_loop.create_datagram_endpoint(
- Closer,
- local_addr=("127.0.0.1", port),
- reuse_port=False,
- )
-
- transport1.abort()
- transport2.abort()
- transport3.abort()
-
-
-def test_unused_port_factory_duplicate(unused_tcp_port_factory, monkeypatch):
- """Test correct avoidance of duplicate ports."""
- counter = 0
-
- def mock_unused_tcp_port(_ignored):
- """Force some duplicate ports."""
- nonlocal counter
- counter += 1
- if counter < 5:
- return 10000
- else:
- return 10000 + counter
-
- monkeypatch.setattr(pytest_asyncio.plugin, "_unused_port", mock_unused_tcp_port)
-
- assert unused_tcp_port_factory() == 10000
- assert unused_tcp_port_factory() > 10000
-
-
-def test_unused_udp_port_factory_duplicate(unused_udp_port_factory, monkeypatch):
- """Test correct avoidance of duplicate UDP ports."""
- counter = 0
-
- def mock_unused_udp_port(_ignored):
- """Force some duplicate ports."""
- nonlocal counter
- counter += 1
- if counter < 5:
- return 10000
- else:
- return 10000 + counter
-
- monkeypatch.setattr(pytest_asyncio.plugin, "_unused_port", mock_unused_udp_port)
-
- assert unused_udp_port_factory() == 10000
- assert unused_udp_port_factory() > 10000
-
-
class TestMarkerInClassBasedTests:
"""Test that asyncio marked functions work for methods of test classes."""
- @pytest.mark.asyncio
- async def test_asyncio_marker_with_explicit_loop_fixture(self, event_loop):
- """Test the "asyncio" marker works on a method in
- a class-based test with explicit loop fixture."""
- ret = await async_coro()
- assert ret == "ok"
-
@pytest.mark.asyncio
async def test_asyncio_marker_with_implicit_loop_fixture(self):
"""Test the "asyncio" marker works on a method in
@@ -257,81 +92,14 @@ async def test_no_event_loop(self, loop):
assert await loop.run_in_executor(None, self.foo) == 1
@pytest.mark.asyncio
- async def test_event_loop_after_fixture(self, loop, event_loop):
+ async def test_event_loop_after_fixture(self, loop):
assert await loop.run_in_executor(None, self.foo) == 1
@pytest.mark.asyncio
- async def test_event_loop_before_fixture(self, event_loop, loop):
+ async def test_event_loop_before_fixture(self, loop):
assert await loop.run_in_executor(None, self.foo) == 1
-def test_asyncio_marker_compatibility_with_skip(pytester: Pytester):
- pytester.makepyfile(
- dedent(
- """\
- import pytest
-
- pytest_plugins = "pytest_asyncio"
-
- @pytest.mark.asyncio
- async def test_no_warning_on_skip():
- pytest.skip("Test a skip error inside asyncio")
- """
- )
- )
- result = pytester.runpytest("--asyncio-mode=strict")
- result.assert_outcomes(skipped=1)
-
-
-def test_asyncio_auto_mode_compatibility_with_skip(pytester: Pytester):
- pytester.makepyfile(
- dedent(
- """\
- import pytest
-
- pytest_plugins = "pytest_asyncio"
-
- async def test_no_warning_on_skip():
- pytest.skip("Test a skip error inside asyncio")
- """
- )
- )
- result = pytester.runpytest("--asyncio-mode=auto")
- result.assert_outcomes(skipped=1)
-
-
-def test_warn_asyncio_marker_for_regular_func(testdir):
- testdir.makepyfile(
- dedent(
- """\
- import pytest
-
- pytest_plugins = 'pytest_asyncio'
-
- @pytest.mark.asyncio
- def test_a():
- pass
- """
- )
- )
- testdir.makefile(
- ".ini",
- pytest=dedent(
- """\
- [pytest]
- asyncio_mode = strict
- filterwarnings =
- default
- """
- ),
- )
- result = testdir.runpytest()
- result.assert_outcomes(passed=1)
- result.stdout.fnmatch_lines(
- ["*is marked with '@pytest.mark.asyncio' but it is not an async function.*"]
- )
-
-
def test_invalid_asyncio_mode(testdir):
result = testdir.runpytest("-o", "asyncio_mode=True")
result.stderr.no_fnmatch_line("INTERNALERROR> *")
diff --git a/tests/test_skips.py b/tests/test_skips.py
new file mode 100644
index 00000000..abd9dd70
--- /dev/null
+++ b/tests/test_skips.py
@@ -0,0 +1,107 @@
+from textwrap import dedent
+
+from pytest import Pytester
+
+
+def test_asyncio_strict_mode_skip(pytester: Pytester):
+ pytester.makepyfile(
+ dedent(
+ """\
+ import pytest
+
+ pytest_plugins = "pytest_asyncio"
+
+ @pytest.mark.asyncio
+ async def test_no_warning_on_skip():
+ pytest.skip("Test a skip error inside asyncio")
+ """
+ )
+ )
+ result = pytester.runpytest("--asyncio-mode=strict")
+ result.assert_outcomes(skipped=1)
+
+
+def test_asyncio_auto_mode_skip(pytester: Pytester):
+ pytester.makepyfile(
+ dedent(
+ """\
+ import pytest
+
+ pytest_plugins = "pytest_asyncio"
+
+ async def test_no_warning_on_skip():
+ pytest.skip("Test a skip error inside asyncio")
+ """
+ )
+ )
+ result = pytester.runpytest("--asyncio-mode=auto")
+ result.assert_outcomes(skipped=1)
+
+
+def test_asyncio_strict_mode_module_level_skip(pytester: Pytester):
+ pytester.makepyfile(
+ dedent(
+ """\
+ import pytest
+
+ pytest.skip("Skip all tests", allow_module_level=True)
+
+ @pytest.mark.asyncio
+ async def test_is_skipped():
+ pass
+ """
+ )
+ )
+ result = pytester.runpytest("--asyncio-mode=strict")
+ result.assert_outcomes(skipped=1)
+
+
+def test_asyncio_auto_mode_module_level_skip(pytester: Pytester):
+ pytester.makepyfile(
+ dedent(
+ """\
+ import pytest
+
+ pytest.skip("Skip all tests", allow_module_level=True)
+
+ async def test_is_skipped():
+ pass
+ """
+ )
+ )
+ result = pytester.runpytest("--asyncio-mode=auto")
+ result.assert_outcomes(skipped=1)
+
+
+def test_asyncio_auto_mode_wrong_skip_usage(pytester: Pytester):
+ pytester.makepyfile(
+ dedent(
+ """\
+ import pytest
+
+ pytest.skip("Skip all tests")
+
+ async def test_is_skipped():
+ pass
+ """
+ )
+ )
+ result = pytester.runpytest("--asyncio-mode=auto")
+ result.assert_outcomes(errors=1)
+
+
+def test_unittest_skiptest_compatibility(pytester: Pytester):
+ pytester.makepyfile(
+ dedent(
+ """\
+ from unittest import SkipTest
+
+ raise SkipTest("Skip all tests")
+
+ async def test_is_skipped():
+ pass
+ """
+ )
+ )
+ result = pytester.runpytest("--asyncio-mode=auto")
+ result.assert_outcomes(skipped=1)
diff --git a/tests/test_subprocess.py b/tests/test_subprocess.py
index 79c5109d..14d3498a 100644
--- a/tests/test_subprocess.py
+++ b/tests/test_subprocess.py
@@ -15,20 +15,8 @@ def event_loop():
loop.close()
-@pytest.mark.skipif(
- sys.version_info < (3, 8),
- reason="""
- When run with Python 3.7 asyncio.subprocess.create_subprocess_exec seems to be
- affected by an issue that prevents correct cleanup. Tests using pytest-trio
- will report that signal handling is already performed by another library and
- fail. [1] This is possibly a bug in CPython 3.7, so we ignore this test for
- that Python version.
-
- [1] https://github.com/python-trio/pytest-trio/issues/126
- """,
-)
@pytest.mark.asyncio
-async def test_subprocess(event_loop):
+async def test_subprocess():
"""Starting a subprocess should be possible."""
proc = await asyncio.subprocess.create_subprocess_exec(
sys.executable, "--version", stdout=asyncio.subprocess.PIPE
diff --git a/tests/trio/test_fixtures.py b/tests/trio/test_fixtures.py
deleted file mode 100644
index 42b28437..00000000
--- a/tests/trio/test_fixtures.py
+++ /dev/null
@@ -1,25 +0,0 @@
-from textwrap import dedent
-
-
-def test_strict_mode_ignores_trio_fixtures(testdir):
- testdir.makepyfile(
- dedent(
- """\
- import pytest
- import pytest_asyncio
- import pytest_trio
-
- pytest_plugins = ["pytest_asyncio", "pytest_trio"]
-
- @pytest_trio.trio_fixture
- async def any_fixture():
- return True
-
- @pytest.mark.trio
- async def test_anything(any_fixture):
- pass
- """
- )
- )
- result = testdir.runpytest("--asyncio-mode=strict")
- result.assert_outcomes(passed=1)
diff --git a/tox.ini b/tox.ini
index 4987355b..7bab7350 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,32 +1,43 @@
[tox]
minversion = 3.14.0
-envlist = py37, py38, py39, py310, py311, py312, pytest-min
+envlist = py38, py39, py310, py311, py312, pytest-min, docs
isolated_build = true
passenv =
CI
[testenv]
extras = testing
-deps =
- --requirement dependencies/default/requirements.txt
- --constraint dependencies/default/constraints.txt
+install_command = python -m pip install \
+ --requirement dependencies/default/requirements.txt \
+ --constraint dependencies/default/constraints.txt \
+ {opts} {packages}
commands = make test
allowlist_externals =
make
[testenv:pytest-min]
extras = testing
-deps =
- --requirement dependencies/pytest-min/requirements.txt
- --constraint dependencies/pytest-min/constraints.txt
+install_command = python -m pip install \
+ --requirement dependencies/pytest-min/requirements.txt \
+ --constraint dependencies/pytest-min/constraints.txt \
+ {opts} {packages}
commands = make test
allowlist_externals =
make
+[testenv:docs]
+extras = docs
+deps =
+ --requirement dependencies/docs/requirements.txt
+ --constraint dependencies/docs/constraints.txt
+change_dir = docs
+commands = make html
+allowlist_externals =
+ make
+
[gh-actions]
python =
- 3.7: py37, pytest-min
- 3.8: py38
+ 3.8: py38, pytest-min
3.9: py39
3.10: py310
3.11: py311