diff --git a/funcy/decorators.py b/funcy/decorators.py index b73b9af..e001629 100644 --- a/funcy/decorators.py +++ b/funcy/decorators.py @@ -103,7 +103,6 @@ def has_1pos_and_kwonly(func): def get_argnames(func): func = getattr(func, '__original__', None) or unwrap(func) - # import ipdb; ipdb.set_trace() return func.__code__.co_varnames[:func.__code__.co_argcount] def arggetter(func, _cache={}): @@ -118,7 +117,11 @@ def arggetter(func, _cache={}): n = code.co_argcount kwonlynames = code.co_varnames[n:n + code.co_kwonlyargcount] n += code.co_kwonlyargcount - kwnames = posnames[code.co_posonlyargcount:] + kwonlynames + # TODO: remove this check once we drop Python 3.7 + if hasattr(code, 'co_posonlyargcount'): + kwnames = posnames[code.co_posonlyargcount:] + kwonlynames + else: + kwnames = posnames + kwonlynames varposname = varkwname = None if code.co_flags & inspect.CO_VARARGS: @@ -128,8 +131,6 @@ def arggetter(func, _cache={}): varkwname = code.co_varnames[n] allnames = set(code.co_varnames) - - # argnames = get_argnames(original) indexes = {name: i for i, name in enumerate(posnames)} defaults = {} if original.__defaults__: diff --git a/tests/py38_decorators.py b/tests/py38_decorators.py new file mode 100644 index 0000000..87bd06c --- /dev/null +++ b/tests/py38_decorators.py @@ -0,0 +1,42 @@ +import pytest +from funcy.decorators import decorator + + +def test_decorator_access_args(): + @decorator + def return_x(call): + return call.x + + # no arg + with pytest.raises(AttributeError): return_x(lambda y: None)(10) + + # pos arg + assert return_x(lambda x: None)(10) == 10 + with pytest.raises(AttributeError): return_x(lambda x: None)() + assert return_x(lambda x=11: None)(10) == 10 + assert return_x(lambda x=11: None)() == 11 + + # pos-only + assert return_x(lambda x, /: None)(10) == 10 + with pytest.raises(AttributeError): return_x(lambda x, /: None)() + assert return_x(lambda x=11, /: None)(10) == 10 + assert return_x(lambda x=11, /: None)() == 11 + # try to pass by name + with pytest.raises(AttributeError): return_x(lambda x, /: None)(x=10) + assert return_x(lambda x=11, /: None)(x=10) == 11 + + # kw-only + assert return_x(lambda _, /, *, x: None)(x=10) == 10 + with pytest.raises(AttributeError): return_x(lambda _, /, *, x: None)() + assert return_x(lambda _, /, *, x=11: None)(x=10) == 10 + assert return_x(lambda _, /, *, x=11: None)() == 11 + + # varargs + assert return_x(lambda *x: None)(1, 2) == (1, 2) + assert return_x(lambda _, *x: None)(1, 2) == (2,) + + # varkeywords + assert return_x(lambda **x: None)(a=1, b=2) == {'a': 1, 'b': 2} + assert return_x(lambda **x: None)(a=1, x=3) == {'a': 1, 'x': 3} # Not just 3 + assert return_x(lambda a, **x: None)(a=1, b=2) == {'b': 2} + assert return_x(lambda a, /, **x: None)(a=1, b=2) == {'a': 1, 'b': 2} diff --git a/tests/test_decorators.py b/tests/test_decorators.py index 5fde257..661563f 100644 --- a/tests/test_decorators.py +++ b/tests/test_decorators.py @@ -1,3 +1,4 @@ +import sys import pytest from funcy.decorators import * @@ -40,6 +41,7 @@ def ten(a, b): assert add(ten)(1, 2) == 11 +# TODO: replace this with a full version once we drop Python 3.7 def test_decorator_access_args(): @decorator def return_x(call): @@ -54,21 +56,6 @@ def return_x(call): assert return_x(lambda x=11: None)(10) == 10 assert return_x(lambda x=11: None)() == 11 - # pos-only - assert return_x(lambda x, /: None)(10) == 10 - with pytest.raises(AttributeError): return_x(lambda x, /: None)() - assert return_x(lambda x=11, /: None)(10) == 10 - assert return_x(lambda x=11, /: None)() == 11 - # try to pass by name - with pytest.raises(AttributeError): return_x(lambda x, /: None)(x=10) - assert return_x(lambda x=11, /: None)(x=10) == 11 - - # kw-only - assert return_x(lambda _, /, *, x: None)(x=10) == 10 - with pytest.raises(AttributeError): return_x(lambda _, /, *, x: None)() - assert return_x(lambda _, /, *, x=11: None)(x=10) == 10 - assert return_x(lambda _, /, *, x=11: None)() == 11 - # varargs assert return_x(lambda *x: None)(1, 2) == (1, 2) assert return_x(lambda _, *x: None)(1, 2) == (2,) @@ -77,7 +64,11 @@ def return_x(call): assert return_x(lambda **x: None)(a=1, b=2) == {'a': 1, 'b': 2} assert return_x(lambda **x: None)(a=1, x=3) == {'a': 1, 'x': 3} # Not just 3 assert return_x(lambda a, **x: None)(a=1, b=2) == {'b': 2} - assert return_x(lambda a, /, **x: None)(a=1, b=2) == {'a': 1, 'b': 2} + + +if sys.version_info >= (3, 8): + pytest.register_assert_rewrite("tests.py38_decorators") + from .py38_decorators import test_decorator_access_args # noqa def test_double_decorator_defaults(): diff --git a/tests/test_decorators.py.orig b/tests/test_decorators.py.orig new file mode 100644 index 0000000..4217dee --- /dev/null +++ b/tests/test_decorators.py.orig @@ -0,0 +1,192 @@ +import pytest +from funcy.decorators import * + + +def test_decorator_no_args(): + @decorator + def inc(call): + return call() + 1 + + @inc + def ten(): + return 10 + + assert ten() == 11 + + +def test_decorator_with_args(): + @decorator + def add(call, n): + return call() + n + + @add(2) + def ten(): + return 10 + + assert ten() == 12 + + +def test_decorator_kw_only_args(): + @decorator +<<<<<<< Updated upstream + def add(call, *, n=1): + return call() + n +======= + def add(call, *, n=1): # TODO: use real kw-only args in Python 3 + return call() + call.n + # @decorator + # def add(call, **kwargs): # TODO: use real kw-only args in Python 3 + # return call() + kwargs.get("n", 1) +>>>>>>> Stashed changes + + def ten(a, b): + return 10 + + # Should work with or without parentheses + assert add(n=2)(ten)(1, 2) == 12 + assert add()(ten)(1, 2) == 11 + assert add(ten)(1, 2) == 11 + + +def test_decorator_access_arg(): + @decorator + def multiply(call): + return call() * call.n + + @multiply + def square(n): + return n + + assert square(5) == 25 + + +def test_decorator_access_nonexistent_arg(): + @decorator + def return_x(call): + return call.x + + @return_x + def f(): + pass + + with pytest.raises(AttributeError): f() + + +def test_decorator_required_arg(): + @decorator + def deco(call): + call.x + + @deco + def f(x, y=42): + pass + + with pytest.raises(AttributeError): f() + + +def test_double_decorator_defaults(): + @decorator + def deco(call): + return call.y + + @decorator + def noop(call): + return call() + + @deco + @noop + def f(x, y=1): + pass + + assert f(42) == 1 + + +def test_decorator_defaults(): + @decorator + def deco(call): + return call.y, call.z + + @deco + def f(x, y=1, z=2): + pass + + assert f(42) == (1, 2) + + +def test_decorator_with_method(): + @decorator + def inc(call): + return call() + 1 + + class A(object): + def ten(self): + return 10 + + @classmethod + def ten_cls(cls): + return 10 + + @staticmethod + def ten_static(): + return 10 + + assert inc(A().ten)() == 11 + assert inc(A.ten_cls)() == 11 + assert inc(A.ten_static)() == 11 + + +def test_decorator_with_method_descriptor(): + @decorator + def exclaim(call): + return call() + '!' + + assert exclaim(str.upper)('hi') == 'HI!' + + +def test_chain_arg_access(): + @decorator + def decor(call): + return call.x + call() + + @decor + @decor + def func(x): + return x + + assert func(2) == 6 + + +def test_meta_attribtes(): + @decorator + def decor(call): + return call() + + def func(x): + "Some doc" + return x + + decorated = decor(func) + double_decorated = decor(decorated) + + assert decorated.__name__ == 'func' + assert decorated.__module__ == __name__ + assert decorated.__doc__ == "Some doc" + assert decorated.__wrapped__ is func + assert decorated.__original__ is func + + assert double_decorated.__wrapped__ is decorated + assert double_decorated.__original__ is func + + +def test_decorator_introspection(): + @decorator + def decor(call, x): + return call() + + assert decor.__name__ == 'decor' + + decor_x = decor(42) + assert decor_x.__name__ == 'decor' + assert decor_x._func is decor.__wrapped__ + assert decor_x._args == (42,) + assert decor_x._kwargs == {}