deprecations.py 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273
  1. # Copyright (C) 2012 Anaconda, Inc
  2. # SPDX-License-Identifier: BSD-3-Clause
  3. from __future__ import annotations
  4. import warnings
  5. from functools import wraps
  6. from types import ModuleType
  7. from typing import Any, Callable
  8. from packaging.version import Version, parse
  9. from . import __version__
  10. class DeprecatedError(RuntimeError):
  11. pass
  12. # inspired by deprecation (https://deprecation.readthedocs.io/en/latest/) and
  13. # CPython's warnings._deprecated
  14. class DeprecationHandler:
  15. _version: Version
  16. def __init__(self, version: Version | str):
  17. """Factory to create a deprecation handle for the specified version.
  18. :param version: The version to compare against when checking deprecation statuses.
  19. """
  20. try:
  21. self._version = parse(version)
  22. except TypeError:
  23. self._version = parse("0.0.0.dev0+placeholder")
  24. def __call__(
  25. self,
  26. deprecate_in: str,
  27. remove_in: str,
  28. *,
  29. addendum: str | None = None,
  30. stack: int = 0,
  31. ) -> Callable[(Callable), Callable]:
  32. """Deprecation decorator for functions, methods, & classes.
  33. :param deprecate_in: Version in which code will be marked as deprecated.
  34. :param remove_in: Version in which code is expected to be removed.
  35. :param addendum: Optional additional messaging. Useful to indicate what to do instead.
  36. :param stack: Optional stacklevel increment.
  37. """
  38. def deprecated_decorator(func: Callable) -> Callable:
  39. # detect function name and generate message
  40. category, message = self._generate_message(
  41. deprecate_in,
  42. remove_in,
  43. f"{func.__module__}.{func.__qualname__}",
  44. addendum=addendum,
  45. )
  46. # alert developer that it's time to remove something
  47. if not category:
  48. raise DeprecatedError(message)
  49. # alert user that it's time to remove something
  50. @wraps(func)
  51. def inner(*args, **kwargs):
  52. warnings.warn(message, category, stacklevel=2 + stack)
  53. return func(*args, **kwargs)
  54. return inner
  55. return deprecated_decorator
  56. def argument(
  57. self,
  58. deprecate_in: str,
  59. remove_in: str,
  60. argument: str,
  61. *,
  62. rename: str | None = None,
  63. addendum: str | None = None,
  64. stack: int = 0,
  65. ) -> Callable[(Callable), Callable]:
  66. """Deprecation decorator for keyword arguments.
  67. :param deprecate_in: Version in which code will be marked as deprecated.
  68. :param remove_in: Version in which code is expected to be removed.
  69. :param argument: The argument to deprecate.
  70. :param rename: Optional new argument name.
  71. :param addendum: Optional additional messaging. Useful to indicate what to do instead.
  72. :param stack: Optional stacklevel increment.
  73. """
  74. def deprecated_decorator(func: Callable) -> Callable:
  75. # detect function name and generate message
  76. category, message = self._generate_message(
  77. deprecate_in,
  78. remove_in,
  79. f"{func.__module__}.{func.__qualname__}({argument})",
  80. # provide a default addendum if renaming and no addendum is provided
  81. addendum=f"Use '{rename}' instead."
  82. if rename and not addendum
  83. else addendum,
  84. )
  85. # alert developer that it's time to remove something
  86. if not category:
  87. raise DeprecatedError(message)
  88. # alert user that it's time to remove something
  89. @wraps(func)
  90. def inner(*args, **kwargs):
  91. # only warn about argument deprecations if the argument is used
  92. if argument in kwargs:
  93. warnings.warn(message, category, stacklevel=2 + stack)
  94. # rename argument deprecations as needed
  95. value = kwargs.pop(argument, None)
  96. if rename:
  97. kwargs.setdefault(rename, value)
  98. return func(*args, **kwargs)
  99. return inner
  100. return deprecated_decorator
  101. def module(
  102. self,
  103. deprecate_in: str,
  104. remove_in: str,
  105. *,
  106. addendum: str | None = None,
  107. stack: int = 0,
  108. ) -> None:
  109. """Deprecation function for modules.
  110. :param deprecate_in: Version in which code will be marked as deprecated.
  111. :param remove_in: Version in which code is expected to be removed.
  112. :param addendum: Optional additional messaging. Useful to indicate what to do instead.
  113. :param stack: Optional stacklevel increment.
  114. """
  115. self.topic(
  116. deprecate_in=deprecate_in,
  117. remove_in=remove_in,
  118. topic=self._get_module(stack)[1],
  119. addendum=addendum,
  120. stack=2 + stack,
  121. )
  122. def constant(
  123. self,
  124. deprecate_in: str,
  125. remove_in: str,
  126. constant: str,
  127. value: Any,
  128. *,
  129. addendum: str | None = None,
  130. stack: int = 0,
  131. ) -> None:
  132. """Deprecation function for module constant/global.
  133. :param deprecate_in: Version in which code will be marked as deprecated.
  134. :param remove_in: Version in which code is expected to be removed.
  135. :param constant:
  136. :param value:
  137. :param addendum: Optional additional messaging. Useful to indicate what to do instead.
  138. :param stack: Optional stacklevel increment.
  139. """
  140. # detect calling module
  141. module, fullname = self._get_module(stack)
  142. # detect function name and generate message
  143. category, message = self._generate_message(
  144. deprecate_in,
  145. remove_in,
  146. f"{fullname}.{constant}",
  147. addendum,
  148. )
  149. # alert developer that it's time to remove something
  150. if not category:
  151. raise DeprecatedError(message)
  152. # patch module level __getattr__ to alert user that it's time to remove something
  153. super_getattr = getattr(module, "__getattr__", None)
  154. def __getattr__(name: str) -> Any:
  155. if name == constant:
  156. warnings.warn(message, category, stacklevel=2 + stack)
  157. return value
  158. if super_getattr:
  159. return super_getattr(name)
  160. raise AttributeError(f"module '{fullname}' has no attribute '{name}'")
  161. module.__getattr__ = __getattr__
  162. def topic(
  163. self,
  164. deprecate_in: str,
  165. remove_in: str,
  166. *,
  167. topic: str,
  168. addendum: str | None = None,
  169. stack: int = 0,
  170. ) -> None:
  171. """Deprecation function for a topic.
  172. :param deprecate_in: Version in which code will be marked as deprecated.
  173. :param remove_in: Version in which code is expected to be removed.
  174. :param topic: The topic being deprecated.
  175. :param addendum: Optional additional messaging. Useful to indicate what to do instead.
  176. :param stack: Optional stacklevel increment.
  177. """
  178. # detect function name and generate message
  179. category, message = self._generate_message(
  180. deprecate_in, remove_in, topic, addendum
  181. )
  182. # alert developer that it's time to remove something
  183. if not category:
  184. raise DeprecatedError(message)
  185. # alert user that it's time to remove something
  186. warnings.warn(message, category, stacklevel=2 + stack)
  187. def _get_module(self, stack: int) -> tuple[ModuleType, str]:
  188. """Detect the module from which we are being called.
  189. :param stack: The stacklevel increment.
  190. :return: The module and module name.
  191. """
  192. import inspect # expensive
  193. try:
  194. frame = inspect.stack()[2 + stack]
  195. module = inspect.getmodule(frame[0])
  196. return (module, module.__name__)
  197. except (IndexError, AttributeError):
  198. raise DeprecatedError("unable to determine the calling module") from None
  199. def _generate_message(
  200. self, deprecate_in: str, remove_in: str, prefix: str, addendum: str
  201. ) -> tuple[type[Warning] | None, str]:
  202. """Deprecation decorator for functions, methods, & classes.
  203. :param deprecate_in: Version in which code will be marked as deprecated.
  204. :param remove_in: Version in which code is expected to be removed.
  205. :param prefix: The message prefix, usually the function name.
  206. :param addendum: Additional messaging. Useful to indicate what to do instead.
  207. :return: The warning category (if applicable) and the message.
  208. """
  209. deprecate_version = parse(deprecate_in)
  210. remove_version = parse(remove_in)
  211. if self._version < deprecate_version:
  212. category = PendingDeprecationWarning
  213. warning = f"is pending deprecation and will be removed in {remove_in}."
  214. elif self._version < remove_version:
  215. category = DeprecationWarning
  216. warning = f"is deprecated and will be removed in {remove_in}."
  217. else:
  218. category = None
  219. warning = f"was slated for removal in {remove_in}."
  220. return (
  221. category,
  222. " ".join(filter(None, [prefix, warning, addendum])), # message
  223. )
  224. deprecated = DeprecationHandler(__version__)