summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMichele Simionato <michele.simionato@gmail.com>2021-04-04 07:24:17 +0200
committerGitHub <noreply@github.com>2021-04-04 07:24:17 +0200
commit13aa60958d40e8ac9ba46d5f0abd8c497e96c95c (patch)
tree34a15b47a8040cb6d1cc025354ea76ea91387241
parent7fb1e34632b8f27578eb89c1c583c58b7dc74fde (diff)
parent91e9e734c2cac21d534b2c7d7904ff50ddfa2639 (diff)
downloadpython-decorator-git-13aa60958d40e8ac9ba46d5f0abd8c497e96c95c.tar.gz
Merge pull request #108 from micheles/kwsyntax
Restored old semantics and added kwsyntax flag
-rw-r--r--CHANGES.md11
-rw-r--r--docs/documentation.md169
-rw-r--r--src/decorator.py36
-rw-r--r--src/tests/documentation.py131
4 files changed, 200 insertions, 147 deletions
diff --git a/CHANGES.md b/CHANGES.md
index 8077af3..1243c96 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -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):