1"""
2Helper functions for deprecating parts of the Matplotlib API.
3
4This documentation is only relevant for Matplotlib developers, not for users.
5
6.. warning::
7
8 This module is for internal use only. Do not use it in your own code.
9 We may change the API at any time with no warning.
10
11"""
12
13import contextlib
14import functools
15import inspect
16import math
17import warnings
18
19
20class MatplotlibDeprecationWarning(DeprecationWarning):
21 """A class for issuing deprecation warnings for Matplotlib users."""
22
23
24def _generate_deprecation_warning(
25 since, message='', name='', alternative='', pending=False, obj_type='',
26 addendum='', *, removal=''):
27 if pending:
28 if removal:
29 raise ValueError(
30 "A pending deprecation cannot have a scheduled removal")
31 else:
32 if not removal:
33 macro, meso, *_ = since.split('.')
34 removal = f'{macro}.{int(meso) + 2}'
35 removal = f"in {removal}"
36 if not message:
37 message = (
38 ("The %(name)s %(obj_type)s" if obj_type else "%(name)s")
39 + (" will be deprecated in a future version"
40 if pending else
41 " was deprecated in Matplotlib %(since)s and will be removed %(removal)s"
42 )
43 + "."
44 + (" Use %(alternative)s instead." if alternative else "")
45 + (" %(addendum)s" if addendum else ""))
46 warning_cls = (PendingDeprecationWarning if pending
47 else MatplotlibDeprecationWarning)
48 return warning_cls(message % dict(
49 func=name, name=name, obj_type=obj_type, since=since, removal=removal,
50 alternative=alternative, addendum=addendum))
51
52
53def warn_deprecated(
54 since, *, message='', name='', alternative='', pending=False,
55 obj_type='', addendum='', removal=''):
56 """
57 Display a standardized deprecation.
58
59 Parameters
60 ----------
61 since : str
62 The release at which this API became deprecated.
63 message : str, optional
64 Override the default deprecation message. The ``%(since)s``,
65 ``%(name)s``, ``%(alternative)s``, ``%(obj_type)s``, ``%(addendum)s``,
66 and ``%(removal)s`` format specifiers will be replaced by the values
67 of the respective arguments passed to this function.
68 name : str, optional
69 The name of the deprecated object.
70 alternative : str, optional
71 An alternative API that the user may use in place of the deprecated
72 API. The deprecation warning will tell the user about this alternative
73 if provided.
74 pending : bool, optional
75 If True, uses a PendingDeprecationWarning instead of a
76 DeprecationWarning. Cannot be used together with *removal*.
77 obj_type : str, optional
78 The object type being deprecated.
79 addendum : str, optional
80 Additional text appended directly to the final message.
81 removal : str, optional
82 The expected removal version. With the default (an empty string), a
83 removal version is automatically computed from *since*. Set to other
84 Falsy values to not schedule a removal date. Cannot be used together
85 with *pending*.
86
87 Examples
88 --------
89 ::
90
91 # To warn of the deprecation of "matplotlib.name_of_module"
92 warn_deprecated('1.4.0', name='matplotlib.name_of_module',
93 obj_type='module')
94 """
95 warning = _generate_deprecation_warning(
96 since, message, name, alternative, pending, obj_type, addendum,
97 removal=removal)
98 from . import warn_external
99 warn_external(warning, category=MatplotlibDeprecationWarning)
100
101
102def deprecated(since, *, message='', name='', alternative='', pending=False,
103 obj_type=None, addendum='', removal=''):
104 """
105 Decorator to mark a function, a class, or a property as deprecated.
106
107 When deprecating a classmethod, a staticmethod, or a property, the
108 ``@deprecated`` decorator should go *under* ``@classmethod`` and
109 ``@staticmethod`` (i.e., `deprecated` should directly decorate the
110 underlying callable), but *over* ``@property``.
111
112 When deprecating a class ``C`` intended to be used as a base class in a
113 multiple inheritance hierarchy, ``C`` *must* define an ``__init__`` method
114 (if ``C`` instead inherited its ``__init__`` from its own base class, then
115 ``@deprecated`` would mess up ``__init__`` inheritance when installing its
116 own (deprecation-emitting) ``C.__init__``).
117
118 Parameters are the same as for `warn_deprecated`, except that *obj_type*
119 defaults to 'class' if decorating a class, 'attribute' if decorating a
120 property, and 'function' otherwise.
121
122 Examples
123 --------
124 ::
125
126 @deprecated('1.4.0')
127 def the_function_to_deprecate():
128 pass
129 """
130
131 def deprecate(obj, message=message, name=name, alternative=alternative,
132 pending=pending, obj_type=obj_type, addendum=addendum):
133 from matplotlib._api import classproperty
134
135 if isinstance(obj, type):
136 if obj_type is None:
137 obj_type = "class"
138 func = obj.__init__
139 name = name or obj.__name__
140 old_doc = obj.__doc__
141
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
150 elif isinstance(obj, (property, classproperty)):
151 if obj_type is None:
152 obj_type = "attribute"
153 func = None
154 name = name or obj.fget.__name__
155 old_doc = obj.__doc__
156
157 class _deprecated_property(type(obj)):
158 def __get__(self, instance, owner=None):
159 if instance is not None or owner is not None \
160 and isinstance(self, classproperty):
161 emit_warning()
162 return super().__get__(instance, owner)
163
164 def __set__(self, instance, value):
165 if instance is not None:
166 emit_warning()
167 return super().__set__(instance, value)
168
169 def __delete__(self, instance):
170 if instance is not None:
171 emit_warning()
172 return super().__delete__(instance)
173
174 def __set_name__(self, owner, set_name):
175 nonlocal name
176 if name == "<lambda>":
177 name = set_name
178
179 def finalize(_, new_doc):
180 return _deprecated_property(
181 fget=obj.fget, fset=obj.fset, fdel=obj.fdel, doc=new_doc)
182
183 else:
184 if obj_type is None:
185 obj_type = "function"
186 func = obj
187 name = name or obj.__name__
188 old_doc = func.__doc__
189
190 def finalize(wrapper, new_doc):
191 wrapper = functools.wraps(func)(wrapper)
192 wrapper.__doc__ = new_doc
193 return wrapper
194
195 def emit_warning():
196 warn_deprecated(
197 since, message=message, name=name, alternative=alternative,
198 pending=pending, obj_type=obj_type, addendum=addendum,
199 removal=removal)
200
201 def wrapper(*args, **kwargs):
202 emit_warning()
203 return func(*args, **kwargs)
204
205 old_doc = inspect.cleandoc(old_doc or '').strip('\n')
206
207 notes_header = '\nNotes\n-----'
208 second_arg = ' '.join([t.strip() for t in
209 (message, f"Use {alternative} instead."
210 if alternative else "", addendum) if t])
211 new_doc = (f"[*Deprecated*] {old_doc}\n"
212 f"{notes_header if notes_header not in old_doc else ''}\n"
213 f".. deprecated:: {since}\n"
214 f" {second_arg}")
215
216 if not old_doc:
217 # This is to prevent a spurious 'unexpected unindent' warning from
218 # docutils when the original docstring was blank.
219 new_doc += r'\ '
220
221 return finalize(wrapper, new_doc)
222
223 return deprecate
224
225
226class deprecate_privatize_attribute:
227 """
228 Helper to deprecate public access to an attribute (or method).
229
230 This helper should only be used at class scope, as follows::
231
232 class Foo:
233 attr = _deprecate_privatize_attribute(*args, **kwargs)
234
235 where *all* parameters are forwarded to `deprecated`. This form makes
236 ``attr`` a property which forwards read and write access to ``self._attr``
237 (same name but with a leading underscore), with a deprecation warning.
238 Note that the attribute name is derived from *the name this helper is
239 assigned to*. This helper also works for deprecating methods.
240 """
241
242 def __init__(self, *args, **kwargs):
243 self.deprecator = deprecated(*args, **kwargs)
244
245 def __set_name__(self, owner, name):
246 setattr(owner, name, self.deprecator(
247 property(lambda self: getattr(self, f"_{name}"),
248 lambda self, value: setattr(self, f"_{name}", value)),
249 name=name))
250
251
252# Used by _copy_docstring_and_deprecators to redecorate pyplot wrappers and
253# boilerplate.py to retrieve original signatures. It may seem natural to store
254# this information as an attribute on the wrapper, but if the wrapper gets
255# itself functools.wraps()ed, then such attributes are silently propagated to
256# the outer wrapper, which is not desired.
257DECORATORS = {}
258
259
260def rename_parameter(since, old, new, func=None):
261 """
262 Decorator indicating that parameter *old* of *func* is renamed to *new*.
263
264 The actual implementation of *func* should use *new*, not *old*. If *old*
265 is passed to *func*, a DeprecationWarning is emitted, and its value is
266 used, even if *new* is also passed by keyword (this is to simplify pyplot
267 wrapper functions, which always pass *new* explicitly to the Axes method).
268 If *new* is also passed but positionally, a TypeError will be raised by the
269 underlying function during argument binding.
270
271 Examples
272 --------
273 ::
274
275 @_api.rename_parameter("3.1", "bad_name", "good_name")
276 def func(good_name): ...
277 """
278
279 decorator = functools.partial(rename_parameter, since, old, new)
280
281 if func is None:
282 return decorator
283
284 signature = inspect.signature(func)
285 assert old not in signature.parameters, (
286 f"Matplotlib internal error: {old!r} cannot be a parameter for "
287 f"{func.__name__}()")
288 assert new in signature.parameters, (
289 f"Matplotlib internal error: {new!r} must be a parameter for "
290 f"{func.__name__}()")
291
292 @functools.wraps(func)
293 def wrapper(*args, **kwargs):
294 if old in kwargs:
295 warn_deprecated(
296 since, message=f"The {old!r} parameter of {func.__name__}() "
297 f"has been renamed {new!r} since Matplotlib {since}; support "
298 f"for the old name will be dropped %(removal)s.")
299 kwargs[new] = kwargs.pop(old)
300 return func(*args, **kwargs)
301
302 # wrapper() must keep the same documented signature as func(): if we
303 # instead made both *old* and *new* appear in wrapper()'s signature, they
304 # would both show up in the pyplot function for an Axes method as well and
305 # pyplot would explicitly pass both arguments to the Axes method.
306
307 DECORATORS[wrapper] = decorator
308 return wrapper
309
310
311class _deprecated_parameter_class:
312 def __repr__(self):
313 return "<deprecated parameter>"
314
315
316_deprecated_parameter = _deprecated_parameter_class()
317
318
319def delete_parameter(since, name, func=None, **kwargs):
320 """
321 Decorator indicating that parameter *name* of *func* is being deprecated.
322
323 The actual implementation of *func* should keep the *name* parameter in its
324 signature, or accept a ``**kwargs`` argument (through which *name* would be
325 passed).
326
327 Parameters that come after the deprecated parameter effectively become
328 keyword-only (as they cannot be passed positionally without triggering the
329 DeprecationWarning on the deprecated parameter), and should be marked as
330 such after the deprecation period has passed and the deprecated parameter
331 is removed.
332
333 Parameters other than *since*, *name*, and *func* are keyword-only and
334 forwarded to `.warn_deprecated`.
335
336 Examples
337 --------
338 ::
339
340 @_api.delete_parameter("3.1", "unused")
341 def func(used_arg, other_arg, unused, more_args): ...
342 """
343
344 decorator = functools.partial(delete_parameter, since, name, **kwargs)
345
346 if func is None:
347 return decorator
348
349 signature = inspect.signature(func)
350 # Name of `**kwargs` parameter of the decorated function, typically
351 # "kwargs" if such a parameter exists, or None if the decorated function
352 # doesn't accept `**kwargs`.
353 kwargs_name = next((param.name for param in signature.parameters.values()
354 if param.kind == inspect.Parameter.VAR_KEYWORD), None)
355 if name in signature.parameters:
356 kind = signature.parameters[name].kind
357 is_varargs = kind is inspect.Parameter.VAR_POSITIONAL
358 is_varkwargs = kind is inspect.Parameter.VAR_KEYWORD
359 if not is_varargs and not is_varkwargs:
360 name_idx = (
361 # Deprecated parameter can't be passed positionally.
362 math.inf if kind is inspect.Parameter.KEYWORD_ONLY
363 # If call site has no more than this number of parameters, the
364 # deprecated parameter can't have been passed positionally.
365 else [*signature.parameters].index(name))
366 func.__signature__ = signature = signature.replace(parameters=[
367 param.replace(default=_deprecated_parameter)
368 if param.name == name else param
369 for param in signature.parameters.values()])
370 else:
371 name_idx = -1 # Deprecated parameter can always have been passed.
372 else:
373 is_varargs = is_varkwargs = False
374 # Deprecated parameter can't be passed positionally.
375 name_idx = math.inf
376 assert kwargs_name, (
377 f"Matplotlib internal error: {name!r} must be a parameter for "
378 f"{func.__name__}()")
379
380 addendum = kwargs.pop('addendum', None)
381
382 @functools.wraps(func)
383 def wrapper(*inner_args, **inner_kwargs):
384 if len(inner_args) <= name_idx and name not in inner_kwargs:
385 # Early return in the simple, non-deprecated case (much faster than
386 # calling bind()).
387 return func(*inner_args, **inner_kwargs)
388 arguments = signature.bind(*inner_args, **inner_kwargs).arguments
389 if is_varargs and arguments.get(name):
390 warn_deprecated(
391 since, message=f"Additional positional arguments to "
392 f"{func.__name__}() are deprecated since %(since)s and "
393 f"support for them will be removed %(removal)s.")
394 elif is_varkwargs and arguments.get(name):
395 warn_deprecated(
396 since, message=f"Additional keyword arguments to "
397 f"{func.__name__}() are deprecated since %(since)s and "
398 f"support for them will be removed %(removal)s.")
399 # We cannot just check `name not in arguments` because the pyplot
400 # wrappers always pass all arguments explicitly.
401 elif any(name in d and d[name] != _deprecated_parameter
402 for d in [arguments, arguments.get(kwargs_name, {})]):
403 deprecation_addendum = (
404 f"If any parameter follows {name!r}, they should be passed as "
405 f"keyword, not positionally.")
406 warn_deprecated(
407 since,
408 name=repr(name),
409 obj_type=f"parameter of {func.__name__}()",
410 addendum=(addendum + " " + deprecation_addendum) if addendum
411 else deprecation_addendum,
412 **kwargs)
413 return func(*inner_args, **inner_kwargs)
414
415 DECORATORS[wrapper] = decorator
416 return wrapper
417
418
419def make_keyword_only(since, name, func=None):
420 """
421 Decorator indicating that passing parameter *name* (or any of the following
422 ones) positionally to *func* is being deprecated.
423
424 When used on a method that has a pyplot wrapper, this should be the
425 outermost decorator, so that :file:`boilerplate.py` can access the original
426 signature.
427 """
428
429 decorator = functools.partial(make_keyword_only, since, name)
430
431 if func is None:
432 return decorator
433
434 signature = inspect.signature(func)
435 POK = inspect.Parameter.POSITIONAL_OR_KEYWORD
436 KWO = inspect.Parameter.KEYWORD_ONLY
437 assert (name in signature.parameters
438 and signature.parameters[name].kind == POK), (
439 f"Matplotlib internal error: {name!r} must be a positional-or-keyword "
440 f"parameter for {func.__name__}()")
441 names = [*signature.parameters]
442 name_idx = names.index(name)
443 kwonly = [name for name in names[name_idx:]
444 if signature.parameters[name].kind == POK]
445
446 @functools.wraps(func)
447 def wrapper(*args, **kwargs):
448 # Don't use signature.bind here, as it would fail when stacked with
449 # rename_parameter and an "old" argument name is passed in
450 # (signature.bind would fail, but the actual call would succeed).
451 if len(args) > name_idx:
452 warn_deprecated(
453 since, message="Passing the %(name)s %(obj_type)s "
454 "positionally is deprecated since Matplotlib %(since)s; the "
455 "parameter will become keyword-only %(removal)s.",
456 name=name, obj_type=f"parameter of {func.__name__}()")
457 return func(*args, **kwargs)
458
459 # Don't modify *func*'s signature, as boilerplate.py needs it.
460 wrapper.__signature__ = signature.replace(parameters=[
461 param.replace(kind=KWO) if param.name in kwonly else param
462 for param in signature.parameters.values()])
463 DECORATORS[wrapper] = decorator
464 return wrapper
465
466
467def deprecate_method_override(method, obj, *, allow_empty=False, **kwargs):
468 """
469 Return ``obj.method`` with a deprecation if it was overridden, else None.
470
471 Parameters
472 ----------
473 method
474 An unbound method, i.e. an expression of the form
475 ``Class.method_name``. Remember that within the body of a method, one
476 can always use ``__class__`` to refer to the class that is currently
477 being defined.
478 obj
479 Either an object of the class where *method* is defined, or a subclass
480 of that class.
481 allow_empty : bool, default: False
482 Whether to allow overrides by "empty" methods without emitting a
483 warning.
484 **kwargs
485 Additional parameters passed to `warn_deprecated` to generate the
486 deprecation warning; must at least include the "since" key.
487 """
488
489 def empty(): pass
490 def empty_with_docstring(): """doc"""
491
492 name = method.__name__
493 bound_child = getattr(obj, name)
494 bound_base = (
495 method # If obj is a class, then we need to use unbound methods.
496 if isinstance(bound_child, type(empty)) and isinstance(obj, type)
497 else method.__get__(obj))
498 if (bound_child != bound_base
499 and (not allow_empty
500 or (getattr(getattr(bound_child, "__code__", None),
501 "co_code", None)
502 not in [empty.__code__.co_code,
503 empty_with_docstring.__code__.co_code]))):
504 warn_deprecated(**{"name": name, "obj_type": "method", **kwargs})
505 return bound_child
506 return None
507
508
509@contextlib.contextmanager
510def suppress_matplotlib_deprecation_warning():
511 with warnings.catch_warnings():
512 warnings.simplefilter("ignore", MatplotlibDeprecationWarning)
513 yield