classic.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301
  1. # -*- coding: utf-8 -*-
  2. """
  3. Classic deprecation warning
  4. ===========================
  5. Classic ``@deprecated`` decorator to deprecate old python classes, functions or methods.
  6. .. _The Warnings Filter: https://docs.python.org/3/library/warnings.html#the-warnings-filter
  7. """
  8. import functools
  9. import inspect
  10. import platform
  11. import warnings
  12. import wrapt
  13. try:
  14. # If the C extension for wrapt was compiled and wrapt/_wrappers.pyd exists, then the
  15. # stack level that should be passed to warnings.warn should be 2. However, if using
  16. # a pure python wrapt, an extra stacklevel is required.
  17. import wrapt._wrappers
  18. _routine_stacklevel = 2
  19. _class_stacklevel = 2
  20. except ImportError:
  21. _routine_stacklevel = 3
  22. if platform.python_implementation() == "PyPy":
  23. _class_stacklevel = 2
  24. else:
  25. _class_stacklevel = 3
  26. string_types = (type(b''), type(u''))
  27. class ClassicAdapter(wrapt.AdapterFactory):
  28. """
  29. Classic adapter -- *for advanced usage only*
  30. This adapter is used to get the deprecation message according to the wrapped object type:
  31. class, function, standard method, static method, or class method.
  32. This is the base class of the :class:`~deprecated.sphinx.SphinxAdapter` class
  33. which is used to update the wrapped object docstring.
  34. You can also inherit this class to change the deprecation message.
  35. In the following example, we change the message into "The ... is deprecated.":
  36. .. code-block:: python
  37. import inspect
  38. from deprecated.classic import ClassicAdapter
  39. from deprecated.classic import deprecated
  40. class MyClassicAdapter(ClassicAdapter):
  41. def get_deprecated_msg(self, wrapped, instance):
  42. if instance is None:
  43. if inspect.isclass(wrapped):
  44. fmt = "The class {name} is deprecated."
  45. else:
  46. fmt = "The function {name} is deprecated."
  47. else:
  48. if inspect.isclass(instance):
  49. fmt = "The class method {name} is deprecated."
  50. else:
  51. fmt = "The method {name} is deprecated."
  52. if self.reason:
  53. fmt += " ({reason})"
  54. if self.version:
  55. fmt += " -- Deprecated since version {version}."
  56. return fmt.format(name=wrapped.__name__,
  57. reason=self.reason or "",
  58. version=self.version or "")
  59. Then, you can use your ``MyClassicAdapter`` class like this in your source code:
  60. .. code-block:: python
  61. @deprecated(reason="use another function", adapter_cls=MyClassicAdapter)
  62. def some_old_function(x, y):
  63. return x + y
  64. """
  65. def __init__(self, reason="", version="", action=None, category=DeprecationWarning, extra_stacklevel=0):
  66. """
  67. Construct a wrapper adapter.
  68. :type reason: str
  69. :param reason:
  70. Reason message which documents the deprecation in your library (can be omitted).
  71. :type version: str
  72. :param version:
  73. Version of your project which deprecates this feature.
  74. If you follow the `Semantic Versioning <https://semver.org/>`_,
  75. the version number has the format "MAJOR.MINOR.PATCH".
  76. :type action: Literal["default", "error", "ignore", "always", "module", "once"]
  77. :param action:
  78. A warning filter used to activate or not the deprecation warning.
  79. Can be one of "error", "ignore", "always", "default", "module", or "once".
  80. If ``None`` or empty, the global filtering mechanism is used.
  81. See: `The Warnings Filter`_ in the Python documentation.
  82. :type category: Type[Warning]
  83. :param category:
  84. The warning category to use for the deprecation warning.
  85. By default, the category class is :class:`~DeprecationWarning`,
  86. you can inherit this class to define your own deprecation warning category.
  87. :type extra_stacklevel: int
  88. :param extra_stacklevel:
  89. Number of additional stack levels to consider instrumentation rather than user code.
  90. With the default value of 0, the warning refers to where the class was instantiated
  91. or the function was called.
  92. .. versionchanged:: 1.2.15
  93. Add the *extra_stacklevel* parameter.
  94. """
  95. self.reason = reason or ""
  96. self.version = version or ""
  97. self.action = action
  98. self.category = category
  99. self.extra_stacklevel = extra_stacklevel
  100. super(ClassicAdapter, self).__init__()
  101. def get_deprecated_msg(self, wrapped, instance):
  102. """
  103. Get the deprecation warning message for the user.
  104. :param wrapped: Wrapped class or function.
  105. :param instance: The object to which the wrapped function was bound when it was called.
  106. :return: The warning message.
  107. """
  108. if instance is None:
  109. if inspect.isclass(wrapped):
  110. fmt = "Call to deprecated class {name}."
  111. else:
  112. fmt = "Call to deprecated function (or staticmethod) {name}."
  113. else:
  114. if inspect.isclass(instance):
  115. fmt = "Call to deprecated class method {name}."
  116. else:
  117. fmt = "Call to deprecated method {name}."
  118. if self.reason:
  119. fmt += " ({reason})"
  120. if self.version:
  121. fmt += " -- Deprecated since version {version}."
  122. return fmt.format(name=wrapped.__name__, reason=self.reason or "", version=self.version or "")
  123. def __call__(self, wrapped):
  124. """
  125. Decorate your class or function.
  126. :param wrapped: Wrapped class or function.
  127. :return: the decorated class or function.
  128. .. versionchanged:: 1.2.4
  129. Don't pass arguments to :meth:`object.__new__` (other than *cls*).
  130. .. versionchanged:: 1.2.8
  131. The warning filter is not set if the *action* parameter is ``None`` or empty.
  132. """
  133. if inspect.isclass(wrapped):
  134. old_new1 = wrapped.__new__
  135. def wrapped_cls(cls, *args, **kwargs):
  136. msg = self.get_deprecated_msg(wrapped, None)
  137. stacklevel = _class_stacklevel + self.extra_stacklevel
  138. if self.action:
  139. with warnings.catch_warnings():
  140. warnings.simplefilter(self.action, self.category)
  141. warnings.warn(msg, category=self.category, stacklevel=stacklevel)
  142. else:
  143. warnings.warn(msg, category=self.category, stacklevel=stacklevel)
  144. if old_new1 is object.__new__:
  145. return old_new1(cls)
  146. # actually, we don't know the real signature of *old_new1*
  147. return old_new1(cls, *args, **kwargs)
  148. wrapped.__new__ = staticmethod(wrapped_cls)
  149. elif inspect.isroutine(wrapped):
  150. @wrapt.decorator
  151. def wrapper_function(wrapped_, instance_, args_, kwargs_):
  152. msg = self.get_deprecated_msg(wrapped_, instance_)
  153. stacklevel = _routine_stacklevel + self.extra_stacklevel
  154. if self.action:
  155. with warnings.catch_warnings():
  156. warnings.simplefilter(self.action, self.category)
  157. warnings.warn(msg, category=self.category, stacklevel=stacklevel)
  158. else:
  159. warnings.warn(msg, category=self.category, stacklevel=stacklevel)
  160. return wrapped_(*args_, **kwargs_)
  161. return wrapper_function(wrapped)
  162. else:
  163. raise TypeError(repr(type(wrapped)))
  164. return wrapped
  165. def deprecated(*args, **kwargs):
  166. """
  167. This is a decorator which can be used to mark functions
  168. as deprecated. It will result in a warning being emitted
  169. when the function is used.
  170. **Classic usage:**
  171. To use this, decorate your deprecated function with **@deprecated** decorator:
  172. .. code-block:: python
  173. from deprecated import deprecated
  174. @deprecated
  175. def some_old_function(x, y):
  176. return x + y
  177. You can also decorate a class or a method:
  178. .. code-block:: python
  179. from deprecated import deprecated
  180. class SomeClass(object):
  181. @deprecated
  182. def some_old_method(self, x, y):
  183. return x + y
  184. @deprecated
  185. class SomeOldClass(object):
  186. pass
  187. You can give a *reason* message to help the developer to choose another function/class,
  188. and a *version* number to specify the starting version number of the deprecation.
  189. .. code-block:: python
  190. from deprecated import deprecated
  191. @deprecated(reason="use another function", version='1.2.0')
  192. def some_old_function(x, y):
  193. return x + y
  194. The *category* keyword argument allow you to specify the deprecation warning class of your choice.
  195. By default, :exc:`DeprecationWarning` is used, but you can choose :exc:`FutureWarning`,
  196. :exc:`PendingDeprecationWarning` or a custom subclass.
  197. .. code-block:: python
  198. from deprecated import deprecated
  199. @deprecated(category=PendingDeprecationWarning)
  200. def some_old_function(x, y):
  201. return x + y
  202. The *action* keyword argument allow you to locally change the warning filtering.
  203. *action* can be one of "error", "ignore", "always", "default", "module", or "once".
  204. If ``None``, empty or missing, the global filtering mechanism is used.
  205. See: `The Warnings Filter`_ in the Python documentation.
  206. .. code-block:: python
  207. from deprecated import deprecated
  208. @deprecated(action="error")
  209. def some_old_function(x, y):
  210. return x + y
  211. The *extra_stacklevel* keyword argument allows you to specify additional stack levels
  212. to consider instrumentation rather than user code. With the default value of 0, the
  213. warning refers to where the class was instantiated or the function was called.
  214. """
  215. if args and isinstance(args[0], string_types):
  216. kwargs['reason'] = args[0]
  217. args = args[1:]
  218. if args and not callable(args[0]):
  219. raise TypeError(repr(type(args[0])))
  220. if args:
  221. adapter_cls = kwargs.pop('adapter_cls', ClassicAdapter)
  222. adapter = adapter_cls(**kwargs)
  223. wrapped = args[0]
  224. return adapter(wrapped)
  225. return functools.partial(deprecated, **kwargs)