Skip to content

Add mechanism for explicit marking of fixtures which should be run with asyncio #125

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

82 changes: 79 additions & 3 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ Features
- pytest markers for treating tests as asyncio coroutines
- easy testing with non-default event loops
- support for `async def` fixtures and async generator fixtures
- support *auto* mode to handle all async fixtures and tests automatically by asyncio;
provide *strict* mode if a test suite should work with different async frameworks
simultaneously, e.g. ``asyncio`` and ``trio``.

Installation
------------
Expand All @@ -51,6 +54,70 @@ To install pytest-asyncio, simply:

This is enough for pytest to pick up pytest-asyncio.

Modes
-----

Strting from ``pytest-asyncio>=0.17``, three modes are provided: *auto*, *strict* and
*legacy* (deault).

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

.. code-block:: ini

# pytest.ini
[pytest]
asyncio_mode = auto

The value can be overriden by command-line option for ``pytest`` invocation:

.. code-block:: bash

$ pytest tests --asyncio-mode=strict

Auto mode
~~~~~~~~~

When the mode is auto, all discovered *async* tests are considered *asyncio-driven* even
if they have no ``@pytest.mark.asyncio`` marker.

All async fixtures are considered *asyncio-driven* as well, even if they are decorated
with a regular ``@pytest.fixture`` decorator instead of dedicated
``@pytest_asyncio.fixture`` counterpart.

*asyncio-driven* means that tests and fixtures are executed by ``pytest-asyncio``
plugin.

This mode requires the simpliest tests and fixtures configuration and is
recommended for default usage *unless* the same project and its test suite should
execute tests from different async frameworks, e.g. ``asyncio`` and ``trio``. In this
case, auto-handling can break tests designed for other framework; plase use *strict*
mode instead.

Strict mode
~~~~~~~~~~~

Strict mode enforces ``@pytest.mark.asyncio`` and ``@pytest_asyncio.fixture`` usage.
Without these markers, tests and fixtures are not considered as *asyncio-driven*, other
pytest plugin can handle them.

Please use this mode if multiple async frameworks should be combined in the same test
suite.


Legacy mode
~~~~~~~~~~~

This mode follows rules used by ``pytest-asyncio<0.17``: tests are not auto-marked but
fixtures are.

This mode is used by default for the sake of backward compatibility, deprecation
warnings are emitted with suggestion to either switching to ``auto`` mode or using
``strict`` mode with ``@pytest_asyncio.fixture`` decorators.

In future, the default will be changed.


Fixtures
--------

Expand Down Expand Up @@ -116,16 +183,18 @@ Work just like their TCP counterparts but return unused UDP ports.

Async fixtures
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Asynchronous fixtures are defined just like ordinary pytest fixtures, except they should be coroutines or asynchronous generators.
Asynchronous fixtures are defined just like ordinary pytest fixtures, except they should be decorated with ``@pytest_asyncio.fixture``.

.. code-block:: python3

@pytest.fixture
import pytest_asyncio

@pytest_asyncio.fixture
async def async_gen_fixture():
await asyncio.sleep(0.1)
yield 'a value'

@pytest.fixture(scope='module')
@pytest_asyncio.fixture(scope='module')
async def async_fixture():
return await asyncio.sleep(0.1)

Expand All @@ -134,6 +203,9 @@ 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.

*auto* and *legacy* mode automatically converts async fixtures declared with the
standard ``@pytest.fixture`` decorator to *asyncio-driven* versions.


