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

Scoped event loops based on pytest marks #620

Merged
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
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions docs/source/reference/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ Changelog
==================
- Remove support for Python 3.7
- Declare support for Python 3.12
- Class-scoped and module-scoped event loops can be requested
via the _asyncio_event_loop_ mark. `#620 <https://github.com/pytest-dev/pytest-asyncio/pull/620>`_

0.21.1 (2023-07-12)
===================
Expand Down
146 changes: 146 additions & 0 deletions docs/source/reference/markers.rst
Original file line number Diff line number Diff line change
Expand Up @@ -30,5 +30,151 @@ In *auto* mode, the ``pytest.mark.asyncio`` marker can be omitted, the marker is
automatically to *async* test functions.


``pytest.mark.asyncio_event_loop``
==================================
Test classes or modules with this mark provide a class-scoped or module-scoped asyncio event loop.

This functionality is orthogonal to the `asyncio` mark.
That means the presence of this mark does not imply that async test functions inside the class or module are collected by pytest-asyncio.
The collection happens automatically in `auto` mode.
However, if you're using strict mode, you still have to apply the `asyncio` mark to your async test functions.

The following code example uses the `asyncio_event_loop` mark to provide a shared event loop for all tests in `TestClassScopedLoop`:

.. code-block:: python

import asyncio

import pytest


@pytest.mark.asyncio_event_loop
class TestClassScopedLoop:
loop: asyncio.AbstractEventLoop

@pytest.mark.asyncio
async def test_remember_loop(self):
TestClassScopedLoop.loop = asyncio.get_running_loop()

@pytest.mark.asyncio
async def test_this_runs_in_same_loop(self):
assert asyncio.get_running_loop() is TestClassScopedLoop.loop

In *auto* mode, the ``pytest.mark.asyncio`` marker can be omitted:

.. code-block:: python

import asyncio

import pytest


@pytest.mark.asyncio_event_loop
class TestClassScopedLoop:
loop: asyncio.AbstractEventLoop

async def test_remember_loop(self):
TestClassScopedLoop.loop = asyncio.get_running_loop()

async def test_this_runs_in_same_loop(self):
assert asyncio.get_running_loop() is TestClassScopedLoop.loop

Similarly, a module-scoped loop is provided when adding the `asyncio_event_loop` mark to the module:

.. code-block:: python

import asyncio

import pytest

pytestmark = pytest.mark.asyncio_event_loop

loop: asyncio.AbstractEventLoop


async def test_remember_loop():
global loop
loop = asyncio.get_running_loop()


async def test_this_runs_in_same_loop():
global loop
assert asyncio.get_running_loop() is loop


class TestClassA:
async def test_this_runs_in_same_loop(self):
global loop
assert asyncio.get_running_loop() is loop

The `asyncio_event_loop` mark supports an optional `policy` keyword argument to set the asyncio event loop policy.

.. code-block:: python

import asyncio

import pytest


class CustomEventLoopPolicy(asyncio.DefaultEventLoopPolicy):
pass


@pytest.mark.asyncio_event_loop(policy=CustomEventLoopPolicy())
class TestUsesCustomEventLoopPolicy:
@pytest.mark.asyncio
async def test_uses_custom_event_loop_policy(self):
assert isinstance(asyncio.get_event_loop_policy(), CustomEventLoopPolicy)


The ``policy`` keyword argument may also take an iterable of event loop policies. This causes tests under by the `asyncio_event_loop` mark to be parametrized with different policies:

.. code-block:: python

import asyncio

import pytest

import pytest_asyncio


@pytest.mark.asyncio_event_loop(
policy=[
asyncio.DefaultEventLoopPolicy(),
uvloop.EventLoopPolicy(),
]
)
class TestWithDifferentLoopPolicies:
@pytest.mark.asyncio
async def test_parametrized_loop(self):
pass


If no explicit policy is provided, the mark will use the loop policy returned by ``asyncio.get_event_loop_policy()``.

Fixtures and tests sharing the same `asyncio_event_loop` mark are executed in the same event loop:

.. code-block:: python

import asyncio

import pytest

import pytest_asyncio


@pytest.mark.asyncio_event_loop
class TestClassScopedLoop:
loop: asyncio.AbstractEventLoop

@pytest_asyncio.fixture
async def my_fixture(self):
TestClassScopedLoop.loop = asyncio.get_running_loop()

@pytest.mark.asyncio
async def test_runs_is_same_loop_as_fixture(self, my_fixture):
assert asyncio.get_running_loop() is TestClassScopedLoop.loop


.. |pytestmark| replace:: ``pytestmark``
.. _pytestmark: http://doc.pytest.org/en/latest/example/markers.html#marking-whole-classes-or-modules
Loading