8000 Support alternatives to trio.run by altendky · Pull Request #105 · python-trio/pytest-trio · GitHub
[go: up one dir, main page]

Skip to content

Support alternatives to trio.run #105

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

Merged
merged 36 commits into from
Oct 11, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
4279e12
Add trio_run for configurable substitution for trio.run
altendky Aug 27, 2020
3d213fe
--upgrade for the Trio PR
altendky Aug 27, 2020
5b1448d
Revert "--upgrade for the Trio PR"
altendky Aug 27, 2020
05aa01d
add qtrio for testing
altendky Aug 27, 2020
1354761
Update test-requirements.txt
altendky Aug 27, 2020
d128020
Update test-requirements.txt
altendky Aug 27, 2020
b6b7d01
no "s i guess
altendky Aug 27, 2020
a021e3f
use fake qtrio for test to avoid the complicated system setup
altendky Aug 27, 2020
9d05728
yapf
altendky Aug 27, 2020
1104289
yapf (actually)
altendky Aug 27, 2020
8cd81b0
yapf (again (again)) (but with 0.22.0)
altendky Aug 27, 2020
1ebf018
Move trio_test() here
altendky Aug 27, 2020
a4bb174
Apply trio_run config for @pytest.mark.trio as well
altendky Aug 27, 2020
cf9288c
Drop that global config_run
altendky Aug 28, 2020
a7be067
yapf
altendky Aug 28, 2020
dbcb6dc
Replace 1 / 0 with real exceptions
altendky Aug 28, 2020
6ace199
yapf
altendky Aug 28, 2020
0c01e6f
requiring calling as @_trio_test(run=something)
altendky Aug 28, 2020
2be7a24
add test for invalid trio_run name
altendky Aug 28, 2020
e9335c7
add test for multiple third party runners error
altendky Aug 28, 2020
1639e34
add test for qtrio.run marker over trio mode
altendky Aug 28, 2020
a2cf6e6
yapf
altendky Aug 28, 2020
910de1d
more tests and coverage
altendky Aug 28, 2020
511a6f6
#egg=trio
altendky Aug 29, 2020
571128b
Add trio_run documentation
altendky Aug 29, 2020
339b34b
Drop no longer needed Trio PR dependency
altendky Aug 29, 2020
ad87be9
Add trio-run-config anchor in the docs
altendky Aug 29, 2020
3df1b2a
Merge branch 'master' into configurable_trio_run
altendky Sep 3, 2020
99fc0d2
Merge branch 'master' into configurable_trio_run
altendky Sep 17, 2020
f2ab692
Use f.__qualname__ when reporting too many runners
altendky Sep 19, 2020
a45d314
Explain reason for fake qtrio module-in-a-string
altendky Oct 2, 2020
7f7c4e1
Make trio_run a regular string ini option
altendky Oct 2, 2020
c998963
Tweak string split across lines
altendky Oct 2, 2020
68860e0
Remove checks for multiple conflicting runner specification
altendky Oct 2, 2020
2b89fc1
Use closest run-specifying marker, else config
altendky Oct 11, 2020
682499f
remove blank line for yapf
altendky Oct 11, 2020
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
39 changes: 39 additions & 0 deletions docs/source/reference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -381,3 +381,42 @@ write `stateful tests
<https://hypothesis.readthedocs.io/en/latest/stateful.html>`__ for
Trio-based libraries, then check out `hypothesis-trio
<https://github.com/python-trio/hypothesis-trio>`__.


.. _trio-run-config:

Using alternative Trio runners
------------------------------

If you are working with a library that provides integration with Trio,
such as via :ref:`guest mode <trio:guest-mode>`, it can be used with
pytest-trio as well. Setting ``trio_run`` in the pytest configuration
makes your choice the global default for both tests explicitly marked
with ``@pytest.mark.trio`` and those automatically marked by Trio mode.
``trio_run`` presently supports ``trio`` and ``qtrio``.

.. code-block:: ini

# pytest.ini
[pytest]
trio_mode = true
trio_run = qtrio

.. code-block:: python

import pytest

@pytest.mark.trio
async def test():
assert True

If you want more granular control or need to use a specific function,
it can be passed directly to the marker.

.. code-block:: python

import pytest

@pytest.mark.trio(run=qtrio.run)
async def test():
assert True
13 changes: 11 additions & 2 deletions pytest_trio/_tests/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,20 @@ def enable_trio_mode_via_pytest_ini(testdir):
testdir.makefile(".ini", pytest="[pytest]\ntrio_mode = true\n")


