From a0ea1d750fd6f806e0262dd4f74cd9eb888abc11 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Thu, 14 May 2020 18:08:11 -0400 Subject: [PATCH 1/5] Make decorators not require being called --- README.rst | 4 --- pytest_twisted.py | 36 ++++++++++++++++++++----- testing/test_basic.py | 63 ++++++++++++++++++++++++++++++++++++++----- 3 files changed, 86 insertions(+), 17 deletions(-) diff --git a/README.rst b/README.rst index b154914..eb3419a 100644 --- a/README.rst +++ b/README.rst @@ -164,10 +164,6 @@ async/await fixtures pytest fixture semantics of setup, value, and teardown. At present only function and module scope are supported. -Note: You must *call* ``pytest_twisted.async_fixture()`` and -``pytest_twisted.async_yield_fixture()``. -This requirement may be removed in a future release. - .. code-block:: python # No yield (coroutine function) diff --git a/pytest_twisted.py b/pytest_twisted.py index 8e45086..3d3903a 100644 --- a/pytest_twisted.py +++ b/pytest_twisted.py @@ -119,6 +119,29 @@ def decorator_apply(dec, func): dict(decfunc=dec(func)), __wrapped__=func) +def _optional_arguments(): + def decorator_decorator(d): + # TODO: this should get the signature of d minus the f or something + def decorator_wrapper(f=None, **decorator_arguments): + """this is decorator_wrapper""" + if f is not None: + if len(decorator_arguments) > 0 or not callable(f): + raise Exception('positional options not allowed') + + return d(f) + + # TODO: this should get the signature of d minus the kwargs + def decorator_closure_on_arguments(f): + return d(f, **decorator_arguments) + + return decorator_closure_on_arguments + + return decorator_wrapper + + return decorator_decorator + + +@_optional_arguments() def inlineCallbacks(f): """ Mark as inline callbacks test for pytest-twisted processing and apply @@ -135,6 +158,7 @@ def inlineCallbacks(f): return decorated +@_optional_arguments() def ensureDeferred(f): """ Mark as async test for pytest-twisted processing. @@ -177,7 +201,8 @@ def _set_mark(o, mark): def _marked_async_fixture(mark): @functools.wraps(pytest.fixture) - def fixture(*args, **kwargs): + @_optional_arguments() + def fixture(f, *args, **kwargs): try: scope = args[0] except IndexError: @@ -197,13 +222,10 @@ def fixture(*args, **kwargs): # https://github.com/pytest-dev/pytest-twisted/issues/56 raise AsyncFixtureUnsupportedScopeError.from_scope(scope=scope) - def decorator(f): - _set_mark(f, mark) - result = pytest.fixture(*args, **kwargs)(f) - - return result + _set_mark(f, mark) + result = pytest.fixture(*args, **kwargs)(f) - return decorator + return result return fixture diff --git a/testing/test_basic.py b/testing/test_basic.py index 05ce714..5412faf 100755 --- a/testing/test_basic.py +++ b/testing/test_basic.py @@ -226,7 +226,16 @@ def test_more_fail(): assert_outcomes(rr, {"failed": 1}) -def test_inlineCallbacks(testdir, cmd_opts): +@pytest.fixture( + name="empty_optional_call", + params=["", "()"], + ids=["no call", "empty call"], +) +def empty_optional_call_fixture(request): + return request.param + + +def test_inlineCallbacks(testdir, cmd_opts, empty_optional_call): test_file = """ from twisted.internet import reactor, defer import pytest @@ -236,19 +245,19 @@ def test_inlineCallbacks(testdir, cmd_opts): def foo(request): return request.param - @pytest_twisted.inlineCallbacks + @pytest_twisted.inlineCallbacks{optional_call} def test_succeed(foo): yield defer.succeed(foo) if foo == "web": raise RuntimeError("baz") - """ + """.format(optional_call=empty_optional_call) testdir.makepyfile(test_file) rr = testdir.run(sys.executable, "-m", "pytest", "-v", *cmd_opts) assert_outcomes(rr, {"passed": 2, "failed": 1}) @skip_if_no_async_await() -def test_async_await(testdir, cmd_opts): +def test_async_await(testdir, cmd_opts, empty_optional_call): test_file = """ from twisted.internet import reactor, defer import pytest @@ -258,12 +267,12 @@ def test_async_await(testdir, cmd_opts): def foo(request): return request.param - @pytest_twisted.ensureDeferred + @pytest_twisted.ensureDeferred{optional_call} async def test_succeed(foo): await defer.succeed(foo) if foo == "web": raise RuntimeError("baz") - """ + """.format(optional_call=empty_optional_call) testdir.makepyfile(test_file) rr = testdir.run(sys.executable, "-m", "pytest", "-v", *cmd_opts) assert_outcomes(rr, {"passed": 2, "failed": 1}) @@ -376,6 +385,25 @@ def test_succeed_blue(foo): assert_outcomes(rr, {"passed": 2, "failed": 1}) +@skip_if_no_async_await() +def test_async_fixture_no_arguments(testdir, cmd_opts, empty_optional_call): + test_file = """ + from twisted.internet import reactor, defer + import pytest + import pytest_twisted + + @pytest_twisted.async_fixture{optional_call} + async def scope(request): + return request.scope + + def test_is_function_scope(scope): + assert scope == "function" + """.format(optional_call=empty_optional_call) + 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_yield_fixture_concurrent_teardown(testdir, cmd_opts): test_file = """ @@ -453,6 +481,29 @@ def test_succeed(foo): assert_outcomes(rr, {"passed": 4, "failed": 1, "errors": 2}) +@skip_if_no_async_generators() +def test_async_yield_fixture_no_arguments( + testdir, + cmd_opts, + empty_optional_call, +): + test_file = """ + from twisted.internet import reactor, defer + import pytest + import pytest_twisted + + @pytest_twisted.async_yield_fixture{optional_call} + async def scope(request): + yield request.scope + + def test_is_function_scope(scope): + assert scope == "function" + """.format(optional_call=empty_optional_call) + 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_yield_fixture_function_scope(testdir, cmd_opts): test_file = """ From 52bf29052e9a3e44f4dbabdb49263194a52d2a8e Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Thu, 14 May 2020 20:08:00 -0400 Subject: [PATCH 2/5] linting --- testing/test_basic.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/testing/test_basic.py b/testing/test_basic.py index 5412faf..8d7194f 100755 --- a/testing/test_basic.py +++ b/testing/test_basic.py @@ -391,7 +391,7 @@ def test_async_fixture_no_arguments(testdir, cmd_opts, empty_optional_call): from twisted.internet import reactor, defer import pytest import pytest_twisted - + @pytest_twisted.async_fixture{optional_call} async def scope(request): return request.scope @@ -491,7 +491,7 @@ def test_async_yield_fixture_no_arguments( from twisted.internet import reactor, defer import pytest import pytest_twisted - + @pytest_twisted.async_yield_fixture{optional_call} async def scope(request): yield request.scope From 500b558f2d025048f1a3e8fd0246b6597cb08ed0 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Thu, 11 Jun 2020 16:26:43 -0400 Subject: [PATCH 3/5] Catch-up --- testing/test_basic.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/testing/test_basic.py b/testing/test_basic.py index 43eec58..2611a21 100755 --- a/testing/test_basic.py +++ b/testing/test_basic.py @@ -408,7 +408,7 @@ def test_is_function_scope(scope): assert scope == "function" """.format(optional_call=empty_optional_call) testdir.makepyfile(test_file) - rr = testdir.run(sys.executable, "-m", "pytest", "-v", *cmd_opts) + rr = testdir.run(*cmd_opts, timeout=timeout) assert_outcomes(rr, {"passed": 1}) @@ -508,7 +508,7 @@ def test_is_function_scope(scope): assert scope == "function" """.format(optional_call=empty_optional_call) testdir.makepyfile(test_file) - rr = testdir.run(sys.executable, "-m", "pytest", "-v", *cmd_opts) + rr = testdir.run(*cmd_opts, timeout=timeout) assert_outcomes(rr, {"passed": 1}) From a4a6c0aca6f7451a629fa07b183a9a67f38ab3f8 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Mon, 15 Jun 2020 09:03:08 -0400 Subject: [PATCH 4/5] avoid consuming keyword 'f' args in optional arguments --- pytest_twisted.py | 36 ++++++++++++++++++++++++++++++++---- 1 file changed, 32 insertions(+), 4 deletions(-) diff --git a/pytest_twisted.py b/pytest_twisted.py index 64b6f8b..6f311e7 100644 --- a/pytest_twisted.py +++ b/pytest_twisted.py @@ -1,5 +1,6 @@ import functools import inspect +import itertools import sys import warnings @@ -120,15 +121,42 @@ def decorator_apply(dec, func): dict(decfunc=dec(func)), __wrapped__=func) +class DecoratorArgumentsError(Exception): + pass + + +def repr_args_kwargs(*args, **kwargs): + arguments = ', '.join(itertools.chain( + (repr(x) for x in args), + ('{}={}'.format(k, repr(v)) for k, v in kwargs.items()) + )) + + return '({})'.format(arguments) + + +def positional_not_allowed_exception(*args, **kwargs): + arguments = repr_args_kwargs(**args, **kwargs) + + return DecoratorArgumentsError( + 'Positional decorator arguments not allowed: {}'.format(arguments), + ) + + def _optional_arguments(): def decorator_decorator(d): # TODO: this should get the signature of d minus the f or something - def decorator_wrapper(f=None, **decorator_arguments): + def decorator_wrapper(*args, **decorator_arguments): """this is decorator_wrapper""" - if f is not None: - if len(decorator_arguments) > 0 or not callable(f): - raise Exception('positional options not allowed') + if len(args) > 1: + raise positional_not_allowed_exception() + + if len(args) == 1: + maybe_f = args[0] + + if len(decorator_arguments) > 0 or not callable(maybe_f): + raise positional_not_allowed_exception() + f = maybe_f return d(f) # TODO: this should get the signature of d minus the kwargs From 8c9ada7ccea9a072591ae8e5f9eaad88d4a92541 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Mon, 15 Jun 2020 13:33:21 -0400 Subject: [PATCH 5/5] its *args not **args... --- pytest_twisted.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pytest_twisted.py b/pytest_twisted.py index 6f311e7..6ce904c 100644 --- a/pytest_twisted.py +++ b/pytest_twisted.py @@ -135,7 +135,7 @@ def repr_args_kwargs(*args, **kwargs): def positional_not_allowed_exception(*args, **kwargs): - arguments = repr_args_kwargs(**args, **kwargs) + arguments = repr_args_kwargs(*args, **kwargs) return DecoratorArgumentsError( 'Positional decorator arguments not allowed: {}'.format(arguments),