@@ -11,7 +11,9 @@ def isasyncgenfunction(x):
11
11
12
12
import pytest
13
13
import trio
14
+ from trio ._util import acontextmanager
14
15
from trio .testing import MockClock , trio_test
16
+ from async_generator import async_generator , yield_
15
17
16
18
17
19
def pytest_configure (config ):
@@ -29,34 +31,69 @@ def _trio_test_runner_factory(item):
29
31
@trio_test
30
32
async def _bootstrap_fixture_and_run_test (** kwargs ):
31
33
__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
35
60
36
61
return _bootstrap_fixture_and_run_test
37
62
38
63
64
+ @acontextmanager
65
+ @async_generator
39
66
async def _setup_async_fixtures_in (deps ):
40
- resolved_deps = { ** deps }
67
+ __tracebackhide__ = True
41
68
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
+ ]
44
72
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
53
76
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 )})
60
97
61
98
62
99
class BaseAsyncFixture :
@@ -68,49 +105,41 @@ def __init__(self, fixturedef, deps={}):
68
105
self .fixturedef = fixturedef
69
106
self .deps = deps
70
107
self .setup_done = False
71
- self .teardown_done = False
72
108
self .result = None
73
- self .lock = trio .Lock ()
74
109
110
+ @acontextmanager
111
+ @async_generator
75
112
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 )
81
121
82
122
async def _setup (self ):
83
123
raise NotImplementedError ()
84
124
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
-
94
125
95
126
class AsyncYieldFixture (BaseAsyncFixture ):
96
127
"""
97
128
Async generator fixture.
98
129
"""
99
130
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 )
103
136
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 ))
108
138
109
- async def _teardown (self ):
110
139
try :
111
- await self . agen .asend (None )
140
+ await agen .asend (None )
112
141
except StopAsyncIteration :
113
- await _teardown_async_fixtures_in ( self . deps )
142
+ pass
114
143
else :
115
144
raise RuntimeError ('Only one yield in fixture is allowed' )
116
145
@@ -120,33 +149,30 @@ class SyncFixtureWithAsyncDeps(BaseAsyncFixture):
120
149
Synchronous function fixture with asynchronous dependencies fixtures.
121
150
"""
122
151
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 ))
129
157
130
158
131
159
class SyncYieldFixtureWithAsyncDeps (BaseAsyncFixture ):
132
160
"""
133
161
Synchronous generator fixture with asynchronous dependencies fixtures.
134
162
"""
135
163
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 )
139
169
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 ))
144
171
145
- async def _teardown (self ):
146
172
try :
147
- await self . gen .send (None )
173
+ gen .send (None )
148
174
except StopIteration :
149
- await _teardown_async_fixtures_in ( self . deps )
175
+ pass
150
176
else :
151
177
raise RuntimeError ('Only one yield in fixture is allowed' )
152
178
@@ -156,12 +182,11 @@ class AsyncFixture(BaseAsyncFixture):
156
182
Regular async fixture (i.e. coroutine).
157
183
"""
158
184
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 ))
165
190
166
191
167
192
def _install_async_fixture_if_needed (fixturedef , request ):
@@ -215,3 +240,8 @@ def mock_clock():
215
240
@pytest .fixture
216
241
def autojump_clock ():
217
242
return MockClock (autojump_threshold = 0 )
243
+
244
+
245
+ @pytest .fixture
246
+ async def nursery (request ):
247
+ return request .node ._trio_nursery
0 commit comments