diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/decorator.py | 23 | ||||
| -rw-r--r-- | src/tests/documentation.py | 47 | ||||
| -rw-r--r-- | src/tests/test.py | 15 |
3 files changed, 62 insertions, 23 deletions
diff --git a/src/decorator.py b/src/decorator.py index 13aeef4..49c1747 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 @@ -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 (<filename>, @@ -225,9 +223,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) @@ -244,29 +240,20 @@ 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__) - fun = getfullargspec(callerfunc).args[1] # second arg elif inspect.isfunction(caller): if caller.__name__ == '<lambda>': name = '_lambda_' else: 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 + evaldict = dict(_call_=caller, _decorate_=decorate) return FunctionMaker.create( - '%s(%s)' % (name, fun), - 'return _decorate_(%s, _call_)' % fun, + '%s(func)' % name, 'return _decorate_(func, _call_)', evaldict, doc=doc, module=caller.__module__, __wrapped__=caller) diff --git a/src/tests/documentation.py b/src/tests/documentation.py index 04d62eb..b704b91 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) - <BLANKLINE> ``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') # doctest: +ELLIPSIS + Traceback (most recent call last): + ... + 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, +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 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 +``_func_`` you will get a ``NameError``: .. code-block:: python diff --git a/src/tests/test.py b/src/tests/test.py index ab65dfa..5bd34e8 100644 --- a/src/tests/test.py +++ b/src/tests/test.py @@ -77,7 +77,20 @@ 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 + + # 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 |
