From 10f362eccabc5285f6dce795d256e764906eddea Mon Sep 17 00:00:00 2001 From: Michele Simionato Date: Mon, 2 Nov 2015 06:45:13 +0100 Subject: "f" is no more a special name --- src/decorator.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) (limited to 'src') diff --git a/src/decorator.py b/src/decorator.py index 13aeef4..16b061c 100644 --- a/src/decorator.py +++ b/src/decorator.py @@ -33,8 +33,6 @@ for the documentation. """ from __future__ import print_function -__version__ = '4.0.4' - import re import sys import inspect @@ -42,6 +40,8 @@ import operator import itertools import collections +__version__ = '4.0.5' + if sys.version >= '3': from inspect import getfullargspec @@ -247,7 +247,6 @@ def decorator(caller, _func=None): callerfunc = get_init(caller) doc = 'decorator(%s) converts functions/generators into ' \ 'factories of %s objects' % (caller.__name__, caller.__name__) - fun = getfullargspec(callerfunc).args[1] # second arg elif inspect.isfunction(caller): if caller.__name__ == '': name = '_lambda_' @@ -255,18 +254,15 @@ def decorator(caller, _func=None): name = caller.__name__ callerfunc = caller doc = caller.__doc__ - fun = getfullargspec(callerfunc).args[0] # first arg else: # assume caller is an object with a __call__ method name = caller.__class__.__name__.lower() callerfunc = caller.__call__.__func__ doc = caller.__call__.__doc__ - fun = getfullargspec(callerfunc).args[1] # second arg evaldict = callerfunc.__globals__.copy() evaldict['_call_'] = caller evaldict['_decorate_'] = decorate return FunctionMaker.create( - '%s(%s)' % (name, fun), - 'return _decorate_(%s, _call_)' % fun, + '%s(_f_)' % name, 'return _decorate_(_f_, _call_)', evaldict, doc=doc, module=caller.__module__, __wrapped__=caller) -- cgit v1.2.1 From d6abda047e0b52aa1a379d06bd700edda8c623b7 Mon Sep 17 00:00:00 2001 From: Michele Simionato Date: Mon, 2 Nov 2015 07:29:05 +0100 Subject: Small renaming --- src/decorator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/decorator.py b/src/decorator.py index 16b061c..05f7056 100644 --- a/src/decorator.py +++ b/src/decorator.py @@ -262,7 +262,7 @@ def decorator(caller, _func=None): evaldict['_call_'] = caller evaldict['_decorate_'] = decorate return FunctionMaker.create( - '%s(_f_)' % name, 'return _decorate_(_f_, _call_)', + '%s(func)' % name, 'return _decorate_(func, _call_)', evaldict, doc=doc, module=caller.__module__, __wrapped__=caller) -- cgit v1.2.1 From f22e94ae1bcf816328c2ee9b5e41a32276e32b30 Mon Sep 17 00:00:00 2001 From: Michele Simionato Date: Wed, 9 Dec 2015 08:58:38 +0100 Subject: Avoided copying the globals --- src/decorator.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) (limited to 'src') diff --git a/src/decorator.py b/src/decorator.py index 05f7056..a8c22e7 100644 --- a/src/decorator.py +++ b/src/decorator.py @@ -225,9 +225,7 @@ def decorate(func, caller): """ decorate(func, caller) decorates a function using a caller. """ - evaldict = func.__globals__.copy() - evaldict['_call_'] = caller - evaldict['_func_'] = func + evaldict = dict(_call_=caller, _func_=func) fun = FunctionMaker.create( func, "return _call_(_func_, %(shortsignature)s)", evaldict, __wrapped__=func) @@ -258,9 +256,7 @@ def decorator(caller, _func=None): name = caller.__class__.__name__.lower() callerfunc = caller.__call__.__func__ doc = caller.__call__.__doc__ - evaldict = callerfunc.__globals__.copy() - evaldict['_call_'] = caller - evaldict['_decorate_'] = decorate + evaldict = dict(_call_=caller, _decorate_=decorate) return FunctionMaker.create( '%s(func)' % name, 'return _decorate_(func, _call_)', evaldict, doc=doc, module=caller.__module__, -- cgit v1.2.1 From 1ed48fd60939e752c5a7ffb2b1b17744d73239ee Mon Sep 17 00:00:00 2001 From: Michele Simionato Date: Wed, 9 Dec 2015 09:10:42 +0100 Subject: Some cleanup --- src/decorator.py | 3 --- 1 file changed, 3 deletions(-) (limited to 'src') diff --git a/src/decorator.py b/src/decorator.py index a8c22e7..4e2bb7a 100644 --- a/src/decorator.py +++ b/src/decorator.py @@ -242,7 +242,6 @@ def decorator(caller, _func=None): # else return a decorator function if inspect.isclass(caller): name = caller.__name__.lower() - callerfunc = get_init(caller) doc = 'decorator(%s) converts functions/generators into ' \ 'factories of %s objects' % (caller.__name__, caller.__name__) elif inspect.isfunction(caller): @@ -250,11 +249,9 @@ def decorator(caller, _func=None): name = '_lambda_' else: name = caller.__name__ - callerfunc = caller doc = caller.__doc__ else: # assume caller is an object with a __call__ method name = caller.__class__.__name__.lower() - callerfunc = caller.__call__.__func__ doc = caller.__call__.__doc__ evaldict = dict(_call_=caller, _decorate_=decorate) return FunctionMaker.create( -- cgit v1.2.1 From a26c55c0d527e1b09b996e29f9d9d816d085addd Mon Sep 17 00:00:00 2001 From: Michele Simionato Date: Wed, 9 Dec 2015 10:08:23 +0100 Subject: Documented the quirk when decorating functions with keyword arguments --- src/decorator.py | 2 -- src/tests/documentation.py | 47 ++++++++++++++++++++++++++++++++++++++++++---- src/tests/test.py | 14 +++++++++++++- 3 files changed, 56 insertions(+), 7 deletions(-) (limited to 'src') diff --git a/src/decorator.py b/src/decorator.py index 4e2bb7a..49c1747 100644 --- a/src/decorator.py +++ b/src/decorator.py @@ -178,8 +178,6 @@ class FunctionMaker(object): for n in names: if n in ('_func_', '_call_'): raise NameError('%s is overridden in\n%s' % (n, src)) - if not src.endswith('\n'): # add a newline just for safety - src += '\n' # this is needed in old versions of Python # Ensure each generated function has a unique filename for profilers # (such as cProfile) that depend on the tuple of (, diff --git a/src/tests/documentation.py b/src/tests/documentation.py index 04d62eb..69e9f4b 100644 --- a/src/tests/documentation.py +++ b/src/tests/documentation.py @@ -526,7 +526,6 @@ be added to the generated function: >>> print(f1.__source__) def f1(a, b): f(a, b) - ``FunctionMaker.create`` can take as first argument a string, as in the examples before, or a function. This is the most common @@ -1007,9 +1006,49 @@ callable objects, nor on built-in functions, due to limitations of the ``inspect`` module in the standard library, especially for Python 2.X (in Python 3.5 a lot of such limitations have been removed). -There is a restriction on the names of the arguments: for instance, -if try to call an argument ``_call_`` or ``_func_`` -you will get a ``NameError``: +There is a strange quirk when decorating functions that take keyword +arguments, if one of such arguments has the same name used in the +caller function for the first argument. The quirk was reported by +David Goldstein and here is an example where it is manifest: + +.. code-block: python + + >>> @memoize + ... def getkeys(**kw): + ... return kw.keys() + >>> getkeys(func='a') + Traceback (most recent call last): + ... + TypeError: _memoize() got multiple values for argument 'func' + +The error message looks really strange until you realize that +the caller function `_memoize` uses `func` as first argument, +so there is a confusion between the positional argument and the +keywork arguments. The solution is to change the name of the +first argument in `_memoize`, or to change the implementation as +follows: + +.. code-block: python + + def _memoize(*all_args, **kw): + func = all_args[0] + args = all_args[1:] + if kw: # frozenset is used to ensure hashability + key = args, frozenset(kw.items()) + else: + key = args + cache = func.cache # attribute added by memoize + if key not in cache: + cache[key] = func(*args, **kw) + return cache[key] + +We have avoided the need to name the first argument, so the problem +simply disappear. This is a technique that you should keep in mind +when writing decorator for functions with keyword arguments. + +On a similar tone, there is a restriction on the names of the +arguments: for instance, if try to call an argument ``_call_`` or +``_func_`` you will get a ``NameError``: .. code-block:: python diff --git a/src/tests/test.py b/src/tests/test.py index ab65dfa..b01bc10 100644 --- a/src/tests/test.py +++ b/src/tests/test.py @@ -77,7 +77,19 @@ class ExtraTestCase(unittest.TestCase): self.assertNotEqual(d1.__code__.co_filename, d2.__code__.co_filename) self.assertNotEqual(f1.__code__.co_filename, f2.__code__.co_filename) - self.assertNotEqual(f1_orig.__code__.co_filename, f1.__code__.co_filename) + self.assertNotEqual(f1_orig.__code__.co_filename, + f1.__code__.co_filename) + + def test_no_first_arg(self): + @decorator + def example(*args, **kw): + return args[0](*args[1:], **kw) + + @example + def func(**kw): + return kw + + self.assertEqual(func(f='a'), {'f': 'a'}) # ################### test dispatch_on ############################# # # adapted from test_functools in Python 3.5 -- cgit v1.2.1 From 51f1f8057605df65e39649333139e7d55abfd646 Mon Sep 17 00:00:00 2001 From: Michele Simionato Date: Wed, 9 Dec 2015 10:19:54 +0100 Subject: Added a doctest: +ELLIPSIS to fix old Python versions --- src/tests/documentation.py | 8 ++++---- src/tests/test.py | 3 ++- 2 files changed, 6 insertions(+), 5 deletions(-) (limited to 'src') diff --git a/src/tests/documentation.py b/src/tests/documentation.py index 69e9f4b..81409fc 100644 --- a/src/tests/documentation.py +++ b/src/tests/documentation.py @@ -1016,10 +1016,10 @@ David Goldstein and here is an example where it is manifest: >>> @memoize ... def getkeys(**kw): ... return kw.keys() - >>> getkeys(func='a') + >>> getkeys(func='a') # doctest: +ELLIPSIS Traceback (most recent call last): ... - TypeError: _memoize() got multiple values for argument 'func' + TypeError: _memoize() got multiple values for ... 'func' The error message looks really strange until you realize that the caller function `_memoize` uses `func` as first argument, @@ -1043,8 +1043,8 @@ follows: return cache[key] We have avoided the need to name the first argument, so the problem -simply disappear. This is a technique that you should keep in mind -when writing decorator for functions with keyword arguments. +simply disappears. This is a technique that you should keep in mind +when writing decorators for functions with keyword arguments. On a similar tone, there is a restriction on the names of the arguments: for instance, if try to call an argument ``_call_`` or diff --git a/src/tests/test.py b/src/tests/test.py index b01bc10..5bd34e8 100644 --- a/src/tests/test.py +++ b/src/tests/test.py @@ -89,7 +89,8 @@ class ExtraTestCase(unittest.TestCase): def func(**kw): return kw - self.assertEqual(func(f='a'), {'f': 'a'}) + # there is no confusion when passing args as a keyword argument + self.assertEqual(func(args='a'), {'args': 'a'}) # ################### test dispatch_on ############################# # # adapted from test_functools in Python 3.5 -- cgit v1.2.1 From 0734bb4caf32083152a1571742e07415af54747c Mon Sep 17 00:00:00 2001 From: Michele Simionato Date: Wed, 9 Dec 2015 14:58:43 +0100 Subject: Fixed code-block:: --- src/tests/documentation.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/tests/documentation.py b/src/tests/documentation.py index 81409fc..b704b91 100644 --- a/src/tests/documentation.py +++ b/src/tests/documentation.py @@ -1011,7 +1011,7 @@ arguments, if one of such arguments has the same name used in the caller function for the first argument. The quirk was reported by David Goldstein and here is an example where it is manifest: -.. code-block: python +.. code-block:: python >>> @memoize ... def getkeys(**kw): @@ -1028,7 +1028,7 @@ keywork arguments. The solution is to change the name of the first argument in `_memoize`, or to change the implementation as follows: -.. code-block: python +.. code-block:: python def _memoize(*all_args, **kw): func = all_args[0] -- cgit v1.2.1