diff options
| author | robot-piglet <[email protected]> | 2026-06-03 20:26:47 +0300 |
|---|---|---|
| committer | robot-piglet <[email protected]> | 2026-06-03 21:04:31 +0300 |
| commit | c77baca42e90aa0577b707af41e220697d9f4a57 (patch) | |
| tree | fbb444db47b4d32650edf7f751e260da9e86e170 /contrib/python | |
| parent | 6d593280f38d4e75fe4a8f02dc27674fa5bf684c (diff) | |
Intermediate changes
commit_hash:c6bfd78f6fd8d5473bd2c5e716e68726ebb4841c
Diffstat (limited to 'contrib/python')
| -rw-r--r-- | contrib/python/decorator/py3/.dist-info/METADATA | 14 | ||||
| -rw-r--r-- | contrib/python/decorator/py3/decorator.py | 237 | ||||
| -rw-r--r-- | contrib/python/decorator/py3/tests/documentation.py | 753 | ||||
| -rw-r--r-- | contrib/python/decorator/py3/tests/test.py | 117 | ||||
| -rw-r--r-- | contrib/python/decorator/py3/ya.make | 2 |
5 files changed, 571 insertions, 552 deletions
diff --git a/contrib/python/decorator/py3/.dist-info/METADATA b/contrib/python/decorator/py3/.dist-info/METADATA index fd12277a011..df407f8057f 100644 --- a/contrib/python/decorator/py3/.dist-info/METADATA +++ b/contrib/python/decorator/py3/.dist-info/METADATA @@ -1,6 +1,6 @@ Metadata-Version: 2.1 Name: decorator -Version: 4.4.2 +Version: 5.1.1 Summary: Decorators for Humans Home-page: https://github.com/micheles/decorator Author: Michele Simionato @@ -14,20 +14,16 @@ Classifier: License :: OSI Approved :: BSD License Classifier: Natural Language :: English Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python -Classifier: Programming Language :: Python :: 2 -Classifier: Programming Language :: Python :: 2.6 -Classifier: Programming Language :: Python :: 2.7 -Classifier: Programming Language :: Python :: 3 -Classifier: Programming Language :: Python :: 3.2 -Classifier: Programming Language :: Python :: 3.3 -Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 +Classifier: Programming Language :: Python :: 3.8 +Classifier: Programming Language :: Python :: 3.9 +Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Topic :: Software Development :: Libraries Classifier: Topic :: Utilities -Requires-Python: >=2.6, !=3.0.*, !=3.1.* +Requires-Python: >=3.5 Decorators for Humans ===================== diff --git a/contrib/python/decorator/py3/decorator.py b/contrib/python/decorator/py3/decorator.py index b1f8b567e95..2479b6f7ba7 100644 --- a/contrib/python/decorator/py3/decorator.py +++ b/contrib/python/decorator/py3/decorator.py @@ -1,6 +1,6 @@ # ######################### LICENSE ############################ # -# Copyright (c) 2005-2018, Michele Simionato +# Copyright (c) 2005-2021, Michele Simionato # All rights reserved. # Redistribution and use in source and binary forms, with or without @@ -28,55 +28,26 @@ # DAMAGE. """ -Decorator module, see http://pypi.python.org/pypi/decorator +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__ = '5.1.1' 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. @@ -100,7 +71,7 @@ class FunctionMaker(object): self.name = '_lambda_' self.doc = func.__doc__ self.module = func.__module__ - if inspect.isfunction(func): + if inspect.isroutine(func): argspec = getfullargspec(func) self.annotations = getattr(func, '__annotations__', {}) for a in ('args', 'varargs', 'varkw', 'defaults', 'kwonlyargs', @@ -143,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', {}) @@ -160,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) @@ -221,105 +196,127 @@ 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 """ - evaldict = dict(_call_=caller, _func_=func) - es = '' - for i, extra in enumerate(extras): - ex = '_e%d_' % i - evaldict[ex] = extra - es += ex + ', ' + ba = sig.bind(*args, **kwargs) + ba.apply_defaults() # needed for test_dan_schult + return ba.args, ba.kwargs - 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) - 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) + +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: - 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): + if not kwsyntax: + args, kw = fix(args, kw, sig) + return caller(func, *(extras + args), **kw) + fun.__name__ = func.__name__ + fun.__doc__ = func.__doc__ + fun.__wrapped__ = func + fun.__signature__ = sig + fun.__qualname__ = func.__qualname__ + # builtin functions like defaultdict.__setitem__ lack many attributes + try: + fun.__defaults__ = func.__defaults__ + except AttributeError: + pass + try: + fun.__kwdefaults__ = func.__kwdefaults__ + except AttributeError: + pass + try: + fun.__annotations__ = func.__annotations__ + except AttributeError: + pass + try: + fun.__module__ = func.__module__ + except AttributeError: + pass + try: + fun.__dict__.update(func.__dict__) + except AttributeError: + pass return fun -def decorator(caller, _func=None): - """decorator(caller) converts a caller function into a decorator""" +def decoratorx(caller): + """ + A version of "decorator" implemented via "exec" and not via the + Signature object. Use this if you are want to preserve the `.__code__` + object properties (https://github.com/micheles/decorator/issues/129). + """ + def dec(func): + return FunctionMaker.create( + func, + "return _call_(_func_, %(shortsignature)s)", + dict(_call_=caller, _func_=func), + __wrapped__=func, __qualname__=func.__qualname__) + return dec + + +def decorator(caller, _func=None, kwsyntax=False): + """ + 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) + return decorate(_func, caller, (), kwsyntax) # 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, kwsyntax) 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, kwsyntax) + 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) + def __init__(self, g, *a, **k): + _GeneratorContextManager.__init__(self, g, a, k) + def __call__(self, func): + def caller(f, *a, **k): + with self.__class__(self.func, *self.args, **self.kwds): + return f(*a, **k) + return decorate(func, caller) -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__ _contextmanager = decorator(ContextManager) diff --git a/contrib/python/decorator/py3/tests/documentation.py b/contrib/python/decorator/py3/tests/documentation.py index 46a932aa77c..bf3a0b08325 100644 --- a/contrib/python/decorator/py3/tests/documentation.py +++ b/contrib/python/decorator/py3/tests/documentation.py @@ -1,58 +1,52 @@ -from __future__ import print_function -import sys +import inspect import threading import time import functools import itertools import collections -try: - import collections.abc as c -except ImportError: - c = collections - collections.abc = collections -from decorator import (decorator, decorate, FunctionMaker, contextmanager, +import collections.abc as c +from decorator import (decorator, decorate, FunctionMaker, dispatch_on, __version__) -doc = r"""Decorators for Humans ----------------------------------- +doc = r"""# Decorators for Humans |Author | Michele Simionato| |---|---| |E-mail | [email protected]| |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, 3.10| |Download page| http://pypi.python.org/pypi/decorator/$VERSION| |Installation| ``pip install decorator``| |License | BSD license| -Introduction ------------------------------------------ +## 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. Versions 5.X +supports Python versions greater than 3.4, versions 4.X supports Python +versions back to 2.6; versions 3.X are able to support even Python 2.5 and +2.4. -What's New in version 4 ------------------------ +## What's New in version 5 + +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 - **New documentation** There is now a single manual for all Python versions, so I took the - opportunity to overhaul the documentation and to move it to readthedocs.org. + opportunity to overhaul the documentation. Even if you are a long-time user, you may want to revisit the docs, since several examples have been improved. @@ -87,15 +81,14 @@ What's New in version 4 From version 4.2 there is facility to define factories of decorators in a simple way, a feature requested by the users since a long time. -Usefulness of decorators ------------------------------------------------- +## Usefulness of decorators Python decorators are an interesting example of why syntactic sugar matters. In principle, their introduction in Python 2.4 changed nothing, since they did not provide any new functionality which was not already present in the language. In practice, their introduction has -significantly changed the way we structure our programs in Python. I -believe the change is for the best, and that decorators are a great +significantly changed the way we structure our programs. +I believe the change is for the best, and that decorators are a great idea since: * decorators help reducing boilerplate code; @@ -118,8 +111,7 @@ You may find the source code for all the examples discussed here in the ``documentation.py`` file, which contains the documentation you are reading in the form of doctests. -Definitions ------------------------------------- +## Definitions Technically speaking, any Python object which can be called with one argument can be used as a decorator. However, this definition is somewhat too large @@ -148,8 +140,7 @@ obvious, especially if one wants to define proper decorators that can accept functions with any signature. A simple example will clarify the issue. -Statement of the problem ------------------------------- +## Statement of the problem A very common use case for decorators is the memoization of functions. A ``memoize`` decorator works by caching @@ -176,10 +167,6 @@ in Python 2.5 to simplify the writing of decorators. ``__name__``, ``__doc__``, ``__module__``, and ``__dict__`` to the decorated function by hand). -Here is an example of usage: - -$$f1 - This works insofar as the decorator accepts functions with generic signatures. Unfortunately, it is *not* a signature-preserving decorator, since ``memoize_uw`` generally returns a function with a *different signature* @@ -194,17 +181,17 @@ but the decorated function takes any number of arguments and keyword arguments: ```python ->>> from decorator import getfullargspec +>>> from inspect import getfullargspec >>> print(getfullargspec(f1)) FullArgSpec(args=[], varargs='args', varkw='kw', defaults=None, kwonlyargs=[], kwonlydefaults=None, annotations={}) ``` -This means that introspection tools (like ``pydoc``) will give false -information about the signature of ``f1`` -- unless you are using -Python 3.5. This is pretty bad: ``pydoc`` will tell you that the -function accepts the generic signature ``*args, **kw``, but -calling the function with more than one argument raises an error: +This means that introspection tools like ``getfullargspec`` will give +you false information about the signature of ``f1`` This is pretty bad: +``getfullargspec`` says that the function accepts the generic +signature ``*args, **kw``, but calling the function with more than one +argument raises an error: ```python >>> f1(0, 1) # doctest: +IGNORE_EXCEPTION_DETAIL @@ -214,12 +201,10 @@ TypeError: f1() takes exactly 1 positional argument (2 given) ``` -Notice that ``inspect.getfullargspec`` -will give the wrong signature, even in the latest Python, i.e. version 3.6 -at the time of writing. +Notice that ``pydoc`` will give the right signature, but only in Python +versions greater than 3.5. -The solution ------------------------------------------ +## The solution The solution is to provide a generic factory of generators, which hides the complexity of making signature-preserving decorators @@ -276,8 +261,7 @@ FullArgSpec(args=[], varargs=None, varkw=None, defaults=None, kwonlyargs=[], kwo ``` -A ``trace`` decorator ------------------------------------------------------- +## A ``trace`` decorator Here is an example of how to define a simple ``trace`` decorator, which prints a message whenever the traced function is called: @@ -315,20 +299,72 @@ 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 + +Python 3 introduced the concept of [function annotations]( +http://www.python.org/dev/peps/pep-3107/): the ability +to annotate the signature of a function with additional information, +stored in a dictionary named ``__annotations__``. The ``decorator`` module +(starting from release 3.3) will understand and preserve these annotations. + +Here is an example: + +```python +>>> @trace +... def f(x: 'the first argument', y: 'default argument'=1, z=2, +... *args: 'varargs', **kw: 'kwargs'): +... pass ``` -$FUNCTION_ANNOTATIONS -``decorator.decorator`` ---------------------------------------------- +In order to introspect functions with annotations, one needs +``inspect.getfullargspec`` (introduced in Python 3, then +deprecated in Python 3.5, then undeprecated in Python 3.6): + +```python +>>> from inspect import getfullargspec +>>> argspec = getfullargspec(f) +>>> argspec.args +['x', 'y', 'z'] +>>> argspec.varargs +'args' +>>> argspec.varkw +'kw' +>>> argspec.defaults +(1, 2) +>>> argspec.kwonlyargs +[] +>>> argspec.kwonlydefaults + +``` + +You can check that the ``__annotations__`` dictionary is preserved: + +```python +>>> f.__annotations__ is f.__wrapped__.__annotations__ +True + +``` + +Here ``f.__wrapped__`` is the original undecorated function. +This attribute exists for consistency with the behavior of +``functools.update_wrapper``. + +Another attribute copied from the original function is ``__qualname__``, +the qualified name. This attribute was introduced in Python 3.3. + +## ``decorator.decorator`` It can become tedious to write a caller function (like the above ``_trace`` example) and then a trivial wrapper @@ -340,8 +376,6 @@ It is the ``decorator`` function: ```python >>> from decorator import decorator ->>> print(decorator.__doc__) -decorator(caller) converts a caller function into a decorator ``` The ``decorator`` function can be used as a signature-changing @@ -380,6 +414,67 @@ 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) +() [('x', 1), ('y', 2)] +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, i.e. they belongs to the ``args`` tuple and not to ``kwargs``: + +```python +>>> printsum2(1, 2) +(1, 2) [] +3 + +``` + +## Decorator factories + The `decorator` function can also be used to define factories of decorators, i.e. functions returning decorators. In general you can just write something like this: @@ -392,9 +487,8 @@ def decfactory(param1, param2, ...): ``` This is fully general but requires an additional level of nesting. For this -reason since version 4.2 there is a facility to build -decorator factories by using a single caller with default arguments i.e. -writing something like this: +reason since version 4.2 there is a facility to build decorator factories by +using a single caller with default arguments: ```python def caller(f, param1=default1, param2=default2, ..., *args, **kw): @@ -408,22 +502,17 @@ restriction, there exists an unique default decorator, i.e. the member of the family which uses the default values for all parameters. Such decorator can be written as ``decfactory()`` with no parameters specified; moreover, as a shortcut, it is also possible to elide the parenthesis, -a feature much requested by the users. For years I have been opposite -to this feature request, since having explicit parenthesis to me is more clear +a feature much requested by the users. For years I have been opposing +the request, since having explicit parenthesis to me is more clear and less magic; however once this feature entered in decorators of the Python standard library (I am referring to the [dataclass decorator]( https://www.python.org/dev/peps/pep-0557/)) I finally gave up. -The example below will show how it works in practice. - -Decorator factories -------------------------------------------- - -Sometimes one has to deal with blocking resources, such as ``stdin``. -Sometimes it is better to receive a "busy" message than just blocking -everything. -This can be accomplished with a suitable family of decorators (decorator -factory), parameterize by a string, the busy message: +The example below shows how it works in practice. The goal is to +convert a function relying on a blocking resource into a function +returning a "busy" message if the resource is not available. +This can be accomplished with a suitable family of decorators +parameterize by a string, the busy message: $$blocking @@ -474,8 +563,7 @@ TypeError: You are decorating a non function: <class '__main__.User'> Be careful! -``decorator(cls)`` --------------------------------------------- +## ``decorator(cls)`` The ``decorator`` facility can also produce a decorator starting from a class with the signature of a caller. In such a case the @@ -511,8 +599,7 @@ Here is the minimalistic usage: ``` -contextmanager -------------------------------------- +## contextmanager Python's standard library has the ``contextmanager`` decorator, which converts a generator function into a ``GeneratorContextManager`` @@ -529,8 +616,7 @@ factory. For instance, if you write this... ``` ...then ``before_after`` is a factory function that returns -``GeneratorContextManager`` objects, which provide the -use of the ``with`` statement: +``GeneratorContextManager`` objects, usable with the ``with`` statement: ```python >>> with before_after('BEFORE', 'AFTER'): @@ -550,11 +636,11 @@ a ``__call__`` method, so that they can be used as decorators, like so: ```python >>> ba = before_after('BEFORE', 'AFTER') >>> ->>> @ba # doctest: +SKIP +>>> @ba ... def hello(): ... print('hello') ... ->>> hello() # doctest: +SKIP +>>> hello() BEFORE hello AFTER @@ -564,17 +650,10 @@ AFTER The ``ba`` decorator basically inserts a ``with ba:`` block inside the function. -However, there are two issues: - -1. ``GeneratorContextManager`` objects are only callable in Python 3.2, - so the previous example breaks in older versions of Python. - (You can solve this by installing ``contextlib2``, which backports - the Python 3 functionality to Python 2.) - -2. ``GeneratorContextManager`` objects do not preserve the signature of - the decorated functions. The decorated ``hello`` function above will - have the generic signature ``hello(*args, **kwargs)``, but fails if - called with more than zero arguments. +However ``GeneratorContextManager`` objects do not preserve the signature of +the decorated functions. The decorated ``hello`` function above will +have the generic signature ``hello(*args, **kwargs)``, but fails if +called with more than zero arguments. For these reasons, the `decorator` module, starting from release 3.4, offers a ``decorator.contextmanager`` decorator that solves both problems, @@ -585,17 +664,15 @@ instances of ``ContextManager``, a subclass of the standard library's an improved ``__call__`` method, which acts as a signature-preserving decorator. -The ``FunctionMaker`` class ---------------------------------------------------------------- +## The ``FunctionMaker`` class -You may wonder how the functionality of the ``decorator`` module -is implemented. The basic building block is -a ``FunctionMaker`` class. It generates on-the-fly functions +The ``decorator`` module also provides a ``FunctionMaker`` class, which +is able to generate on-the-fly functions with a given name and signature from a function template passed as a string. If you're just writing ordinary decorators, then you probably won't -need to use ``FunctionMaker`` directly. But in some circumstances, it +need to use ``FunctionMaker``. But in some circumstances, it can be handy. You will see an example shortly--in the implementation of a cool decorator utility (``decorator_apply``). @@ -655,9 +732,9 @@ Here is what happens: - If first argument of ``FunctionMaker.create`` is a function, an instance of ``FunctionMaker`` is created with the attributes - ``args``, ``varargs``, ``keywords``, and ``defaults``. - (These mirror the return values of the standard library's - ``inspect.getfullargspec``.) + ``args``, ``varargs``, ``keywords``, and ``defaults`` + (these mirror the return values of the standard library's + ``inspect.getfullargspec``). - For each item in ``args`` (a list of strings of the names of all required arguments), an attribute ``arg0``, ``arg1``, ..., ``argN`` is also generated. @@ -677,36 +754,13 @@ FullArgSpec(args=['a', 'b'], varargs=None, varkw=None, defaults=(None,), kwonlya ``` -Getting the source code ---------------------------------------------------- +## Getting the source code Internally, ``FunctionMaker.create`` uses ``exec`` to generate the decorated function. Therefore ``inspect.getsource`` will not work for decorated functions. In IPython, this means that the usual ``??`` trick will give you the (right on the spot) message ``Dynamically generated function. No source code available``. - -In the past, I considered this acceptable, since ``inspect.getsource`` -does not really work with "regular" decorators. In those cases, -``inspect.getsource`` gives you the wrapper source code, which is probably -not what you want: - -$$identity_dec -$$example - -```python ->>> import inspect ->>> print(inspect.getsource(example)) - def wrapper(*args, **kw): - return func(*args, **kw) -<BLANKLINE> - -``` - -(See bug report [1764286](http://bugs.python.org/issue1764286) -for an explanation of what is happening). -Unfortunately the bug still exists in all versions of Python < 3.5. - However, there is a workaround. The decorated function has the ``__wrapped__`` attribute, pointing to the original function. The simplest way to get the source code is to call ``inspect.getsource`` on the undecorated function: @@ -723,8 +777,7 @@ def factorial(n, acc=1): ``` -Dealing with third-party decorators ------------------------------------------------------------------ +## Dealing with third-party decorators Sometimes on the net you find some cool decorator that you would like to include in your code. However, more often than not, the cool @@ -788,14 +841,12 @@ following: - returns a value without making a recursive call; or, - returns directly the result of a recursive call. -Python 3.5 coroutines ------------------------ +## Python 3.5 coroutines -I am personally not using Python 3.5 coroutines yet, because at work we are -still maintaining compatibility with Python 2.7. However, some users requested -support for coroutines and since version 4.1 the decorator module has it. -You should consider the support experimental and kindly report issues if -you find any. +I am personally not using Python 3.5 coroutines yet. However, some +users requested support for coroutines and since version 4.1 the +decorator module has it. You should consider the support experimental +and kindly report issues if you find any. Here I will give a single example of usage. Suppose you want to log the moment a coroutine starts and the moment it stops for debugging purposes. You could @@ -807,7 +858,7 @@ import logging from asyncio import get_event_loop, sleep, wait from decorator import decorator - @decorator +@decorator async def log_start_stop(coro, *args, **kwargs): logging.info('Starting %s%s', coro.__name__, args) t0 = time.time() @@ -855,13 +906,12 @@ def coro_to_func(coro, *args, **kw): return get_event_loop().run_until_complete(coro(*args, **kw)) ``` -Notice the diffence: the caller in ``log_start_stop`` was a coroutine -function and the associate decorator was converting coroutines->coroutines; +Notice the difference: the caller in ``log_start_stop`` was a coroutine +function and the associate decorator was converting coroutines in coroutines; the caller in ``coro_to_func`` is a regular function and converts coroutines -> functions. -Multiple dispatch -------------------------------------------- +## Multiple dispatch There has been talk of implementing multiple dispatch functions (i.e. "generic functions") in Python for over ten years. Last year, @@ -982,7 +1032,7 @@ inherited from the parent: ``` -You can introspect the precedence used by the dispath algorithm by +You can introspect the precedence used by the dispatch algorithm by calling ``.dispatch_info(*types)``: ```python @@ -998,8 +1048,7 @@ product of the class precedence lists (or *Method Resolution Orders*, [MRO](http://www.python.org/2.3/mro.html) for short) of ``StrongRock`` and ``Scissors``, respectively. -Generic functions and virtual ancestors -------------------------------------------------- +## Generic functions and virtual ancestors In Python, generic functions are complicated by the existence of "virtual ancestors": superclasses which are not in the class hierarchy. @@ -1143,110 +1192,49 @@ call-next-method in Lisp, or to ``super`` in Python. Finally, let me notice that the decorator module implementation does not use any cache, whereas the ``singledispatch`` implementation does. -Caveats and limitations -------------------------------------------- +## Caveats and limitations -One thing you should be aware of, is the performance penalty of decorators. -The worse case is shown by the following example: - -```bash - $ cat performance.sh - python3 -m timeit -s " - from decorator import decorator - - @decorator - def do_nothing(func, *args, **kw): - return func(*args, **kw) - - @do_nothing - def f(): - pass - " "f()" - - python3 -m timeit -s " - def f(): - pass - " "f()" - -``` -On my laptop, using the ``do_nothing`` decorator instead of the -plain function is five times slower: - -```bash - $ bash performance.sh - 1000000 loops, best of 3: 1.39 usec per loop - 1000000 loops, best of 3: 0.278 usec per loop -``` -Of course, a real life function probably does something more useful -than the function ``f`` here, so the real life performance penalty -*could* be negligible. As always, the only way to know if there is a -penalty in your specific use case is to measure it. - -More importantly, you should be aware that decorators will make your -tracebacks longer and more difficult to understand. +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 +decorate things like classmethods/staticmethods and general callables - +which I will never support in the decorator module - I suggest you +to look at the [wrapt](https://wrapt.readthedocs.io/en/latest/) +project by Graeme Dumpleton. -Consider this example: +Since version 5 the ``decorator`` module uses the ``inspect.Signature`` +object in the standard library. Unfortunately, for legacy reasons, some +applications introspect decorated functions by using low-level entities like +the ``__code__`` object and not signature objects. An example will make +the issue clear: ```python ->>> @trace -... def f(): -... 1/0 +>>> def f(a, b): pass +>>> f_dec = decorator(_trace)(f) +>>> f_dec.__code__.co_argcount +0 +>>> f_dec.__code__.co_varnames +('args', 'kw') ``` - -Calling ``f()`` gives you a ``ZeroDivisionError``. -But since the function is decorated, the traceback is longer: +This is not what one would expect: the `argcount` should be 2 since +the original functions has two arguments and the `varnames` should be +`a` and `b`. The only way to fix the issue is to go back to an implementation +of the decorator using ``exec``, which is provided for convenience since +version 5.1: ```python ->>> f() # doctest: +ELLIPSIS -Traceback (most recent call last): - ... - File "<string>", line 2, in f - File "<doctest __main__[22]>", line 4, in trace - return f(*args, **kw) - File "<doctest __main__[51]>", line 3, in f - 1/0 -ZeroDivisionError: ... +>>> from decorator import decoratorx +>>> f_dec = decoratorx(_trace)(f) +>>> f_dec.__code__.co_argcount +2 +>>> f_dec.__code__.co_varnames +('a', 'b') ``` - -You see here the inner call to the decorator ``trace``, which calls -``f(*args, **kw)``, and a reference to ``File "<string>", line 2, in f``. - -This latter reference is due to the fact that, internally, the decorator -module uses ``exec`` to generate the decorated function. Notice that -``exec`` is *not* responsible for the performance penalty, since is the -called *only once* (at function decoration time); it is *not* called -each time the decorated function is called. - -Presently, there is no clean way to avoid ``exec``. A clean solution -would require changing the CPython implementation, by -adding a hook to functions (to allow changing their signature directly). - -Even in Python 3.5, it is impossible to change the -function signature directly. Thus, the ``decorator`` module is -still useful! As a matter of fact, this is the main reason why I still -maintain the module and release new versions. - -It should be noted that in Python 3.5, a *lot* of improvements have -been made: you can decorate a function with -``func_tools.update_wrapper``, and ``pydoc`` will see the correct -signature. Unfortunately, the function will still have an incorrect -signature internally, as you can see by using -``inspect.getfullargspec``; so, all documentation tools using -``inspect.getfullargspec`` - which has been rightly deprecated - -will see the wrong signature. - -In the present implementation, decorators generated by ``decorator`` -can only be used on user-defined Python functions or methods. -They cannot be used on generic callable objects or built-in functions, -due to limitations of the standard library's ``inspect`` module, especially -for Python 2. In Python 3.5, many such limitations have been removed, but -I still think that it is cleaner and safer to decorate only functions and -coroutines. If you want to decorate things like classmethods/staticmethods -and general callables - which I will never support in the decorator module - -I suggest you to look at the [wrapt](https://wrapt.readthedocs.io/en/latest/) -project by Graeme Dumpleton. +Rather than using `decoratorx`, you should fix your introspection +routines to use ``inspect.Signature`` without fiddling with the +``__code__`` object. There is a strange quirk when decorating functions with keyword arguments, if one of the arguments has the same name used in the @@ -1270,7 +1258,7 @@ 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. +keyword arguments. The solution is to change the name of the first argument in `_memoize`, or to change the implementation like so: @@ -1297,22 +1285,7 @@ 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 +The implementation is such that the decorated function makes a (shallow) copy of the original function dictionary: ```python @@ -1330,8 +1303,44 @@ a (shallow) copy of the original function dictionary: ``` -LICENSE (2-clause BSD) ---------------------------------------------- +Finally, you should be aware of the performance penalty of decorators. +The worse case is shown by the following example: + +```bash + $ cat performance.sh + python3 -m timeit -s " + from decorator import decorator + + @decorator + def do_nothing(func, *args, **kw): + return func(*args, **kw) + + @do_nothing + def f(): + pass + " "f()" + + python3 -m timeit -s " + def f(): + pass + " "f()" + +``` +On my laptop, using the ``do_nothing`` decorator instead of the +plain function is five times slower: + +```bash + $ bash performance.sh + 1000000 loops, best of 3: 1.39 usec per loop + 1000000 loops, best of 3: 0.278 usec per loop +``` + +Of course, a real life function probably does something more useful +than the function ``f`` here, so the real life performance penalty +*could* be negligible. As always, the only way to know if there is a +penalty in your specific use case is to measure it. + +## LICENSE (2-clause BSD) Copyright (c) 2005-2020, Michele Simionato All rights reserved. @@ -1365,69 +1374,9 @@ note, just to gratify my ego. On the other hand, if you use this software and you are unhappy with it, send me a patch! """ -function_annotations = """Function annotations ---------------------------------------------- - -Python 3 introduced the concept of [function annotations]( -http://www.python.org/dev/peps/pep-3107/): the ability -to annotate the signature of a function with additional information, -stored in a dictionary named ``__annotations__``. The ``decorator`` module -(starting from release 3.3) will understand and preserve these annotations. - -Here is an example: - -```python ->>> @trace -... def f(x: 'the first argument', y: 'default argument'=1, z=2, -... *args: 'varargs', **kw: 'kwargs'): -... pass - -``` - -In order to introspect functions with annotations, one needs the -utility ``inspect.getfullargspec`` (introduced in Python 3, then -deprecated in Python 3.5, then undeprecated in Python 3.6): - -```python ->>> from inspect import getfullargspec ->>> argspec = getfullargspec(f) ->>> argspec.args -['x', 'y', 'z'] ->>> argspec.varargs -'args' ->>> argspec.varkw -'kw' ->>> argspec.defaults -(1, 2) ->>> argspec.kwonlyargs -[] ->>> argspec.kwonlydefaults - -``` - -You can check that the ``__annotations__`` dictionary is preserved: - -```python ->>> f.__annotations__ is f.__wrapped__.__annotations__ -True - -``` - -Here ``f.__wrapped__`` is the original undecorated function. -This attribute exists for consistency with the behavior of -``functools.update_wrapper``. - -Another attribute copied from the original function is ``__qualname__``, -the qualified name. This attribute was introduced in Python 3.3. -""" - -if sys.version_info < (3,): - function_annotations = '' - today = time.strftime('%Y-%m-%d') -__doc__ = (doc.replace('$VERSION', __version__).replace('$DATE', today) - .replace('$FUNCTION_ANNOTATIONS', function_annotations)) +__doc__ = doc.replace('$VERSION', __version__).replace('$DATE', today) def decorator_apply(dec, func): @@ -1436,7 +1385,7 @@ def decorator_apply(dec, func): is not a signature-preserving decorator. """ return FunctionMaker.create( - func, 'return decfunc(%(signature)s)', + func, 'return decfunc(%(shortsignature)s)', dict(decfunc=dec(func)), __wrapped__=func) @@ -1654,70 +1603,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_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_kwonlydefaults(): + """ + >>> @trace + ... def f(arg, defarg=1, *args, kwonly=2): pass + ... + >>> f.__kwdefaults__ + {'kwonly': 2} + """ -@contextmanager -def before_after(before, after): - print(before) - yield - print(after) +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') + """ -ba = before_after('BEFORE', 'AFTER') # ContextManager instance +def test_kwonly_no_args(): + """# this was broken with decorator 3.3.3 + >>> @trace + ... def f(**kw): pass + ... + >>> f() + calling f with args (), {} + """ -@ba -def hello(user): +def test_kwonly_star_notation(): """ - >>> ba.__class__.__name__ - 'ContextManager' - >>> hello('michele') - BEFORE - hello michele - AFTER + >>> @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={}) """ - print('hello %s' % user) - # ####################### multiple dispatch ############################ # @@ -1892,6 +1818,73 @@ def operation2(): time.sleep(.1) +def chatty(func, *args, **kwargs): + print(args, sorted(kwargs.items())) + return func(*args, **kwargs) + + +@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): + print(args, kwargs) + return func(*args, **kwargs) + return functools.wraps(wrapper) + +# ####################### changing the signature ########################## # + + +# see https://github.com/micheles/decorator/pull/85 +def to_method(f): + """ + Takes a function with signature (..., context) and returns a new + function with signature (self, ...) to be used a a method in a + class with a .context attribute. + """ + sig = inspect.signature(f) + params = list(sig.parameters.values()) + assert params[-1].name == 'context' + self = inspect.Parameter('self', inspect.Parameter.POSITIONAL_OR_KEYWORD) + params.insert(0, self) # insert self + del params[-1] # remove context + newsig = '%s%s' % (f.__name__, sig.replace(parameters=params)) + return FunctionMaker.create( + newsig, 'context = self.context; return _func_%s' % sig, + dict(_func_=f)) + + +def foo(x, context=None): + return x + + +def bar(x, y, context): + return x + y + + +class Client: + def __init__(self, context): + self.context = context + + +def test_change_sig(): + """ + >>> Client.foo = to_method(foo) + >>> Client.bar = to_method(bar) + >>> c = Client(None) + >>> assert c.foo(1) == 1 + >>> assert c.bar(1, 2) == 3 + """ + + if __name__ == '__main__': import doctest doctest.testmod() diff --git a/contrib/python/decorator/py3/tests/test.py b/contrib/python/decorator/py3/tests/test.py index 7ddfaf45b9f..be9f8511e5b 100644 --- a/contrib/python/decorator/py3/tests/test.py +++ b/contrib/python/decorator/py3/tests/test.py @@ -1,21 +1,15 @@ -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, ChainMap, 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 + from . import documentation as doc # good with pytest +except ImportError: + import documentation as doc # good with `python src/tests/test.py` @contextmanager @@ -29,22 +23,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 +48,6 @@ class CoroutineTestCase(unittest.TestCase): return x self.assertFalse(inspect.iscoroutinefunction(coro)) self.assertEqual(coro('x'), 'x') -''') def gen123(): @@ -80,28 +72,42 @@ class DocumentationTestCase(unittest.TestCase): err = doctest.testmod(doc)[0] self.assertEqual(err, 0) + def test_copy_dunder_attrs(self): + traced = doc.trace(doc.foo) + self.assertIn('documentation', traced.__module__) + self.assertEqual(traced.__annotations__, {}) + self.assertEqual(traced.__defaults__, (None,)) + def test_singledispatch1(self): - if hasattr(functools, 'singledispatch'): - with assertRaises(RuntimeError): - doc.singledispatch_example1() + with assertRaises(RuntimeError): + doc.singledispatch_example1() def test_singledispatch2(self): - if hasattr(functools, 'singledispatch'): - doc.singledispatch_example2() + doc.singledispatch_example2() + + def test_context_manager(self): + + @contextmanager + def before_after(before, after): + print(before) + yield + print(after) + + @before_after('BEFORE', 'AFTER') + def hello_user(user): + print('hello %s' % user) + + argspec = inspect.getfullargspec(hello_user) + self.assertEqual(argspec.args, ['user']) class ExtraTestCase(unittest.TestCase): def test_qualname(self): - if sys.version_info >= (3, 3): - self.assertEqual(doc.hello.__qualname__, 'hello') - else: - with assertRaises(AttributeError): - doc.hello.__qualname__ + self.assertEqual(doc.operation1.__qualname__, 'operation1') def test_signature(self): - if hasattr(inspect, 'signature'): - sig = inspect.signature(doc.f1) - self.assertEqual(str(sig), '(x)') + sig = inspect.signature(doc.f1) + self.assertEqual(str(sig), '(x)') def test_unique_filenames(self): @decorator @@ -125,10 +131,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 @@ -137,17 +146,19 @@ class ExtraTestCase(unittest.TestCase): @example def func(**kw): + "Docstring" return kw # there is no confusion when passing args as a keyword argument self.assertEqual(func(args='a'), {'args': 'a'}) + self.assertEqual(func.__doc__, "Docstring") def test_decorator_factory(self): # similar to what IPython is doing in traitlets.config.application @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 @@ -159,6 +170,29 @@ class ExtraTestCase(unittest.TestCase): return x self.assertEqual(add(f, 2)(0), 2) + def test_dan_schult(self): + # see https://github.com/micheles/decorator/issues/120 + @decorator + def prnt(func, index=0, *args, **kw): + print(args[index]) + return func(*args, **kw) + + @prnt(index=2) # print the value of the third argument + def f(a, b, c=None): + return [a, b, c] + + self.assertEqual(f(0, 1), [0, 1, None]) + + def test_slow_wrapper(self): + # see https://github.com/micheles/decorator/issues/123 + dd = defaultdict(list) + doc.trace(defaultdict.__setitem__)(dd, 'x', [1]) + self.assertEqual(dd['x'], [1]) + doc.trace(defaultdict.__delitem__)(dd, 'x') + self.assertEqual(dd['x'], []) + # NB: defaultdict.__getitem__ has no signature and cannot be + # decorated in CPython, while it is regular in PyPy + # ################### test dispatch_on ############################# # # adapted from test_functools in Python 3.5 @@ -290,14 +324,13 @@ class TestSingleDispatch(unittest.TestCase): self.assertEqual(g(f), "sized") self.assertEqual(g(t), "sized") - if hasattr(c, 'ChainMap'): - g.register(c.ChainMap)(lambda obj: "chainmap") - # irrelevant ABCs registered - self.assertEqual(g(d), "mutablemapping") - self.assertEqual(g(l), "sized") - self.assertEqual(g(s), "sized") - self.assertEqual(g(f), "sized") - self.assertEqual(g(t), "sized") + g.register(ChainMap)(lambda obj: "chainmap") + # irrelevant ABCs registered + self.assertEqual(g(d), "mutablemapping") + self.assertEqual(g(l), "sized") + self.assertEqual(g(s), "sized") + self.assertEqual(g(f), "sized") + self.assertEqual(g(t), "sized") g.register(c.MutableSequence)(lambda obj: "mutablesequence") self.assertEqual(g(d), "mutablemapping") diff --git a/contrib/python/decorator/py3/ya.make b/contrib/python/decorator/py3/ya.make index e6b2b2bdeb8..57317c64f85 100644 --- a/contrib/python/decorator/py3/ya.make +++ b/contrib/python/decorator/py3/ya.make @@ -2,7 +2,7 @@ PY3_LIBRARY() -VERSION(4.4.2) +VERSION(5.1.1) LICENSE(BSD-3-Clause) |
