PageRenderTime 40ms CodeModel.GetById 16ms RepoModel.GetById 0ms app.codeStats 0ms

/astropy/utils/decorators.py

https://github.com/astropy/astropy
Python | 1087 lines | 956 code | 19 blank | 112 comment | 26 complexity | 195cac9762699441cac6af67b823f6a7 MD5 | raw file
  1. # Licensed under a 3-clause BSD style license - see LICENSE.rst
  2. """Sundry function and class decorators."""
  3. import functools
  4. import inspect
  5. import textwrap
  6. import threading
  7. import types
  8. import warnings
  9. from inspect import signature
  10. from .exceptions import (AstropyDeprecationWarning, AstropyUserWarning,
  11. AstropyPendingDeprecationWarning)
  12. __all__ = ['classproperty', 'deprecated', 'deprecated_attribute',
  13. 'deprecated_renamed_argument', 'format_doc',
  14. 'lazyproperty', 'sharedmethod']
  15. _NotFound = object()
  16. def deprecated(since, message='', name='', alternative='', pending=False,
  17. obj_type=None, warning_type=AstropyDeprecationWarning):
  18. """
  19. Used to mark a function or class as deprecated.
  20. To mark an attribute as deprecated, use `deprecated_attribute`.
  21. Parameters
  22. ----------
  23. since : str
  24. The release at which this API became deprecated. This is
  25. required.
  26. message : str, optional
  27. Override the default deprecation message. The format
  28. specifier ``func`` may be used for the name of the function,
  29. and ``alternative`` may be used in the deprecation message
  30. to insert the name of an alternative to the deprecated
  31. function. ``obj_type`` may be used to insert a friendly name
  32. for the type of object being deprecated.
  33. name : str, optional
  34. The name of the deprecated function or class; if not provided
  35. the name is automatically determined from the passed in
  36. function or class, though this is useful in the case of
  37. renamed functions, where the new function is just assigned to
  38. the name of the deprecated function. For example::
  39. def new_function():
  40. ...
  41. oldFunction = new_function
  42. alternative : str, optional
  43. An alternative function or class name that the user may use in
  44. place of the deprecated object. The deprecation warning will
  45. tell the user about this alternative if provided.
  46. pending : bool, optional
  47. If True, uses a AstropyPendingDeprecationWarning instead of a
  48. ``warning_type``.
  49. obj_type : str, optional
  50. The type of this object, if the automatically determined one
  51. needs to be overridden.
  52. warning_type : Warning
  53. Warning to be issued.
  54. Default is `~astropy.utils.exceptions.AstropyDeprecationWarning`.
  55. """
  56. method_types = (classmethod, staticmethod, types.MethodType)
  57. def deprecate_doc(old_doc, message):
  58. """
  59. Returns a given docstring with a deprecation message prepended
  60. to it.
  61. """
  62. if not old_doc:
  63. old_doc = ''
  64. old_doc = textwrap.dedent(old_doc).strip('\n')
  65. new_doc = (('\n.. deprecated:: {since}'
  66. '\n {message}\n\n'.format(
  67. **{'since': since, 'message': message.strip()})) + old_doc)
  68. if not old_doc:
  69. # This is to prevent a spurious 'unexpected unindent' warning from
  70. # docutils when the original docstring was blank.
  71. new_doc += r'\ '
  72. return new_doc
  73. def get_function(func):
  74. """
  75. Given a function or classmethod (or other function wrapper type), get
  76. the function object.
  77. """
  78. if isinstance(func, method_types):
  79. func = func.__func__
  80. return func
  81. def deprecate_function(func, message, warning_type=warning_type):
  82. """
  83. Returns a wrapped function that displays ``warning_type``
  84. when it is called.
  85. """
  86. if isinstance(func, method_types):
  87. func_wrapper = type(func)
  88. else:
  89. func_wrapper = lambda f: f # noqa: E731
  90. func = get_function(func)
  91. def deprecated_func(*args, **kwargs):
  92. if pending:
  93. category = AstropyPendingDeprecationWarning
  94. else:
  95. category = warning_type
  96. warnings.warn(message, category, stacklevel=2)
  97. return func(*args, **kwargs)
  98. # If this is an extension function, we can't call
  99. # functools.wraps on it, but we normally don't care.
  100. # This crazy way to get the type of a wrapper descriptor is
  101. # straight out of the Python 3.3 inspect module docs.
  102. if type(func) is not type(str.__dict__['__add__']): # noqa: E721
  103. deprecated_func = functools.wraps(func)(deprecated_func)
  104. deprecated_func.__doc__ = deprecate_doc(
  105. deprecated_func.__doc__, message)
  106. return func_wrapper(deprecated_func)
  107. def deprecate_class(cls, message, warning_type=warning_type):
  108. """
  109. Update the docstring and wrap the ``__init__`` in-place (or ``__new__``
  110. if the class or any of the bases overrides ``__new__``) so it will give
  111. a deprecation warning when an instance is created.
  112. This won't work for extension classes because these can't be modified
  113. in-place and the alternatives don't work in the general case:
  114. - Using a new class that looks and behaves like the original doesn't
  115. work because the __new__ method of extension types usually makes sure
  116. that it's the same class or a subclass.
  117. - Subclassing the class and return the subclass can lead to problems
  118. with pickle and will look weird in the Sphinx docs.
  119. """
  120. cls.__doc__ = deprecate_doc(cls.__doc__, message)
  121. if cls.__new__ is object.__new__:
  122. cls.__init__ = deprecate_function(get_function(cls.__init__),
  123. message, warning_type)
  124. else:
  125. cls.__new__ = deprecate_function(get_function(cls.__new__),
  126. message, warning_type)
  127. return cls
  128. def deprecate(obj, message=message, name=name, alternative=alternative,
  129. pending=pending, warning_type=warning_type):
  130. if obj_type is None:
  131. if isinstance(obj, type):
  132. obj_type_name = 'class'
  133. elif inspect.isfunction(obj):
  134. obj_type_name = 'function'
  135. elif inspect.ismethod(obj) or isinstance(obj, method_types):
  136. obj_type_name = 'method'
  137. else:
  138. obj_type_name = 'object'
  139. else:
  140. obj_type_name = obj_type
  141. if not name:
  142. name = get_function(obj).__name__
  143. altmessage = ''
  144. if not message or type(message) is type(deprecate):
  145. if pending:
  146. message = ('The {func} {obj_type} will be deprecated in a '
  147. 'future version.')
  148. else:
  149. message = ('The {func} {obj_type} is deprecated and may '
  150. 'be removed in a future version.')
  151. if alternative:
  152. altmessage = f'\n Use {alternative} instead.'
  153. message = ((message.format(**{
  154. 'func': name,
  155. 'name': name,
  156. 'alternative': alternative,
  157. 'obj_type': obj_type_name})) +
  158. altmessage)
  159. if isinstance(obj, type):
  160. return deprecate_class(obj, message, warning_type)
  161. else:
  162. return deprecate_function(obj, message, warning_type)
  163. if type(message) is type(deprecate):
  164. return deprecate(message)
  165. return deprecate
  166. def deprecated_attribute(name, since, message=None, alternative=None,
  167. pending=False, warning_type=AstropyDeprecationWarning):
  168. """
  169. Used to mark a public attribute as deprecated. This creates a
  170. property that will warn when the given attribute name is accessed.
  171. To prevent the warning (i.e. for internal code), use the private
  172. name for the attribute by prepending an underscore
  173. (i.e. ``self._name``).
  174. Parameters
  175. ----------
  176. name : str
  177. The name of the deprecated attribute.
  178. since : str
  179. The release at which this API became deprecated. This is
  180. required.
  181. message : str, optional
  182. Override the default deprecation message. The format
  183. specifier ``name`` may be used for the name of the attribute,
  184. and ``alternative`` may be used in the deprecation message
  185. to insert the name of an alternative to the deprecated
  186. function.
  187. alternative : str, optional
  188. An alternative attribute that the user may use in place of the
  189. deprecated attribute. The deprecation warning will tell the
  190. user about this alternative if provided.
  191. pending : bool, optional
  192. If True, uses a AstropyPendingDeprecationWarning instead of
  193. ``warning_type``.
  194. warning_type : Warning
  195. Warning to be issued.
  196. Default is `~astropy.utils.exceptions.AstropyDeprecationWarning`.
  197. Examples
  198. --------
  199. ::
  200. class MyClass:
  201. # Mark the old_name as deprecated
  202. old_name = misc.deprecated_attribute('old_name', '0.1')
  203. def method(self):
  204. self._old_name = 42
  205. """
  206. private_name = '_' + name
  207. specific_deprecated = deprecated(since, name=name, obj_type='attribute',
  208. message=message, alternative=alternative,
  209. pending=pending,
  210. warning_type=warning_type)
  211. @specific_deprecated
  212. def get(self):
  213. return getattr(self, private_name)
  214. @specific_deprecated
  215. def set(self, val):
  216. setattr(self, private_name, val)
  217. @specific_deprecated
  218. def delete(self):
  219. delattr(self, private_name)
  220. return property(get, set, delete)
  221. def deprecated_renamed_argument(old_name, new_name, since,
  222. arg_in_kwargs=False, relax=False,
  223. pending=False,
  224. warning_type=AstropyDeprecationWarning,
  225. alternative='', message=''):
  226. """Deprecate a _renamed_ or _removed_ function argument.
  227. The decorator assumes that the argument with the ``old_name`` was removed
  228. from the function signature and the ``new_name`` replaced it at the
  229. **same position** in the signature. If the ``old_name`` argument is
  230. given when calling the decorated function the decorator will catch it and
  231. issue a deprecation warning and pass it on as ``new_name`` argument.
  232. Parameters
  233. ----------
  234. old_name : str or sequence of str
  235. The old name of the argument.
  236. new_name : str or sequence of str or None
  237. The new name of the argument. Set this to `None` to remove the
  238. argument ``old_name`` instead of renaming it.
  239. since : str or number or sequence of str or number
  240. The release at which the old argument became deprecated.
  241. arg_in_kwargs : bool or sequence of bool, optional
  242. If the argument is not a named argument (for example it
  243. was meant to be consumed by ``**kwargs``) set this to
  244. ``True``. Otherwise the decorator will throw an Exception
  245. if the ``new_name`` cannot be found in the signature of
  246. the decorated function.
  247. Default is ``False``.
  248. relax : bool or sequence of bool, optional
  249. If ``False`` a ``TypeError`` is raised if both ``new_name`` and
  250. ``old_name`` are given. If ``True`` the value for ``new_name`` is used
  251. and a Warning is issued.
  252. Default is ``False``.
  253. pending : bool or sequence of bool, optional
  254. If ``True`` this will hide the deprecation warning and ignore the
  255. corresponding ``relax`` parameter value.
  256. Default is ``False``.
  257. warning_type : Warning
  258. Warning to be issued.
  259. Default is `~astropy.utils.exceptions.AstropyDeprecationWarning`.
  260. alternative : str, optional
  261. An alternative function or class name that the user may use in
  262. place of the deprecated object if ``new_name`` is None. The deprecation
  263. warning will tell the user about this alternative if provided.
  264. message : str, optional
  265. A custom warning message. If provided then ``since`` and
  266. ``alternative`` options will have no effect.
  267. Raises
  268. ------
  269. TypeError
  270. If the new argument name cannot be found in the function
  271. signature and arg_in_kwargs was False or if it is used to
  272. deprecate the name of the ``*args``-, ``**kwargs``-like arguments.
  273. At runtime such an Error is raised if both the new_name
  274. and old_name were specified when calling the function and
  275. "relax=False".
  276. Notes
  277. -----
  278. The decorator should be applied to a function where the **name**
  279. of an argument was changed but it applies the same logic.
  280. .. warning::
  281. If ``old_name`` is a list or tuple the ``new_name`` and ``since`` must
  282. also be a list or tuple with the same number of entries. ``relax`` and
  283. ``arg_in_kwarg`` can be a single bool (applied to all) or also a
  284. list/tuple with the same number of entries like ``new_name``, etc.
  285. Examples
  286. --------
  287. The deprecation warnings are not shown in the following examples.
  288. To deprecate a positional or keyword argument::
  289. >>> from astropy.utils.decorators import deprecated_renamed_argument
  290. >>> @deprecated_renamed_argument('sig', 'sigma', '1.0')
  291. ... def test(sigma):
  292. ... return sigma
  293. >>> test(2)
  294. 2
  295. >>> test(sigma=2)
  296. 2
  297. >>> test(sig=2) # doctest: +SKIP
  298. 2
  299. To deprecate an argument caught inside the ``**kwargs`` the
  300. ``arg_in_kwargs`` has to be set::
  301. >>> @deprecated_renamed_argument('sig', 'sigma', '1.0',
  302. ... arg_in_kwargs=True)
  303. ... def test(**kwargs):
  304. ... return kwargs['sigma']
  305. >>> test(sigma=2)
  306. 2
  307. >>> test(sig=2) # doctest: +SKIP
  308. 2
  309. By default providing the new and old keyword will lead to an Exception. If
  310. a Warning is desired set the ``relax`` argument::
  311. >>> @deprecated_renamed_argument('sig', 'sigma', '1.0', relax=True)
  312. ... def test(sigma):
  313. ... return sigma
  314. >>> test(sig=2) # doctest: +SKIP
  315. 2
  316. It is also possible to replace multiple arguments. The ``old_name``,
  317. ``new_name`` and ``since`` have to be `tuple` or `list` and contain the
  318. same number of entries::
  319. >>> @deprecated_renamed_argument(['a', 'b'], ['alpha', 'beta'],
  320. ... ['1.0', 1.2])
  321. ... def test(alpha, beta):
  322. ... return alpha, beta
  323. >>> test(a=2, b=3) # doctest: +SKIP
  324. (2, 3)
  325. In this case ``arg_in_kwargs`` and ``relax`` can be a single value (which
  326. is applied to all renamed arguments) or must also be a `tuple` or `list`
  327. with values for each of the arguments.
  328. """
  329. cls_iter = (list, tuple)
  330. if isinstance(old_name, cls_iter):
  331. n = len(old_name)
  332. # Assume that new_name and since are correct (tuple/list with the
  333. # appropriate length) in the spirit of the "consenting adults". But the
  334. # optional parameters may not be set, so if these are not iterables
  335. # wrap them.
  336. if not isinstance(arg_in_kwargs, cls_iter):
  337. arg_in_kwargs = [arg_in_kwargs] * n
  338. if not isinstance(relax, cls_iter):
  339. relax = [relax] * n
  340. if not isinstance(pending, cls_iter):
  341. pending = [pending] * n
  342. if not isinstance(message, cls_iter):
  343. message = [message] * n
  344. else:
  345. # To allow a uniform approach later on, wrap all arguments in lists.
  346. n = 1
  347. old_name = [old_name]
  348. new_name = [new_name]
  349. since = [since]
  350. arg_in_kwargs = [arg_in_kwargs]
  351. relax = [relax]
  352. pending = [pending]
  353. message = [message]
  354. def decorator(function):
  355. # The named arguments of the function.
  356. arguments = signature(function).parameters
  357. keys = list(arguments.keys())
  358. position = [None] * n
  359. for i in range(n):
  360. # Determine the position of the argument.
  361. if arg_in_kwargs[i]:
  362. pass
  363. else:
  364. if new_name[i] is None:
  365. param = arguments[old_name[i]]
  366. elif new_name[i] in arguments:
  367. param = arguments[new_name[i]]
  368. # In case the argument is not found in the list of arguments
  369. # the only remaining possibility is that it should be caught
  370. # by some kind of **kwargs argument.
  371. # This case has to be explicitly specified, otherwise throw
  372. # an exception!
  373. else:
  374. raise TypeError(
  375. f'"{new_name[i]}" was not specified in the function '
  376. 'signature. If it was meant to be part of '
  377. '"**kwargs" then set "arg_in_kwargs" to "True"')
  378. # There are several possibilities now:
  379. # 1.) Positional or keyword argument:
  380. if param.kind == param.POSITIONAL_OR_KEYWORD:
  381. if new_name[i] is None:
  382. position[i] = keys.index(old_name[i])
  383. else:
  384. position[i] = keys.index(new_name[i])
  385. # 2.) Keyword only argument:
  386. elif param.kind == param.KEYWORD_ONLY:
  387. # These cannot be specified by position.
  388. position[i] = None
  389. # 3.) positional-only argument, varargs, varkwargs or some
  390. # unknown type:
  391. else:
  392. raise TypeError(f'cannot replace argument "{new_name[i]}" '
  393. f'of kind {repr(param.kind)}.')
  394. @functools.wraps(function)
  395. def wrapper(*args, **kwargs):
  396. for i in range(n):
  397. msg = message[i] or (f'"{old_name[i]}" was deprecated in '
  398. f'version {since[i]} and will be removed '
  399. 'in a future version. ')
  400. # The only way to have oldkeyword inside the function is
  401. # that it is passed as kwarg because the oldkeyword
  402. # parameter was renamed to newkeyword.
  403. if old_name[i] in kwargs:
  404. value = kwargs.pop(old_name[i])
  405. # Display the deprecation warning only when it's not
  406. # pending.
  407. if not pending[i]:
  408. if not message[i]:
  409. if new_name[i] is not None:
  410. msg += f'Use argument "{new_name[i]}" instead.'
  411. elif alternative:
  412. msg += f'\n Use {alternative} instead.'
  413. warnings.warn(msg, warning_type, stacklevel=2)
  414. # Check if the newkeyword was given as well.
  415. newarg_in_args = (position[i] is not None and
  416. len(args) > position[i])
  417. newarg_in_kwargs = new_name[i] in kwargs
  418. if newarg_in_args or newarg_in_kwargs:
  419. if not pending[i]:
  420. # If both are given print a Warning if relax is
  421. # True or raise an Exception is relax is False.
  422. if relax[i]:
  423. warnings.warn(
  424. f'"{old_name[i]}" and "{new_name[i]}" '
  425. 'keywords were set. '
  426. f'Using the value of "{new_name[i]}".',
  427. AstropyUserWarning)
  428. else:
  429. raise TypeError(
  430. f'cannot specify both "{old_name[i]}" and '
  431. f'"{new_name[i]}".')
  432. else:
  433. # Pass the value of the old argument with the
  434. # name of the new argument to the function
  435. if new_name[i] is not None:
  436. kwargs[new_name[i]] = value
  437. # If old argument has no replacement, cast it back.
  438. # https://github.com/astropy/astropy/issues/9914
  439. else:
  440. kwargs[old_name[i]] = value
  441. # Deprecated keyword without replacement is given as
  442. # positional argument.
  443. elif (not pending[i] and not new_name[i] and position[i] and
  444. len(args) > position[i]):
  445. if alternative and not message[i]:
  446. msg += f'\n Use {alternative} instead.'
  447. warnings.warn(msg, warning_type, stacklevel=2)
  448. return function(*args, **kwargs)
  449. return wrapper
  450. return decorator
  451. # TODO: This can still be made to work for setters by implementing an
  452. # accompanying metaclass that supports it; we just don't need that right this
  453. # second
  454. class classproperty(property):
  455. """
  456. Similar to `property`, but allows class-level properties. That is,
  457. a property whose getter is like a `classmethod`.
  458. The wrapped method may explicitly use the `classmethod` decorator (which
  459. must become before this decorator), or the `classmethod` may be omitted
  460. (it is implicit through use of this decorator).
  461. .. note::
  462. classproperty only works for *read-only* properties. It does not
  463. currently allow writeable/deletable properties, due to subtleties of how
  464. Python descriptors work. In order to implement such properties on a class
  465. a metaclass for that class must be implemented.
  466. Parameters
  467. ----------
  468. fget : callable
  469. The function that computes the value of this property (in particular,
  470. the function when this is used as a decorator) a la `property`.
  471. doc : str, optional
  472. The docstring for the property--by default inherited from the getter
  473. function.
  474. lazy : bool, optional
  475. If True, caches the value returned by the first call to the getter
  476. function, so that it is only called once (used for lazy evaluation
  477. of an attribute). This is analogous to `lazyproperty`. The ``lazy``
  478. argument can also be used when `classproperty` is used as a decorator
  479. (see the third example below). When used in the decorator syntax this
  480. *must* be passed in as a keyword argument.
  481. Examples
  482. --------
  483. ::
  484. >>> class Foo:
  485. ... _bar_internal = 1
  486. ... @classproperty
  487. ... def bar(cls):
  488. ... return cls._bar_internal + 1
  489. ...
  490. >>> Foo.bar
  491. 2
  492. >>> foo_instance = Foo()
  493. >>> foo_instance.bar
  494. 2
  495. >>> foo_instance._bar_internal = 2
  496. >>> foo_instance.bar # Ignores instance attributes
  497. 2
  498. As previously noted, a `classproperty` is limited to implementing
  499. read-only attributes::
  500. >>> class Foo:
  501. ... _bar_internal = 1
  502. ... @classproperty
  503. ... def bar(cls):
  504. ... return cls._bar_internal
  505. ... @bar.setter
  506. ... def bar(cls, value):
  507. ... cls._bar_internal = value
  508. ...
  509. Traceback (most recent call last):
  510. ...
  511. NotImplementedError: classproperty can only be read-only; use a
  512. metaclass to implement modifiable class-level properties
  513. When the ``lazy`` option is used, the getter is only called once::
  514. >>> class Foo:
  515. ... @classproperty(lazy=True)
  516. ... def bar(cls):
  517. ... print("Performing complicated calculation")
  518. ... return 1
  519. ...
  520. >>> Foo.bar
  521. Performing complicated calculation
  522. 1
  523. >>> Foo.bar
  524. 1
  525. If a subclass inherits a lazy `classproperty` the property is still
  526. re-evaluated for the subclass::
  527. >>> class FooSub(Foo):
  528. ... pass
  529. ...
  530. >>> FooSub.bar
  531. Performing complicated calculation
  532. 1
  533. >>> FooSub.bar
  534. 1
  535. """
  536. def __new__(cls, fget=None, doc=None, lazy=False):
  537. if fget is None:
  538. # Being used as a decorator--return a wrapper that implements
  539. # decorator syntax
  540. def wrapper(func):
  541. return cls(func, lazy=lazy)
  542. return wrapper
  543. return super().__new__(cls)
  544. def __init__(self, fget, doc=None, lazy=False):
  545. self._lazy = lazy
  546. if lazy:
  547. self._lock = threading.RLock() # Protects _cache
  548. self._cache = {}
  549. fget = self._wrap_fget(fget)
  550. super().__init__(fget=fget, doc=doc)
  551. # There is a buglet in Python where self.__doc__ doesn't
  552. # get set properly on instances of property subclasses if
  553. # the doc argument was used rather than taking the docstring
  554. # from fget
  555. # Related Python issue: https://bugs.python.org/issue24766
  556. if doc is not None:
  557. self.__doc__ = doc
  558. def __get__(self, obj, objtype):
  559. if self._lazy:
  560. val = self._cache.get(objtype, _NotFound)
  561. if val is _NotFound:
  562. with self._lock:
  563. # Check if another thread initialised before we locked.
  564. val = self._cache.get(objtype, _NotFound)
  565. if val is _NotFound:
  566. val = self.fget.__wrapped__(objtype)
  567. self._cache[objtype] = val
  568. else:
  569. # The base property.__get__ will just return self here;
  570. # instead we pass objtype through to the original wrapped
  571. # function (which takes the class as its sole argument)
  572. val = self.fget.__wrapped__(objtype)
  573. return val
  574. def getter(self, fget):
  575. return super().getter(self._wrap_fget(fget))
  576. def setter(self, fset):
  577. raise NotImplementedError(
  578. "classproperty can only be read-only; use a metaclass to "
  579. "implement modifiable class-level properties")
  580. def deleter(self, fdel):
  581. raise NotImplementedError(
  582. "classproperty can only be read-only; use a metaclass to "
  583. "implement modifiable class-level properties")
  584. @staticmethod
  585. def _wrap_fget(orig_fget):
  586. if isinstance(orig_fget, classmethod):
  587. orig_fget = orig_fget.__func__
  588. # Using stock functools.wraps instead of the fancier version
  589. # found later in this module, which is overkill for this purpose
  590. @functools.wraps(orig_fget)
  591. def fget(obj):
  592. return orig_fget(obj.__class__)
  593. return fget
  594. # Adapted from the recipe at
  595. # http://code.activestate.com/recipes/363602-lazy-property-evaluation
  596. class lazyproperty(property):
  597. """
  598. Works similarly to property(), but computes the value only once.
  599. This essentially memorizes the value of the property by storing the result
  600. of its computation in the ``__dict__`` of the object instance. This is
  601. useful for computing the value of some property that should otherwise be
  602. invariant. For example::
  603. >>> class LazyTest:
  604. ... @lazyproperty
  605. ... def complicated_property(self):
  606. ... print('Computing the value for complicated_property...')
  607. ... return 42
  608. ...
  609. >>> lt = LazyTest()
  610. >>> lt.complicated_property
  611. Computing the value for complicated_property...
  612. 42
  613. >>> lt.complicated_property
  614. 42
  615. As the example shows, the second time ``complicated_property`` is accessed,
  616. the ``print`` statement is not executed. Only the return value from the
  617. first access off ``complicated_property`` is returned.
  618. By default, a setter and deleter are used which simply overwrite and
  619. delete, respectively, the value stored in ``__dict__``. Any user-specified
  620. setter or deleter is executed before executing these default actions.
  621. The one exception is that the default setter is not run if the user setter
  622. already sets the new value in ``__dict__`` and returns that value and the
  623. returned value is not ``None``.
  624. """
  625. def __init__(self, fget, fset=None, fdel=None, doc=None):
  626. super().__init__(fget, fset, fdel, doc)
  627. self._key = self.fget.__name__
  628. self._lock = threading.RLock()
  629. def __get__(self, obj, owner=None):
  630. try:
  631. obj_dict = obj.__dict__
  632. val = obj_dict.get(self._key, _NotFound)
  633. if val is _NotFound:
  634. with self._lock:
  635. # Check if another thread beat us to it.
  636. val = obj_dict.get(self._key, _NotFound)
  637. if val is _NotFound:
  638. val = self.fget(obj)
  639. obj_dict[self._key] = val
  640. return val
  641. except AttributeError:
  642. if obj is None:
  643. return self
  644. raise
  645. def __set__(self, obj, val):
  646. obj_dict = obj.__dict__
  647. if self.fset:
  648. ret = self.fset(obj, val)
  649. if ret is not None and obj_dict.get(self._key) is ret:
  650. # By returning the value set the setter signals that it
  651. # took over setting the value in obj.__dict__; this
  652. # mechanism allows it to override the input value
  653. return
  654. obj_dict[self._key] = val
  655. def __delete__(self, obj):
  656. if self.fdel:
  657. self.fdel(obj)
  658. obj.__dict__.pop(self._key, None) # Delete if present
  659. class sharedmethod(classmethod):
  660. """
  661. This is a method decorator that allows both an instancemethod and a
  662. `classmethod` to share the same name.
  663. When using `sharedmethod` on a method defined in a class's body, it
  664. may be called on an instance, or on a class. In the former case it
  665. behaves like a normal instance method (a reference to the instance is
  666. automatically passed as the first ``self`` argument of the method)::
  667. >>> class Example:
  668. ... @sharedmethod
  669. ... def identify(self, *args):
  670. ... print('self was', self)
  671. ... print('additional args were', args)
  672. ...
  673. >>> ex = Example()
  674. >>> ex.identify(1, 2)
  675. self was <astropy.utils.decorators.Example object at 0x...>
  676. additional args were (1, 2)
  677. In the latter case, when the `sharedmethod` is called directly from a
  678. class, it behaves like a `classmethod`::
  679. >>> Example.identify(3, 4)
  680. self was <class 'astropy.utils.decorators.Example'>
  681. additional args were (3, 4)
  682. This also supports a more advanced usage, where the `classmethod`
  683. implementation can be written separately. If the class's *metaclass*
  684. has a method of the same name as the `sharedmethod`, the version on
  685. the metaclass is delegated to::
  686. >>> class ExampleMeta(type):
  687. ... def identify(self):
  688. ... print('this implements the {0}.identify '
  689. ... 'classmethod'.format(self.__name__))
  690. ...
  691. >>> class Example(metaclass=ExampleMeta):
  692. ... @sharedmethod
  693. ... def identify(self):
  694. ... print('this implements the instancemethod')
  695. ...
  696. >>> Example().identify()
  697. this implements the instancemethod
  698. >>> Example.identify()
  699. this implements the Example.identify classmethod
  700. """
  701. def __get__(self, obj, objtype=None):
  702. if obj is None:
  703. mcls = type(objtype)
  704. clsmeth = getattr(mcls, self.__func__.__name__, None)
  705. if callable(clsmeth):
  706. func = clsmeth
  707. else:
  708. func = self.__func__
  709. return self._make_method(func, objtype)
  710. else:
  711. return self._make_method(self.__func__, obj)
  712. @staticmethod
  713. def _make_method(func, instance):
  714. return types.MethodType(func, instance)
  715. def format_doc(docstring, *args, **kwargs):
  716. """
  717. Replaces the docstring of the decorated object and then formats it.
  718. The formatting works like :meth:`str.format` and if the decorated object
  719. already has a docstring this docstring can be included in the new
  720. documentation if you use the ``{__doc__}`` placeholder.
  721. Its primary use is for reusing a *long* docstring in multiple functions
  722. when it is the same or only slightly different between them.
  723. Parameters
  724. ----------
  725. docstring : str or object or None
  726. The docstring that will replace the docstring of the decorated
  727. object. If it is an object like a function or class it will
  728. take the docstring of this object. If it is a string it will use the
  729. string itself. One special case is if the string is ``None`` then
  730. it will use the decorated functions docstring and formats it.
  731. args :
  732. passed to :meth:`str.format`.
  733. kwargs :
  734. passed to :meth:`str.format`. If the function has a (not empty)
  735. docstring the original docstring is added to the kwargs with the
  736. keyword ``'__doc__'``.
  737. Raises
  738. ------
  739. ValueError
  740. If the ``docstring`` (or interpreted docstring if it was ``None``
  741. or not a string) is empty.
  742. IndexError, KeyError
  743. If a placeholder in the (interpreted) ``docstring`` was not filled. see
  744. :meth:`str.format` for more information.
  745. Notes
  746. -----
  747. Using this decorator allows, for example Sphinx, to parse the
  748. correct docstring.
  749. Examples
  750. --------
  751. Replacing the current docstring is very easy::
  752. >>> from astropy.utils.decorators import format_doc
  753. >>> @format_doc('''Perform num1 + num2''')
  754. ... def add(num1, num2):
  755. ... return num1+num2
  756. ...
  757. >>> help(add) # doctest: +SKIP
  758. Help on function add in module __main__:
  759. <BLANKLINE>
  760. add(num1, num2)
  761. Perform num1 + num2
  762. sometimes instead of replacing you only want to add to it::
  763. >>> doc = '''
  764. ... {__doc__}
  765. ... Parameters
  766. ... ----------
  767. ... num1, num2 : Numbers
  768. ... Returns
  769. ... -------
  770. ... result: Number
  771. ... '''
  772. >>> @format_doc(doc)
  773. ... def add(num1, num2):
  774. ... '''Perform addition.'''
  775. ... return num1+num2
  776. ...
  777. >>> help(add) # doctest: +SKIP
  778. Help on function add in module __main__:
  779. <BLANKLINE>
  780. add(num1, num2)
  781. Perform addition.
  782. Parameters
  783. ----------
  784. num1, num2 : Numbers
  785. Returns
  786. -------
  787. result : Number
  788. in case one might want to format it further::
  789. >>> doc = '''
  790. ... Perform {0}.
  791. ... Parameters
  792. ... ----------
  793. ... num1, num2 : Numbers
  794. ... Returns
  795. ... -------
  796. ... result: Number
  797. ... result of num1 {op} num2
  798. ... {__doc__}
  799. ... '''
  800. >>> @format_doc(doc, 'addition', op='+')
  801. ... def add(num1, num2):
  802. ... return num1+num2
  803. ...
  804. >>> @format_doc(doc, 'subtraction', op='-')
  805. ... def subtract(num1, num2):
  806. ... '''Notes: This one has additional notes.'''
  807. ... return num1-num2
  808. ...
  809. >>> help(add) # doctest: +SKIP
  810. Help on function add in module __main__:
  811. <BLANKLINE>
  812. add(num1, num2)
  813. Perform addition.
  814. Parameters
  815. ----------
  816. num1, num2 : Numbers
  817. Returns
  818. -------
  819. result : Number
  820. result of num1 + num2
  821. >>> help(subtract) # doctest: +SKIP
  822. Help on function subtract in module __main__:
  823. <BLANKLINE>
  824. subtract(num1, num2)
  825. Perform subtraction.
  826. Parameters
  827. ----------
  828. num1, num2 : Numbers
  829. Returns
  830. -------
  831. result : Number
  832. result of num1 - num2
  833. Notes : This one has additional notes.
  834. These methods can be combined; even taking the docstring from another
  835. object is possible as docstring attribute. You just have to specify the
  836. object::
  837. >>> @format_doc(add)
  838. ... def another_add(num1, num2):
  839. ... return num1 + num2
  840. ...
  841. >>> help(another_add) # doctest: +SKIP
  842. Help on function another_add in module __main__:
  843. <BLANKLINE>
  844. another_add(num1, num2)
  845. Perform addition.
  846. Parameters
  847. ----------
  848. num1, num2 : Numbers
  849. Returns
  850. -------
  851. result : Number
  852. result of num1 + num2
  853. But be aware that this decorator *only* formats the given docstring not
  854. the strings passed as ``args`` or ``kwargs`` (not even the original
  855. docstring)::
  856. >>> @format_doc(doc, 'addition', op='+')
  857. ... def yet_another_add(num1, num2):
  858. ... '''This one is good for {0}.'''
  859. ... return num1 + num2
  860. ...
  861. >>> help(yet_another_add) # doctest: +SKIP
  862. Help on function yet_another_add in module __main__:
  863. <BLANKLINE>
  864. yet_another_add(num1, num2)
  865. Perform addition.
  866. Parameters
  867. ----------
  868. num1, num2 : Numbers
  869. Returns
  870. -------
  871. result : Number
  872. result of num1 + num2
  873. This one is good for {0}.
  874. To work around it you could specify the docstring to be ``None``::
  875. >>> @format_doc(None, 'addition')
  876. ... def last_add_i_swear(num1, num2):
  877. ... '''This one is good for {0}.'''
  878. ... return num1 + num2
  879. ...
  880. >>> help(last_add_i_swear) # doctest: +SKIP
  881. Help on function last_add_i_swear in module __main__:
  882. <BLANKLINE>
  883. last_add_i_swear(num1, num2)
  884. This one is good for addition.
  885. Using it with ``None`` as docstring allows to use the decorator twice
  886. on an object to first parse the new docstring and then to parse the
  887. original docstring or the ``args`` and ``kwargs``.
  888. """
  889. def set_docstring(obj):
  890. if docstring is None:
  891. # None means: use the objects __doc__
  892. doc = obj.__doc__
  893. # Delete documentation in this case so we don't end up with
  894. # awkwardly self-inserted docs.
  895. obj.__doc__ = None
  896. elif isinstance(docstring, str):
  897. # String: use the string that was given
  898. doc = docstring
  899. else:
  900. # Something else: Use the __doc__ of this
  901. doc = docstring.__doc__
  902. if not doc:
  903. # In case the docstring is empty it's probably not what was wanted.
  904. raise ValueError('docstring must be a string or containing a '
  905. 'docstring that is not empty.')
  906. # If the original has a not-empty docstring append it to the format
  907. # kwargs.
  908. kwargs['__doc__'] = obj.__doc__ or ''
  909. obj.__doc__ = doc.format(*args, **kwargs)
  910. return obj
  911. return set_docstring