decorator.py
254 lines
| 10.0 KiB
| text/x-python
|
PythonLexer
Brian Granger
|
r2268 | ########################## LICENCE ############################### | ||
## | ||||
## Copyright (c) 2005, 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. | ||||
""" | ||||
Decorator module, see http://pypi.python.org/pypi/decorator | ||||
for the documentation. | ||||
""" | ||||
__all__ = ["decorator", "FunctionMaker", "partial", | ||||
"deprecated", "getinfo", "new_wrapper"] | ||||
import os, sys, re, inspect, string, warnings | ||||
try: | ||||
from functools import partial | ||||
except ImportError: # for Python version < 2.5 | ||||
class partial(object): | ||||
"A simple replacement of functools.partial" | ||||
def __init__(self, func, *args, **kw): | ||||
self.func = func | ||||
self.args = args | ||||
self.keywords = kw | ||||
def __call__(self, *otherargs, **otherkw): | ||||
kw = self.keywords.copy() | ||||
kw.update(otherkw) | ||||
return self.func(*(self.args + otherargs), **kw) | ||||
DEF = re.compile('\s*def\s*([_\w][_\w\d]*)\s*\(') | ||||
# basic functionality | ||||
class FunctionMaker(object): | ||||
""" | ||||
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): | ||||
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 = inspect.getargspec(func) | ||||
self.args, self.varargs, self.keywords, self.defaults = argspec | ||||
for i, arg in enumerate(self.args): | ||||
setattr(self, 'arg%d' % i, arg) | ||||
self.signature = inspect.formatargspec( | ||||
formatvalue=lambda val: "", *argspec)[1:-1] | ||||
self.dict = func.__dict__.copy() | ||||
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) | ||||
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', ()) | ||||
callermodule = sys._getframe(3).f_globals.get('__name__', '?') | ||||
func.__module__ = getattr(self, 'module', callermodule) | ||||
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" | ||||
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 | ||||
reserved_names = set([name] + [ | ||||
arg.strip(' *') for arg in self.signature.split(',')]) | ||||
for n, v in evaldict.iteritems(): | ||||
if n in reserved_names: | ||||
raise NameError('%s is overridden in\n%s' % (n, src)) | ||||
if not src.endswith('\n'): # add a newline just for safety | ||||
src += '\n' | ||||
try: | ||||
code = compile(src, '<string>', 'single') | ||||
exec code in evaldict | ||||
except: | ||||
print >> sys.stderr, 'Error in generated code:' | ||||
print >> sys.stderr, src | ||||
raise | ||||
func = evaldict[name] | ||||
if addsource: | ||||
attrs['__source__'] = src | ||||
self.update(func, **attrs) | ||||
return func | ||||
@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)" | ||||
name, rest = obj.strip().split('(', 1) | ||||
signature = rest[:-1] #strip a right parens | ||||
func = None | ||||
else: # a function | ||||
name = None | ||||
signature = None | ||||
func = obj | ||||
fun = cls(func, name, signature, defaults, doc, module) | ||||
ibody = '\n'.join(' ' + line for line in body.splitlines()) | ||||
return fun.make('def %(name)s(%(signature)s):\n' + ibody, | ||||
evaldict, addsource, **attrs) | ||||
def decorator(caller, func=None): | ||||
""" | ||||
decorator(caller) converts a caller function into a decorator; | ||||
decorator(caller, func) decorates a function using a caller. | ||||
""" | ||||
if func is not None: # returns a decorated function | ||||
return FunctionMaker.create( | ||||
func, "return _call_(_func_, %(signature)s)", | ||||
dict(_call_=caller, _func_=func), undecorated=func) | ||||
else: # returns a decorator | ||||
if isinstance(caller, partial): | ||||
return partial(decorator, caller) | ||||
# otherwise assume caller is a function | ||||
f = inspect.getargspec(caller)[0][0] # first arg | ||||
return FunctionMaker.create( | ||||
'%s(%s)' % (caller.__name__, f), | ||||
'return decorator(_call_, %s)' % f, | ||||
dict(_call_=caller, decorator=decorator), undecorated=caller, | ||||
doc=caller.__doc__, module=caller.__module__) | ||||
###################### deprecated functionality ######################### | ||||
@decorator | ||||
def deprecated(func, *args, **kw): | ||||
"A decorator for deprecated functions" | ||||
warnings.warn( | ||||
('Calling the deprecated function %r\n' | ||||
'Downgrade to decorator 2.3 if you want to use this functionality') | ||||
% func.__name__, DeprecationWarning, stacklevel=3) | ||||
return func(*args, **kw) | ||||
@deprecated | ||||
def getinfo(func): | ||||
""" | ||||
Returns an info dictionary containing: | ||||
- name (the name of the function : str) | ||||
- argnames (the names of the arguments : list) | ||||
- defaults (the values of the default arguments : tuple) | ||||
- signature (the signature : str) | ||||
- doc (the docstring : str) | ||||
- module (the module name : str) | ||||
- dict (the function __dict__ : str) | ||||
>>> def f(self, x=1, y=2, *args, **kw): pass | ||||
>>> info = getinfo(f) | ||||
>>> info["name"] | ||||
'f' | ||||
>>> info["argnames"] | ||||
['self', 'x', 'y', 'args', 'kw'] | ||||
>>> info["defaults"] | ||||
(1, 2) | ||||
>>> info["signature"] | ||||
'self, x, y, *args, **kw' | ||||
""" | ||||
assert inspect.ismethod(func) or inspect.isfunction(func) | ||||
regargs, varargs, varkwargs, defaults = inspect.getargspec(func) | ||||
argnames = list(regargs) | ||||
if varargs: | ||||
argnames.append(varargs) | ||||
if varkwargs: | ||||
argnames.append(varkwargs) | ||||
signature = inspect.formatargspec(regargs, varargs, varkwargs, defaults, | ||||
formatvalue=lambda value: "")[1:-1] | ||||
return dict(name=func.__name__, argnames=argnames, signature=signature, | ||||
defaults = func.func_defaults, doc=func.__doc__, | ||||
module=func.__module__, dict=func.__dict__, | ||||
globals=func.func_globals, closure=func.func_closure) | ||||
@deprecated | ||||
def update_wrapper(wrapper, model, infodict=None): | ||||
"A replacement for functools.update_wrapper" | ||||
infodict = infodict or getinfo(model) | ||||
wrapper.__name__ = infodict['name'] | ||||
wrapper.__doc__ = infodict['doc'] | ||||
wrapper.__module__ = infodict['module'] | ||||
wrapper.__dict__.update(infodict['dict']) | ||||
wrapper.func_defaults = infodict['defaults'] | ||||
wrapper.undecorated = model | ||||
return wrapper | ||||
@deprecated | ||||
def new_wrapper(wrapper, model): | ||||
""" | ||||
An improvement over functools.update_wrapper. The wrapper is a generic | ||||
callable object. It works by generating a copy of the wrapper with the | ||||
right signature and by updating the copy, not the original. | ||||
Moreovoer, 'model' can be a dictionary with keys 'name', 'doc', 'module', | ||||
'dict', 'defaults'. | ||||
""" | ||||
if isinstance(model, dict): | ||||
infodict = model | ||||
else: # assume model is a function | ||||
infodict = getinfo(model) | ||||
assert not '_wrapper_' in infodict["argnames"], ( | ||||
'"_wrapper_" is a reserved argument name!') | ||||
src = "lambda %(signature)s: _wrapper_(%(signature)s)" % infodict | ||||
funcopy = eval(src, dict(_wrapper_=wrapper)) | ||||
return update_wrapper(funcopy, model, infodict) | ||||