#!/usr/bin/env python
# -*- coding: utf-8 -*-
# ---------------------------------------------------------------------
# Copyright (c) Merchise Autrement [~º/~] and Contributors
# All rights reserved.
#
# This is free software; you can do what the LICENCE file allows you to.
#
"""Decorator-making facilities.
This module provides a signature-keeping version of the
`xotl.tools.decorators.decorator`:func:, which is now deprecated in favor of
this module's version.
We scinded the decorator-making facilities from decorators per se to allow the
module `xotl.tools.deprecation`:mod: to be used by decorators and at the same
time, implement the decorator `~xotl.tools.deprecation.deprecated`:func: more
easily.
This module is an adapted work from the decorator version 3.3.2 package and is
copyright of its owner as stated below. Adaptation work is done by Merchise.
Original copyright and license notices from decorator package:
Copyright (c) 2005-2011, Michele Simionato
All rights reserved.
Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer. Redistributions in
bytecode form must reproduce the above copyright notice, this list of
conditions and the following disclaimer in the documentation and/or other
materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.
"""
import sys
import re
import inspect
from functools import wraps, partial
from types import FunctionType as function
from inspect import getfullargspec as _getfullargspec
__all__ = ("FunctionMaker", "flat_decorator", "decorator")
DEF = re.compile(r"\s*def\s*([_\w][_\w\d]*)\s*\(")
# basic functionality
[docs]class FunctionMaker:
"""
An object with the ability to create functions with a given signature.
It has attributes name, doc, module, signature, defaults, dict and
methods update and make.
"""
def __init__(
self,
func=None,
name=None,
signature=None,
defaults=None,
doc=None,
module=None,
funcdict=None,
):
self.shortsignature = signature
if func:
# func can be a class or a callable, but not an instance method
self.name = func.__name__
if self.name == "<lambda>": # small hack for lambda functions
self.name = "_lambda_"
self.doc = func.__doc__
self.module = func.__module__
if inspect.isfunction(func):
argspec = _getfullargspec(func)
for a in (
"args",
"varargs",
"varkw",
"defaults",
"kwonlyargs",
"kwonlydefaults",
"annotations",
):
setattr(self, a, getattr(argspec, a, None))
for i, arg in enumerate(self.args):
setattr(self, "arg%d" % i, arg)
self.signature = inspect.formatargspec(
formatvalue=lambda val: "", *argspec
)[1:-1]
allargs = list(self.args)
if self.varargs:
allargs.append("*" + self.varargs)
if self.varkw:
allargs.append("**" + self.varkw)
try:
self.shortsignature = ", ".join(allargs)
except TypeError:
# exotic signature, valid only in Python 2.X
self.shortsignature = self.signature
self.dict = func.__dict__.copy()
# func=None happens when decorating a caller
if name:
self.name = name
if signature is not None:
self.signature = signature
if defaults:
self.defaults = defaults
if doc:
self.doc = doc
if module:
self.module = module
if funcdict:
self.dict = funcdict
# check existence required attributes
assert hasattr(self, "name")
if not hasattr(self, "signature"):
raise TypeError("You are decorating a non function: %s" % func)
[docs] def update(self, func, **kw):
"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", {})
func.func_defaults = getattr(self, "defaults", ())
func.__kwdefaults__ = getattr(self, "kwonlydefaults", None)
callermodule = sys._getframe(3).f_globals.get("__name__", "?")
func.__module__ = getattr(self, "module", callermodule)
func.__dict__.update(kw)
[docs] def make(self, src_templ, evaldict=None, addsource=False, **attrs):
"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.match(src)
if mo is None:
raise SyntaxError("not a valid function template\n%s" % src)
name = mo.group(1) # extract the function name
names = set(
[name] + [arg.strip(" *") for arg in self.shortsignature.split(",")]
)
for n in names:
if n in ("_func_", "_call_"):
raise NameError("%s is overridden in\n%s" % (n, src))
if not src.endswith("\n"): # add a newline just for safety
src += "\n" # this is needed in old versions of Python
try:
code = compile(src, "<string>", "single")
eval(code, evaldict, evaldict)
except Exception:
raise
func = evaldict[name]
if addsource:
attrs["__source__"] = src
self.update(func, **attrs)
return func
[docs] @classmethod
def create(
cls,
obj,
body,
evaldict,
defaults=None,
doc=None,
module=None,
addsource=True,
**attrs
):
"""
Create a function from the strings name, signature and body.
"evaldict" is the evaluation dictionary. If addsource is true an
attribute __source__ is added to the result. The attributes attrs are
added,
if any.
"""
if isinstance(obj, str): # "name(signature)"
obj = str(obj)
name, rest = obj.strip().split(str("("), 1)
signature = rest[:-1] # strip a right parens
func = None
else: # a function
name = None
signature = None
func = obj
self = cls(func, name, signature, defaults, doc, module)
ibody = "\n".join(" " + line for line in body.splitlines())
return self.make(
"def %(name)s(%(signature)s):\n" + ibody, evaldict, addsource, **attrs
)
[docs]def flat_decorator(caller, func=None):
"""Creates a signature keeping decorator.
``decorator(caller)`` converts a caller function into a decorator.
``decorator(caller, func)`` decorates a function using a caller.
.. deprecated:: 1.9.9 Use the `decorator
<https://pypi.org/project/decorator/>`__ package.
"""
if func is not None: # returns a decorated function
evaldict = func.__globals__.copy()
evaldict["_call_"] = caller
evaldict["_func_"] = func
return FunctionMaker.create(
func,
"return _call_(_func_, %(shortsignature)s)",
evaldict,
undecorated=func,
__wrapped__=func,
)
else: # returns a decorator
if isinstance(caller, partial):
return partial(decorator, caller)
# otherwise assume caller is a function
try:
first = inspect.getargspec(caller)[0][0] # first arg
deco_sign = "%s(%s)" % (caller.__name__, first)
deco_body = "return flat_decorator(_call_, %s)" % first
except IndexError:
deco_sign = "%s()" % caller.__name__
deco_body = "return _call_"
evaldict = caller.__globals__.copy()
evaldict["_call_"] = caller
evaldict["flat_decorator"] = evaldict["decorator"] = flat_decorator
return FunctionMaker.create(
deco_sign,
deco_body,
evaldict,
undecorated=caller,
__wrapped__=caller,
doc=caller.__doc__,
module=caller.__module__,
)
# -- End of decorators package
# FIX: This meta-decorator fails in some scenarios (old classes?)
[docs]def decorator(caller):
"""Eases the creation of decorators with arguments. Normally a decorator
with arguments needs three nested functions like this::
def decorator(*decorator_arguments):
def real_decorator(target):
def inner(*args, **kwargs):
return target(*args, **kwargs)
return inner
return real_decorator
This decorator reduces the need of the first level by comprising both into
a single function definition. However it does not removes the need for an
``inner`` function::
>>> @decorator
... def plus(target, value):
... from functools import wraps
... @wraps(target)
... def inner(*args):
... return target(*args) + value
... return inner
>>> @plus(10)
... def ident(val):
... return val
>>> ident(1)
11
A decorator with default values for all its arguments (except, of course,
the first one which is the decorated `target`) may be invoked
without parenthesis::
>>> @decorator
... def plus2(func, value=1, missing=2):
... from functools import wraps
... @wraps(func)
... def inner(*args):
... print(missing)
... return func(*args) + value
... return inner
>>> @plus2
... def ident2(val):
... return val
>>> ident2(10)
2
11
But (if you like) you may place the parenthesis::
>>> @plus2()
... def ident3(val):
... return val
>>> ident3(10)
2
11
However, this is not for free, you cannot pass a single positional argument
which type is a function::
>>> def p():
... print('This is p!!!')
>>> @plus2(p) # doctest: +ELLIPSIS
... def dummy():
... print('This is dummy')
Traceback (most recent call last):
...
TypeError: p() takes ...
The workaround for this case is to use a keyword argument.
"""
@wraps(caller)
def outer_decorator(*args, **kwargs):
try:
from zope.interface import Interface
except ImportError:
Interface = None
# from xotl.tools.symbols import Unset as Interface
if (
len(args) == 1
and not kwargs
and (
isinstance(args[0], (function, type))
or issubclass(type(args[0]), type(Interface))
)
):
# This tries to solve the case of missing () on the decorator::
#
# @decorator
# def somedec(func, *args, **kwargs)
# ...
#
# @somedec
# def decorated(*args, **kwargs):
# pass
#
# Notice, however, that this is not general enough, since we try
# to avoid inspecting the calling frame to see if the () are in
# place.
func = args[0]
return caller(func)
elif len(args) > 0 or len(kwargs) > 0:
def _decorator(func):
return partial(caller, **kwargs)(*((func,) + args))
return _decorator
else:
return caller
return outer_decorator