Skip to content

Commit 7f5c034

Browse files
authored
Merge pull request #105 from altendky/configurable_trio_run
Support alternatives to trio.run
2 parents 9a4b70e + 682499f commit 7f5c034

File tree

5 files changed

+295
-8
lines changed

5 files changed

+295
-8
lines changed

docs/source/reference.rst

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -381,3 +381,42 @@ write `stateful tests
381381
<https://hypothesis.readthedocs.io/en/latest/stateful.html>`__ for
382382
Trio-based libraries, then check out `hypothesis-trio
383383
<https://github.com/python-trio/hypothesis-trio>`__.
384+
385+
386+
.. _trio-run-config:
387+
388+
Using alternative Trio runners
389+
------------------------------
390+
391+
If you are working with a library that provides integration with Trio,
392+
such as via :ref:`guest mode <trio:guest-mode>`, it can be used with
393+
pytest-trio as well. Setting ``trio_run`` in the pytest configuration
394+
makes your choice the global default for both tests explicitly marked
395+
with ``@pytest.mark.trio`` and those automatically marked by Trio mode.
396+
``trio_run`` presently supports ``trio`` and ``qtrio``.
397+
398+
.. code-block:: ini
399+
400+
# pytest.ini
401+
[pytest]
402+
trio_mode = true
403+
trio_run = qtrio
404+
405+
.. code-block:: python
406+
407+
import pytest
408+
409+
@pytest.mark.trio
410+
async def test():
411+
assert True
412+
413+
If you want more granular control or need to use a specific function,
414+
it can be passed directly to the marker.
415+
416+
.. code-block:: python
417+
418+
import pytest
419+
420+
@pytest.mark.trio(run=qtrio.run)
421+
async def test():
422+
assert True

pytest_trio/_tests/helpers.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,20 @@ def enable_trio_mode_via_pytest_ini(testdir):
55
testdir.makefile(".ini", pytest="[pytest]\ntrio_mode = true\n")
66

77

8+
def enable_trio_mode_trio_run_via_pytest_ini(testdir):
9+
testdir.makefile(
10+
".ini", pytest="[pytest]\ntrio_mode = true\ntrio_run = trio\n"
11+
)
12+
13+
814
def enable_trio_mode_via_conftest_py(testdir):
915
testdir.makeconftest("from pytest_trio.enable_trio_mode import *")
1016

1117

1218
enable_trio_mode = pytest.mark.parametrize(
13-
"enable_trio_mode",
14-
[enable_trio_mode_via_pytest_ini, enable_trio_mode_via_conftest_py]
19+
"enable_trio_mode", [
20+
enable_trio_mode_via_pytest_ini,
21+
enable_trio_mode_trio_run_via_pytest_ini,
22+
enable_trio_mode_via_conftest_py,
23+
]
1524
)

pytest_trio/_tests/test_fixture_mistakes.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,3 +146,28 @@ async def test_whatever(async_fixture):
146146

147147
result.assert_outcomes(failed=1)
148148
result.stdout.fnmatch_lines(["*async_fixture*cancelled the test*"])
149+
150+
151+
@enable_trio_mode
152+
def test_too_many_clocks(testdir, enable_trio_mode):
153+
enable_trio_mode(testdir)
154+
155+
testdir.makepyfile(
156+
"""
157+
import pytest
158+
159+
@pytest.fixture
160+
def extra_clock(mock_clock):
161+
return mock_clock
162+
163+
async def test_whatever(mock_clock, extra_clock):
164+
pass
165+
"""
166+
)
167+
168+
result = testdir.runpytest()
169+
170+
result.assert_outcomes(failed=1)
171+
result.stdout.fnmatch_lines(
172+
["*ValueError: too many clocks spoil the broth!*"]
173+
)

pytest_trio/_tests/test_trio_mode.py

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,3 +36,148 @@ def test_trio_mode(testdir, enable_trio_mode):
3636

