From 346ff5520d7b2e9c7037a0592de27237715c40ff Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Fri, 21 Sep 2018 10:50:49 -0400 Subject: [PATCH 01/26] Support async/await fixtures Redo of pytest-dev/pytest-twisted#35 but straight off of master --- pytest_twisted.py | 37 +++++++++++++++++++++++++++++++++---- testing/test_basic.py | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 4 deletions(-) diff --git a/pytest_twisted.py b/pytest_twisted.py index 769c175..d8f1f3b 100644 --- a/pytest_twisted.py +++ b/pytest_twisted.py @@ -1,4 +1,13 @@ import inspect +import sys + +ASYNC_AWAIT = sys.version_info >= (3, 5) + +if ASYNC_AWAIT: + import asyncio +else: + asyncio = None + import decorator import greenlet @@ -83,19 +92,33 @@ def stop_twisted_greenlet(): _instances.gr_twisted.switch() +def is_coroutine(maybe_coroutine): + if ASYNC_AWAIT: + return asyncio.iscoroutine(maybe_coroutine) + + return False + + +@defer.inlineCallbacks def _pytest_pyfunc_call(pyfuncitem): testfunction = pyfuncitem.obj if pyfuncitem._isyieldedfunction(): - return testfunction(*pyfuncitem._args) + defer.returnValue(testfunction(*pyfuncitem._args)) else: funcargs = pyfuncitem.funcargs if hasattr(pyfuncitem, "_fixtureinfo"): testargs = {} for arg in pyfuncitem._fixtureinfo.argnames: - testargs[arg] = funcargs[arg] + maybe_coroutine = funcargs[arg] + if is_coroutine(maybe_coroutine): + maybe_coroutine = yield defer.ensureDeferred( + maybe_coroutine, + ) + testargs[arg] = maybe_coroutine else: testargs = funcargs - return testfunction(**testargs) + result = yield testfunction(**testargs) + defer.returnValue(result) def pytest_pyfunc_call(pyfuncitem): @@ -103,8 +126,14 @@ def pytest_pyfunc_call(pyfuncitem): if _instances.gr_twisted.dead: raise RuntimeError("twisted reactor has stopped") + @defer.inlineCallbacks def in_reactor(d, f, *args): - return defer.maybeDeferred(f, *args).chainDeferred(d) + try: + result = yield f(*args) + except Exception as e: + d.callback(failure.Failure(e)) + else: + d.callback(result) d = defer.Deferred() _instances.reactor.callLater( diff --git a/testing/test_basic.py b/testing/test_basic.py index f062d81..bdeab38 100755 --- a/testing/test_basic.py +++ b/testing/test_basic.py @@ -3,6 +3,8 @@ import pytest +import pytest_twisted + def assert_outcomes(run_result, outcomes): formatted_output = format_run_result_output_for_assert(run_result) @@ -37,6 +39,13 @@ def skip_if_reactor_not(expected_reactor): ) +def skip_if_no_async_await(): + return pytest.mark.skipif( + not pytest_twisted.ASYNC_AWAIT, + reason="async/await syntax not support on Python <3.5", + ) + + @pytest.fixture def cmd_opts(request): reactor = request.config.getoption("reactor", "default") @@ -165,6 +174,33 @@ def test_succeed(foo): assert_outcomes(rr, {"passed": 2, "failed": 1}) +@skip_if_no_async_await() +def test_async_fixture(testdir, cmd_opts): + test_file = """ + from twisted.internet import reactor, defer + import pytest + import pytest_twisted + + @pytest.fixture(scope="function", params=["fs", "imap", "web"]) + async def foo(request): + d1, d2 = defer.Deferred(), defer.Deferred() + reactor.callLater(0.01, d1.callback, 1) + reactor.callLater(0.02, d2.callback, request.param) + await d1 + return d2, + + @pytest_twisted.inlineCallbacks + def test_succeed(foo): + x = yield foo[0] + print('+++', x) + if x == "web": + raise RuntimeError("baz") + """ + testdir.makepyfile(test_file) + rr = testdir.run(sys.executable, "-m", "pytest", "-v", *cmd_opts) + assert_outcomes(rr, {"passed": 2, "failed": 1}) + + @skip_if_reactor_not("default") def test_blockon_in_hook(testdir, cmd_opts): conftest_file = """ From ea36e2f2e580376eebf53c94cda007d97c37c4ad Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Fri, 21 Sep 2018 12:37:19 -0400 Subject: [PATCH 02/26] Support async/await fixture setup with yield (not shutdown yet) --- pytest_twisted.py | 24 +++++++++++++++++------- testing/test_basic.py | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 7 deletions(-) diff --git a/pytest_twisted.py b/pytest_twisted.py index d8f1f3b..8cd7f24 100644 --- a/pytest_twisted.py +++ b/pytest_twisted.py @@ -2,6 +2,7 @@ import sys ASYNC_AWAIT = sys.version_info >= (3, 5) +ASYNC_GENERATORS = sys.version_info >= (3, 6) if ASYNC_AWAIT: import asyncio @@ -92,9 +93,16 @@ def stop_twisted_greenlet(): _instances.gr_twisted.switch() -def is_coroutine(maybe_coroutine): +def is_coroutine(something): if ASYNC_AWAIT: - return asyncio.iscoroutine(maybe_coroutine) + return asyncio.iscoroutine(something) + + return False + + +def is_async_generator(something): + if ASYNC_GENERATORS: + return inspect.isasyncgen(something) return False @@ -109,12 +117,14 @@ def _pytest_pyfunc_call(pyfuncitem): if hasattr(pyfuncitem, "_fixtureinfo"): testargs = {} for arg in pyfuncitem._fixtureinfo.argnames: - maybe_coroutine = funcargs[arg] - if is_coroutine(maybe_coroutine): - maybe_coroutine = yield defer.ensureDeferred( - maybe_coroutine, + something = funcargs[arg] + if is_coroutine(something): + something = yield defer.ensureDeferred(something) + elif is_async_generator(something): + something = yield defer.ensureDeferred( + something.__anext__(), ) - testargs[arg] = maybe_coroutine + testargs[arg] = something else: testargs = funcargs result = yield testfunction(**testargs) diff --git a/testing/test_basic.py b/testing/test_basic.py index bdeab38..eeb2fc3 100755 --- a/testing/test_basic.py +++ b/testing/test_basic.py @@ -46,6 +46,13 @@ def skip_if_no_async_await(): ) +def skip_if_no_async_generators(): + return pytest.mark.skipif( + not pytest_twisted.ASYNC_GENERATORS, + reason="async generators not support on Python <3.6", + ) + + @pytest.fixture def cmd_opts(request): reactor = request.config.getoption("reactor", "default") @@ -201,6 +208,33 @@ def test_succeed(foo): assert_outcomes(rr, {"passed": 2, "failed": 1}) +@skip_if_no_async_generators() +def test_async_fixture_yield(testdir, cmd_opts): + test_file = """ + from twisted.internet import reactor, defer + import pytest + import pytest_twisted + + @pytest.fixture(scope="function", params=["fs", "imap", "web"]) + async def foo(request): + d1, d2 = defer.Deferred(), defer.Deferred() + reactor.callLater(0.01, d1.callback, 1) + reactor.callLater(0.02, d2.callback, request.param) + await d1 + yield d2, + + @pytest_twisted.inlineCallbacks + def test_succeed(foo): + x = yield foo[0] + print('+++', x) + if x == "web": + raise RuntimeError("baz") + """ + testdir.makepyfile(test_file) + rr = testdir.run(sys.executable, "-m", "pytest", "-v", *cmd_opts) + assert_outcomes(rr, {"passed": 2, "failed": 1}) + + @skip_if_reactor_not("default") def test_blockon_in_hook(testdir, cmd_opts): conftest_file = """ From a9d848422bfc5fc93010870304c199af620b2b14 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Fri, 21 Sep 2018 13:45:09 -0400 Subject: [PATCH 03/26] Support async/await fixture shutdown --- pytest_twisted.py | 13 +++++++++++++ testing/test_basic.py | 14 ++++++++++++-- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/pytest_twisted.py b/pytest_twisted.py index 8cd7f24..521356c 100644 --- a/pytest_twisted.py +++ b/pytest_twisted.py @@ -110,6 +110,7 @@ def is_async_generator(something): @defer.inlineCallbacks def _pytest_pyfunc_call(pyfuncitem): testfunction = pyfuncitem.obj + async_generators = [] if pyfuncitem._isyieldedfunction(): defer.returnValue(testfunction(*pyfuncitem._args)) else: @@ -121,6 +122,7 @@ def _pytest_pyfunc_call(pyfuncitem): if is_coroutine(something): something = yield defer.ensureDeferred(something) elif is_async_generator(something): + async_generators.append(something) something = yield defer.ensureDeferred( something.__anext__(), ) @@ -128,6 +130,17 @@ def _pytest_pyfunc_call(pyfuncitem): else: testargs = funcargs result = yield testfunction(**testargs) + + for async_generator in async_generators: + try: + yield defer.ensureDeferred( + async_generator.__anext__(), + ) + except StopAsyncIteration: + continue + else: + raise RuntimeError('async generator did not stop') + defer.returnValue(result) diff --git a/testing/test_basic.py b/testing/test_basic.py index eeb2fc3..6bceec3 100755 --- a/testing/test_basic.py +++ b/testing/test_basic.py @@ -215,14 +215,24 @@ def test_async_fixture_yield(testdir, cmd_opts): import pytest import pytest_twisted - @pytest.fixture(scope="function", params=["fs", "imap", "web"]) + @pytest.fixture( + scope="function", + params=["fs", "imap", "web", "gopher", "archie"], + ) async def foo(request): d1, d2 = defer.Deferred(), defer.Deferred() reactor.callLater(0.01, d1.callback, 1) reactor.callLater(0.02, d2.callback, request.param) await d1 + yield d2, + if request.param == "gopher": + raise RuntimeError("gaz") + + if request.param == "archie": + yield 42 + @pytest_twisted.inlineCallbacks def test_succeed(foo): x = yield foo[0] @@ -232,7 +242,7 @@ def test_succeed(foo): """ testdir.makepyfile(test_file) rr = testdir.run(sys.executable, "-m", "pytest", "-v", *cmd_opts) - assert_outcomes(rr, {"passed": 2, "failed": 1}) + assert_outcomes(rr, {"passed": 2, "failed": 3}) @skip_if_reactor_not("default") From a85ab00388b7a9a9a9db8cc05980fadb3904579d Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Fri, 21 Sep 2018 22:46:58 -0400 Subject: [PATCH 04/26] Drop @inlineCallbacks from in_reactor() --- pytest_twisted.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/pytest_twisted.py b/pytest_twisted.py index 521356c..b080243 100644 --- a/pytest_twisted.py +++ b/pytest_twisted.py @@ -149,14 +149,8 @@ def pytest_pyfunc_call(pyfuncitem): if _instances.gr_twisted.dead: raise RuntimeError("twisted reactor has stopped") - @defer.inlineCallbacks def in_reactor(d, f, *args): - try: - result = yield f(*args) - except Exception as e: - d.callback(failure.Failure(e)) - else: - d.callback(result) + return defer.maybeDeferred(f, *args).chainDeferred(d) d = defer.Deferred() _instances.reactor.callLater( From 7a689278cf3c9c4206f1dc7b920bc120fdd6435f Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Mon, 24 Sep 2018 22:08:48 -0400 Subject: [PATCH 05/26] remove diagnostic prints from tests --- testing/test_basic.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/testing/test_basic.py b/testing/test_basic.py index 6bceec3..a8889cd 100755 --- a/testing/test_basic.py +++ b/testing/test_basic.py @@ -199,7 +199,6 @@ async def foo(request): @pytest_twisted.inlineCallbacks def test_succeed(foo): x = yield foo[0] - print('+++', x) if x == "web": raise RuntimeError("baz") """ @@ -236,7 +235,6 @@ async def foo(request): @pytest_twisted.inlineCallbacks def test_succeed(foo): x = yield foo[0] - print('+++', x) if x == "web": raise RuntimeError("baz") """ From 6e9951d2dc613a7f5c04430e42adf39c2d881e9f Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Mon, 24 Sep 2018 22:27:46 -0400 Subject: [PATCH 06/26] Add asycn/await fixtures to README.rst --- README.rst | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/README.rst b/README.rst index 50c875d..8d57108 100644 --- a/README.rst +++ b/README.rst @@ -72,6 +72,21 @@ Waiting for deferreds in fixtures return pytest_twisted.blockon(d) +async/await fixtures +==================== +``async``/``await`` fixtures can be used along with ``yield`` for normal +pytest fixture semantics of setup, value teardown. + + @pytest.fixture + async def foo(): + d1, d2 = defer.Deferred(), defer.Deferred() + reactor.callLater(0.01, d1.callback, 42) + reactor.callLater(0.02, d2.callback, 37) + value = await d1 + yield value + await d2 + + The twisted greenlet ==================== Some libraries (e.g. corotwine) need to know the greenlet, which is From fae01a125ed08252fee45c5ae2ab061e6facc5fc Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Mon, 24 Sep 2018 22:28:26 -0400 Subject: [PATCH 07/26] readme typo --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 8d57108..d304be4 100644 --- a/README.rst +++ b/README.rst @@ -75,7 +75,7 @@ Waiting for deferreds in fixtures async/await fixtures ==================== ``async``/``await`` fixtures can be used along with ``yield`` for normal -pytest fixture semantics of setup, value teardown. +pytest fixture semantics of setup, value, and teardown. @pytest.fixture async def foo(): From eac48431410492017e50cfc839d0286ee4e7f98a Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Tue, 2 Oct 2018 12:10:27 -0400 Subject: [PATCH 08/26] Explain why the test is yielding a deferred-containing tuple --- testing/test_basic.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/testing/test_basic.py b/testing/test_basic.py index a8889cd..8fd697f 100755 --- a/testing/test_basic.py +++ b/testing/test_basic.py @@ -224,6 +224,9 @@ async def foo(request): reactor.callLater(0.02, d2.callback, request.param) await d1 + # Twisted doesn't allow calling back with a Deferred as a value. + # This deferred is being wrapped up in a tuple to sneak through. + # https://github.com/twisted/twisted/blob/c0f1394c7bfb04d97c725a353a1f678fa6a1c602/src/twisted/internet/defer.py#L459 yield d2, if request.param == "gopher": From 7643a177e1fc4fa4cb008bf2bc796c4f703d76e8 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Tue, 2 Oct 2018 12:26:30 -0400 Subject: [PATCH 09/26] Let multiple async fixtures teardown concurrently --- pytest_twisted.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/pytest_twisted.py b/pytest_twisted.py index b080243..cc71946 100644 --- a/pytest_twisted.py +++ b/pytest_twisted.py @@ -131,11 +131,14 @@ def _pytest_pyfunc_call(pyfuncitem): testargs = funcargs result = yield testfunction(**testargs) - for async_generator in async_generators: + async_generator_deferreds = [ + defer.ensureDeferred(g.__anext__()) + for g in async_generators + ] + + for d in async_generator_deferreds: try: - yield defer.ensureDeferred( - async_generator.__anext__(), - ) + yield d except StopAsyncIteration: continue else: From a03f2ca29ab8d71bfbbe39bc365ded9279973bd7 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Tue, 2 Oct 2018 12:31:26 -0400 Subject: [PATCH 10/26] Report async fixture name if it yields multiple times --- pytest_twisted.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/pytest_twisted.py b/pytest_twisted.py index cc71946..2c0f419 100644 --- a/pytest_twisted.py +++ b/pytest_twisted.py @@ -122,7 +122,7 @@ def _pytest_pyfunc_call(pyfuncitem): if is_coroutine(something): something = yield defer.ensureDeferred(something) elif is_async_generator(something): - async_generators.append(something) + async_generators.append((arg, something)) something = yield defer.ensureDeferred( something.__anext__(), ) @@ -132,17 +132,19 @@ def _pytest_pyfunc_call(pyfuncitem): result = yield testfunction(**testargs) async_generator_deferreds = [ - defer.ensureDeferred(g.__anext__()) - for g in async_generators + (arg, defer.ensureDeferred(g.__anext__())) + for arg, g in async_generators ] - for d in async_generator_deferreds: + for arg, d in async_generator_deferreds: try: yield d except StopAsyncIteration: continue else: - raise RuntimeError('async generator did not stop') + raise RuntimeError( + 'async fixture did not stop: {}'.format(arg), + ) defer.returnValue(result) From 29c81853bbeb2c8bcea404d0d2bc9084779fa00e Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Tue, 2 Oct 2018 16:23:22 -0400 Subject: [PATCH 11/26] Add concurrent teardown test --- testing/test_basic.py | 54 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/testing/test_basic.py b/testing/test_basic.py index 8fd697f..85cdda3 100755 --- a/testing/test_basic.py +++ b/testing/test_basic.py @@ -207,6 +207,60 @@ def test_succeed(foo): assert_outcomes(rr, {"passed": 2, "failed": 1}) +@skip_if_no_async_await() +def test_async_fixture_concurrent_teardown(testdir, cmd_opts): + test_file = """ + import time + + from twisted.internet import reactor, defer + import pytest + import pytest_twisted + + def sleep(seconds): + d = defer.Deferred() + + reactor.callLater(seconds, d.callback, None) + + return d + + short_times = [] + long_times = [] + + short_time = 0.100 + long_time = 1 + + @pytest.fixture + async def short(): + yield 42 + + short_times.append(time.time()) + await sleep(short_time) + short_times.append(time.time()) + + @pytest.fixture + async def long(): + yield 37 + + long_times.append(time.time()) + await sleep(short_time) + long_times.append(time.time()) + + assert len(short_times) == 2 + + # long should start before short finishes + assert long_times[0] < short_times[1] + # short should finish before long finishes + assert short_times[1] < long_times[1] + + @pytest_twisted.inlineCallbacks + def test_succeed(short, long): + yield sleep(0) + """ + testdir.makepyfile(test_file) + rr = testdir.run(sys.executable, "-m", "pytest", "-v", *cmd_opts) + assert_outcomes(rr, {"passed": 1}) + + @skip_if_no_async_generators() def test_async_fixture_yield(testdir, cmd_opts): test_file = """ From 540671b6b8b968030b55e92932c487497bf42edc Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Tue, 2 Oct 2018 16:39:45 -0400 Subject: [PATCH 12/26] Correct concurrent teardown marker to skip_if_no_async_generators --- testing/test_basic.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/test_basic.py b/testing/test_basic.py index 85cdda3..200ed4c 100755 --- a/testing/test_basic.py +++ b/testing/test_basic.py @@ -207,7 +207,7 @@ def test_succeed(foo): assert_outcomes(rr, {"passed": 2, "failed": 1}) -@skip_if_no_async_await() +@skip_if_no_async_generators() def test_async_fixture_concurrent_teardown(testdir, cmd_opts): test_file = """ import time From 14b16ca114e735d46d52cd36dc2f2306ed0ec636 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Tue, 2 Oct 2018 16:44:19 -0400 Subject: [PATCH 13/26] Link to changelogs for ASYNC_ present checks --- pytest_twisted.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pytest_twisted.py b/pytest_twisted.py index 2c0f419..30b77d0 100644 --- a/pytest_twisted.py +++ b/pytest_twisted.py @@ -1,7 +1,10 @@ import inspect import sys +# https://docs.python.org/3/whatsnew/3.5.html#pep-492-coroutines-with-async-and-await-syntax ASYNC_AWAIT = sys.version_info >= (3, 5) + +# https://docs.python.org/3/whatsnew/3.6.html#pep-525-asynchronous-generators ASYNC_GENERATORS = sys.version_info >= (3, 6) if ASYNC_AWAIT: From f2da6450de46dd2aaee8e849987a6ca397c72bc7 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Thu, 4 Oct 2018 16:02:25 -0400 Subject: [PATCH 14/26] Rework test_async_fixture_concurrent_teardown to avoid time-dependence --- testing/test_basic.py | 43 ++++++++++++------------------------------- 1 file changed, 12 insertions(+), 31 deletions(-) diff --git a/testing/test_basic.py b/testing/test_basic.py index 200ed4c..be2418c 100755 --- a/testing/test_basic.py +++ b/testing/test_basic.py @@ -210,53 +210,34 @@ def test_succeed(foo): @skip_if_no_async_generators() def test_async_fixture_concurrent_teardown(testdir, cmd_opts): test_file = """ - import time - from twisted.internet import reactor, defer import pytest import pytest_twisted - def sleep(seconds): - d = defer.Deferred() - - reactor.callLater(seconds, d.callback, None) - - return d - - short_times = [] - long_times = [] - short_time = 0.100 - long_time = 1 + here = defer.Deferred() + there = defer.Deferred() @pytest.fixture - async def short(): + async def this(): yield 42 - short_times.append(time.time()) - await sleep(short_time) - short_times.append(time.time()) + there.callback(None) + await here @pytest.fixture - async def long(): + async def that(): yield 37 - long_times.append(time.time()) - await sleep(short_time) - long_times.append(time.time()) + here.callback(None) + await there - assert len(short_times) == 2 - - # long should start before short finishes - assert long_times[0] < short_times[1] - # short should finish before long finishes - assert short_times[1] < long_times[1] - - @pytest_twisted.inlineCallbacks - def test_succeed(short, long): - yield sleep(0) + def test_succeed(this, that): + pass """ testdir.makepyfile(test_file) + # TODO: add a timeout, failure just hangs indefinitely for now + # https://github.com/pytest-dev/pytest/issues/4073 rr = testdir.run(sys.executable, "-m", "pytest", "-v", *cmd_opts) assert_outcomes(rr, {"passed": 1}) From b0b623e3453324644d52f1a1b1f354cb6d76f230 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Fri, 5 Oct 2018 10:51:45 -0400 Subject: [PATCH 15/26] Timeout on failure test_async_fixture_concurrent_teardown --- testing/test_basic.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/testing/test_basic.py b/testing/test_basic.py index be2418c..42f7c0c 100755 --- a/testing/test_basic.py +++ b/testing/test_basic.py @@ -223,6 +223,7 @@ async def this(): yield 42 there.callback(None) + reactor.callLater(5, here.cancel) await here @pytest.fixture @@ -230,6 +231,7 @@ async def that(): yield 37 here.callback(None) + reactor.callLater(5, there.cancel) await there def test_succeed(this, that): From 3ac4209f8fb1486305b1223de16c61e7d261d677 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Sat, 27 Oct 2018 16:52:26 -0400 Subject: [PATCH 16/26] Require explicit async_fixture() or async_yield_fixture() decorator --- pytest_twisted.py | 54 ++++++++++++++++++++++++++++++------------- testing/test_basic.py | 11 +++++---- 2 files changed, 44 insertions(+), 21 deletions(-) diff --git a/pytest_twisted.py b/pytest_twisted.py index 30b77d0..f93f90e 100644 --- a/pytest_twisted.py +++ b/pytest_twisted.py @@ -1,3 +1,4 @@ +import functools import inspect import sys @@ -96,18 +97,36 @@ def stop_twisted_greenlet(): _instances.gr_twisted.switch() -def is_coroutine(something): - if ASYNC_AWAIT: - return asyncio.iscoroutine(something) +class _CoroutineWrapper: + def __init__(self, coroutine, mark): + self.coroutine = coroutine + self.mark = mark - return False +def _marked_async_fixture(mark): + def fixture(*args, **kwargs): + def marker(f): + @functools.wraps(f) + def w(*args, **kwargs): + return _CoroutineWrapper( + coroutine=f(*args, **kwargs), + mark=mark, + ) + + return w + + def decorator(f): + result = pytest.fixture(*args, **kwargs)(marker(f)) + + return result + + return decorator + + return fixture -def is_async_generator(something): - if ASYNC_GENERATORS: - return inspect.isasyncgen(something) - return False +async_fixture = _marked_async_fixture('async_fixture') +async_yield_fixture = _marked_async_fixture('async_yield_fixture') @defer.inlineCallbacks @@ -122,20 +141,23 @@ def _pytest_pyfunc_call(pyfuncitem): testargs = {} for arg in pyfuncitem._fixtureinfo.argnames: something = funcargs[arg] - if is_coroutine(something): - something = yield defer.ensureDeferred(something) - elif is_async_generator(something): - async_generators.append((arg, something)) - something = yield defer.ensureDeferred( - something.__anext__(), - ) + if isinstance(something, _CoroutineWrapper): + if something.mark == 'async_fixture': + something = yield defer.ensureDeferred( + something.coroutine + ) + elif something.mark == 'async_yield_fixture': + async_generators.append((arg, something)) + something = yield defer.ensureDeferred( + something.coroutine.__anext__(), + ) testargs[arg] = something else: testargs = funcargs result = yield testfunction(**testargs) async_generator_deferreds = [ - (arg, defer.ensureDeferred(g.__anext__())) + (arg, defer.ensureDeferred(g.coroutine.__anext__())) for arg, g in async_generators ] diff --git a/testing/test_basic.py b/testing/test_basic.py index 42f7c0c..95fb912 100755 --- a/testing/test_basic.py +++ b/testing/test_basic.py @@ -188,7 +188,8 @@ def test_async_fixture(testdir, cmd_opts): import pytest import pytest_twisted - @pytest.fixture(scope="function", params=["fs", "imap", "web"]) + @pytest_twisted.async_fixture(scope="function", params=["fs", "imap", "web"]) + @pytest.mark.redgreenblue async def foo(request): d1, d2 = defer.Deferred(), defer.Deferred() reactor.callLater(0.01, d1.callback, 1) @@ -197,7 +198,7 @@ async def foo(request): return d2, @pytest_twisted.inlineCallbacks - def test_succeed(foo): + def test_succeed_blue(foo): x = yield foo[0] if x == "web": raise RuntimeError("baz") @@ -218,7 +219,7 @@ def test_async_fixture_concurrent_teardown(testdir, cmd_opts): here = defer.Deferred() there = defer.Deferred() - @pytest.fixture + @pytest_twisted.async_yield_fixture() async def this(): yield 42 @@ -226,7 +227,7 @@ async def this(): reactor.callLater(5, here.cancel) await here - @pytest.fixture + @pytest_twisted.async_yield_fixture() async def that(): yield 37 @@ -251,7 +252,7 @@ def test_async_fixture_yield(testdir, cmd_opts): import pytest import pytest_twisted - @pytest.fixture( + @pytest_twisted.async_yield_fixture( scope="function", params=["fs", "imap", "web", "gopher", "archie"], ) From f4d95d6c0c8d67e24028922ab311aa785bfd4370 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Tue, 15 Jan 2019 22:25:02 -0500 Subject: [PATCH 17/26] Remove stale import --- pytest_twisted.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/pytest_twisted.py b/pytest_twisted.py index 989e35b..e8b1b68 100644 --- a/pytest_twisted.py +++ b/pytest_twisted.py @@ -8,11 +8,6 @@ # https://docs.python.org/3/whatsnew/3.6.html#pep-525-asynchronous-generators ASYNC_GENERATORS = sys.version_info >= (3, 6) -if ASYNC_AWAIT: - import asyncio -else: - asyncio = None - import decorator import greenlet From cb6d4a386b0ddf23d5c645bda47d3ba9d46956a9 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Tue, 15 Jan 2019 22:38:32 -0500 Subject: [PATCH 18/26] Move ASYNC_AWAIT/GENERATORS to testing/test_basic.py --- pytest_twisted.py | 8 -------- testing/test_basic.py | 11 +++++++++-- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/pytest_twisted.py b/pytest_twisted.py index e8b1b68..7167533 100644 --- a/pytest_twisted.py +++ b/pytest_twisted.py @@ -1,13 +1,5 @@ import functools import inspect -import sys - -# https://docs.python.org/3/whatsnew/3.5.html#pep-492-coroutines-with-async-and-await-syntax -ASYNC_AWAIT = sys.version_info >= (3, 5) - -# https://docs.python.org/3/whatsnew/3.6.html#pep-525-asynchronous-generators -ASYNC_GENERATORS = sys.version_info >= (3, 6) - import decorator import greenlet diff --git a/testing/test_basic.py b/testing/test_basic.py index 5438e98..b1edf41 100755 --- a/testing/test_basic.py +++ b/testing/test_basic.py @@ -6,6 +6,13 @@ import pytest_twisted +# https://docs.python.org/3/whatsnew/3.5.html#pep-492-coroutines-with-async-and-await-syntax +ASYNC_AWAIT = sys.version_info >= (3, 5) + +# https://docs.python.org/3/whatsnew/3.6.html#pep-525-asynchronous-generators +ASYNC_GENERATORS = sys.version_info >= (3, 6) + + def assert_outcomes(run_result, outcomes): formatted_output = format_run_result_output_for_assert(run_result) @@ -39,14 +46,14 @@ def skip_if_reactor_not(request, expected_reactor): def skip_if_no_async_await(): return pytest.mark.skipif( - not pytest_twisted.ASYNC_AWAIT, + not ASYNC_AWAIT, reason="async/await syntax not support on Python <3.5", ) def skip_if_no_async_generators(): return pytest.mark.skipif( - not pytest_twisted.ASYNC_GENERATORS, + not ASYNC_GENERATORS, reason="async generators not support on Python <3.6", ) From 5205ad718260edcfdabe179a947ea8a50de6b99b Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Tue, 15 Jan 2019 23:22:40 -0500 Subject: [PATCH 19/26] Correct readme example to @pytest_twisted.async_fixture --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index c5c5863..0b7e61c 100644 --- a/README.rst +++ b/README.rst @@ -90,7 +90,7 @@ async/await fixtures ``async``/``await`` fixtures can be used along with ``yield`` for normal pytest fixture semantics of setup, value, and teardown. - @pytest.fixture + @pytest_twisted.async_fixture async def foo(): d1, d2 = defer.Deferred(), defer.Deferred() reactor.callLater(0.01, d1.callback, 42) From 50c833dbe44a8f13c9d923ca0df2de1b46ffc9ec Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Wed, 16 Jan 2019 09:51:55 -0500 Subject: [PATCH 20/26] `something` isn't a descriptive variable name --- pytest_twisted.py | 36 ++++++++++++++++++++++++++---------- 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/pytest_twisted.py b/pytest_twisted.py index 0e700d7..56afb5a 100644 --- a/pytest_twisted.py +++ b/pytest_twisted.py @@ -14,6 +14,14 @@ class WrongReactorAlreadyInstalledError(Exception): pass +class UnrecognizedCoroutineMarkError(Exception): + @classmethod + def from_mark(cls, mark): + return cls( + 'Coroutine wrapper mark not recognized: {}'.format(repr(mark)), + ) + + class _config: external_reactor = False @@ -128,18 +136,26 @@ def _pytest_pyfunc_call(pyfuncitem): if hasattr(pyfuncitem, "_fixtureinfo"): testargs = {} for arg in pyfuncitem._fixtureinfo.argnames: - something = funcargs[arg] - if isinstance(something, _CoroutineWrapper): - if something.mark == 'async_fixture': - something = yield defer.ensureDeferred( - something.coroutine + if isinstance(funcargs[arg], _CoroutineWrapper): + wrapper = funcargs[arg] + + if wrapper.mark == 'async_fixture': + arg_value = yield defer.ensureDeferred( + wrapper.coroutine + ) + elif wrapper.mark == 'async_yield_fixture': + async_generators.append((arg, wrapper)) + arg_value = yield defer.ensureDeferred( + wrapper.coroutine.__anext__(), ) - elif something.mark == 'async_yield_fixture': - async_generators.append((arg, something)) - something = yield defer.ensureDeferred( - something.coroutine.__anext__(), + else: + raise UnrecognizedCoroutineMarkError.from_mark( + wrapper.mark, ) - testargs[arg] = something + else: + arg_value = funcargs[arg] + + testargs[arg] = arg_value else: testargs = funcargs result = yield testfunction(**testargs) From 64d7498d0d6a0b4d95f81f5c99aeccc72e8bf0f7 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Wed, 16 Jan 2019 10:02:44 -0500 Subject: [PATCH 21/26] @functools.wraps(pytest.fixture) in _marked_async_fixture() --- pytest_twisted.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pytest_twisted.py b/pytest_twisted.py index 56afb5a..a1e4fc5 100644 --- a/pytest_twisted.py +++ b/pytest_twisted.py @@ -100,6 +100,7 @@ def __init__(self, coroutine, mark): def _marked_async_fixture(mark): + @functools.wraps(pytest.fixture) def fixture(*args, **kwargs): def marker(f): @functools.wraps(f) From 86403f0c588d273933eb67d5d78d13f43dc67c48 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Wed, 16 Jan 2019 10:09:47 -0500 Subject: [PATCH 22/26] RuntimeError -> AsyncGeneratorFixtureDidNotStopError --- pytest_twisted.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/pytest_twisted.py b/pytest_twisted.py index a1e4fc5..4113175 100644 --- a/pytest_twisted.py +++ b/pytest_twisted.py @@ -22,6 +22,14 @@ def from_mark(cls, mark): ) +class AsyncGeneratorFixtureDidNotStopError(Exception): + @classmethod + def from_generator(cls, generator): + return cls( + 'async fixture did not stop: {}'.format(generator), + ) + + class _config: external_reactor = False @@ -172,8 +180,8 @@ def _pytest_pyfunc_call(pyfuncitem): except StopAsyncIteration: continue else: - raise RuntimeError( - 'async fixture did not stop: {}'.format(arg), + raise AsyncGeneratorFixtureDidNotStopError.from_generator( + generator=arg, ) defer.returnValue(result) From 72558a465e1b7943303b0244f6b23d95f77287d3 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Wed, 16 Jan 2019 10:21:31 -0500 Subject: [PATCH 23/26] Finish async generator fixtures in reverse order they were brought up --- pytest_twisted.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pytest_twisted.py b/pytest_twisted.py index 4113175..f6c0fa1 100644 --- a/pytest_twisted.py +++ b/pytest_twisted.py @@ -171,7 +171,7 @@ def _pytest_pyfunc_call(pyfuncitem): async_generator_deferreds = [ (arg, defer.ensureDeferred(g.coroutine.__anext__())) - for arg, g in async_generators + for arg, g in reversed(async_generators) ] for arg, d in async_generator_deferreds: From bdf459a6367d61837d153aee920d3428e1d748f1 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Wed, 16 Jan 2019 11:32:11 -0500 Subject: [PATCH 24/26] Only function scope supported for async fixtures --- README.rst | 3 ++- pytest_twisted.py | 18 +++++++++++++++- testing/test_basic.py | 48 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 67 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 0b7e61c..df28b51 100644 --- a/README.rst +++ b/README.rst @@ -88,7 +88,8 @@ Waiting for deferreds in fixtures async/await fixtures ==================== ``async``/``await`` fixtures can be used along with ``yield`` for normal -pytest fixture semantics of setup, value, and teardown. +pytest fixture semantics of setup, value, and teardown. At present only +function scope is supported. @pytest_twisted.async_fixture async def foo(): diff --git a/pytest_twisted.py b/pytest_twisted.py index f6c0fa1..e200cf4 100644 --- a/pytest_twisted.py +++ b/pytest_twisted.py @@ -30,6 +30,14 @@ def from_generator(cls, generator): ) +class AsyncFixtureUnsupportedScopeError(Exception): + @classmethod + def from_scope(cls, scope): + return cls( + 'Unsupported scope used for async fixture: {}'.format(scope) + ) + + class _config: external_reactor = False @@ -110,6 +118,14 @@ def __init__(self, coroutine, mark): def _marked_async_fixture(mark): @functools.wraps(pytest.fixture) def fixture(*args, **kwargs): + try: + scope = args[0] + except IndexError: + scope = kwargs.get('scope', 'function') + + if scope != 'function': + raise AsyncFixtureUnsupportedScopeError.from_scope(scope=scope) + def marker(f): @functools.wraps(f) def w(*args, **kwargs): @@ -159,7 +175,7 @@ def _pytest_pyfunc_call(pyfuncitem): ) else: raise UnrecognizedCoroutineMarkError.from_mark( - wrapper.mark, + mark=wrapper.mark, ) else: arg_value = funcargs[arg] diff --git a/testing/test_basic.py b/testing/test_basic.py index 5d99527..1806584 100755 --- a/testing/test_basic.py +++ b/testing/test_basic.py @@ -345,6 +345,54 @@ def test_succeed(foo): assert_outcomes(rr, {"passed": 2, "failed": 3}) +@skip_if_no_async_generators() +def test_async_fixture_function_scope(testdir, cmd_opts): + test_file = """ + from twisted.internet import reactor, defer + import pytest + import pytest_twisted + + check_me = 0 + + @pytest_twisted.async_yield_fixture(scope="function") + async def foo(): + global check_me + + if check_me != 0: + raise Exception('check_me already modified before fixture run') + + check_me = 1 + + yield 42 + + if check_me != 2: + raise Exception( + 'check_me not updated properly: {}'.format(check_me), + ) + + check_me = 0 + + def test_first(foo): + global check_me + + assert check_me == 1 + assert foo == 42 + + check_me = 2 + + def test_second(foo): + global check_me + + assert check_me == 1 + assert foo == 42 + + check_me = 2 + """ + testdir.makepyfile(test_file) + rr = testdir.run(sys.executable, "-m", "pytest", "-v", *cmd_opts) + assert_outcomes(rr, {"passed": 2}) + + def test_blockon_in_hook(testdir, cmd_opts, request): skip_if_reactor_not(request, "default") conftest_file = """ From bfbbd60c1045b534fb37f401e18ab4eb979b4a6f Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Thu, 26 Sep 2019 08:19:22 -0400 Subject: [PATCH 25/26] Correct new readme example to show as a code block --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 7593394..ae4b9d4 100644 --- a/README.rst +++ b/README.rst @@ -87,7 +87,7 @@ async/await fixtures ==================== ``async``/``await`` fixtures can be used along with ``yield`` for normal pytest fixture semantics of setup, value, and teardown. At present only -function scope is supported. +function scope is supported:: @pytest_twisted.async_fixture async def foo(): From 3e4d008788d98417da5fd854faa1ba3e560f2119 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Thu, 26 Sep 2019 08:26:54 -0400 Subject: [PATCH 26/26] Correct a few test names to `*async_yield*` --- testing/test_basic.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/testing/test_basic.py b/testing/test_basic.py index ab68ec4..1c8ccf3 100755 --- a/testing/test_basic.py +++ b/testing/test_basic.py @@ -342,7 +342,7 @@ def test_succeed_blue(foo): @skip_if_no_async_generators() -def test_async_fixture_concurrent_teardown(testdir, cmd_opts): +def test_async_yield_fixture_concurrent_teardown(testdir, cmd_opts): test_file = """ from twisted.internet import reactor, defer import pytest @@ -379,7 +379,7 @@ def test_succeed(this, that): @skip_if_no_async_generators() -def test_async_fixture_yield(testdir, cmd_opts): +def test_async_yield_fixture(testdir, cmd_opts): test_file = """ from twisted.internet import reactor, defer import pytest @@ -418,7 +418,7 @@ def test_succeed(foo): @skip_if_no_async_generators() -def test_async_fixture_function_scope(testdir, cmd_opts): +def test_async_yield_fixture_function_scope(testdir, cmd_opts): test_file = """ from twisted.internet import reactor, defer import pytest