summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMichele Simionato <michele.simionato@gmail.com>2021-03-31 07:20:18 +0200
committerGitHub <noreply@github.com>2021-03-31 07:20:18 +0200
commit7f222ab066bb390b251616490f8488900a5d5221 (patch)
tree00e776f332d4b654b42b3be42b88e75220463702
parent5b078615f9ccd2b4bf25ae808ea3820dca06adf2 (diff)
parent3bbdb6dd1fb04c8b8379c751e5e0e0afe04c1d3f (diff)
downloadpython-decorator-git-7f222ab066bb390b251616490f8488900a5d5221.tar.gz
Merge pull request #97 from micheles/4.5
Removing support for Python < 3.5
-rw-r--r--.github/workflows/python-package.yml2
-rw-r--r--CHANGES.md3
-rw-r--r--src/decorator.py168
-rw-r--r--src/tests/documentation.py125
-rw-r--r--src/tests/test.py35
5 files changed, 121 insertions, 212 deletions
diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml
index 6135835..47dd3cd 100644
--- a/.github/workflows/python-package.yml
+++ b/.github/workflows/python-package.yml
@@ -15,7 +15,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
- python-version: ['2.7', '3.5', '3.6', '3.7', '3.8', '3.9']
+ python-version: ['3.5', '3.6', '3.7', '3.8', '3.9']
steps:
- uses: actions/checkout@v2
diff --git a/CHANGES.md b/CHANGES.md
index 89ca8eb..f37ee53 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -3,7 +3,8 @@ HISTORY
## unreleased
-Ported CI from Travis to GitHub
+Dropped support for Python < 3.5 with a substantial simplification of
+the code base. Ported CI from Travis to GitHub.
## 4.4.2 (2020-02-29)
diff --git a/src/decorator.py b/src/decorator.py
index a5081e0..2293c6e 100644
--- a/src/decorator.py
+++ b/src/decorator.py
@@ -1,6 +1,6 @@
# ######################### LICENSE ############################ #
-# Copyright (c) 2005-2020, Michele Simionato
+# Copyright (c) 2005-2021, Michele Simionato
# All rights reserved.
# Redistribution and use in source and binary forms, with or without
@@ -32,52 +32,22 @@ Decorator module, see
https://github.com/micheles/decorator/blob/master/docs/documentation.md
for the documentation.
"""
-from __future__ import print_function
-
import re
import sys
import inspect
import operator
import itertools
-import collections
-
-__version__ = '4.4.2'
-
-if sys.version_info >= (3,):
- from inspect import getfullargspec
-
- def get_init(cls):
- return cls.__init__
-else:
- FullArgSpec = collections.namedtuple(
- 'FullArgSpec', 'args varargs varkw defaults '
- 'kwonlyargs kwonlydefaults annotations')
-
- def getfullargspec(f):
- "A quick and dirty replacement for getfullargspec for Python 2.X"
- return FullArgSpec._make(inspect.getargspec(f) + ([], None, {}))
-
- def get_init(cls):
- return cls.__init__.__func__
-
-try:
- iscoroutinefunction = inspect.iscoroutinefunction
-except AttributeError:
- # let's assume there are no coroutine functions in old Python
- def iscoroutinefunction(f):
- return False
-try:
- from inspect import isgeneratorfunction
-except ImportError:
- # assume no generator function in old Python versions
- def isgeneratorfunction(caller):
- return False
+from contextlib import _GeneratorContextManager
+from inspect import getfullargspec, iscoroutinefunction, isgeneratorfunction
+__version__ = '4.5.0'
DEF = re.compile(r'\s*def\s*([_\w][_\w\d]*)\s*\(')
+POS = inspect.Parameter.POSITIONAL_OR_KEYWORD
+EMPTY = inspect.Parameter.empty
-# basic functionality
+# this is not used anymore in the core, but kept for backward compatibility
class FunctionMaker(object):
"""
An object with the ability to create functions with a given signature.
@@ -144,7 +114,9 @@ class FunctionMaker(object):
raise TypeError('You are decorating a non function: %s' % func)
def update(self, func, **kw):
- "Update the signature of func with the data in self"
+ """
+ Update the signature of func with the data in self
+ """
func.__name__ = self.name
func.__doc__ = getattr(self, 'doc', None)
func.__dict__ = getattr(self, 'dict', {})
@@ -161,7 +133,9 @@ class FunctionMaker(object):
func.__dict__.update(kw)
def make(self, src_templ, evaldict=None, addsource=False, **attrs):
- "Make a new function from a given template and update the signature"
+ """
+ Make a new function from a given template and update the signature
+ """
src = src_templ % vars(self) # expand name and signature
evaldict = evaldict or {}
mo = DEF.search(src)
@@ -228,99 +202,67 @@ def decorate(func, caller, extras=()):
If the caller is a generator function, the resulting function
will be a generator function.
"""
- evaldict = dict(_call_=caller, _func_=func)
- es = ''
- for i, extra in enumerate(extras):
- ex = '_e%d_' % i
- evaldict[ex] = extra
- es += ex + ', '
-
- if '3.5' <= sys.version < '3.6':
- # with Python 3.5 isgeneratorfunction returns True for all coroutines
- # however we know that it is NOT possible to have a generator
- # coroutine in python 3.5: PEP525 was not there yet
- generatorcaller = isgeneratorfunction(
- caller) and not iscoroutinefunction(caller)
+ if iscoroutinefunction(caller):
+ async def fun(*args, **kw):
+ return await caller(func, *(extras + args), **kw)
+ elif isgeneratorfunction(caller):
+ def fun(*args, **kw):
+ for res in caller(func, *(extras + args), **kw):
+ yield res
else:
- generatorcaller = isgeneratorfunction(caller)
- if generatorcaller:
- fun = FunctionMaker.create(
- func, "for res in _call_(_func_, %s%%(shortsignature)s):\n"
- " yield res" % es, evaldict, __wrapped__=func)
- else:
- fun = FunctionMaker.create(
- func, "return _call_(_func_, %s%%(shortsignature)s)" % es,
- evaldict, __wrapped__=func)
- if hasattr(func, '__qualname__'):
- fun.__qualname__ = func.__qualname__
+ def fun(*args, **kw):
+ return caller(func, *(extras + args), **kw)
+ fun.__name__ = func.__name__
+ fun.__signature__ = inspect.signature(func)
+ fun.__wrapped__ = func
+ fun.__qualname__ = func.__qualname__
+ fun.__annotations__ = func.__annotations__
+ fun.__kwdefaults__ = func.__kwdefaults__
+ fun.__dict__.update(func.__dict__)
return fun
def decorator(caller, _func=None):
- """decorator(caller) converts a caller function into a decorator"""
+ "decorator(caller) converts a caller function into a decorator"
if _func is not None: # return a decorated function
# this is obsolete behavior; you should use decorate instead
return decorate(_func, caller)
# else return a decorator function
- defaultargs, defaults = '', ()
- if inspect.isclass(caller):
- name = caller.__name__.lower()
- doc = 'decorator(%s) converts functions/generators into ' \
- 'factories of %s objects' % (caller.__name__, caller.__name__)
- elif inspect.isfunction(caller):
- if caller.__name__ == '<lambda>':
- name = '_lambda_'
+ sig = inspect.signature(caller)
+ dec_params = [p for p in sig.parameters.values() if p.kind is POS]
+
+ def dec(func=None, *args, **kw):
+ na = len(args) + 1
+ extras = args + tuple(kw.get(p.name, p.default)
+ for p in dec_params[na:]
+ if p.default is not EMPTY)
+ if func is None:
+ return lambda func: decorate(func, caller, extras)
else:
- name = caller.__name__
- doc = caller.__doc__
- nargs = caller.__code__.co_argcount
- ndefs = len(caller.__defaults__ or ())
- defaultargs = ', '.join(caller.__code__.co_varnames[nargs-ndefs:nargs])
- if defaultargs:
- defaultargs += ','
- defaults = caller.__defaults__
- else: # assume caller is an object with a __call__ method
- name = caller.__class__.__name__.lower()
- doc = caller.__call__.__doc__
- evaldict = dict(_call=caller, _decorate_=decorate)
- dec = FunctionMaker.create(
- '%s(func, %s)' % (name, defaultargs),
- 'if func is None: return lambda func: _decorate_(func, _call, (%s))\n'
- 'return _decorate_(func, _call, (%s))' % (defaultargs, defaultargs),
- evaldict, doc=doc, module=caller.__module__, __wrapped__=caller)
- if defaults:
- dec.__defaults__ = (None,) + defaults
+ return decorate(func, caller, extras)
+ dec.__signature__ = sig.replace(parameters=dec_params)
+ dec.__name__ = caller.__name__
+ dec.__doc__ = caller.__doc__
+ dec.__wrapped__ = caller
+ dec.__qualname__ = caller.__qualname__
+ dec.__kwdefaults__ = getattr(caller, '__kwdefaults__', None)
+ dec.__dict__.update(caller.__dict__)
return dec
# ####################### contextmanager ####################### #
-try: # Python >= 3.2
- from contextlib import _GeneratorContextManager
-except ImportError: # Python >= 2.5
- from contextlib import GeneratorContextManager as _GeneratorContextManager
-
class ContextManager(_GeneratorContextManager):
- def __call__(self, func):
- """Context manager decorator"""
- return FunctionMaker.create(
- func, "with _self_: return _func_(%(shortsignature)s)",
- dict(_self_=self, _func_=func), __wrapped__=func)
-
-
-init = getfullargspec(_GeneratorContextManager.__init__)
-n_args = len(init.args)
-if n_args == 2 and not init.varargs: # (self, genobj) Python 2.7
- def __init__(self, g, *a, **k):
- return _GeneratorContextManager.__init__(self, g(*a, **k))
- ContextManager.__init__ = __init__
-elif n_args == 2 and init.varargs: # (self, gen, *a, **k) Python 3.4
- pass
-elif n_args == 4: # (self, gen, args, kwds) Python 3.5
def __init__(self, g, *a, **k):
return _GeneratorContextManager.__init__(self, g, a, k)
- ContextManager.__init__ = __init__
+
+ def __call__(self, func):
+ def caller(f, *a, **k):
+ with self:
+ return f(*a, **k)
+ return decorate(func, caller)
+
_contextmanager = decorator(ContextManager)
diff --git a/src/tests/documentation.py b/src/tests/documentation.py
index 29fe75c..ec04c6d 100644
--- a/src/tests/documentation.py
+++ b/src/tests/documentation.py
@@ -6,11 +6,7 @@ import time
import functools
import itertools
import collections
-try:
- import collections.abc as c
-except ImportError:
- c = collections
- collections.abc = collections
+import collections.abc as c
from decorator import (decorator, decorate, FunctionMaker, contextmanager,
dispatch_on, __version__)
@@ -21,7 +17,7 @@ doc = r"""Decorators for Humans
|---|---|
|E-mail | michele.simionato@gmail.com|
|Version| $VERSION ($DATE)|
-|Supports| Python 2.6, 2.7, 3.0, 3.1, 3.2, 3.3, 3.4, 3.5, 3.6, 3.7, 3.8|
+|Supports| Python 3.5, 3.6, 3.7, 3.8, 3.9|
|Download page| http://pypi.python.org/pypi/decorator/$VERSION|
|Installation| ``pip install decorator``|
|License | BSD license|
@@ -31,22 +27,12 @@ Introduction
The ``decorator`` module is over ten years old, but still alive and
kicking. It is used by several frameworks (IPython, scipy, authkit,
-pylons, pycuda, sugar, ...) and has been stable for a *long*
-time. It is your best option if you want to preserve the signature of
-decorated functions in a consistent way across Python
-releases. Version 4 is fully compatible with the past, except for
-one thing: support for Python 2.4 and 2.5 has been dropped. That
-decision made it possible to use a single code base both for Python
-2.X and Python 3.X. This is a *huge* bonus, since I could remove over
-2,000 lines of duplicated documentation/doctests. Having to maintain
-separate docs for Python 2 and Python 3 effectively stopped any
-development on the module for several years. Moreover, it is now
-trivial to distribute the module as an universal
- [wheel](http://pythonwheels.com) since 2to3 is no more
-required. Since Python 2.5 has been released ages ago (in 2006), I felt that
-it was reasonable to drop the support for it. If you need to support
-ancient versions of Python, stick with the decorator module version
-3.4.2. The current version supports all Python releases from 2.6 up.
+pylons, pycuda, sugar, ...) and has been stable for a *long* time. It
+is your best option if you want to preserve the signature of decorated
+functions in a consistent way across Python releases. Version 5.X
+requires Python versions greater than 3.4, but you can support back to
+Python 2.6 by using version 4.X and version 3.X is able to support
+even Python 2.4 and 2.5.
What's New in version 4
-----------------------
@@ -316,16 +302,17 @@ The decorator works with functions of any signature:
```python
>>> @trace
-... def f(x, y=1, z=2, *args, **kw):
+... def f(x, y=1, *args, **kw):
... pass
>>> f(0, 3)
-calling f with args (0, 3, 2), {}
+calling f with args (0, 3), {}
>>> print(getfullargspec(f))
-FullArgSpec(args=['x', 'y', 'z'], varargs='args', varkw='kw', defaults=(1, 2), kwonlyargs=[], kwonlydefaults=None, annotations={})
+FullArgSpec(args=['x', 'y'], varargs='args', varkw='kw', defaults=(1,), kwonlyargs=[], kwonlydefaults=None, annotations={})
```
+
$FUNCTION_ANNOTATIONS
``decorator.decorator``
@@ -1295,21 +1282,6 @@ notice that lately I have come to believe that decorating functions with
keyword arguments is not such a good idea, and you may want not to do
that.
-On a similar note, there is a restriction on argument names. For instance,
-if you name an argument ``_call_`` or ``_func_``, you will get a ``NameError``:
-
-```python
->>> @trace
-... def f(_func_): print(f)
-...
-Traceback (most recent call last):
- ...
-NameError: _func_ is overridden in
-def f(_func_):
- return _call_(_func_, _func_)
-
-```
-
Finally, the implementation is such that the decorated function makes
a (shallow) copy of the original function dictionary:
@@ -1651,46 +1623,47 @@ def a_test_for_pylons():
"""
-if sys.version_info >= (3,): # tests for signatures specific to Python 3
+def test_kwonlydefaults():
+ """
+ >>> @trace
+ ... def f(arg, defarg=1, *args, kwonly=2): pass
+ ...
+ >>> f.__kwdefaults__
+ {'kwonly': 2}
+ """
- def test_kwonlydefaults():
- """
- >>> @trace
- ... def f(arg, defarg=1, *args, kwonly=2): pass
- ...
- >>> f.__kwdefaults__
- {'kwonly': 2}
- """
- def test_kwonlyargs():
- """
- >>> @trace
- ... def func(a, b, *args, y=2, z=3, **kwargs):
- ... return y, z
- ...
- >>> func('a', 'b', 'c', 'd', 'e', y='y', z='z', cat='dog')
- calling func with args ('a', 'b', 'c', 'd', 'e'), {'cat': 'dog', 'y': 'y', 'z': 'z'}
- ('y', 'z')
- """
+def test_kwonlyargs():
+ """
+ >>> @trace
+ ... def func(a, b, *args, y=2, z=3, **kwargs):
+ ... return y, z
+ ...
+ >>> func('a', 'b', 'c', 'd', 'e', y='y', z='z', cat='dog')
+ calling func with args ('a', 'b', 'c', 'd', 'e'), {'cat': 'dog', 'y': 'y', 'z': 'z'}
+ ('y', 'z')
+ """
- def test_kwonly_no_args():
- """# this was broken with decorator 3.3.3
- >>> @trace
- ... def f(**kw): pass
- ...
- >>> f()
- calling f with args (), {}
- """
- def test_kwonly_star_notation():
- """
- >>> @trace
- ... def f(*, a=1, **kw): pass
- ...
- >>> import inspect
- >>> inspect.getfullargspec(f)
- FullArgSpec(args=[], varargs=None, varkw='kw', defaults=None, kwonlyargs=['a'], kwonlydefaults={'a': 1}, annotations={})
- """
+def test_kwonly_no_args():
+ """# this was broken with decorator 3.3.3
+ >>> @trace
+ ... def f(**kw): pass
+ ...
+ >>> f()
+ calling f with args (), {}
+ """
+
+
+def test_kwonly_star_notation():
+ """
+ >>> @trace
+ ... def f(*, a=1, **kw): pass
+ ...
+ >>> import inspect
+ >>> inspect.getfullargspec(f)
+ FullArgSpec(args=[], varargs=None, varkw='kw', defaults=None, kwonlyargs=['a'], kwonlydefaults={'a': 1}, annotations={})
+ """
@contextmanager
diff --git a/src/tests/test.py b/src/tests/test.py
index 7ddfaf4..83c5649 100644
--- a/src/tests/test.py
+++ b/src/tests/test.py
@@ -1,21 +1,13 @@
-from __future__ import absolute_import
import sys
import doctest
import unittest
import decimal
import inspect
import functools
-import collections
-from collections import defaultdict
-try:
- c = collections.abc
-except AttributeError:
- c = collections
+from asyncio import get_event_loop
+from collections import defaultdict, abc as c
from decorator import dispatch_on, contextmanager, decorator
-try:
- from . import documentation as doc
-except (ImportError, ValueError, SystemError): # depending on the py-version
- import documentation as doc
+import documentation as doc
@contextmanager
@@ -29,22 +21,21 @@ def assertRaises(etype):
raise Exception('Expected %s' % etype.__name__)
-if sys.version_info >= (3, 5):
- exec('''from asyncio import get_event_loop
-
@decorator
async def before_after(coro, *args, **kwargs):
return "<before>" + (await coro(*args, **kwargs)) + "<after>"
+
@decorator
def coro_to_func(coro, *args, **kw):
return get_event_loop().run_until_complete(coro(*args, **kw))
+
class CoroutineTestCase(unittest.TestCase):
def test_before_after(self):
@before_after
async def coro(x):
- return x
+ return x
self.assertTrue(inspect.iscoroutinefunction(coro))
out = get_event_loop().run_until_complete(coro('x'))
self.assertEqual(out, '<before>x<after>')
@@ -55,7 +46,6 @@ class CoroutineTestCase(unittest.TestCase):
return x
self.assertFalse(inspect.iscoroutinefunction(coro))
self.assertEqual(coro('x'), 'x')
-''')
def gen123():
@@ -125,10 +115,13 @@ class ExtraTestCase(unittest.TestCase):
@d1
def f1(x, y, z):
pass
- 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.assertEqual(d1.__code__.co_filename,
+ d2.__code__.co_filename)
+ self.assertEqual(f1.__code__.co_filename,
+ f2.__code__.co_filename)
+ self.assertEqual(f1_orig.__code__.co_filename,
+ f1.__code__.co_filename)
def test_no_first_arg(self):
@decorator
@@ -147,7 +140,7 @@ class ExtraTestCase(unittest.TestCase):
@decorator
def catch_config_error(method, app, *args, **kwargs):
return method(app)
- catch_config_error(lambda app: None)
+ catch_config_error(lambda app, **kw: None)(1)
def test_add1(self):
# similar to what IPython is doing in traitlets.config.application