Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow independent caching scope and event loop scopes for async fixtures #871

Merged
merged 11 commits into from
Jul 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion docs/source/concepts.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
Concepts
========

.. _concepts/event_loops:

asyncio event loops
===================
In order to understand how pytest-asyncio works, it helps to understand how pytest collectors work.
Expand Down Expand Up @@ -32,7 +34,7 @@ You may notice that the individual levels resemble the possible `scopes of a pyt
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.
If two or more tests share a common ancestor collector, the tests can be configured to run in their ancestor's loop by passing the appropriate *loop_scope* keyword argument to the *asyncio* mark.
For example, the following two tests use the asyncio event loop provided by the *Module* collector:

.. include:: concepts_module_scope_example.py
Expand Down
4 changes: 2 additions & 2 deletions docs/source/concepts_module_scope_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@
loop: asyncio.AbstractEventLoop


@pytest.mark.asyncio(scope="module")
@pytest.mark.asyncio(loop_scope="module")
async def test_remember_loop():
global loop
loop = asyncio.get_running_loop()


@pytest.mark.asyncio(scope="module")
@pytest.mark.asyncio(loop_scope="module")
async def test_runs_in_a_loop():
global loop
assert asyncio.get_running_loop() is loop
24 changes: 24 additions & 0 deletions docs/source/how-to-guides/change_default_fixture_loop.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
==========================================================
How to change the default event loop scope of all fixtures
==========================================================
The :ref:`configuration/asyncio_default_fixture_loop_scope` configuration option sets the default event loop scope for asynchronous fixtures. The following code snippets configure all fixtures to run in a session-scoped loop by default:

.. code-block:: ini
:caption: pytest.ini

[pytest]
asyncio_default_fixture_loop_scope = session

.. code-block:: toml
:caption: pyproject.toml

[tool.pytest.ini_options]
asyncio_default_fixture_loop_scope = "session"

.. code-block:: ini
:caption: setup.cfg

[tool:pytest]
asyncio_default_fixture_loop_scope = session

Please refer to :ref:`configuration/asyncio_default_fixture_loop_scope` for other valid scopes.
7 changes: 7 additions & 0 deletions docs/source/how-to-guides/change_fixture_loop.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
===============================================
How to change the event loop scope of a fixture
===============================================
The event loop scope of an asynchronous fixture is specified via the *loop_scope* keyword argument to :ref:`pytest_asyncio.fixture <decorators/pytest_asyncio_fixture>`. The following fixture runs in the module-scoped event loop:

.. include:: change_fixture_loop_example.py
:code: python
15 changes: 15 additions & 0 deletions docs/source/how-to-guides/change_fixture_loop_example.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import asyncio

import pytest

import pytest_asyncio


@pytest_asyncio.fixture(loop_scope="module")
async def current_loop():
return asyncio.get_running_loop()


@pytest.mark.asyncio(loop_scope="module")
async def test_runs_in_module_loop(current_loop):
assert current_loop is asyncio.get_running_loop()
2 changes: 1 addition & 1 deletion docs/source/how-to-guides/class_scoped_loop_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import pytest


@pytest.mark.asyncio(scope="class")
@pytest.mark.asyncio(loop_scope="class")
class TestInOneEventLoopPerClass:
loop: asyncio.AbstractEventLoop

Expand Down
2 changes: 2 additions & 0 deletions docs/source/how-to-guides/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ How-To Guides
.. toctree::
:hidden:

change_fixture_loop
change_default_fixture_loop
run_class_tests_in_same_loop
run_module_tests_in_same_loop
run_package_tests_in_same_loop
Expand Down
2 changes: 1 addition & 1 deletion docs/source/how-to-guides/module_scoped_loop_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import pytest

pytestmark = pytest.mark.asyncio(scope="module")
pytestmark = pytest.mark.asyncio(loop_scope="module")

loop: asyncio.AbstractEventLoop

Expand Down
2 changes: 1 addition & 1 deletion docs/source/how-to-guides/package_scoped_loop_example.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
import pytest

pytestmark = pytest.mark.asyncio(scope="package")
pytestmark = pytest.mark.asyncio(loop_scope="package")
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
======================================================
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")``.
All tests can be run inside the same event loop by marking them with ``pytest.mark.asyncio(loop_scope="class")``.
This is easily achieved by using the *asyncio* marker as a class decorator.

.. include:: class_scoped_loop_example.py
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
=======================================================
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")``.
All tests can be run inside the same event loop by marking them with ``pytest.mark.asyncio(loop_scope="module")``.
This is easily achieved by adding a `pytestmark` statement to your module.

.. include:: module_scoped_loop_example.py
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
========================================================
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")``.
All tests can be run inside the same event loop by marking them with ``pytest.mark.asyncio(loop_scope="package")``.
Add the following code to the ``__init__.py`` of the test package:

.. include:: package_scoped_loop_example.py
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
==========================================================
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")``.
All tests can be run inside the same event loop by marking them with ``pytest.mark.asyncio(loop_scope="session")``.
The easiest way to mark all tests is via a ``pytest_collection_modifyitems`` hook in the ``conftest.py`` at the root folder of your test suite.

.. include:: session_scoped_loop_example.py
Expand Down
2 changes: 1 addition & 1 deletion docs/source/how-to-guides/session_scoped_loop_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@

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")
session_scope_marker = pytest.mark.asyncio(loop_scope="session")
for async_test in pytest_asyncio_tests:
async_test.add_marker(session_scope_marker, append=False)
6 changes: 6 additions & 0 deletions docs/source/reference/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@
Changelog
=========

0.24.0 (UNRELEASED)
===================
- Adds an optional `loop_scope` keyword argument to `pytest.mark.asyncio`. This argument controls which event loop is used to run the marked async test. `#706`_, `#871 <https://github.com/pytest-dev/pytest-asyncio/pull/871>`_
- Deprecates the optional `scope` keyword argument to `pytest.mark.asyncio` for API consistency with ``pytest_asyncio.fixture``. Users are encouraged to use the `loop_scope` keyword argument, which does exactly the same.


