Skip to content

Commit 086f44b

Browse files
authored
Merge pull request #28 from touilleMan/fixture-in-recursive-acontextmanager-with-nursery-fixture
Recursive async context managers for fixtures + nursery fixture
2 parents ce66103 + 57d564c commit 086f44b

File tree

5 files changed

+202
-69
lines changed

5 files changed

+202
-69
lines changed

pytest_trio/_tests/test_async_yield_fixture.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,3 +224,40 @@ async def test_actual_test(fix1):
224224
# TODO: should trigger error instead of failure
225225
# result.assert_outcomes(error=1)
226226
result.assert_outcomes(failed=1)
227+
228+
229+
@pytest.mark.skipif(sys.version_info < (3, 6), reason="requires python3.6")
230+
def test_async_yield_fixture_with_nursery(testdir):
231+
232+
testdir.makepyfile(
233+
"""
234+
import pytest
235+
import trio
236+
237+
238+
async def handle_client(stream):
239+
while True:
240+
buff = await stream.receive_some(4)
241+
await stream.send_all(buff)
242+
243+
244+
@pytest.fixture
245+
async def server():
246+
async with trio.open_nursery() as nursery:
247+
listeners = await nursery.start(trio.serve_tcp, handle_client, 0)
248+
yield listeners[0]
249+
nursery.cancel_scope.cancel()
250+
251+
252+
@pytest.mark.trio
253+
async def test_actual_test(server):
254+
stream = await trio.testing.open_stream_to_socket_listener(server)
255+
await stream.send_all(b'ping')
256+
rep = await stream.receive_some(4)
257+
assert rep == b'ping'
258+
"""
259+
)
260+
261+
result = testdir.runpytest()
262+
263+
result.assert_outcomes(passed=1)
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import pytest
2+
import trio
3+
4+
5+
async def handle_client(stream):
6+
while True:
7+
buff = await stream.receive_some(4)
8+
await stream.send_all(buff)
9+
10+
11+
@pytest.fixture
12+
async def server(nursery):
13+
listeners = await nursery.start(trio.serve_tcp, handle_client, 0)
14+
return listeners[0]
15+
16+
17+
@pytest.mark.trio
18+
async def test_try(server):
19+
stream = await trio.testing.open_stream_to_socket_listener(server)
20+
await stream.send_all(b'ping')
21+
rep = await stream.receive_some(4)
22+
assert rep == b'ping'

pytest_trio/_tests/test_sync_fixture.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,3 +44,46 @@ def test_after():
4444
result = testdir.runpytest()
4545

4646
result.assert_outcomes(passed=3)
47+
48+
49+
def test_single_yield_fixture_with_async_deps(testdir):
50+
51+
testdir.makepyfile(
52+
"""
53+
import pytest
54+
import trio
55+
56+
events = []
57+
58+
@pytest.fixture
59+
async def fix0():
60+
events.append('fix0 setup')
61+
await trio.sleep(0)
62+
return 'fix0'
63+
64+
@pytest.fixture
65+
def fix1(fix0):
66+
events.append('fix1 setup')
67+
yield 'fix1 - ' + fix0
68+
events.append('fix1 teardown')
69+
70+
def test_before():
71+
assert not events
72+
73+
@pytest.mark.trio
74+
async def test_actual_test(fix1):
75+
assert events == ['fix0 setup', 'fix1 setup']
76+
assert fix1 == 'fix1 - fix0'
77+
78+
def test_after():
79+
assert events == [
80+
'fix0 setup',
81+
'fix1 setup',
82+
'fix1 teardown',
83+
]
84+
"""
85+
)
86+
87+
result = testdir.runpytest()
88+
89+
result.assert_outcomes(passed=3)

pytest_trio/plugin.py

Lines changed: 98 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@ def isasyncgenfunction(x):
1111

1212
import pytest
1313
import trio
14+
from trio._util import acontextmanager
1415
from trio.testing import MockClock, trio_test
16+
from async_generator import async_generator, yield_
1517

1618

