fixes #854: republish a book with parent correctly
[wolnelektury.git] / apps / piston / decorator.py
1 """
2 Decorator module, see
3 http://www.phyast.pitt.edu/~micheles/python/documentation.html
4 for the documentation and below for the licence.
5 """
6
7 ## The basic trick is to generate the source code for the decorated function
8 ## with the right signature and to evaluate it.
9 ## Uncomment the statement 'print >> sys.stderr, func_src'  in _decorator
10 ## to understand what is going on.
11
12 __all__ = ["decorator", "new_wrapper", "getinfo"]
13
14 import inspect, sys
15
16 try:
17     set
18 except NameError:
19     from sets import Set as set
20
21 def getinfo(func):
22     """
23     Returns an info dictionary containing:
24     - name (the name of the function : str)
25     - argnames (the names of the arguments : list)
26     - defaults (the values of the default arguments : tuple)
27     - signature (the signature : str)
28     - doc (the docstring : str)
29     - module (the module name : str)
30     - dict (the function __dict__ : str)
31
32     >>> def f(self, x=1, y=2, *args, **kw): pass
33
34     >>> info = getinfo(f)
35
36     >>> info["name"]
37     'f'
38     >>> info["argnames"]
39     ['self', 'x', 'y', 'args', 'kw']
40
41     >>> info["defaults"]
42     (1, 2)
43
44     >>> info["signature"]
45     'self, x, y, *args, **kw'
46     """
47     assert inspect.ismethod(func) or inspect.isfunction(func)
48     regargs, varargs, varkwargs, defaults = inspect.getargspec(func)
49     argnames = list(regargs)
50     if varargs:
51         argnames.append(varargs)
52     if varkwargs:
53         argnames.append(varkwargs)
54     signature = inspect.formatargspec(regargs, varargs, varkwargs, defaults,
55                                       formatvalue=lambda value: "")[1:-1]
56     return dict(name=func.__name__, argnames=argnames, signature=signature,
57                 defaults = func.func_defaults, doc=func.__doc__,
58                 module=func.__module__, dict=func.__dict__,
59                 globals=func.func_globals, closure=func.func_closure)
60
61 # akin to functools.update_wrapper
62 def update_wrapper(wrapper, model, infodict=None):
63     infodict = infodict or getinfo(model)
64     try:
65         wrapper.__name__ = infodict['name']
66     except: # Python version < 2.4
67         pass
68     wrapper.__doc__ = infodict['doc']
69     wrapper.__module__ = infodict['module']
70     wrapper.__dict__.update(infodict['dict'])
71     wrapper.func_defaults = infodict['defaults']
72     wrapper.undecorated = model
73     return wrapper
74
75 def new_wrapper(wrapper, model):
76     """
77     An improvement over functools.update_wrapper. The wrapper is a generic
78     callable object. It works by generating a copy of the wrapper with the
79     right signature and by updating the copy, not the original.
80     Moreovoer, 'model' can be a dictionary with keys 'name', 'doc', 'module',
81     'dict', 'defaults'.
82     """
83     if isinstance(model, dict):
84         infodict = model
85     else: # assume model is a function
86         infodict = getinfo(model)
87     assert not '_wrapper_' in infodict["argnames"], (
88         '"_wrapper_" is a reserved argument name!')
89     src = "lambda %(signature)s: _wrapper_(%(signature)s)" % infodict
90     funcopy = eval(src, dict(_wrapper_=wrapper))
91     return update_wrapper(funcopy, model, infodict)
92
93 # helper used in decorator_factory
94 def __call__(self, func):
95     infodict = getinfo(func)
96     for name in ('_func_', '_self_'):
97         assert not name in infodict["argnames"], (
98            '%s is a reserved argument name!' % name)
99     src = "lambda %(signature)s: _self_.call(_func_, %(signature)s)"
100     new = eval(src % infodict, dict(_func_=func, _self_=self))
101     return update_wrapper(new, func, infodict)
102
103 def decorator_factory(cls):
104     """
105     Take a class with a ``.caller`` method and return a callable decorator
106     object. It works by adding a suitable __call__ method to the class;
107     it raises a TypeError if the class already has a nontrivial __call__
108     method.
109     """
110     attrs = set(dir(cls))
111     if '__call__' in attrs:
112         raise TypeError('You cannot decorate a class with a nontrivial '
113                         '__call__ method')
114     if 'call' not in attrs:
115         raise TypeError('You cannot decorate a class without a '
116                         '.call method')
117     cls.__call__ = __call__
118     return cls
119
120 def decorator(caller):
121     """
122     General purpose decorator factory: takes a caller function as
123     input and returns a decorator with the same attributes.
124     A caller function is any function like this::
125
126      def caller(func, *args, **kw):
127          # do something
128          return func(*args, **kw)
129
130     Here is an example of usage:
131
132     >>> @decorator
133     ... def chatty(f, *args, **kw):
134     ...     print "Calling %r" % f.__name__
135     ...     return f(*args, **kw)
136
137     >>> chatty.__name__
138     'chatty'
139
140     >>> @chatty
141     ... def f(): pass
142     ...
143     >>> f()
144     Calling 'f'
145
146     decorator can also take in input a class with a .caller method; in this
147     case it converts the class into a factory of callable decorator objects.
148     See the documentation for an example.
149     """
150     if inspect.isclass(caller):
151         return decorator_factory(caller)
152     def _decorator(func): # the real meat is here
153         infodict = getinfo(func)
154         argnames = infodict['argnames']
155         assert not ('_call_' in argnames or '_func_' in argnames), (
156             'You cannot use _call_ or _func_ as argument names!')
157         src = "lambda %(signature)s: _call_(_func_, %(signature)s)" % infodict
158         # import sys; print >> sys.stderr, src # for debugging purposes
159         dec_func = eval(src, dict(_func_=func, _call_=caller))
160         return update_wrapper(dec_func, func, infodict)
161     return update_wrapper(_decorator, caller)
162
163 if __name__ == "__main__":
164     import doctest; doctest.testmod()
165
166 ##########################     LEGALESE    ###############################
167
168 ##   Redistributions of source code must retain the above copyright
169 ##   notice, this list of conditions and the following disclaimer.
170 ##   Redistributions in bytecode form must reproduce the above copyright
171 ##   notice, this list of conditions and the following disclaimer in
172 ##   the documentation and/or other materials provided with the
173 ##   distribution.
174
175 ##   THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
176 ##   "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
177 ##   LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
178 ##   A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
179 ##   HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
180 ##   INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
181 ##   BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
182 ##   OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
183 ##   ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
184 ##   TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
185 ##   USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
186 ##   DAMAGE.