0.23.8 (2024-07-17)
===================
- Fixes a bug that caused duplicate markers in async tests `#813 <https://github.com/pytest-dev/pytest-asyncio/issues/813>`_
Expand Down
8 changes: 8 additions & 0 deletions docs/source/reference/configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@
Configuration
=============

.. _configuration/asyncio_default_fixture_loop_scope:

asyncio_default_fixture_loop_scope
==================================
Determines the default event loop scope of asynchronous fixtures. When this configuration option is unset, it defaults to the fixture scope. In future versions of pytest-asyncio, the value will default to ``function`` when unset. Possible values are: ``function``, ``class``, ``module``, ``package``, ``session``

asyncio_mode
============
The pytest-asyncio mode can be set by the ``asyncio_mode`` configuration option in the `configuration file
<https://docs.pytest.org/en/latest/reference/customize.html>`_:

Expand Down
14 changes: 0 additions & 14 deletions docs/source/reference/decorators/fixture_strict_mode_example.py

This file was deleted.

25 changes: 16 additions & 9 deletions docs/source/reference/decorators/index.rst
Original file line number Diff line number Diff line change
@@ -1,15 +1,22 @@
.. _decorators/pytest_asyncio_fixture:

==========
Decorators
==========
Asynchronous fixtures are defined just like ordinary pytest fixtures, except they should be decorated with ``@pytest_asyncio.fixture``.
The ``@pytest_asyncio.fixture`` decorator allows coroutines and async generator functions to be used as pytest fixtures.

.. include:: fixture_strict_mode_example.py
:code: python
The decorator takes all arguments supported by `@pytest.fixture`.
Additionally, ``@pytest_asyncio.fixture`` supports the *loop_scope* keyword argument, which selects the event loop in which the fixture is run (see :ref:`concepts/event_loops`).
The default event loop scope is *function* scope.
Possible loop scopes are *session,* *package,* *module,* *class,* and *function*.

The *loop_scope* of a fixture can be chosen independently from its caching *scope*.
However, the event loop scope must be larger or the same as the fixture's caching scope.
In other words, it's possible to reevaluate an async fixture multiple times within the same event loop, but it's not possible to switch out the running event loop in an async fixture.

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.
Async fixtures need the event loop, and so must have the same or narrower scope
than the ``event_loop`` fixture.
Examples:

.. include:: pytest_asyncio_fixture_example.py
:code: python

*auto* mode automatically converts async fixtures declared with the
standard ``@pytest.fixture`` decorator to *asyncio-driven* versions.
*auto* mode automatically converts coroutines and async generator functions declared with the standard ``@pytest.fixture`` decorator to pytest-asyncio fixtures.
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import pytest_asyncio


@pytest_asyncio.fixture
async def fixture_runs_in_fresh_loop_for_every_function(): ...


@pytest_asyncio.fixture(loop_scope="session", scope="module")
async def fixture_runs_in_session_loop_once_per_module(): ...


@pytest_asyncio.fixture(loop_scope="module", scope="module")
async def fixture_runs_in_module_loop_once_per_module(): ...


@pytest_asyncio.fixture(loop_scope="module")
async def fixture_runs_in_module_loop_once_per_function(): ...
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,6 @@ def event_loop_policy(request):
return CustomEventLoopPolicy()


@pytest.mark.asyncio(scope="module")
@pytest.mark.asyncio(loop_scope="module")
async def test_uses_custom_event_loop_policy():
assert isinstance(asyncio.get_event_loop_policy(), CustomEventLoopPolicy)
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import pytest


@pytest.mark.asyncio(scope="class")
@pytest.mark.asyncio(loop_scope="class")
class TestClassScopedLoop:
loop: asyncio.AbstractEventLoop

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import pytest_asyncio


@pytest.mark.asyncio(scope="class")
@pytest.mark.asyncio(loop_scope="class")
class TestClassScopedLoop:
loop: asyncio.AbstractEventLoop

Expand Down
2 changes: 1 addition & 1 deletion docs/source/reference/markers/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ Multiple async tests in a single class or module can be marked using |pytestmark
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.
Multiple tests can share the same event loop by providing a *loop_scope* keyword argument to the *asyncio* mark.
The supported scopes are *class,* and *module,* and *package*.
The following code example provides a shared event loop for all tests in `TestClassScopedLoop`:

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import pytest

pytestmark = pytest.mark.asyncio(scope="module")
pytestmark = pytest.mark.asyncio(loop_scope="module")

loop: asyncio.AbstractEventLoop

Expand Down
Loading