Skip to content

Commit

Permalink
Restored old semantic and added kwsyntax flag
Browse files Browse the repository at this point in the history
  • Loading branch information
micheles committed Apr 4, 2021
1 parent 7fb1e34 commit 04bb645
Show file tree
Hide file tree
Showing 4 changed files with 172 additions and 147 deletions.
11 changes: 4 additions & 7 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
155 changes: 80 additions & 75 deletions docs/documentation.md
Original file line number Diff line number Diff line change
Expand Up @@ -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|

Expand All @@ -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 ``kw_syntax`` flag below.

What's New in version 4
-----------------------
Expand Down Expand Up @@ -469,6 +468,75 @@ 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 semantic 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:

```python

def chatty(func, *args, **kwargs):
print(args, kwargs)
return func(*args, **kwargs)
```

```python

@decorator(chatty)
def printsum(x=1, y=2):
print(x + y)
```

In this example ``x`` and ``y`` are positional arguments (with defaults).
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
inside the caller:

```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

```

Decorator factories
-------------------------------------------

Expand Down Expand Up @@ -1456,69 +1524,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
Expand Down
36 changes: 27 additions & 9 deletions src/decorator.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
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
Expand Down Expand Up @@ -196,24 +196,42 @@ def create(cls, obj, body, evaldict, defaults=None,
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__
Expand All @@ -223,7 +241,7 @@ def fun(*args, **kw):
return fun


def decorator(caller, _func=None):
def decorator(caller, _func=None, kwsyntax=False):
"""
decorator(caller) converts a caller function into a decorator
"""
Expand All @@ -240,9 +258,9 @@ def dec(func=None, *args, **kw):
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__
Expand Down
Loading

0 comments on commit 04bb645

Please sign in to comment.