3737
result = testdir.runpytest()
3838
result.assert_outcomes(passed=2, failed=2)
39+
40+
41+
# This is faking qtrio due to real qtrio's dependence on either
42+
# PyQt5 or PySide2. They are both large and require special
43+
# handling in CI. The testing here is able to focus on the
44+
# pytest-trio features with just this minimal substitute.
45+
qtrio_text = """
46+
import trio
47+
48+
fake_used = False
49+
50+
def run(*args, **kwargs):
51+
global fake_used
52+
fake_used = True
53+
54+
return trio.run(*args, **kwargs)
55+
"""
56+
57+
58+
def test_trio_mode_and_qtrio_run_configuration(testdir):
59+
testdir.makefile(
60+
".ini", pytest="[pytest]\ntrio_mode = true\ntrio_run = qtrio\n"
61+
)
62+
63+
testdir.makepyfile(qtrio=qtrio_text)
64+
65+
test_text = """
66+
import qtrio
67+
import trio
68+
69+
async def test_fake_qtrio_used():
70+
await trio.sleep(0)
71+
assert qtrio.fake_used
72+
"""
73+
testdir.makepyfile(test_text)
74+
75+
result = testdir.runpytest()
76+
result.assert_outcomes(passed=1)
77+
78+
79+
def test_trio_mode_and_qtrio_marker(testdir):
80+
testdir.makefile(".ini", pytest="[pytest]\ntrio_mode = true\n")
81+
82+
testdir.makepyfile(qtrio=qtrio_text)
83+
84+
test_text = """
85+
import pytest
86+
import qtrio
87+
import trio
88+
89+
@pytest.mark.trio(run=qtrio.run)
90+
async def test_fake_qtrio_used():
91+
await trio.sleep(0)
92+
assert qtrio.fake_used
93+
"""
94+
testdir.makepyfile(test_text)
95+
96+
result = testdir.runpytest()
97+
result.assert_outcomes(passed=1)
98+
99+
100+
def test_qtrio_just_run_configuration(testdir):
101+
testdir.makefile(".ini", pytest="[pytest]\ntrio_run = qtrio\n")
102+
103+
testdir.makepyfile(qtrio=qtrio_text)
104+
105+
test_text = """
106+
import pytest
107+
import qtrio
108+
import trio
109+
110+
@pytest.mark.trio
111+
async def test_fake_qtrio_used():
112+
await trio.sleep(0)
113+
assert qtrio.fake_used
114+
"""
115+
testdir.makepyfile(test_text)
116+
117+
result = testdir.runpytest()
118+
result.assert_outcomes(passed=1)
119+
120+
121+
def test_invalid_trio_run_fails(testdir):
122+
run_name = "invalid_trio_run"
123+
124+
testdir.makefile(
125+
".ini", pytest=f"[pytest]\ntrio_mode = true\ntrio_run = {run_name}\n"
126+
)
127+
128+
test_text = """
129+
async def test():
130+
pass
131+
"""
132+
testdir.makepyfile(test_text)
133+
134+
result = testdir.runpytest()
135+
result.assert_outcomes()
136+
result.stdout.fnmatch_lines(
137+
[
138+
f"*ValueError: {run_name!r} not valid for 'trio_run' config. Must be one of: *"
139+
]
140+
)
141+
142+
143+
def test_closest_explicit_run_wins(testdir):
144+
testdir.makefile(
145+
".ini", pytest=f"[pytest]\ntrio_mode = true\ntrio_run = trio\n"
146+
)
147+
testdir.makepyfile(qtrio=qtrio_text)
148+
149+
test_text = """
150+
import pytest
151+
import pytest_trio
152+
import qtrio
153+
154+
@pytest.mark.trio(run='should be ignored')
155+
@pytest.mark.trio(run=qtrio.run)
156+
async def test():
157+
assert qtrio.fake_used
158+
"""
159+
testdir.makepyfile(test_text)
160+
161+
result = testdir.runpytest()
162+
result.assert_outcomes(passed=1)
163+
164+
165+
def test_ini_run_wins_with_blank_marker(testdir):
166+
testdir.makefile(
167+
".ini", pytest=f"[pytest]\ntrio_mode = true\ntrio_run = qtrio\n"
168+
)
169+
testdir.makepyfile(qtrio=qtrio_text)
170+
171+
test_text = """
172+
import pytest
173+
import pytest_trio
174+
import qtrio
175+
176+
@pytest.mark.trio
177+
async def test():
178+
assert qtrio.fake_used
179+
"""
180+
testdir.makepyfile(test_text)
181+
182+
result = testdir.runpytest()
183+
result.assert_outcomes(passed=1)