Markers
-------
Expand Down Expand Up @@ -164,6 +236,10 @@ Only test coroutines will be affected (by default, coroutines prefixed by
"""No marker!"""
await asyncio.sleep(0, loop=event_loop)

In *auto* mode, the ``pytest.mark.asyncio`` marker can be omited, the merker 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

Expand Down
4 changes: 2 additions & 2 deletions pytest_asyncio/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""The main point for importing pytest-asyncio items."""
__version__ = "0.16.0"

from .plugin import asyncio_fixture
from .plugin import fixture


__all__ = ("asyncio_fixture",)
__all__ = ("fixture",)
27 changes: 17 additions & 10 deletions pytest_asyncio/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,15 +59,18 @@ def pytest_addoption(parser, pluginmanager):
)


def asyncio_fixture(fixture_function=None, **kwargs):
def fixture(fixture_function=None, **kwargs):
if fixture_function is not None:
_set_explicit_asyncio_mark(fixture_function)
return pytest.fixture(fixture_function, **kwargs)

@functools.wraps(asyncio_fixture)
def inner(fixture_function, **kwargs):
return asyncio_fixture(fixture_function, **kwargs)
else:

_set_explicit_asyncio_mark(fixture_function)
return pytest.fixture(fixture_function, **kwargs)
@functools.wraps(fixture)
def inner(fixture_function):
return fixture(fixture_function, **kwargs)

return inner


def _has_explicit_asyncio_mark(obj):
Expand All @@ -77,6 +80,7 @@ def _has_explicit_asyncio_mark(obj):

def _set_explicit_asyncio_mark(obj):
if hasattr(obj, "__func__"):
# instance method, check the function object
obj = obj.__func__
obj._force_asyncio_fixture = True

Expand Down Expand Up @@ -128,12 +132,15 @@ def pytest_pycollect_makeitem(collector, name, obj):
"""A pytest hook to collect asyncio coroutines."""
if collector.funcnamefilter(name) and _is_coroutine(obj):
item = pytest.Function.from_parent(collector, name=name)
if "asyncio" not in item.keywords:
if _get_asyncio_mode(item.config) == Mode.AUTO:
# implicitly add asyncio marker if asyncio mode is on
item.add_marker("asyncio")
if "asyncio" in item.keywords:
return list(collector._genfunctions(name, obj))
else:
if _get_asyncio_mode(item.config) == Mode.AUTO:
# implicitly add asyncio marker if asyncio mode is on
ret = list(collector._genfunctions(name, obj))
for elem in ret:
elem.add_marker("asyncio")
return ret


class FixtureStripper:
Expand Down
91 changes: 91 additions & 0 deletions tests/modes/test_auto_mode.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
from textwrap import dedent

pytest_plugins = "pytester"


def test_auto_mode_cmdline(pytester):
pytester.makepyfile(
dedent(
"""\
import asyncio
import pytest

pytest_plugins = 'pytest_asyncio'

async def test_a():
await asyncio.sleep(0)
"""
)
)
result = pytester.runpytest("--asyncio-mode=auto")
result.assert_outcomes(passed=1)


def test_auto_mode_cfg(pytester):
pytester.makepyfile(
dedent(
"""\
import asyncio
import pytest

pytest_plugins = 'pytest_asyncio'

async def test_a():
await asyncio.sleep(0)
"""
)
)
pytester.makefile(".ini", pytest="[pytest]\nasyncio_mode = auto\n")
result = pytester.runpytest()
result.assert_outcomes(passed=1)


def test_auto_mode_async_fixture(pytester):
pytester.makepyfile(
dedent(
"""\
import asyncio
import pytest

pytest_plugins = 'pytest_asyncio'

@pytest.fixture
async def fixture_a():
await asyncio.sleep(0)
return 1

async def test_a(fixture_a):
await asyncio.sleep(0)
assert fixture_a == 1
"""
)
)
result = pytester.runpytest("--asyncio-mode=auto")
result.assert_outcomes(passed=1)


def test_auto_mode_method_fixture(pytester):
pytester.makepyfile(
dedent(
"""\
import asyncio
import pytest

pytest_plugins = 'pytest_asyncio'


class TestA:

@pytest.fixture
async def fixture_a(self):
await asyncio.sleep(0)
return 1

async def test_a(self, fixture_a):
await asyncio.sleep(0)
assert fixture_a == 1
"""
)
)
result = pytester.runpytest("--asyncio-mode=auto")
result.assert_outcomes(passed=1)
115 changes: 115 additions & 0 deletions tests/modes/test_legacy_mode.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
from textwrap import dedent

pytest_plugins = "pytester"


LEGACY_MODE = (
"The 'asyncio_mode' default value will change to 'strict' in future, "
"please explicitly use 'asyncio_mode=strict' or 'asyncio_mode=auto' "
"in pytest configuration file."
)

LEGACY_ASYNCIO_FIXTURE = (
"'@pytest.fixture' is applied to {name} "
"in 'legacy' mode, "
"please replace it with '@pytest_asyncio.pytest_asyncio' as a preparation "
"for switching to 'strict' mode (or use 'auto' mode to seamlessly handle "
"all these fixtures as asyncio-driven)."
).format(name="*")


def test_warning_for_legacy_mode_cmdline(pytester):
pytester.makepyfile(
dedent(
"""\
import asyncio
import pytest

pytest_plugins = 'pytest_asyncio'

@pytest.mark.asyncio
async def test_a():
await asyncio.sleep(0)
"""
)
)
result = pytester.runpytest("--asyncio-mode=legacy")
assert result.parseoutcomes()["warnings"] == 1
result.stdout.fnmatch_lines(["*" + LEGACY_MODE + "*"])


def test_warning_for_legacy_mode_cfg(pytester):
pytester.makepyfile(
dedent(
"""\
import asyncio
import pytest

pytest_plugins = 'pytest_asyncio'

@pytest.mark.asyncio
async def test_a():
await asyncio.sleep(0)
"""
)
)
pytester.makefile(".ini", pytest="[pytest]\nasyncio_mode = legacy\n")
result = pytester.runpytest()
assert result.parseoutcomes()["warnings"] == 1
result.stdout.fnmatch_lines(["*" + LEGACY_MODE + "*"])
result.stdout.no_fnmatch_line("*" + LEGACY_ASYNCIO_FIXTURE + "*")


def test_warning_for_legacy_fixture(pytester):
pytester.makepyfile(
dedent(
"""\
import asyncio
import pytest

pytest_plugins = 'pytest_asyncio'

@pytest.fixture
async def fixture_a():
await asyncio.sleep(0)
return 1

@pytest.mark.asyncio
async def test_a(fixture_a):
await asyncio.sleep(0)
assert fixture_a == 1
"""
)
)
result = pytester.runpytest("--asyncio-mode=legacy")
assert result.parseoutcomes()["warnings"] == 2
result.stdout.fnmatch_lines(["*" + LEGACY_ASYNCIO_FIXTURE + "*"])


def test_warning_for_legacy_method_fixture(pytester):
pytester.makepyfile(
dedent(
"""\
import asyncio
import pytest

pytest_plugins = 'pytest_asyncio'


class TestA:

@pytest.fixture
async def fixture_a(self):
await asyncio.sleep(0)
return 1

@pytest.mark.asyncio
async def test_a(self, fixture_a):
await asyncio.sleep(0)
assert fixture_a == 1
"""
)
)
result = pytester.runpytest("--asyncio-mode=legacy")
assert result.parseoutcomes()["warnings"] == 2
result.stdout.fnmatch_lines(["*" + LEGACY_ASYNCIO_FIXTURE + "*"])
Loading