def enable_trio_mode_trio_run_via_pytest_ini(testdir):
testdir.makefile(
".ini", pytest="[pytest]\ntrio_mode = true\ntrio_run = trio\n"
)


def enable_trio_mode_via_conftest_py(testdir):
testdir.makeconftest("from pytest_trio.enable_trio_mode import *")


enable_trio_mode = pytest.mark.parametrize(
"enable_trio_mode",
[enable_trio_mode_via_pytest_ini, enable_trio_mode_via_conftest_py]
"enable_trio_mode", [
enable_trio_mode_via_pytest_ini,
enable_trio_mode_trio_run_via_pytest_ini,
enable_trio_mode_via_conftest_py,
]
)
25 changes: 25 additions & 0 deletions pytest_trio/_tests/test_fixture_mistakes.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,3 +146,28 @@ async def test_whatever(async_fixture):

result.assert_outcomes(failed=1)
result.stdout.fnmatch_lines(["*async_fixture*cancelled the test*"])


@enable_trio_mode
def test_too_many_clocks(testdir, enable_trio_mode):
enable_trio_mode(testdir)

testdir.makepyfile(
"""
import pytest

@pytest.fixture
def extra_clock(mock_clock):
return mock_clock

async def test_whatever(mock_clock, extra_clock):
pass
"""
)

result = testdir.runpytest()

result.assert_outcomes(failed=1)
result.stdout.fnmatch_lines(
["*ValueError: too many clocks spoil the broth!*"]
)
145 changes: 145 additions & 0 deletions pytest_trio/_tests/test_trio_mode.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,148 @@ def test_trio_mode(testdir, enable_trio_mode):

result = testdir.runpytest()
result.assert_outcomes(passed=2, failed=2)


# This is faking qtrio due to real qtrio's dependence on either
# PyQt5 or PySide2. They are both large and require special
# handling in CI. The testing here is able to focus on the
# pytest-trio features with just this minimal substitute.
qtrio_text = """
import trio

fake_used = False

def run(*args, **kwargs):
global fake_used
fake_used = True

return trio.run(*args, **kwargs)
"""


def test_trio_mode_and_qtrio_run_configuration(testdir):
testdir.makefile(
".ini", pytest="[pytest]\ntrio_mode = true\ntrio_run = qtrio\n"
)

testdir.makepyfile(qtrio=qtrio_text)

test_text = """
import qtrio
import trio

async def test_fake_qtrio_used():
await trio.sleep(0)
assert qtrio.fake_used
"""
testdir.makepyfile(test_text)

result = testdir.runpytest()
result.assert_outcomes(passed=1)


def test_trio_mode_and_qtrio_marker(testdir):
testdir.makefile(".ini", pytest="[pytest]\ntrio_mode = true\n")

testdir.makepyfile(qtrio=qtrio_text)

test_text = """
import pytest
import qtrio
import trio

@pytest.mark.trio(run=qtrio.run)
async def test_fake_qtrio_used():
await trio.sleep(0)
assert qtrio.fake_used
"""
testdir.makepyfile(test_text)

result = testdir.runpytest()
result.assert_outcomes(passed=1)


def test_qtrio_just_run_configuration(testdir):
testdir.makefile(".ini", pytest="[pytest]\ntrio_run = qtrio\n")

testdir.makepyfile(qtrio=qtrio_text)

test_text = """
import pytest
import qtrio
import trio

@pytest.mark.trio
async def test_fake_qtrio_used():
await trio.sleep(0)
assert qtrio.fake_used
"""
testdir.makepyfile(test_text)

result = testdir.runpytest()
result.assert_outcomes(passed=1)


def test_invalid_trio_run_fails(testdir):
run_name = "invalid_trio_run"

testdir.makefile(
".ini", pytest=f"[pytest]\ntrio_mode = true\ntrio_run = {run_name}\n"
)

test_text = """
async def test():
pass
"""
testdir.makepyfile(test_text)

result = testdir.runpytest()
result.assert_outcomes()
result.stdout.fnmatch_lines(
[
f"*ValueError: {run_name!r} not valid for 'trio_run' config. Must be one of: *"
]
)


def test_closest_explicit_run_wins(testdir):
testdir.makefile(
".ini", pytest=f"[pytest]\ntrio_mode = true\ntrio_run = trio\n"
)
testdir.makepyfile(qtrio=qtrio_text)

