PageRenderTime 44ms CodeModel.GetById 17ms RepoModel.GetById 0ms app.codeStats 0ms

/astropy/utils/decorators.py

https://gitlab.com/Rockyspade/astropy
Python | 575 lines | 471 code | 35 blank | 69 comment | 28 complexity | 687c7f874a93bb8094fdfc35f80669ed MD5 | raw file
  1. # -*- coding: utf-8 -*-
  2. # Licensed under a 3-clause BSD style license - see LICENSE.rst
  3. """Sundry function and class decorators."""
  4. from __future__ import print_function
  5. import functools
  6. import inspect
  7. import sys
  8. import textwrap
  9. import types
  10. import warnings
  11. from .codegen import make_function_with_signature
  12. from .exceptions import (AstropyDeprecationWarning,
  13. AstropyPendingDeprecationWarning)
  14. from ..extern import six
  15. __all__ = ['deprecated', 'deprecated_attribute', 'lazyproperty',
  16. 'sharedmethod', 'wraps']
  17. def deprecated(since, message='', name='', alternative='', pending=False,
  18. obj_type=None):
  19. """
  20. Used to mark a function or class as deprecated.
  21. To mark an attribute as deprecated, use `deprecated_attribute`.
  22. Parameters
  23. ------------
  24. since : str
  25. The release at which this API became deprecated. This is
  26. required.
  27. message : str, optional
  28. Override the default deprecation message. The format
  29. specifier ``func`` may be used for the name of the function,
  30. and ``alternative`` may be used in the deprecation message
  31. to insert the name of an alternative to the deprecated
  32. function. ``obj_type`` may be used to insert a friendly name
  33. for the type of object being deprecated.
  34. name : str, optional
  35. The name of the deprecated function or class; if not provided
  36. the name is automatically determined from the passed in
  37. function or class, though this is useful in the case of
  38. renamed functions, where the new function is just assigned to
  39. the name of the deprecated function. For example::
  40. def new_function():
  41. ...
  42. oldFunction = new_function
  43. alternative : str, optional
  44. An alternative function or class name that the user may use in
  45. place of the deprecated object. The deprecation warning will
  46. tell the user about this alternative if provided.
  47. pending : bool, optional
  48. If True, uses a AstropyPendingDeprecationWarning instead of a
  49. AstropyDeprecationWarning.
  50. obj_type : str, optional
  51. The type of this object, if the automatically determined one
  52. needs to be overridden.
  53. """
  54. method_types = (classmethod, staticmethod, types.MethodType)
  55. def deprecate_doc(old_doc, message):
  56. """
  57. Returns a given docstring with a deprecation message prepended
  58. to it.
  59. """
  60. if not old_doc:
  61. old_doc = ''
  62. old_doc = textwrap.dedent(old_doc).strip('\n')
  63. new_doc = (('\n.. deprecated:: %(since)s'
  64. '\n %(message)s\n\n' %
  65. {'since': since, 'message': message.strip()}) + old_doc)
  66. if not old_doc:
  67. # This is to prevent a spurious 'unexpected unindent' warning from
  68. # docutils when the original docstring was blank.
  69. new_doc += r'\ '
  70. return new_doc
  71. def get_function(func):
  72. """
  73. Given a function or classmethod (or other function wrapper type), get
  74. the function object.
  75. """
  76. if isinstance(func, method_types):
  77. try:
  78. func = func.__func__
  79. except AttributeError:
  80. # classmethods in Python2.6 and below lack the __func__
  81. # attribute so we need to hack around to get it
  82. method = func.__get__(None, object)
  83. if isinstance(method, types.FunctionType):
  84. # For staticmethods anyways the wrapped object is just a
  85. # plain function (not a bound method or anything like that)
  86. func = method
  87. elif hasattr(method, '__func__'):
  88. func = method.__func__
  89. elif hasattr(method, 'im_func'):
  90. func = method.im_func
  91. else:
  92. # Nothing we can do really... just return the original
  93. # classmethod, etc.
  94. return func
  95. return func
  96. def deprecate_function(func, message):
  97. """
  98. Returns a wrapped function that displays an
  99. ``AstropyDeprecationWarning`` when it is called.
  100. """
  101. if isinstance(func, method_types):
  102. func_wrapper = type(func)
  103. else:
  104. func_wrapper = lambda f: f
  105. func = get_function(func)
  106. def deprecated_func(*args, **kwargs):
  107. if pending:
  108. category = AstropyPendingDeprecationWarning
  109. else:
  110. category = AstropyDeprecationWarning
  111. warnings.warn(message, category, stacklevel=2)
  112. return func(*args, **kwargs)
  113. # If this is an extension function, we can't call
  114. # functools.wraps on it, but we normally don't care.
  115. # This crazy way to get the type of a wrapper descriptor is
  116. # straight out of the Python 3.3 inspect module docs.
  117. if type(func) != type(str.__dict__['__add__']):
  118. deprecated_func = functools.wraps(func)(deprecated_func)
  119. deprecated_func.__doc__ = deprecate_doc(
  120. deprecated_func.__doc__, message)
  121. return func_wrapper(deprecated_func)
  122. def deprecate_class(cls, message):
  123. """
  124. Returns a wrapper class with the docstrings updated and an
  125. __init__ function that will raise an
  126. ``AstropyDeprectationWarning`` warning when called.
  127. """
  128. # Creates a new class with the same name and bases as the
  129. # original class, but updates the dictionary with a new
  130. # docstring and a wrapped __init__ method. __module__ needs
  131. # to be manually copied over, since otherwise it will be set
  132. # to *this* module (astropy.utils.misc).
  133. # This approach seems to make Sphinx happy (the new class
  134. # looks enough like the original class), and works with
  135. # extension classes (which functools.wraps does not, since
  136. # it tries to modify the original class).
  137. # We need to add a custom pickler or you'll get
  138. # Can't pickle <class ..>: it's not found as ...
  139. # errors. Picklability is required for any class that is
  140. # documented by Sphinx.
  141. members = cls.__dict__.copy()
  142. members.update({
  143. '__doc__': deprecate_doc(cls.__doc__, message),
  144. '__init__': deprecate_function(get_function(cls.__init__),
  145. message),
  146. })
  147. return type(cls.__name__, cls.__bases__, members)
  148. def deprecate(obj, message=message, name=name, alternative=alternative,
  149. pending=pending):
  150. if obj_type is None:
  151. if isinstance(obj, type):
  152. obj_type_name = 'class'
  153. elif inspect.isfunction(obj):
  154. obj_type_name = 'function'
  155. elif inspect.ismethod(obj) or isinstance(obj, method_types):
  156. obj_type_name = 'method'
  157. else:
  158. obj_type_name = 'object'
  159. else:
  160. obj_type_name = obj_type
  161. if not name:
  162. name = get_function(obj).__name__
  163. altmessage = ''
  164. if not message or type(message) == type(deprecate):
  165. if pending:
  166. message = ('The %(func)s %(obj_type)s will be deprecated in a '
  167. 'future version.')
  168. else:
  169. message = ('The %(func)s %(obj_type)s is deprecated and may '
  170. 'be removed in a future version.')
  171. if alternative:
  172. altmessage = '\n Use %s instead.' % alternative
  173. message = ((message % {
  174. 'func': name,
  175. 'name': name,
  176. 'alternative': alternative,
  177. 'obj_type': obj_type_name}) +
  178. altmessage)
  179. if isinstance(obj, type):
  180. return deprecate_class(obj, message)
  181. else:
  182. return deprecate_function(obj, message)
  183. if type(message) == type(deprecate):
  184. return deprecate(message)
  185. return deprecate
  186. def deprecated_attribute(name, since, message=None, alternative=None,
  187. pending=False):
  188. """
  189. Used to mark a public attribute as deprecated. This creates a
  190. property that will warn when the given attribute name is accessed.
  191. To prevent the warning (i.e. for internal code), use the private
  192. name for the attribute by prepending an underscore
  193. (i.e. ``self._name``).
  194. Parameters
  195. ----------
  196. name : str
  197. The name of the deprecated attribute.
  198. since : str
  199. The release at which this API became deprecated. This is
  200. required.
  201. message : str, optional
  202. Override the default deprecation message. The format
  203. specifier ``name`` may be used for the name of the attribute,
  204. and ``alternative`` may be used in the deprecation message
  205. to insert the name of an alternative to the deprecated
  206. function.
  207. alternative : str, optional
  208. An alternative attribute that the user may use in place of the
  209. deprecated attribute. The deprecation warning will tell the
  210. user about this alternative if provided.
  211. pending : bool, optional
  212. If True, uses a AstropyPendingDeprecationWarning instead of a
  213. AstropyDeprecationWarning.
  214. Examples
  215. --------
  216. ::
  217. class MyClass:
  218. # Mark the old_name as deprecated
  219. old_name = misc.deprecated_attribute('old_name', '0.1')
  220. def method(self):
  221. self._old_name = 42
  222. """
  223. private_name = '_' + name
  224. @deprecated(since, name=name, obj_type='attribute')
  225. def get(self):
  226. return getattr(self, private_name)
  227. @deprecated(since, name=name, obj_type='attribute')
  228. def set(self, val):
  229. setattr(self, private_name, val)
  230. @deprecated(since, name=name, obj_type='attribute')
  231. def delete(self):
  232. delattr(self, private_name)
  233. return property(get, set, delete)
  234. class lazyproperty(object):
  235. """
  236. Works similarly to property(), but computes the value only once.
  237. This essentially memorizes the value of the property by storing the result
  238. of its computation in the ``__dict__`` of the object instance. This is
  239. useful for computing the value of some property that should otherwise be
  240. invariant. For example::
  241. >>> class LazyTest(object):
  242. ... @lazyproperty
  243. ... def complicated_property(self):
  244. ... print('Computing the value for complicated_property...')
  245. ... return 42
  246. ...
  247. >>> lt = LazyTest()
  248. >>> lt.complicated_property
  249. Computing the value for complicated_property...
  250. 42
  251. >>> lt.complicated_property
  252. 42
  253. As the example shows, the second time ``complicated_property`` is accessed,
  254. the ``print`` statement is not executed. Only the return value from the
  255. first access off ``complicated_property`` is returned.
  256. If a setter for this property is defined, it will still be possible to
  257. manually update the value of the property, if that capability is desired.
  258. Adapted from the recipe at
  259. http://code.activestate.com/recipes/363602-lazy-property-evaluation
  260. """
  261. def __init__(self, fget, fset=None, fdel=None, doc=None):
  262. self._fget = fget
  263. self._fset = fset
  264. self._fdel = fdel
  265. if doc is None:
  266. self.__doc__ = fget.__doc__
  267. else:
  268. self.__doc__ = doc
  269. self._key = self._fget.__name__
  270. def __get__(self, obj, owner=None):
  271. try:
  272. return obj.__dict__[self._key]
  273. except KeyError:
  274. val = self._fget(obj)
  275. obj.__dict__[self._key] = val
  276. return val
  277. except AttributeError:
  278. if obj is None:
  279. return self
  280. raise
  281. def __set__(self, obj, val):
  282. obj_dict = obj.__dict__
  283. if self._fset:
  284. ret = self._fset(obj, val)
  285. if ret is not None and obj_dict.get(self._key) is ret:
  286. # By returning the value set the setter signals that it took
  287. # over setting the value in obj.__dict__; this mechanism allows
  288. # it to override the input value
  289. return
  290. obj_dict[self._key] = val
  291. def __delete__(self, obj):
  292. if self._fdel:
  293. self._fdel(obj)
  294. if self._key in obj.__dict__:
  295. del obj.__dict__[self._key]
  296. def getter(self, fget):
  297. return self.__ter(fget, 0)
  298. def setter(self, fset):
  299. return self.__ter(fset, 1)
  300. def deleter(self, fdel):
  301. return self.__ter(fdel, 2)
  302. def __ter(self, f, arg):
  303. args = [self._fget, self._fset, self._fdel, self.__doc__]
  304. args[arg] = f
  305. cls_ns = sys._getframe(1).f_locals
  306. for k, v in six.iteritems(cls_ns):
  307. if v is self:
  308. property_name = k
  309. break
  310. cls_ns[property_name] = lazyproperty(*args)
  311. return cls_ns[property_name]
  312. class sharedmethod(classmethod):
  313. """
  314. This is a method decorator that allows both an instancemethod and a
  315. `classmethod` to share the same name.
  316. When using `sharedmethod` on a method defined in a class's body, it
  317. may be called on an instance, or on a class. In the former case it
  318. behaves like a normal instance method (a reference to the instance is
  319. automatically passed as the first ``self`` argument of the method)::
  320. >>> class Example(object):
  321. ... @sharedmethod
  322. ... def identify(self, *args):
  323. ... print('self was', self)
  324. ... print('additional args were', args)
  325. ...
  326. >>> ex = Example()
  327. >>> ex.identify(1, 2)
  328. self was <astropy.utils.decorators.Example object at 0x...>
  329. additional args were (1, 2)
  330. In the latter case, when the `sharedmethod` is called directly from a
  331. class, it behaves like a `classmethod`::
  332. >>> Example.identify(3, 4)
  333. self was <class 'astropy.utils.decorators.Example'>
  334. additional args were (3, 4)
  335. This also supports a more advanced usage, where the `classmethod`
  336. implementation can be written separately. If the class's *metaclass*
  337. has a method of the same name as the `sharedmethod`, the version on
  338. the metaclass is delegated to::
  339. >>> from astropy.extern.six import add_metaclass
  340. >>> class ExampleMeta(type):
  341. ... def identify(self):
  342. ... print('this implements the {0}.identify '
  343. ... 'classmethod'.format(self.__name__))
  344. ...
  345. >>> @add_metaclass(ExampleMeta)
  346. ... class Example(object):
  347. ... @sharedmethod
  348. ... def identify(self):
  349. ... print('this implements the instancemethod')
  350. ...
  351. >>> Example().identify()
  352. this implements the instancemethod
  353. >>> Example.identify()
  354. this implements the Example.identify classmethod
  355. """
  356. if sys.version_info[:2] < (2, 7):
  357. # Workaround for Python 2.6 which does not have classmethod.__func__
  358. @property
  359. def __func__(self):
  360. try:
  361. meth = classmethod.__get__(self, self.__obj__,
  362. self.__objtype__)
  363. except AttributeError:
  364. # self.__obj__ not set when called from __get__, but then it
  365. # doesn't matter anyways
  366. meth = classmethod.__get__(self, None, object)
  367. return meth.__func__
  368. def __getobjwrapper(orig_get):
  369. """
  370. Used to temporarily set/unset self.__obj__ and self.__objtype__
  371. for use by __func__.
  372. """
  373. def __get__(self, obj, objtype=None):
  374. self.__obj__ = obj
  375. self.__objtype__ = objtype
  376. try:
  377. return orig_get(self, obj, objtype)
  378. finally:
  379. del self.__obj__
  380. del self.__objtype__
  381. return __get__
  382. else:
  383. def __getobjwrapper(func):
  384. return func
  385. @__getobjwrapper
  386. def __get__(self, obj, objtype=None):
  387. if obj is None:
  388. mcls = type(objtype)
  389. clsmeth = getattr(mcls, self.__func__.__name__, None)
  390. if callable(clsmeth):
  391. if isinstance(clsmeth, types.MethodType):
  392. # This case will generally only apply on Python 2, which
  393. # uses MethodType for unbound methods; Python 3 has no
  394. # particular concept of unbound methods and will just
  395. # return a function
  396. func = clsmeth.__func__
  397. else:
  398. func = clsmeth
  399. else:
  400. func = self.__func__
  401. return self._make_method(func, objtype)
  402. else:
  403. return self._make_method(self.__func__, obj)
  404. del __getobjwrapper
  405. if six.PY3:
  406. # The 'instancemethod' type of Python 2 and the method type of
  407. # Python 3 have slightly different constructors
  408. @staticmethod
  409. def _make_method(func, instance):
  410. return types.MethodType(func, instance)
  411. else:
  412. @staticmethod
  413. def _make_method(func, instance):
  414. return types.MethodType(func, instance, type(instance))
  415. def wraps(wrapped, assigned=functools.WRAPPER_ASSIGNMENTS,
  416. updated=functools.WRAPPER_UPDATES):
  417. """
  418. An alternative to `functools.wraps` which also preserves the original
  419. function's call signature by way of
  420. `~astropy.utils.codegen.make_function_with_signature`.
  421. The documentation for the original `functools.wraps` follows:
  422. """
  423. def wrapper(func):
  424. func = make_function_with_signature(func, name=wrapped.__name__,
  425. **_get_function_args(wrapped))
  426. func = functools.update_wrapper(func, wrapped, assigned=assigned,
  427. updated=updated)
  428. return func
  429. return wrapper
  430. wraps.__doc__ += functools.wraps.__doc__
  431. if six.PY3:
  432. def _get_function_args(func):
  433. """
  434. Utility function for `wraps`.
  435. Reads the argspec for the given function and converts it to arguments
  436. for `make_function_with_signature`. This requires different
  437. implementations on Python 2 versus Python 3.
  438. """
  439. argspec = inspect.getfullargspec(func)
  440. if argspec.defaults:
  441. args = argspec.args[:-len(argspec.defaults)]
  442. kwargs = zip(argspec.args[len(args):], argspec.defaults)
  443. else:
  444. args = argspec.args
  445. kwargs = []
  446. if argspec.kwonlyargs:
  447. kwargs.extend((argname, argspec.kwonlydefaults[argname])
  448. for argname in argspec.kwonlyargs)
  449. return {'args': args, 'kwargs': kwargs, 'varargs': argspec.varargs,
  450. 'varkwargs': argspec.varkw}
  451. else:
  452. def _get_function_args(func):
  453. """
  454. Utility function for `wraps`.
  455. Reads the argspec for the given function and converts it to arguments
  456. for `make_function_with_signature`. This requires different
  457. implementations on Python 2 versus Python 3.
  458. """
  459. argspec = inspect.getargspec(func)
  460. if argspec.defaults:
  461. args = argspec.args[:-len(argspec.defaults)]
  462. kwargs = zip(argspec.args[len(args):], argspec.defaults)
  463. else:
  464. args = argspec.args
  465. kwargs = {}
  466. return {'args': args, 'kwargs': kwargs, 'varargs': argspec.varargs,
  467. 'varkwargs': argspec.keywords}