@@ -49,6 +49,11 @@ class _instances:
49
49
reactor = None
50
50
51
51
52
+ class _tracking :
53
+ async_yield_fixture_cache = {}
54
+ to_be_torn_down = []
55
+
56
+
52
57
def _deprecate (deprecated , recommended ):
53
58
def decorator (f ):
54
59
@functools .wraps (f )
@@ -102,14 +107,44 @@ def block_from_thread(d):
102
107
return blockingCallFromThread (_instances .reactor , lambda x : x , d )
103
108
104
109
105
- @decorator .decorator
106
- def inlineCallbacks (fun , * args , ** kw ):
107
- return defer .inlineCallbacks (fun )(* args , ** kw )
110
+ def decorator_apply (dec , func ):
111
+ """
112
+ Decorate a function by preserving the signature even if dec
113
+ is not a signature-preserving decorator.
114
+
115
+ https://github.com/micheles/decorator/blob/55a68b5ef1951614c5c37a6d201b1f3b804dbce6/docs/documentation.md#dealing-with-third-party-decorators
116
+ """
117
+ return decorator .FunctionMaker .create (
118
+ func , 'return decfunc(%(signature)s)' ,
119
+ dict (decfunc = dec (func )), __wrapped__ = func )
120
+
121
+
122
+ def inlineCallbacks (f ):
123
+ """
124
+ Mark as inline callbacks test for pytest-twisted processing and apply
125
+ @inlineCallbacks.
126
+
127
+ Unlike @ensureDeferred, @inlineCallbacks can be applied here because it
128
+ does not call nor schedule the test function. Further, @inlineCallbacks
129
+ must be applied here otherwise pytest identifies the test as a 'yield test'
130
+ for which they dropped support in 4.0 and now they skip.
131
+ """
132
+ decorated = decorator_apply (defer .inlineCallbacks , f )
133
+ _set_mark (o = decorated , mark = 'inline_callbacks_test' )
134
+
135
+ return decorated
108
136
109
137
110
- @decorator .decorator
111
- def ensureDeferred (fun , * args , ** kw ):
112
- return defer .ensureDeferred (fun (* args , ** kw ))
138
+ def ensureDeferred (f ):
139
+ """
140
+ Mark as async test for pytest-twisted processing.
141
+
142
+ Unlike @inlineCallbacks, @ensureDeferred must not be applied here since it
143
+ would call and schedule the test function.
144
+ """
145
+ _set_mark (o = f , mark = 'async_test' )
146
+
147
+ return f
113
148
114
149
115
150
def init_twisted_greenlet ():
@@ -130,10 +165,14 @@ def stop_twisted_greenlet():
130
165
_instances .gr_twisted .switch ()
131
166
132
167
133
- class _CoroutineWrapper :
134
- def __init__ (self , coroutine , mark ):
135
- self .coroutine = coroutine
136
- self .mark = mark
168
+ def _get_mark (o , default = None ):
169
+ """Get the pytest-twisted test or fixture mark."""
170
+ return getattr (o , _mark_attribute_name , default )
171
+
172
+
173
+ def _set_mark (o , mark ):
174
+ """Set the pytest-twisted test or fixture mark."""
175
+ setattr (o , _mark_attribute_name , mark )
137
176
138
177
139
178
def _marked_async_fixture (mark ):
@@ -144,21 +183,23 @@ def fixture(*args, **kwargs):
144
183
except IndexError :
145
184
scope = kwargs .get ('scope' , 'function' )
146
185
147
- if scope != 'function' :
186
+ if scope not in ['function' , 'module' ]:
187
+ # TODO: handle...
188
+ # - class
189
+ # - package
190
+ # - session
191
+ # - dynamic
192
+ #
193
+ # https://docs.pytest.org/en/latest/reference.html#pytest-fixture-api
194
+ # then remove this and update docs, or maybe keep it around
195
+ # in case new options come in without support?
196
+ #
197
+ # https://github.com/pytest-dev/pytest-twisted/issues/56
148
198
raise AsyncFixtureUnsupportedScopeError .from_scope (scope = scope )
149
199
150
- def marker (f ):
151
- @functools .wraps (f )
152
- def w (* args , ** kwargs ):
153
- return _CoroutineWrapper (
154
- coroutine = f (* args , ** kwargs ),
155
- mark = mark ,
156
- )
157
-
158
- return w
159
-
160
200
def decorator (f ):
161
- result = pytest .fixture (* args , ** kwargs )(marker (f ))
201
+ _set_mark (f , mark )
202
+ result = pytest .fixture (* args , ** kwargs )(f )
162
203
163
204
return result
164
205
@@ -167,61 +208,86 @@ def decorator(f):
167
208
return fixture
168
209
169
210
211
+ _mark_attribute_name = '_pytest_twisted_mark'
170
212
async_fixture = _marked_async_fixture ('async_fixture' )
171
213
async_yield_fixture = _marked_async_fixture ('async_yield_fixture' )
172
214
173
215
216
+ def pytest_fixture_setup (fixturedef , request ):
217
+ """Interface pytest to async for async and async yield fixtures."""
218
+ # TODO: what about _adding_ inlineCallbacks fixture support?
219
+ maybe_mark = _get_mark (fixturedef .func )
220
+ if maybe_mark is None :
221
+ return None
222
+
223
+ mark = maybe_mark
224
+
225
+ _run_inline_callbacks (
226
+ _async_pytest_fixture_setup ,
227
+ fixturedef ,
228
+ request ,
229
+ mark ,
230
+ )
231
+
232
+ return not None
233
+
234
+
174
235
@defer .inlineCallbacks
175
- def _pytest_pyfunc_call (pyfuncitem ):
176
- testfunction = pyfuncitem .obj
177
- async_generators = []
178
- funcargs = pyfuncitem .funcargs
179
- if hasattr (pyfuncitem , "_fixtureinfo" ):
180
- testargs = {}
181
- for arg in pyfuncitem ._fixtureinfo .argnames :
182
- if isinstance (funcargs [arg ], _CoroutineWrapper ):
183
- wrapper = funcargs [arg ]
184
-
185
- if wrapper .mark == 'async_fixture' :
186
- arg_value = yield defer .ensureDeferred (
187
- wrapper .coroutine
188
- )
189
- elif wrapper .mark == 'async_yield_fixture' :
190
- async_generators .append ((arg , wrapper ))
191
- arg_value = yield defer .ensureDeferred (
192
- wrapper .coroutine .__anext__ (),
193
- )
194
- else :
195
- raise UnrecognizedCoroutineMarkError .from_mark (
196
- mark = wrapper .mark ,
197
- )
198
- else :
199
- arg_value = funcargs [arg ]
200
-
201
- testargs [arg ] = arg_value
236
+ def _async_pytest_fixture_setup (fixturedef , request , mark ):
237
+ """Setup an async or async yield fixture."""
238
+ fixture_function = fixturedef .func
239
+
240
+ kwargs = {
241
+ name : request .getfixturevalue (name )
242
+ for name in fixturedef .argnames
243
+ }
244
+
245
+ if mark == 'async_fixture' :
246
+ arg_value = yield defer .ensureDeferred (
247
+ fixture_function (** kwargs )
248
+ )
249
+ elif mark == 'async_yield_fixture' :
250
+ coroutine = fixture_function (** kwargs )
251
+
252
+ finalizer = functools .partial (
253
+ _tracking .to_be_torn_down .append ,
254
+ coroutine ,
255
+ )
256
+ request .addfinalizer (finalizer )
257
+
258
+ arg_value = yield defer .ensureDeferred (
259
+ coroutine .__anext__ (),
260
+ )
202
261
else :
203
- testargs = funcargs
204
- result = yield testfunction (** testargs )
262
+ raise UnrecognizedCoroutineMarkError .from_mark (mark = mark )
205
263
206
- async_generator_deferreds = [
207
- (arg , defer .ensureDeferred (g .coroutine .__anext__ ()))
208
- for arg , g in reversed (async_generators )
209
- ]
264
+ fixturedef .cached_result = (arg_value , request .param_index , None )
210
265
211
- for arg , d in async_generator_deferreds :
212
- try :
213
- yield d
214
- except StopAsyncIteration :
215
- continue
216
- else :
217
- raise AsyncGeneratorFixtureDidNotStopError .from_generator (
218
- generator = arg ,
219
- )
266
+ defer .returnValue (arg_value )
220
267
221
- defer .returnValue (result )
222
268
269
+ @defer .inlineCallbacks
270
+ def tear_it_down (deferred ):
271
+ """Tear down a specific async yield fixture."""
272
+ try :
273
+ yield deferred
274
+ except StopAsyncIteration :
275
+ return
276
+ except Exception : # as e:
277
+ pass
278
+ # e = e
279
+ else :
280
+ pass
281
+ # e = None
223
282
224
- def pytest_pyfunc_call (pyfuncitem ):
283
+ # TODO: six.raise_from()
284
+ raise AsyncGeneratorFixtureDidNotStopError .from_generator (
285
+ generator = deferred ,
286
+ )
287
+
288
+
289
+ def _run_inline_callbacks (f , * args ):
290
+ """Interface into Twisted greenlet to run and wait for a deferred."""
225
291
if _instances .gr_twisted is not None :
226
292
if _instances .gr_twisted .dead :
227
293
raise RuntimeError ("twisted reactor has stopped" )
@@ -230,26 +296,68 @@ def in_reactor(d, f, *args):
230
296
return defer .maybeDeferred (f , * args ).chainDeferred (d )
231
297
232
298
d = defer .Deferred ()
233
- _instances .reactor .callLater (
234
- 0.0 , in_reactor , d , _pytest_pyfunc_call , pyfuncitem
235
- )
299
+ _instances .reactor .callLater (0.0 , in_reactor , d , f , * args )
236
300
blockon_default (d )
237
301
else :
238
302
if not _instances .reactor .running :
239
303
raise RuntimeError ("twisted reactor is not running" )
240
- blockingCallFromThread (
241
- _instances .reactor , _pytest_pyfunc_call , pyfuncitem
242
- )
243
- return True
304
+ blockingCallFromThread (_instances .reactor , f , * args )
305
+
306
+
307
+ @pytest .hookimpl (hookwrapper = True )
308
+ def pytest_runtest_teardown (item ):
309
+ """Tear down collected async yield fixtures."""
310
+ yield
311
+
312
+ deferreds = []
313
+ while len (_tracking .to_be_torn_down ) > 0 :
314
+ coroutine = _tracking .to_be_torn_down .pop (0 )
315
+ deferred = defer .ensureDeferred (coroutine .__anext__ ())
316
+
317
+ deferreds .append (deferred )
318
+
319
+ for deferred in deferreds :
320
+ _run_inline_callbacks (tear_it_down , deferred )
321
+
322
+
323
+ def pytest_pyfunc_call (pyfuncitem ):
324
+ """Interface to async test call handler."""
325
+ # TODO: only handle 'our' tests? what is the point of handling others?
326
+ # well, because our interface allowed people to return deferreds
327
+ # from arbitrary tests so we kinda have to keep this up for now
328
+ _run_inline_callbacks (_async_pytest_pyfunc_call , pyfuncitem )
329
+ return not None
330
+
331
+
332
+ @defer .inlineCallbacks
333
+ def _async_pytest_pyfunc_call (pyfuncitem ):
334
+ """Run test function."""
335
+ kwargs = {
336
+ name : value
337
+ for name , value in pyfuncitem .funcargs .items ()
338
+ if name in pyfuncitem ._fixtureinfo .argnames
339
+ }
340
+
341
+ maybe_mark = _get_mark (pyfuncitem .obj )
342
+ if maybe_mark == 'async_test' :
343
+ result = yield defer .ensureDeferred (pyfuncitem .obj (** kwargs ))
344
+ elif maybe_mark == 'inline_callbacks_test' :
345
+ result = yield pyfuncitem .obj (** kwargs )
346
+ else :
347
+ # TODO: maybe deprecate this
348
+ result = yield pyfuncitem .obj (** kwargs )
349
+
350
+ defer .returnValue (result )
244
351
245
352
246
353
@pytest .fixture (scope = "session" , autouse = True )
247
- def twisted_greenlet (request ):
248
- request . addfinalizer ( stop_twisted_greenlet )
354
+ def twisted_greenlet ():
355
+ """Provide the twisted greenlet in fixture form."""
249
356
return _instances .gr_twisted
250
357
251
358
252
359
def init_default_reactor ():
360
+ """Install the default Twisted reactor."""
253
361
import twisted .internet .default
254
362
255
363
module = inspect .getmodule (twisted .internet .default .install )
@@ -265,6 +373,7 @@ def init_default_reactor():
265
373
266
374
267
375
def init_qt5_reactor ():
376
+ """Install the qt5reactor... reactor."""
268
377
import qt5reactor
269
378
270
379
_install_reactor (
@@ -273,6 +382,7 @@ def init_qt5_reactor():
273
382
274
383
275
384
def init_asyncio_reactor ():
385
+ """Install the Twisted reactor for asyncio."""
276
386
from twisted .internet import asyncioreactor
277
387
278
388
_install_reactor (
@@ -289,6 +399,7 @@ def init_asyncio_reactor():
289
399
290
400
291
401
def _install_reactor (reactor_installer , reactor_type ):
402
+ """Install the specified reactor and create the greenlet."""
292
403
try :
293
404
reactor_installer ()
294
405
except error .ReactorAlreadyInstalledError :
@@ -308,6 +419,7 @@ def _install_reactor(reactor_installer, reactor_type):
308
419
309
420
310
421
def pytest_addoption (parser ):
422
+ """Add options into the pytest CLI."""
311
423
group = parser .getgroup ("twisted" )
312
424
group .addoption (
313
425
"--reactor" ,
@@ -317,6 +429,7 @@ def pytest_addoption(parser):
317
429
318
430
319
431
def pytest_configure (config ):
432
+ """Identify and install chosen reactor."""
320
433
pytest .inlineCallbacks = _deprecate (
321
434
deprecated = 'pytest.inlineCallbacks' ,
322
435
recommended = 'pytest_twisted.inlineCallbacks' ,
@@ -329,7 +442,13 @@ def pytest_configure(config):
329
442
reactor_installers [config .getoption ("reactor" )]()
330
443
331
444
445
+ def pytest_unconfigure (config ):
446
+ """Stop the reactor greenlet."""
447
+ stop_twisted_greenlet ()
448
+
449
+
332
450
def _use_asyncio_selector_if_required (config ):
451
+ """Set asyncio selector event loop policy if needed."""
333
452
# https://twistedmatrix.com/trac/ticket/9766
334
453
# https://github.com/pytest-dev/pytest-twisted/issues/80
335
454
0 commit comments