deprecation.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409
  1. import contextlib
  2. import functools
  3. import inspect
  4. import warnings
  5. class MatplotlibDeprecationWarning(UserWarning):
  6. """
  7. A class for issuing deprecation warnings for Matplotlib users.
  8. In light of the fact that Python builtin DeprecationWarnings are ignored
  9. by default as of Python 2.7 (see link below), this class was put in to
  10. allow for the signaling of deprecation, but via UserWarnings which are not
  11. ignored by default.
  12. https://docs.python.org/dev/whatsnew/2.7.html#the-future-for-python-2-x
  13. """
  14. mplDeprecation = MatplotlibDeprecationWarning
  15. """mplDeprecation is deprecated. Use MatplotlibDeprecationWarning instead."""
  16. def _generate_deprecation_warning(
  17. since, message='', name='', alternative='', pending=False, obj_type='',
  18. addendum='', *, removal=''):
  19. if pending:
  20. if removal:
  21. raise ValueError(
  22. "A pending deprecation cannot have a scheduled removal")
  23. else:
  24. if removal:
  25. removal = "in {}".format(removal)
  26. else:
  27. removal = {"2.2": "in 3.1", "3.0": "in 3.2", "3.1": "in 3.3"}.get(
  28. since, "two minor releases later")
  29. if not message:
  30. message = (
  31. "\nThe %(name)s %(obj_type)s"
  32. + (" will be deprecated in a future version"
  33. if pending else
  34. (" was deprecated in Matplotlib %(since)s"
  35. + (" and will be removed %(removal)s"
  36. if removal else
  37. "")))
  38. + "."
  39. + (" Use %(alternative)s instead." if alternative else "")
  40. + (" %(addendum)s" if addendum else ""))
  41. warning_cls = (PendingDeprecationWarning if pending
  42. else MatplotlibDeprecationWarning)
  43. return warning_cls(message % dict(
  44. func=name, name=name, obj_type=obj_type, since=since, removal=removal,
  45. alternative=alternative, addendum=addendum))
  46. def warn_deprecated(
  47. since, *, message='', name='', alternative='', pending=False,
  48. obj_type='', addendum='', removal=''):
  49. """
  50. Used to display deprecation in a standard way.
  51. Parameters
  52. ----------
  53. since : str
  54. The release at which this API became deprecated.
  55. message : str, optional
  56. Override the default deprecation message. The format
  57. specifier `%(name)s` may be used for the name of the function,
  58. and `%(alternative)s` may be used in the deprecation message
  59. to insert the name of an alternative to the deprecated
  60. function. `%(obj_type)s` may be used to insert a friendly name
  61. for the type of object being deprecated.
  62. name : str, optional
  63. The name of the deprecated object.
  64. alternative : str, optional
  65. An alternative API that the user may use in place of the deprecated
  66. API. The deprecation warning will tell the user about this alternative
  67. if provided.
  68. pending : bool, optional
  69. If True, uses a PendingDeprecationWarning instead of a
  70. DeprecationWarning. Cannot be used together with *removal*.
  71. obj_type : str, optional
  72. The object type being deprecated.
  73. addendum : str, optional
  74. Additional text appended directly to the final message.
  75. removal : str, optional
  76. The expected removal version. With the default (an empty string), a
  77. removal version is automatically computed from *since*. Set to other
  78. Falsy values to not schedule a removal date. Cannot be used together
  79. with *pending*.
  80. Examples
  81. --------
  82. Basic example::
  83. # To warn of the deprecation of "matplotlib.name_of_module"
  84. warn_deprecated('1.4.0', name='matplotlib.name_of_module',
  85. obj_type='module')
  86. """
  87. warning = _generate_deprecation_warning(
  88. since, message, name, alternative, pending, obj_type, addendum,
  89. removal=removal)
  90. from . import _warn_external
  91. _warn_external(warning)
  92. def deprecated(since, *, message='', name='', alternative='', pending=False,
  93. obj_type=None, addendum='', removal=''):
  94. """
  95. Decorator to mark a function, a class, or a property as deprecated.
  96. When deprecating a classmethod, a staticmethod, or a property, the
  97. ``@deprecated`` decorator should go *under* the ``@classmethod``, etc.
  98. decorator (i.e., `deprecated` should directly decorate the underlying
  99. callable).
  100. Parameters
  101. ----------
  102. since : str
  103. The release at which this API became deprecated. This is
  104. required.
  105. message : str, optional
  106. Override the default deprecation message. The format
  107. specifier `%(name)s` may be used for the name of the object,
  108. and `%(alternative)s` may be used in the deprecation message
  109. to insert the name of an alternative to the deprecated
  110. object.
  111. name : str, optional
  112. The name used in the deprecation message; if not provided, the name
  113. is automatically determined from the deprecated object.
  114. alternative : str, optional
  115. An alternative API that the user may use in place of the deprecated
  116. API. The deprecation warning will tell the user about this alternative
  117. if provided.
  118. pending : bool, optional
  119. If True, uses a PendingDeprecationWarning instead of a
  120. DeprecationWarning. Cannot be used together with *removal*.
  121. obj_type : str, optional
  122. The object type being deprecated; by default, 'class' if decorating
  123. a class, 'attribute' if decorating a property, 'function' otherwise.
  124. addendum : str, optional
  125. Additional text appended directly to the final message.
  126. removal : str, optional
  127. The expected removal version. With the default (an empty string), a
  128. removal version is automatically computed from *since*. Set to other
  129. Falsy values to not schedule a removal date. Cannot be used together
  130. with *pending*.
  131. Examples
  132. --------
  133. Basic example::
  134. @deprecated('1.4.0')
  135. def the_function_to_deprecate():
  136. pass
  137. """
  138. def deprecate(obj, message=message, name=name, alternative=alternative,
  139. pending=pending, obj_type=obj_type, addendum=addendum):
  140. if isinstance(obj, type):
  141. if obj_type is None:
  142. obj_type = "class"
  143. func = obj.__init__
  144. name = name or obj.__name__
  145. old_doc = obj.__doc__
  146. def finalize(wrapper, new_doc):
  147. try:
  148. obj.__doc__ = new_doc
  149. except AttributeError: # Can't set on some extension objects.
  150. pass
  151. obj.__init__ = wrapper
  152. return obj
  153. elif isinstance(obj, property):
  154. obj_type = "attribute"
  155. func = None
  156. name = name or obj.fget.__name__
  157. old_doc = obj.__doc__
  158. class _deprecated_property(property):
  159. def __get__(self, instance, owner):
  160. if instance is not None:
  161. from . import _warn_external
  162. _warn_external(warning)
  163. return super().__get__(instance, owner)
  164. def __set__(self, instance, value):
  165. if instance is not None:
  166. from . import _warn_external
  167. _warn_external(warning)
  168. return super().__set__(instance, value)
  169. def __delete__(self, instance):
  170. if instance is not None:
  171. from . import _warn_external
  172. _warn_external(warning)
  173. return super().__delete__(instance)
  174. def finalize(_, new_doc):
  175. return _deprecated_property(
  176. fget=obj.fget, fset=obj.fset, fdel=obj.fdel, doc=new_doc)
  177. else:
  178. if obj_type is None:
  179. obj_type = "function"
  180. func = obj
  181. name = name or obj.__name__
  182. old_doc = func.__doc__
  183. def finalize(wrapper, new_doc):
  184. wrapper = functools.wraps(func)(wrapper)
  185. wrapper.__doc__ = new_doc
  186. return wrapper
  187. warning = _generate_deprecation_warning(
  188. since, message, name, alternative, pending, obj_type, addendum,
  189. removal=removal)
  190. def wrapper(*args, **kwargs):
  191. from . import _warn_external
  192. _warn_external(warning)
  193. return func(*args, **kwargs)
  194. old_doc = inspect.cleandoc(old_doc or '').strip('\n')
  195. notes_header = '\nNotes\n-----'
  196. new_doc = (f"[*Deprecated*] {old_doc}\n"
  197. f"{notes_header if notes_header not in old_doc else ''}\n"
  198. f".. deprecated:: {since}\n"
  199. f" {message.strip()}")
  200. if not old_doc:
  201. # This is to prevent a spurious 'unexpected unindent' warning from
  202. # docutils when the original docstring was blank.
  203. new_doc += r'\ '
  204. return finalize(wrapper, new_doc)
  205. return deprecate
  206. def _rename_parameter(since, old, new, func=None):
  207. """
  208. Decorator indicating that parameter *old* of *func* is renamed to *new*.
  209. The actual implementation of *func* should use *new*, not *old*. If *old*
  210. is passed to *func*, a DeprecationWarning is emitted, and its value is
  211. used, even if *new* is also passed by keyword (this is to simplify pyplot
  212. wrapper functions, which always pass *new* explicitly to the Axes method).
  213. If *new* is also passed but positionally, a TypeError will be raised by the
  214. underlying function during argument binding.
  215. Examples
  216. --------
  217. ::
  218. @_rename_parameter("3.1", "bad_name", "good_name")
  219. def func(good_name): ...
  220. """
  221. if func is None:
  222. return functools.partial(_rename_parameter, since, old, new)
  223. signature = inspect.signature(func)
  224. assert old not in signature.parameters, (
  225. f"Matplotlib internal error: {old!r} cannot be a parameter for "
  226. f"{func.__name__}()")
  227. assert new in signature.parameters, (
  228. f"Matplotlib internal error: {new!r} must be a parameter for "
  229. f"{func.__name__}()")
  230. @functools.wraps(func)
  231. def wrapper(*args, **kwargs):
  232. if old in kwargs:
  233. warn_deprecated(
  234. since, message=f"The {old!r} parameter of {func.__name__}() "
  235. f"has been renamed {new!r} since Matplotlib {since}; support "
  236. f"for the old name will be dropped %(removal)s.")
  237. kwargs[new] = kwargs.pop(old)
  238. return func(*args, **kwargs)
  239. # wrapper() must keep the same documented signature as func(): if we
  240. # instead made both *old* and *new* appear in wrapper()'s signature, they
  241. # would both show up in the pyplot function for an Axes method as well and
  242. # pyplot would explicitly pass both arguments to the Axes method.
  243. return wrapper
  244. class _deprecated_parameter_class:
  245. def __repr__(self):
  246. return "<deprecated parameter>"
  247. _deprecated_parameter = _deprecated_parameter_class()
  248. def _delete_parameter(since, name, func=None):
  249. """
  250. Decorator indicating that parameter *name* of *func* is being deprecated.
  251. The actual implementation of *func* should keep the *name* parameter in its
  252. signature.
  253. Parameters that come after the deprecated parameter effectively become
  254. keyword-only (as they cannot be passed positionally without triggering the
  255. DeprecationWarning on the deprecated parameter), and should be marked as
  256. such after the deprecation period has passed and the deprecated parameter
  257. is removed.
  258. Examples
  259. --------
  260. ::
  261. @_delete_parameter("3.1", "unused")
  262. def func(used_arg, other_arg, unused, more_args): ...
  263. """
  264. if func is None:
  265. return functools.partial(_delete_parameter, since, name)
  266. signature = inspect.signature(func)
  267. assert name in signature.parameters, (
  268. f"Matplotlib internal error: {name!r} must be a parameter for "
  269. f"{func.__name__}()")
  270. func.__signature__ = signature.replace(parameters=[
  271. param.replace(default=_deprecated_parameter) if param.name == name
  272. else param
  273. for param in signature.parameters.values()])
  274. @functools.wraps(func)
  275. def wrapper(*args, **kwargs):
  276. arguments = func.__signature__.bind(*args, **kwargs).arguments
  277. # We cannot just check `name not in arguments` because the pyplot
  278. # wrappers always pass all arguments explicitly.
  279. if name in arguments and arguments[name] != _deprecated_parameter:
  280. warn_deprecated(
  281. since, message=f"The {name!r} parameter of {func.__name__}() "
  282. f"is deprecated since Matplotlib {since} and will be removed "
  283. f"%(removal)s. If any parameter follows {name!r}, they "
  284. f"should be pass as keyword, not positionally.")
  285. return func(*args, **kwargs)
  286. return wrapper
  287. def _make_keyword_only(since, name, func=None):
  288. """
  289. Decorator indicating that passing parameter *name* (or any of the following
  290. ones) positionally to *func* is being deprecated.
  291. Note that this decorator **cannot** be applied to a function that has a
  292. pyplot-level wrapper, as the wrapper always pass all arguments by keyword.
  293. If it is used, users will see spurious DeprecationWarnings every time they
  294. call the pyplot wrapper.
  295. """
  296. if func is None:
  297. return functools.partial(_make_keyword_only, since, name)
  298. signature = inspect.signature(func)
  299. POK = inspect.Parameter.POSITIONAL_OR_KEYWORD
  300. KWO = inspect.Parameter.KEYWORD_ONLY
  301. assert (name in signature.parameters
  302. and signature.parameters[name].kind == POK), (
  303. f"Matplotlib internal error: {name!r} must be a positional-or-keyword "
  304. f"parameter for {func.__name__}()")
  305. names = [*signature.parameters]
  306. kwonly = [name for name in names[names.index(name):]
  307. if signature.parameters[name].kind == POK]
  308. func.__signature__ = signature.replace(parameters=[
  309. param.replace(kind=KWO) if param.name in kwonly else param
  310. for param in signature.parameters.values()])
  311. @functools.wraps(func)
  312. def wrapper(*args, **kwargs):
  313. bound = signature.bind(*args, **kwargs)
  314. if name in bound.arguments and name not in kwargs:
  315. warn_deprecated(
  316. since, message="Passing the %(name)s %(obj_type)s "
  317. "positionally is deprecated since Matplotlib %(since)s; the "
  318. "parameter will become keyword-only %(removal)s.",
  319. name=name, obj_type=f"parameter of {func.__name__}()")
  320. return func(*args, **kwargs)
  321. return wrapper
  322. @contextlib.contextmanager
  323. def _suppress_matplotlib_deprecation_warning():
  324. with warnings.catch_warnings():
  325. warnings.simplefilter("ignore", MatplotlibDeprecationWarning)
  326. yield