test_text = """
import pytest
import pytest_trio
import qtrio

@pytest.mark.trio(run='should be ignored')
@pytest.mark.trio(run=qtrio.run)
async def test():
assert qtrio.fake_used
"""
testdir.makepyfile(test_text)

result = testdir.runpytest()
result.assert_outcomes(passed=1)


def test_ini_run_wins_with_blank_marker(testdir):
testdir.makefile(
".ini", pytest=f"[pytest]\ntrio_mode = true\ntrio_run = qtrio\n"
)
testdir.makepyfile(qtrio=qtrio_text)

test_text = """
import pytest
import pytest_trio
import qtrio

@pytest.mark.trio
async def test():
assert qtrio.fake_used
"""
testdir.makepyfile(test_text)

result = testdir.runpytest()
result.assert_outcomes(passed=1)
81 changes: 75 additions & 6 deletions pytest_trio/plugin.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""pytest-trio implementation."""
from functools import wraps, partial
import sys
from traceback import format_exception
from collections.abc import Coroutine, Generator
Expand All @@ -7,7 +8,8 @@
import outcome
import pytest
import trio
from trio.testing import MockClock, trio_test
from trio.abc import Clock, Instrument
from trio.testing import MockClock
from async_generator import (
async_generator, yield_, asynccontextmanager, isasyncgen,
isasyncgenfunction
Expand Down Expand Up @@ -39,6 +41,11 @@ def pytest_addoption(parser):
type="bool",
default=False,
)
parser.addini(
"trio_run",
"what runner should pytest-trio use? [trio, qtrio]",
default="trio",
)


def pytest_configure(config):
Expand Down Expand Up @@ -307,8 +314,53 @@ async def run(self, test_ctx, contextvars_ctx):
raise RuntimeError("too many yields in fixture")


def _trio_test(run):
"""Use:
@trio_test
async def test_whatever():
await ...

Also: if a pytest fixture is passed in that subclasses the ``Clock`` abc, then
that clock is passed to ``trio.run()``.
"""

def decorator(fn):
@wraps(fn)
def wrapper(**kwargs):
__tracebackhide__ = True
clocks = [c for c in kwargs.values() if isinstance(c, Clock)]
if not clocks:
clock = None
elif len(clocks) == 1:
clock = clocks[0]
else:
raise ValueError("too many clocks spoil the broth!")
instruments = [
i for i in kwargs.values() if isinstance(i, Instrument)
]
return run(
partial(fn, **kwargs), clock=clock, instruments=instruments
)

return wrapper

return decorator


def _trio_test_runner_factory(item, testfunc=None):
testfunc = testfunc or item.obj
if testfunc:
run = trio.run
else:
testfunc = item.obj

for marker in item.iter_markers("trio"):
maybe_run = marker.kwargs.get('run')
if maybe_run is not None:
run = maybe_run
break
else:
# no marker found that explicitly specifiers the runner so use config
run = choose_run(config=item.config)

if getattr(testfunc, '_trio_test_runner_wrapped', False):
# We have already wrapped this, perhaps because we combined Hypothesis
Expand All @@ -320,7 +372,7 @@ def _trio_test_runner_factory(item, testfunc=None):
'test function `%r` is marked trio but is not async' % item
)

@trio_test
@_trio_test(run=run)
async def _bootstrap_fixtures_and_run_test(**kwargs):
__tracebackhide__ = True

Expand Down Expand Up @@ -438,19 +490,36 @@ def pytest_fixture_setup(fixturedef, request):
################################################################


def automark(items):
def automark(items, run=trio.run):
for item in items:
if hasattr(item.obj, "hypothesis"):
test_func = item.obj.hypothesis.inner_test
else:
test_func = item.obj
if iscoroutinefunction(test_func):
item.add_marker(pytest.mark.trio)
item.add_marker(pytest.mark.trio(run=run))


def choose_run(config):
run_string = config.getini("trio_run")

if run_string == "trio":
run = trio.run
elif run_string == "qtrio":
import qtrio
run = qtrio.run
else:
raise ValueError(
f"{run_string!r} not valid for 'trio_run' config." +
" Must be one of: trio, qtrio"
)

return run


def pytest_collection_modifyitems(config, items):
if config.getini("trio_mode"):
automark(items)
automark(items, run=choose_run(config=config))


################################################################
Expand Down
0