1719
def pytest_configure(config):
@@ -29,34 +31,69 @@ def _trio_test_runner_factory(item):
2931
@trio_test
3032
async def _bootstrap_fixture_and_run_test(**kwargs):
3133
__tracebackhide__ = True
32-
resolved_kwargs = await _setup_async_fixtures_in(kwargs)
33-
await testfunc(**resolved_kwargs)
34-
await _teardown_async_fixtures_in(kwargs)
34+
user_exc = None
35+
# Open the nursery exposed as fixture
36+
async with trio.open_nursery() as nursery:
37+
item._trio_nursery = nursery
38+
try:
39+
async with _setup_async_fixtures_in(kwargs) as resolved_kwargs:
40+
try:
41+
await testfunc(**resolved_kwargs)
42+
except BaseException as exc:
43+
# Regular pytest fixture don't have access to the test
44+
# exception in there teardown, we mimic this behavior here.
45+
user_exc = exc
46+
except BaseException as exc:
47+
# If we are here, the exception comes from the fixtures setup
48+
# or teardown
49+
if user_exc:
50+
raise exc from user_exc
51+
else:
52+
raise exc
53+
finally:
54+
# No matter what the nursery fixture should be closed when test is over
55+
nursery.cancel_scope.cancel()
56+
57+
# Finally re-raise or original exception coming from the test if needed
58+
if user_exc:
59+
raise user_exc
3560

3661
return _bootstrap_fixture_and_run_test
3762

3863

64+
@acontextmanager
65+
@async_generator
3966
async def _setup_async_fixtures_in(deps):
40-
resolved_deps = {**deps}
67+
__tracebackhide__ = True
4168

42-
async def _resolve_and_update_deps(afunc, deps, entry):
43-
deps[entry] = await afunc()
69+
need_resolved_deps_stack = [
70+
(k, v) for k, v in deps.items() if isinstance(v, BaseAsyncFixture)
71+
]
4472

45-
async with trio.open_nursery() as nursery:
46-
for depname, depval in resolved_deps.items():
47-
if isinstance(depval, BaseAsyncFixture):
48-
nursery.start_soon(
49-
_resolve_and_update_deps, depval.setup, resolved_deps,
50-
depname
51-
)
52-
return resolved_deps
73+
if not need_resolved_deps_stack:
74+
await yield_(deps)
75+
return
5376

54-
55-
async def _teardown_async_fixtures_in(deps):
56-
async with trio.open_nursery() as nursery:
57-
for depval in deps.values():
58-
if isinstance(depval, BaseAsyncFixture):
59-
nursery.start_soon(depval.teardown)
77+
@acontextmanager
78+
@async_generator
79+
async def _recursive_setup(deps_stack):
80+
__tracebackhide__ = True
81+
name, dep = deps_stack.pop()
82+
async with dep.setup() as resolved:
83+
if not deps_stack:
84+
await yield_([(name, resolved)])
85+
else:
86+
async with _recursive_setup(
87+
deps_stack
88+
) as remains_deps_stack_resolved:
89+
await yield_(
90+
remains_deps_stack_resolved + [(name, resolved)]
91+
)
92+
93+
async with _recursive_setup(
94+
need_resolved_deps_stack
95+
) as resolved_deps_stack:
96+
await yield_({**deps, **dict(resolved_deps_stack)})
6097

6198

6299
class BaseAsyncFixture:
@@ -68,49 +105,41 @@ def __init__(self, fixturedef, deps={}):
68105
self.fixturedef = fixturedef
69106
self.deps = deps
70107
self.setup_done = False
71-
self.teardown_done = False
72108
self.result = None
73-
self.lock = trio.Lock()
74109

110+
@acontextmanager
111+
@async_generator
75112
async def setup(self):
76-
async with self.lock:
77-
if not self.setup_done:
78-
self.result = await self._setup()
79-
self.setup_done = True
80-
return self.result
113+
__tracebackhide__ = True
114+
if self.setup_done:
115+
await yield_(self.result)
116+
else:
117+
async with _setup_async_fixtures_in(self.deps) as resolved_deps:
118+
async with self._setup(resolved_deps) as self.result:
119+
self.setup_done = True
120+
await yield_(self.result)
81121

82122
async def _setup(self):
83123
raise NotImplementedError()
84124

85-
async def teardown(self):
86-
async with self.lock:
87-
if not self.teardown_done:
88-
await self._teardown()
89-
self.teardown_done = True
90-
91-
async def _teardown(self):
92-
raise NotImplementedError()
93-
94125

