@@ -49,6 +49,11 @@ class _instances:
4949 reactor = None
5050
5151
52+ class _tracking :
53+ async_yield_fixture_cache = {}
54+ to_be_torn_down = []
55+
56+
5257def _deprecate (deprecated , recommended ):
5358 def decorator (f ):
5459 @functools .wraps (f )
@@ -102,14 +107,44 @@ def block_from_thread(d):
102107 return blockingCallFromThread (_instances .reactor , lambda x : x , d )
103108
104109
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
108136
109137
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
113148
114149
115150def init_twisted_greenlet ():
@@ -130,10 +165,14 @@ def stop_twisted_greenlet():
130165 _instances .gr_twisted .switch ()
131166
132167
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 )
137176
138177
139178def _marked_async_fixture (mark ):
@@ -144,21 +183,23 @@ def fixture(*args, **kwargs):
144183 except IndexError :
145184 scope = kwargs .get ('scope' , 'function' )
146185
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
148198 raise AsyncFixtureUnsupportedScopeError .from_scope (scope = scope )
149199
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-
160200 def decorator (f ):
161- result = pytest .fixture (* args , ** kwargs )(marker (f ))
201+ _set_mark (f , mark )
202+ result = pytest .fixture (* args , ** kwargs )(f )
162203
163204 return result
164205
@@ -167,61 +208,86 @@ def decorator(f):
167208 return fixture
168209
169210
211+ _mark_attribute_name = '_pytest_twisted_mark'
170212async_fixture = _marked_async_fixture ('async_fixture' )
171213async_yield_fixture = _marked_async_fixture ('async_yield_fixture' )
172214
173215
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+
174235@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+ )
202261 else :
203- testargs = funcargs
204- result = yield testfunction (** testargs )
262+ raise UnrecognizedCoroutineMarkError .from_mark (mark = mark )
205263
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 )
210265
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 )
220267
221- defer .returnValue (result )
222268
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
223282
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."""
225291 if _instances .gr_twisted is not None :
226292 if _instances .gr_twisted .dead :
227293 raise RuntimeError ("twisted reactor has stopped" )
@@ -230,26 +296,68 @@ def in_reactor(d, f, *args):
230296 return defer .maybeDeferred (f , * args ).chainDeferred (d )
231297
232298 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 )
236300 blockon_default (d )
237301 else :
238302 if not _instances .reactor .running :
239303 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 )
244351
245352
246353@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."""
249356 return _instances .gr_twisted
250357
251358
252359def init_default_reactor ():
360+ """Install the default Twisted reactor."""
253361 import twisted .internet .default
254362
255363 module = inspect .getmodule (twisted .internet .default .install )
@@ -265,6 +373,7 @@ def init_default_reactor():
265373
266374
267375def init_qt5_reactor ():
376+ """Install the qt5reactor... reactor."""
268377 import qt5reactor
269378
270379 _install_reactor (
@@ -273,6 +382,7 @@ def init_qt5_reactor():
273382
274383
275384def init_asyncio_reactor ():
385+ """Install the Twisted reactor for asyncio."""
276386 from twisted .internet import asyncioreactor
277387
278388 _install_reactor (
@@ -289,6 +399,7 @@ def init_asyncio_reactor():
289399
290400
291401def _install_reactor (reactor_installer , reactor_type ):
402+ """Install the specified reactor and create the greenlet."""
292403 try :
293404 reactor_installer ()
294405 except error .ReactorAlreadyInstalledError :
@@ -308,6 +419,7 @@ def _install_reactor(reactor_installer, reactor_type):
308419
309420
310421def pytest_addoption (parser ):
422+ """Add options into the pytest CLI."""
311423 group = parser .getgroup ("twisted" )
312424 group .addoption (
313425 "--reactor" ,
@@ -317,6 +429,7 @@ def pytest_addoption(parser):
317429
318430
319431def pytest_configure (config ):
432+ """Identify and install chosen reactor."""
320433 pytest .inlineCallbacks = _deprecate (
321434 deprecated = 'pytest.inlineCallbacks' ,
322435 recommended = 'pytest_twisted.inlineCallbacks' ,
@@ -329,7 +442,13 @@ def pytest_configure(config):
329442 reactor_installers [config .getoption ("reactor" )]()
330443
331444
445+ def pytest_unconfigure (config ):
446+ """Stop the reactor greenlet."""
447+ stop_twisted_greenlet ()
448+
449+
332450def _use_asyncio_selector_if_required (config ):
451+ """Set asyncio selector event loop policy if needed."""
333452 # https://twistedmatrix.com/trac/ticket/9766
334453 # https://github.com/pytest-dev/pytest-twisted/issues/80
335454
0 commit comments