decorators.py 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251
  1. """
  2. Decorator module by Michele Simionato <michelesimionato@libero.it>
  3. Copyright Michele Simionato, distributed under the terms of the BSD License (see below).
  4. http://www.phyast.pitt.edu/~micheles/python/documentation.html
  5. Included in NLTK for its support of a nice memoization decorator.
  6. """
  7. __docformat__ = "restructuredtext en"
  8. ## The basic trick is to generate the source code for the decorated function
  9. ## with the right signature and to evaluate it.
  10. ## Uncomment the statement 'print >> sys.stderr, func_src' in _decorator
  11. ## to understand what is going on.
  12. __all__ = ["decorator", "new_wrapper", "getinfo"]
  13. import sys
  14. # Hack to keep NLTK's "tokenize" module from colliding with the "tokenize" in
  15. # the Python standard library.
  16. OLD_SYS_PATH = sys.path[:]
  17. sys.path = [p for p in sys.path if p and "nltk" not in p]
  18. import inspect
  19. sys.path = OLD_SYS_PATH
  20. def __legacysignature(signature):
  21. """
  22. For retrocompatibility reasons, we don't use a standard Signature.
  23. Instead, we use the string generated by this method.
  24. Basically, from a Signature we create a string and remove the default values.
  25. """
  26. listsignature = str(signature)[1:-1].split(",")
  27. for counter, param in enumerate(listsignature):
  28. if param.count("=") > 0:
  29. listsignature[counter] = param[0:param.index("=")].strip()
  30. else:
  31. listsignature[counter] = param.strip()
  32. return ", ".join(listsignature)
  33. def getinfo(func):
  34. """
  35. Returns an info dictionary containing:
  36. - name (the name of the function : str)
  37. - argnames (the names of the arguments : list)
  38. - defaults (the values of the default arguments : tuple)
  39. - signature (the signature : str)
  40. - fullsignature (the full signature : Signature)
  41. - doc (the docstring : str)
  42. - module (the module name : str)
  43. - dict (the function __dict__ : str)
  44. >>> def f(self, x=1, y=2, *args, **kw): pass
  45. >>> info = getinfo(f)
  46. >>> info["name"]
  47. 'f'
  48. >>> info["argnames"]
  49. ['self', 'x', 'y', 'args', 'kw']
  50. >>> info["defaults"]
  51. (1, 2)
  52. >>> info["signature"]
  53. 'self, x, y, *args, **kw'
  54. >>> info["fullsignature"]
  55. <Signature (self, x=1, y=2, *args, **kw)>
  56. """
  57. assert inspect.ismethod(func) or inspect.isfunction(func)
  58. argspec = inspect.getfullargspec(func)
  59. regargs, varargs, varkwargs = argspec[:3]
  60. argnames = list(regargs)
  61. if varargs:
  62. argnames.append(varargs)
  63. if varkwargs:
  64. argnames.append(varkwargs)
  65. fullsignature = inspect.signature(func)
  66. # Convert Signature to str
  67. signature = __legacysignature(fullsignature)
  68. # pypy compatibility
  69. if hasattr(func, "__closure__"):
  70. _closure = func.__closure__
  71. _globals = func.__globals__
  72. else:
  73. _closure = func.func_closure
  74. _globals = func.func_globals
  75. return dict(
  76. name=func.__name__,
  77. argnames=argnames,
  78. signature=signature,
  79. fullsignature=fullsignature,
  80. defaults=func.__defaults__,
  81. doc=func.__doc__,
  82. module=func.__module__,
  83. dict=func.__dict__,
  84. globals=_globals,
  85. closure=_closure,
  86. )
  87. def update_wrapper(wrapper, model, infodict=None):
  88. " akin to functools.update_wrapper "
  89. infodict = infodict or getinfo(model)
  90. wrapper.__name__ = infodict["name"]
  91. wrapper.__doc__ = infodict["doc"]
  92. wrapper.__module__ = infodict["module"]
  93. wrapper.__dict__.update(infodict["dict"])
  94. wrapper.__defaults__ = infodict["defaults"]
  95. wrapper.undecorated = model
  96. return wrapper
  97. def new_wrapper(wrapper, model):
  98. """
  99. An improvement over functools.update_wrapper. The wrapper is a generic
  100. callable object. It works by generating a copy of the wrapper with the
  101. right signature and by updating the copy, not the original.
  102. Moreovoer, 'model' can be a dictionary with keys 'name', 'doc', 'module',
  103. 'dict', 'defaults'.
  104. """
  105. if isinstance(model, dict):
  106. infodict = model
  107. else: # assume model is a function
  108. infodict = getinfo(model)
  109. assert (
  110. not "_wrapper_" in infodict["argnames"]
  111. ), '"_wrapper_" is a reserved argument name!'
  112. src = "lambda %(signature)s: _wrapper_(%(signature)s)" % infodict
  113. funcopy = eval(src, dict(_wrapper_=wrapper))
  114. return update_wrapper(funcopy, model, infodict)
  115. # helper used in decorator_factory
  116. def __call__(self, func):
  117. return new_wrapper(lambda *a, **k: self.call(func, *a, **k), func)
  118. def decorator_factory(cls):
  119. """
  120. Take a class with a ``.caller`` method and return a callable decorator
  121. object. It works by adding a suitable __call__ method to the class;
  122. it raises a TypeError if the class already has a nontrivial __call__
  123. method.
  124. """
  125. attrs = set(dir(cls))
  126. if "__call__" in attrs:
  127. raise TypeError(
  128. "You cannot decorate a class with a nontrivial " "__call__ method"
  129. )
  130. if "call" not in attrs:
  131. raise TypeError("You cannot decorate a class without a " ".call method")
  132. cls.__call__ = __call__
  133. return cls
  134. def decorator(caller):
  135. """
  136. General purpose decorator factory: takes a caller function as
  137. input and returns a decorator with the same attributes.
  138. A caller function is any function like this::
  139. def caller(func, *args, **kw):
  140. # do something
  141. return func(*args, **kw)
  142. Here is an example of usage:
  143. >>> @decorator
  144. ... def chatty(f, *args, **kw):
  145. ... print("Calling %r" % f.__name__)
  146. ... return f(*args, **kw)
  147. >>> chatty.__name__
  148. 'chatty'
  149. >>> @chatty
  150. ... def f(): pass
  151. ...
  152. >>> f()
  153. Calling 'f'
  154. decorator can also take in input a class with a .caller method; in this
  155. case it converts the class into a factory of callable decorator objects.
  156. See the documentation for an example.
  157. """
  158. if inspect.isclass(caller):
  159. return decorator_factory(caller)
  160. def _decorator(func): # the real meat is here
  161. infodict = getinfo(func)
  162. argnames = infodict["argnames"]
  163. assert not (
  164. "_call_" in argnames or "_func_" in argnames
  165. ), "You cannot use _call_ or _func_ as argument names!"
  166. src = "lambda %(signature)s: _call_(_func_, %(signature)s)" % infodict
  167. # import sys; print >> sys.stderr, src # for debugging purposes
  168. dec_func = eval(src, dict(_func_=func, _call_=caller))
  169. return update_wrapper(dec_func, func, infodict)
  170. return update_wrapper(_decorator, caller)
  171. def getattr_(obj, name, default_thunk):
  172. "Similar to .setdefault in dictionaries."
  173. try:
  174. return getattr(obj, name)
  175. except AttributeError:
  176. default = default_thunk()
  177. setattr(obj, name, default)
  178. return default
  179. @decorator
  180. def memoize(func, *args):
  181. dic = getattr_(func, "memoize_dic", dict)
  182. # memoize_dic is created at the first call
  183. if args in dic:
  184. return dic[args]
  185. result = func(*args)
  186. dic[args] = result
  187. return result
  188. ########################## LEGALESE ###############################
  189. ## Redistributions of source code must retain the above copyright
  190. ## notice, this list of conditions and the following disclaimer.
  191. ## Redistributions in bytecode form must reproduce the above copyright
  192. ## notice, this list of conditions and the following disclaimer in
  193. ## the documentation and/or other materials provided with the
  194. ## distribution.
  195. ## THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
  196. ## "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
  197. ## LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
  198. ## A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
  199. ## HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
  200. ## INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
  201. ## BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
  202. ## OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
  203. ## ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
  204. ## TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
  205. ## USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
  206. ## DAMAGE.