diff options
author | Michele Simionato <michele.simionato@gmail.com> | 2021-04-04 07:24:17 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-04-04 07:24:17 +0200 |
commit | 13aa60958d40e8ac9ba46d5f0abd8c497e96c95c (patch) | |
tree | 34a15b47a8040cb6d1cc025354ea76ea91387241 | |
parent | 7fb1e34632b8f27578eb89c1c583c58b7dc74fde (diff) | |
parent | 91e9e734c2cac21d534b2c7d7904ff50ddfa2639 (diff) | |
download | python-decorator-git-13aa60958d40e8ac9ba46d5f0abd8c497e96c95c.tar.gz |
Merge pull request #108 from micheles/kwsyntax
Restored old semantics and added kwsyntax flag
-rw-r--r-- | CHANGES.md | 11 | ||||
-rw-r--r-- | docs/documentation.md | 169 | ||||
-rw-r--r-- | src/decorator.py | 36 | ||||
-rw-r--r-- | src/tests/documentation.py | 131 |
4 files changed, 200 insertions, 147 deletions
@@ -3,15 +3,12 @@ HISTORY ## unreleased -## 5.0.4 (2021-04-03) - -Small fix (decorator.decorate was not copying the function docstring) and -documented the breaking change between versions 5.X and the past. - -## 5.0.3 (2021-04-02) +## 5.0.5 (2021-04-04) Dropped support for Python < 3.5 with a substantial simplification of -the code base. Ported CI from Travis to GitHub. +the code base (now building a decorator does not require calling "exec"). +Added a way to mimic functools.wraps-generated decorators. +Ported the Continuous Integration from Travis to GitHub. ## 4.4.2 (2020-02-29) diff --git a/docs/documentation.md b/docs/documentation.md index 9d7ad9c..858c754 100644 --- a/docs/documentation.md +++ b/docs/documentation.md @@ -4,9 +4,9 @@ Decorators for Humans |Author | Michele Simionato| |---|---| |E-mail | michele.simionato@gmail.com| -|Version| 5.0.4 (2021-04-03)| +|Version| 5.0.5 (2021-04-04)| |Supports| Python 3.5, 3.6, 3.7, 3.8, 3.9| -|Download page| http://pypi.python.org/pypi/decorator/5.0.4| +|Download page| http://pypi.python.org/pypi/decorator/5.0.5| |Installation| ``pip install decorator``| |License | BSD license| @@ -25,16 +25,15 @@ versions back to 2.6; versions 3.X are able to support even Python 2.5 and What's New in version 5 ----------------------- -There are no new features in version 5 of the decorator module, -except a simplification of the code base made possible by dropping -support for Python releases older than 3.5 (from that version -the Signature object works well enough that it is possible to fix the -signature of a decorated function without resorting to "exec" tricks). -The simplification gives a very neat advantage: in case of exceptions -raised in decorated functions the traceback is nicer than it used to be. -That counts as a new feature in my book ;-) -There is also a change of logic that breaks some decorators, see the section -about caveats and limitations. +Version 5 of the decorator module features a major simplification of +the code base made possible by dropping support for Python releases +older than 3.5. From that version the ``Signature`` object works well +enough that it is possible to fix the signature of a decorated +function without resorting to ``exec`` tricks. The simplification +has a very neat advantage: in case of exceptions raised in decorated +functions the traceback is nicer than it used to be. Moreover, it is +now possible to mimic the behavior of decorators defined with +``functool.wraps``: see the section about the ``kwsyntax`` flag below. What's New in version 4 ----------------------- @@ -469,6 +468,89 @@ calling func with args (), {} ``` +Mimicking the behavior of functools.wrap +---------------------------------------- + +Often people are confused by the decorator module since, contrarily +to ``functools.wraps`` in the standard library, it tries very hard +to keep the semantics of the arguments: in particular, positional arguments +stay positional even if they are called with the keyword argument syntax. +An example will make the issue clear. Here is a simple caller + +```python + + def chatty(func, *args, **kwargs): + print(args, kwargs) + return func(*args, **kwargs) +``` + +and here is a function to decorate: + +```python + + @decorator(chatty) + def printsum(x=1, y=2): + print(x + y) +``` + +In this example ``x`` and ``y`` are positional arguments (with +defaults). From the caller perspective, it does not matter if the user +calls them as named arguments, they will stay inside the ``args`` +tuple and not inside the ``kwargs`` dictionary: + +```python +>>> printsum(y=2, x=1) +(1, 2) {} +3 + +``` + +This is quite different from the behavior of ``functools.wraps``; if you +define the decorator as follows + +```python + + def chattywrapper(func): + @functools.wraps(func) + def wrapper(*args, **kwargs): + print(args, kwargs) + return func(*args, **kwargs) + return functools.wraps(wrapper) +``` + +you will see that calling ``printsum`` with named arguments will pass +such arguments to ``kwargs``, while ``args`` will be the empty tuple. +Since version 5 of the decorator module it is possible to mimic that +behavior by using the ``kwsyntax`` flag: + +```python + + @decorator(chatty, kwsyntax=True) + def printsum2(x=1, y=2): + print(x + y) +``` + +Here is how it works: + +```python +>>> printsum2(y=2, x=1) +() {'y': 2, 'x': 1} +3 + +``` + +This is exactly what the ``chattywrapper`` decorator would print: +positional arguments are seen as keyword arguments, but only if the +client code calls them with the keyword syntax. Otherwise they stay +positional: + +```python +>>> printsum2(1, 2) +(1, 2) {} +3 + +``` + Decorator factories ------------------------------------------- @@ -1456,69 +1538,6 @@ not use any cache, whereas the ``singledispatch`` implementation does. Caveats and limitations ------------------------------------------- -Version 5.X breaks compatibility with the past, by making decorators -more similar to the ones that can be defined with ``functools.wraps``. -An example will make the issue clear: - -```python - - @decorator - def chatty(func, *args, **kwargs): - print(args, kwargs) - return func(*args, **kwargs) -``` - -```python - - @chatty - def printsum(x=1, y=2): - print(x + y) -``` - -In this example ``x`` and ``y`` are positional arguments with defaults. -In previous versions of the decorator module -(< 5) a call to ``printsum()`` would have passed ``args==(1, 2)`` to -the caller, with an empty ``kwargs`` dictionary. In version 5.X instead -even ``args`` is empty: - -```python ->>> printsum() -() {} -3 - -``` -``args`` become non-empty only if you pass the arguments as positional - -```python ->>> printsum(1) -(1,) {} -3 - -``` -and not if you pass them as keyword arguments: - -```python ->>> printsum(x=1) -() {'x': 1} -3 - -``` -This can be pretty confusing since non-keyword arguments are passed as -keywork arguments, but it the way it works with ``functools.wraps`` and -the way many people expect it to work. You can play with - -```python - - def chattywrapper(func): - @functools.wraps(func) - def wrapper(*args, **kwargs): - print(args, kwargs) - return func(*args, **kwargs) - return functools.wraps(wrapper) -``` - -and see that we are consistent indeed. - In the present implementation, decorators generated by ``decorator`` can only be used on user-defined Python functions, methods or coroutines. I have no interest in decorating generic callable objects. If you want to diff --git a/src/decorator.py b/src/decorator.py index 1cb1596..37c5675 100644 --- a/src/decorator.py +++ b/src/decorator.py @@ -40,7 +40,7 @@ import itertools from contextlib import _GeneratorContextManager from inspect import getfullargspec, iscoroutinefunction, isgeneratorfunction -__version__ = '5.0.4' +__version__ = '5.0.5' DEF = re.compile(r'\s*def\s*([_\w][_\w\d]*)\s*\(') POS = inspect.Parameter.POSITIONAL_OR_KEYWORD @@ -196,24 +196,42 @@ class FunctionMaker(object): return self.make(body, evaldict, addsource, **attrs) -def decorate(func, caller, extras=()): +def fix(args, kwargs, sig): """ - decorate(func, caller) decorates a function using a caller. - If the caller is a generator function, the resulting function - will be a generator function. + Fix args and kwargs to be consistent with the signature """ + ba = sig.bind(*args, **kwargs) + ba.apply_defaults() + return ba.args, ba.kwargs + + +def decorate(func, caller, extras=(), kwsyntax=False): + """ + Decorates a function/generator/coroutine using a caller. + If kwsyntax is True calling the decorated functions with keyword + syntax will pass the named arguments inside the ``kw`` dictionary, + even if such argument are positional, similarly to what functools.wraps + does. By default kwsyntax is False and the the arguments are untouched. + """ + sig = inspect.signature(func) if iscoroutinefunction(caller): async def fun(*args, **kw): + if not kwsyntax: + args, kw = fix(args, kw, sig) return await caller(func, *(extras + args), **kw) elif isgeneratorfunction(caller): def fun(*args, **kw): + if not kwsyntax: + args, kw = fix(args, kw, sig) for res in caller(func, *(extras + args), **kw): yield res else: def fun(*args, **kw): + if not kwsyntax: + args, kw = fix(args, kw, sig) return caller(func, *(extras + args), **kw) fun.__name__ = func.__name__ - fun.__signature__ = inspect.signature(func) + fun.__signature__ = sig fun.__wrapped__ = func fun.__qualname__ = func.__qualname__ fun.__annotations__ = func.__annotations__ @@ -223,7 +241,7 @@ def decorate(func, caller, extras=()): return fun -def decorator(caller, _func=None): +def decorator(caller, _func=None, kwsyntax=False): """ decorator(caller) converts a caller function into a decorator """ @@ -240,9 +258,9 @@ def decorator(caller, _func=None): for p in dec_params[na:] if p.default is not EMPTY) if func is None: - return lambda func: decorate(func, caller, extras) + return lambda func: decorate(func, caller, extras, kwsyntax) else: - return decorate(func, caller, extras) + return decorate(func, caller, extras, kwsyntax) dec.__signature__ = sig.replace(parameters=dec_params) dec.__name__ = caller.__name__ dec.__doc__ = caller.__doc__ diff --git a/src/tests/documentation.py b/src/tests/documentation.py index 889f97f..02f5b37 100644 --- a/src/tests/documentation.py +++ b/src/tests/documentation.py @@ -36,16 +36,15 @@ versions back to 2.6; versions 3.X are able to support even Python 2.5 and What's New in version 5 ----------------------- -There are no new features in version 5 of the decorator module, -except a simplification of the code base made possible by dropping -support for Python releases older than 3.5 (from that version -the Signature object works well enough that it is possible to fix the -signature of a decorated function without resorting to "exec" tricks). -The simplification gives a very neat advantage: in case of exceptions -raised in decorated functions the traceback is nicer than it used to be. -That counts as a new feature in my book ;-) -There is also a change of logic that breaks some decorators, see the section -about caveats and limitations. +Version 5 of the decorator module features a major simplification of +the code base made possible by dropping support for Python releases +older than 3.5. From that version the ``Signature`` object works well +enough that it is possible to fix the signature of a decorated +function without resorting to ``exec`` tricks. The simplification +has a very neat advantage: in case of exceptions raised in decorated +functions the traceback is nicer than it used to be. Moreover, it is +now possible to mimic the behavior of decorators defined with +``functool.wraps``: see the section about the ``kwsyntax`` flag below. What's New in version 4 ----------------------- @@ -374,6 +373,66 @@ calling func with args (), {} ``` +Mimicking the behavior of functools.wrap +---------------------------------------- + +Often people are confused by the decorator module since, contrarily +to ``functools.wraps`` in the standard library, it tries very hard +to keep the semantics of the arguments: in particular, positional arguments +stay positional even if they are called with the keyword argument syntax. +An example will make the issue clear. Here is a simple caller + +$$chatty + +and here is a function to decorate: + +$$printsum + +In this example ``x`` and ``y`` are positional arguments (with +defaults). From the caller perspective, it does not matter if the user +calls them as named arguments, they will stay inside the ``args`` +tuple and not inside the ``kwargs`` dictionary: + +```python +>>> printsum(y=2, x=1) +(1, 2) {} +3 + +``` + +This is quite different from the behavior of ``functools.wraps``; if you +define the decorator as follows + +$$chattywrapper + +you will see that calling ``printsum`` with named arguments will pass +such arguments to ``kwargs``, while ``args`` will be the empty tuple. +Since version 5 of the decorator module it is possible to mimic that +behavior by using the ``kwsyntax`` flag: + +$$printsum2 + +Here is how it works: + +```python +>>> printsum2(y=2, x=1) +() {'y': 2, 'x': 1} +3 + +``` + +This is exactly what the ``chattywrapper`` decorator would print: +positional arguments are seen as keyword arguments, but only if the +client code calls them with the keyword syntax. Otherwise they stay +positional: + +```python +>>> printsum2(1, 2) +(1, 2) {} +3 + +``` + Decorator factories ------------------------------------------- @@ -1105,50 +1164,6 @@ not use any cache, whereas the ``singledispatch`` implementation does. Caveats and limitations ------------------------------------------- -Version 5.X breaks compatibility with the past, by making decorators -more similar to the ones that can be defined with ``functools.wraps``. -An example will make the issue clear: - -$$chatty - -$$printsum - -In this example ``x`` and ``y`` are positional arguments with defaults. -In previous versions of the decorator module -(< 5) a call to ``printsum()`` would have passed ``args==(1, 2)`` to -the caller, with an empty ``kwargs`` dictionary. In version 5.X instead -even ``args`` is empty: - -```python ->>> printsum() -() {} -3 - -``` -``args`` become non-empty only if you pass the arguments as positional - -```python ->>> printsum(1) -(1,) {} -3 - -``` -and not if you pass them as keyword arguments: - -```python ->>> printsum(x=1) -() {'x': 1} -3 - -``` -This can be pretty confusing since non-keyword arguments are passed as -keywork arguments, but it the way it works with ``functools.wraps`` and -the way many people expect it to work. You can play with - -$$chattywrapper - -and see that we are consistent indeed. - In the present implementation, decorators generated by ``decorator`` can only be used on user-defined Python functions, methods or coroutines. I have no interest in decorating generic callable objects. If you want to @@ -1799,17 +1814,21 @@ def operation2(): time.sleep(.1) -@decorator def chatty(func, *args, **kwargs): print(args, kwargs) return func(*args, **kwargs) -@chatty +@decorator(chatty) def printsum(x=1, y=2): print(x + y) +@decorator(chatty, kwsyntax=True) +def printsum2(x=1, y=2): + print(x + y) + + def chattywrapper(func): @functools.wraps(func) def wrapper(*args, **kwargs): |