95126
class AsyncYieldFixture(BaseAsyncFixture):
96127
"""
97128
Async generator fixture.
98129
"""
99130

100-
def __init__(self, *args):
101-
super().__init__(*args)
102-
self.agen = None
131+
@acontextmanager
132+
@async_generator
133+
async def _setup(self, resolved_deps):
134+
__tracebackhide__ = True
135+
agen = self.fixturedef.func(**resolved_deps)
103136

104-
async def _setup(self):
105-
resolved_deps = await _setup_async_fixtures_in(self.deps)
106-
self.agen = self.fixturedef.func(**resolved_deps)
107-
return await self.agen.asend(None)
137+
await yield_(await agen.asend(None))
108138

109-
async def _teardown(self):
110139
try:
111-
await self.agen.asend(None)
140+
await agen.asend(None)
112141
except StopAsyncIteration:
113-
await _teardown_async_fixtures_in(self.deps)
142+
pass
114143
else:
115144
raise RuntimeError('Only one yield in fixture is allowed')
116145

@@ -120,33 +149,30 @@ class SyncFixtureWithAsyncDeps(BaseAsyncFixture):
120149
Synchronous function fixture with asynchronous dependencies fixtures.
121150
"""
122151

123-
async def _setup(self):
124-
resolved_deps = await _setup_async_fixtures_in(self.deps)
125-
return self.fixturedef.func(**resolved_deps)
126-
127-
async def _teardown(self):
128-
await _teardown_async_fixtures_in(self.deps)
152+
@acontextmanager
153+
@async_generator
154+
async def _setup(self, resolved_deps):
155+
__tracebackhide__ = True
156+
await yield_(self.fixturedef.func(**resolved_deps))
129157

130158

131159
class SyncYieldFixtureWithAsyncDeps(BaseAsyncFixture):
132160
"""
133161
Synchronous generator fixture with asynchronous dependencies fixtures.
134162
"""
135163

136-
def __init__(self, *args):
137-
super().__init__(*args)
138-
self.agen = None
164+
@acontextmanager
165+
@async_generator
166+
async def _setup(self, resolved_deps):
167+
__tracebackhide__ = True
168+
gen = self.fixturedef.func(**resolved_deps)
139169

140-
async def _setup(self):
141-
resolved_deps = await _setup_async_fixtures_in(self.deps)
142-
self.gen = self.fixturedef.func(**resolved_deps)
143-
return self.gen.send(None)
170+
await yield_(gen.send(None))
144171

145-
async def _teardown(self):
146172
try:
147-
await self.gen.send(None)
173+
gen.send(None)
148174
except StopIteration:
149-
await _teardown_async_fixtures_in(self.deps)
175+
pass
150176
else:
151177
raise RuntimeError('Only one yield in fixture is allowed')
152178

@@ -156,12 +182,11 @@ class AsyncFixture(BaseAsyncFixture):
156182
Regular async fixture (i.e. coroutine).
157183
"""
158184

159-
async def _setup(self):
160-
resolved_deps = await _setup_async_fixtures_in(self.deps)
161-
return await self.fixturedef.func(**resolved_deps)
162-
163-
async def _teardown(self):
164-
await _teardown_async_fixtures_in(self.deps)
185+
@acontextmanager
186+
@async_generator
187+
async def _setup(self, resolved_deps):
188+
__tracebackhide__ = True
189+
await yield_(await self.fixturedef.func(**resolved_deps))
165190

166191

167192
def _install_async_fixture_if_needed(fixturedef, request):
@@ -215,3 +240,8 @@ def mock_clock():
215240
@pytest.fixture
216241
def autojump_clock():
217242
return MockClock(autojump_threshold=0)
243+
244+
245+
@pytest.fixture
246+
async def nursery(request):
247+
return request.node._trio_nursery

setup.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,10 @@
1414
author_email="[email protected]",
1515
license="MIT -or- Apache License 2.0",
1616
packages=find_packages(),
17-
entry_points={'pytest11': ['trio = pytest_trio.plugin',]},
17+
entry_points={'pytest11': ['trio = pytest_trio.plugin']},
1818
install_requires=[
1919
"trio",
20+
"async_generator >= 1.6",
2021
],
2122
keywords=[
2223
'async',

0 commit comments

Comments
 (0)