deprecation.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497
  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 is deprecated. Use MatplotlibDeprecationWarning instead.
  15. mplDeprecation = MatplotlibDeprecationWarning
  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. Display a standardized deprecation.
  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 ``%(since)s``,
  57. ``%(name)s``, ``%(alternative)s``, ``%(obj_type)s``, ``%(addendum)s``,
  58. and ``%(removal)s`` format specifiers will be replaced by the values
  59. of the respective arguments passed to this function.
  60. name : str, optional
  61. The name of the deprecated object.
  62. alternative : str, optional
  63. An alternative API that the user may use in place of the deprecated
  64. API. The deprecation warning will tell the user about this alternative
  65. if provided.
  66. pending : bool, optional
  67. If True, uses a PendingDeprecationWarning instead of a
  68. DeprecationWarning. Cannot be used together with *removal*.
  69. obj_type : str, optional
  70. The object type being deprecated.
  71. addendum : str, optional
  72. Additional text appended directly to the final message.
  73. removal : str, optional
  74. The expected removal version. With the default (an empty string), a
  75. removal version is automatically computed from *since*. Set to other
  76. Falsy values to not schedule a removal date. Cannot be used together
  77. with *pending*.
  78. Examples
  79. --------
  80. Basic example::
  81. # To warn of the deprecation of "matplotlib.name_of_module"
  82. warn_deprecated('1.4.0', name='matplotlib.name_of_module',
  83. obj_type='module')
  84. """
  85. warning = _generate_deprecation_warning(
  86. since, message, name, alternative, pending, obj_type, addendum,
  87. removal=removal)
  88. from . import _warn_external
  89. _warn_external(warning, category=MatplotlibDeprecationWarning)
  90. def deprecated(since, *, message='', name='', alternative='', pending=False,
  91. obj_type=None, addendum='', removal=''):
  92. """
  93. Decorator to mark a function, a class, or a property as deprecated.
  94. When deprecating a classmethod, a staticmethod, or a property, the
  95. ``@deprecated`` decorator should go *under* ``@classmethod`` and
  96. ``@staticmethod`` (i.e., `deprecated` should directly decorate the
  97. underlying callable), but *over* ``@property``.
  98. Parameters
  99. ----------
  100. since : str
  101. The release at which this API became deprecated.
  102. message : str, optional
  103. Override the default deprecation message. The ``%(since)s``,
  104. ``%(name)s``, ``%(alternative)s``, ``%(obj_type)s``, ``%(addendum)s``,
  105. and ``%(removal)s`` format specifiers will be replaced by the values
  106. of the respective arguments passed to this function.
  107. name : str, optional
  108. The name used in the deprecation message; if not provided, the name
  109. is automatically determined from the deprecated object.
  110. alternative : str, optional
  111. An alternative API that the user may use in place of the deprecated
  112. API. The deprecation warning will tell the user about this alternative
  113. if provided.
  114. pending : bool, optional
  115. If True, uses a PendingDeprecationWarning instead of a
  116. DeprecationWarning. Cannot be used together with *removal*.
  117. obj_type : str, optional
  118. The object type being deprecated; by default, 'class' if decorating
  119. a class, 'attribute' if decorating a property, 'function' otherwise.
  120. addendum : str, optional
  121. Additional text appended directly to the final message.
  122. removal : str, optional
  123. The expected removal version. With the default (an empty string), a
  124. removal version is automatically computed from *since*. Set to other
  125. Falsy values to not schedule a removal date. Cannot be used together
  126. with *pending*.
  127. Examples
  128. --------
  129. Basic example::
  130. @deprecated('1.4.0')
  131. def the_function_to_deprecate():
  132. pass
  133. """
  134. def deprecate(obj, message=message, name=name, alternative=alternative,
  135. pending=pending, obj_type=obj_type, addendum=addendum):
  136. if isinstance(obj, type):
  137. if obj_type is None:
  138. obj_type = "class"
  139. func = obj.__init__
  140. name = name or obj.__name__
  141. old_doc = obj.__doc__
  142. def finalize(wrapper, new_doc):
  143. try:
  144. obj.__doc__ = new_doc
  145. except AttributeError: # Can't set on some extension objects.
  146. pass
  147. obj.__init__ = functools.wraps(obj.__init__)(wrapper)
  148. return obj
  149. elif isinstance(obj, property):
  150. obj_type = "attribute"
  151. func = None
  152. name = name or obj.fget.__name__
  153. old_doc = obj.__doc__
  154. class _deprecated_property(property):
  155. def __get__(self, instance, owner):
  156. if instance is not None:
  157. from . import _warn_external
  158. _warn_external(warning)
  159. return super().__get__(instance, owner)
  160. def __set__(self, instance, value):
  161. if instance is not None:
  162. from . import _warn_external
  163. _warn_external(warning)
  164. return super().__set__(instance, value)
  165. def __delete__(self, instance):
  166. if instance is not None:
  167. from . import _warn_external
  168. _warn_external(warning)
  169. return super().__delete__(instance)
  170. def finalize(_, new_doc):
  171. return _deprecated_property(
  172. fget=obj.fget, fset=obj.fset, fdel=obj.fdel, doc=new_doc)
  173. else:
  174. if obj_type is None:
  175. obj_type = "function"
  176. func = obj
  177. name = name or obj.__name__
  178. old_doc = func.__doc__
  179. def finalize(wrapper, new_doc):
  180. wrapper = functools.wraps(func)(wrapper)
  181. wrapper.__doc__ = new_doc
  182. return wrapper
  183. warning = _generate_deprecation_warning(
  184. since, message, name, alternative, pending, obj_type, addendum,
  185. removal=removal)
  186. def wrapper(*args, **kwargs):
  187. from . import _warn_external
  188. _warn_external(warning)
  189. return func(*args, **kwargs)
  190. old_doc = inspect.cleandoc(old_doc or '').strip('\n')
  191. notes_header = '\nNotes\n-----'
  192. new_doc = (f"[*Deprecated*] {old_doc}\n"
  193. f"{notes_header if notes_header not in old_doc else ''}\n"
  194. f".. deprecated:: {since}\n"
  195. f" {message.strip()}")
  196. if not old_doc:
  197. # This is to prevent a spurious 'unexpected unindent' warning from
  198. # docutils when the original docstring was blank.
  199. new_doc += r'\ '
  200. return finalize(wrapper, new_doc)
  201. return deprecate
  202. class _deprecate_privatize_attribute:
  203. """
  204. Helper to deprecate public access to an attribute.
  205. This helper should only be used at class scope, as follows::
  206. class Foo:
  207. attr = _deprecate_privatize_attribute(*args, **kwargs)
  208. where *all* parameters are forwarded to `deprecated`. This form makes
  209. ``attr`` a property which forwards access to ``self._attr`` (same name but
  210. with a leading underscore), with a deprecation warning. Note that the
  211. attribute name is derived from *the name this helper is assigned to*.
  212. """
  213. def __init__(self, *args, **kwargs):
  214. self.deprecator = deprecated(*args, **kwargs)
  215. def __set_name__(self, owner, name):
  216. setattr(owner, name, self.deprecator(
  217. property(lambda self: getattr(self, f"_{name}")), name=name))
  218. def _rename_parameter(since, old, new, func=None):
  219. """
  220. Decorator indicating that parameter *old* of *func* is renamed to *new*.
  221. The actual implementation of *func* should use *new*, not *old*. If *old*
  222. is passed to *func*, a DeprecationWarning is emitted, and its value is
  223. used, even if *new* is also passed by keyword (this is to simplify pyplot
  224. wrapper functions, which always pass *new* explicitly to the Axes method).
  225. If *new* is also passed but positionally, a TypeError will be raised by the
  226. underlying function during argument binding.
  227. Examples
  228. --------
  229. ::
  230. @_rename_parameter("3.1", "bad_name", "good_name")
  231. def func(good_name): ...
  232. """
  233. if func is None:
  234. return functools.partial(_rename_parameter, since, old, new)
  235. signature = inspect.signature(func)
  236. assert old not in signature.parameters, (
  237. f"Matplotlib internal error: {old!r} cannot be a parameter for "
  238. f"{func.__name__}()")
  239. assert new in signature.parameters, (
  240. f"Matplotlib internal error: {new!r} must be a parameter for "
  241. f"{func.__name__}()")
  242. @functools.wraps(func)
  243. def wrapper(*args, **kwargs):
  244. if old in kwargs:
  245. warn_deprecated(
  246. since, message=f"The {old!r} parameter of {func.__name__}() "
  247. f"has been renamed {new!r} since Matplotlib {since}; support "
  248. f"for the old name will be dropped %(removal)s.")
  249. kwargs[new] = kwargs.pop(old)
  250. return func(*args, **kwargs)
  251. # wrapper() must keep the same documented signature as func(): if we
  252. # instead made both *old* and *new* appear in wrapper()'s signature, they
  253. # would both show up in the pyplot function for an Axes method as well and
  254. # pyplot would explicitly pass both arguments to the Axes method.
  255. return wrapper
  256. class _deprecated_parameter_class:
  257. def __repr__(self):
  258. return "<deprecated parameter>"
  259. _deprecated_parameter = _deprecated_parameter_class()
  260. def _delete_parameter(since, name, func=None, **kwargs):
  261. """
  262. Decorator indicating that parameter *name* of *func* is being deprecated.
  263. The actual implementation of *func* should keep the *name* parameter in its
  264. signature, or accept a ``**kwargs`` argument (through which *name* would be
  265. passed).
  266. Parameters that come after the deprecated parameter effectively become
  267. keyword-only (as they cannot be passed positionally without triggering the
  268. DeprecationWarning on the deprecated parameter), and should be marked as
  269. such after the deprecation period has passed and the deprecated parameter
  270. is removed.
  271. Parameters other than *since*, *name*, and *func* are keyword-only and
  272. forwarded to `.warn_deprecated`.
  273. Examples
  274. --------
  275. ::
  276. @_delete_parameter("3.1", "unused")
  277. def func(used_arg, other_arg, unused, more_args): ...
  278. """
  279. if func is None:
  280. return functools.partial(_delete_parameter, since, name, **kwargs)
  281. signature = inspect.signature(func)
  282. # Name of `**kwargs` parameter of the decorated function, typically
  283. # "kwargs" if such a parameter exists, or None if the decorated function
  284. # doesn't accept `**kwargs`.
  285. kwargs_name = next((param.name for param in signature.parameters.values()
  286. if param.kind == inspect.Parameter.VAR_KEYWORD), None)
  287. if name in signature.parameters:
  288. kind = signature.parameters[name].kind
  289. is_varargs = kind is inspect.Parameter.VAR_POSITIONAL
  290. is_varkwargs = kind is inspect.Parameter.VAR_KEYWORD
  291. if not is_varargs and not is_varkwargs:
  292. func.__signature__ = signature = signature.replace(parameters=[
  293. param.replace(default=_deprecated_parameter)
  294. if param.name == name else param
  295. for param in signature.parameters.values()])
  296. else:
  297. is_varargs = is_varkwargs = False
  298. assert kwargs_name, (
  299. f"Matplotlib internal error: {name!r} must be a parameter for "
  300. f"{func.__name__}()")
  301. addendum = kwargs.pop('addendum', None)
  302. @functools.wraps(func)
  303. def wrapper(*inner_args, **inner_kwargs):
  304. arguments = signature.bind(*inner_args, **inner_kwargs).arguments
  305. if is_varargs and arguments.get(name):
  306. warn_deprecated(
  307. since, message=f"Additional positional arguments to "
  308. f"{func.__name__}() are deprecated since %(since)s and "
  309. f"support for them will be removed %(removal)s.")
  310. elif is_varkwargs and arguments.get(name):
  311. warn_deprecated(
  312. since, message=f"Additional keyword arguments to "
  313. f"{func.__name__}() are deprecated since %(since)s and "
  314. f"support for them will be removed %(removal)s.")
  315. # We cannot just check `name not in arguments` because the pyplot
  316. # wrappers always pass all arguments explicitly.
  317. elif any(name in d and d[name] != _deprecated_parameter
  318. for d in [arguments, arguments.get(kwargs_name, {})]):
  319. deprecation_addendum = (
  320. f"If any parameter follows {name!r}, they should be passed as "
  321. f"keyword, not positionally.")
  322. warn_deprecated(
  323. since,
  324. name=repr(name),
  325. obj_type=f"parameter of {func.__name__}()",
  326. addendum=(addendum + " " + deprecation_addendum) if addendum
  327. else deprecation_addendum,
  328. **kwargs)
  329. return func(*inner_args, **inner_kwargs)
  330. return wrapper
  331. def _make_keyword_only(since, name, func=None):
  332. """
  333. Decorator indicating that passing parameter *name* (or any of the following
  334. ones) positionally to *func* is being deprecated.
  335. """
  336. if func is None:
  337. return functools.partial(_make_keyword_only, since, name)
  338. signature = inspect.signature(func)
  339. POK = inspect.Parameter.POSITIONAL_OR_KEYWORD
  340. KWO = inspect.Parameter.KEYWORD_ONLY
  341. assert (name in signature.parameters
  342. and signature.parameters[name].kind == POK), (
  343. f"Matplotlib internal error: {name!r} must be a positional-or-keyword "
  344. f"parameter for {func.__name__}()")
  345. names = [*signature.parameters]
  346. kwonly = [name for name in names[names.index(name):]
  347. if signature.parameters[name].kind == POK]
  348. func.__signature__ = signature.replace(parameters=[
  349. param.replace(kind=KWO) if param.name in kwonly else param
  350. for param in signature.parameters.values()])
  351. @functools.wraps(func)
  352. def wrapper(*args, **kwargs):
  353. # Don't use signature.bind here, as it would fail when stacked with
  354. # _rename_parameter and an "old" argument name is passed in
  355. # (signature.bind would fail, but the actual call would succeed).
  356. idx = [*func.__signature__.parameters].index(name)
  357. if len(args) > idx:
  358. warn_deprecated(
  359. since, message="Passing the %(name)s %(obj_type)s "
  360. "positionally is deprecated since Matplotlib %(since)s; the "
  361. "parameter will become keyword-only %(removal)s.",
  362. name=name, obj_type=f"parameter of {func.__name__}()")
  363. return func(*args, **kwargs)
  364. return wrapper
  365. def _deprecate_method_override(method, obj, *, allow_empty=False, **kwargs):
  366. """
  367. Return ``obj.method`` with a deprecation if it was overridden, else None.
  368. Parameters
  369. ----------
  370. method
  371. An unbound method, i.e. an expression of the form
  372. ``Class.method_name``. Remember that within the body of a method, one
  373. can always use ``__class__`` to refer to the class that is currently
  374. being defined.
  375. obj
  376. An object of the class where *method* is defined.
  377. allow_empty : bool, default: False
  378. Whether to allow overrides by "empty" methods without emitting a
  379. warning.
  380. **kwargs
  381. Additional parameters passed to `warn_deprecated` to generate the
  382. deprecation warning; must at least include the "since" key.
  383. """
  384. def empty(): pass
  385. def empty_with_docstring(): """doc"""
  386. name = method.__name__
  387. bound_method = getattr(obj, name)
  388. if (bound_method != method.__get__(obj)
  389. and (not allow_empty
  390. or (getattr(getattr(bound_method, "__code__", None),
  391. "co_code", None)
  392. not in [empty.__code__.co_code,
  393. empty_with_docstring.__code__.co_code]))):
  394. warn_deprecated(**{"name": name, "obj_type": "method", **kwargs})
  395. return bound_method
  396. return None
  397. @contextlib.contextmanager
  398. def _suppress_matplotlib_deprecation_warning():
  399. with warnings.catch_warnings():
  400. warnings.simplefilter("ignore", MatplotlibDeprecationWarning)
  401. yield