pytest_trio/plugin.py

Lines changed: 75 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
"""pytest-trio implementation."""
2+
from functools import wraps, partial
23
import sys
34
from traceback import format_exception
45
from collections.abc import Coroutine, Generator
@@ -7,7 +8,8 @@
78
import outcome
89
import pytest
910
import trio
10-
from trio.testing import MockClock, trio_test
11+
from trio.abc import Clock, Instrument
12+
from trio.testing import MockClock
1113
from async_generator import (
1214
async_generator, yield_, asynccontextmanager, isasyncgen,
1315
isasyncgenfunction
@@ -39,6 +41,11 @@ def pytest_addoption(parser):
3941
type="bool",
4042
default=False,
4143
)
44+
parser.addini(
45+
"trio_run",
46+
"what runner should pytest-trio use? [trio, qtrio]",
47+
default="trio",
48+
)
4249

4350

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

309316

317+
def _trio_test(run):
318+
"""Use:
319+
@trio_test
320+
async def test_whatever():
321+
await ...
322+
323+
Also: if a pytest fixture is passed in that subclasses the ``Clock`` abc, then
324+
that clock is passed to ``trio.run()``.
325+
"""
326+
327+
def decorator(fn):
328+
@wraps(fn)
329+
def wrapper(**kwargs):
330+
__tracebackhide__ = True
331+
clocks = [c for c in kwargs.values() if isinstance(c, Clock)]
332+
if not clocks:
333+
clock = None
334+
elif len(clocks) == 1:
335+
clock = clocks[0]
336+
else:
337+
raise ValueError("too many clocks spoil the broth!")
338+
instruments = [
339+
i for i in kwargs.values() if isinstance(i, Instrument)
340+
]
341+
return run(
342+
partial(fn, **kwargs), clock=clock, instruments=instruments
343+
)
344+
345+
return wrapper
346+
347+
return decorator
348+
349+
310350
def _trio_test_runner_factory(item, testfunc=None):
311-
testfunc = testfunc or item.obj
351+
if testfunc:
352+
run = trio.run
353+
else:
354+
testfunc = item.obj
355+
356+
for marker in item.iter_markers("trio"):
357+
maybe_run = marker.kwargs.get('run')
358+
if maybe_run is not None:
359+
run = maybe_run
360+
break
361+
else:
362+
# no marker found that explicitly specifiers the runner so use config
363+
run = choose_run(config=item.config)
312364

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

323-
@trio_test
375+
@_trio_test(run=run)
324376
async def _bootstrap_fixtures_and_run_test(**kwargs):
325377
__tracebackhide__ = True
326378

@@ -438,19 +490,36 @@ def pytest_fixture_setup(fixturedef, request):
438490
################################################################
439491

440492

441-
def automark(items):
493+
def automark(items, run=trio.run):
442494
for item in items:
443495
if hasattr(item.obj, "hypothesis"):
444496
test_func = item.obj.hypothesis.inner_test
445497
else:
446498
test_func = item.obj
447499
if iscoroutinefunction(test_func):
448-
item.add_marker(pytest.mark.trio)
500+
item.add_marker(pytest.mark.trio(run=run))
501+
502+
503+
def choose_run(config):
504+
run_string = config.getini("trio_run")
505+
506+
if run_string == "trio":
507+
run = trio.run
508+
elif run_string == "qtrio":
509+
import qtrio
510+
run = qtrio.run
511+
else:
512+
raise ValueError(
513+
f"{run_string!r} not valid for 'trio_run' config." +
514+
" Must be one of: trio, qtrio"
515+
)
516+
517+
return run
449518

450519

451520
def pytest_collection_modifyitems(config, items):
452521
if config.getini("trio_mode"):
453-
automark(items)
522+
automark(items, run=choose_run(config=config))
454523

455524

456525
################################################################

0 commit comments

Comments
 (0)