1"""
2Helper functions for managing the Matplotlib API.
3
4This documentation is only relevant for Matplotlib developers, not for users.
5
6.. warning::
7
8 This module and its submodules are for internal use only. Do not use them
9 in your own code. We may change the API at any time with no warning.
10
11"""
12
13import functools
14import itertools
15import re
16import sys
17import warnings
18
19from .deprecation import ( # noqa: F401
20 deprecated, warn_deprecated,
21 rename_parameter, delete_parameter, make_keyword_only,
22 deprecate_method_override, deprecate_privatize_attribute,
23 suppress_matplotlib_deprecation_warning,
24 MatplotlibDeprecationWarning)
25
26
27class classproperty:
28 """
29 Like `property`, but also triggers on access via the class, and it is the
30 *class* that's passed as argument.
31
32 Examples
33 --------
34 ::
35
36 class C:
37 @classproperty
38 def foo(cls):
39 return cls.__name__
40
41 assert C.foo == "C"
42 """
43
44 def __init__(self, fget, fset=None, fdel=None, doc=None):
45 self._fget = fget
46 if fset is not None or fdel is not None:
47 raise ValueError('classproperty only implements fget.')
48 self.fset = fset
49 self.fdel = fdel
50 # docs are ignored for now
51 self._doc = doc
52
53 def __get__(self, instance, owner):
54 return self._fget(owner)
55
56 @property
57 def fget(self):
58 return self._fget
59
60
61# In the following check_foo() functions, the first parameter is positional-only to make
62# e.g. `_api.check_isinstance([...], types=foo)` work.
63
64def check_isinstance(types, /, **kwargs):
65 """
66 For each *key, value* pair in *kwargs*, check that *value* is an instance
67 of one of *types*; if not, raise an appropriate TypeError.
68
69 As a special case, a ``None`` entry in *types* is treated as NoneType.
70
71 Examples
72 --------
73 >>> _api.check_isinstance((SomeClass, None), arg=arg)
74 """
75 none_type = type(None)
76 types = ((types,) if isinstance(types, type) else
77 (none_type,) if types is None else
78 tuple(none_type if tp is None else tp for tp in types))
79
80 def type_name(tp):
81 return ("None" if tp is none_type
82 else tp.__qualname__ if tp.__module__ == "builtins"
83 else f"{tp.__module__}.{tp.__qualname__}")
84
85 for k, v in kwargs.items():
86 if not isinstance(v, types):
87 names = [*map(type_name, types)]
88 if "None" in names: # Move it to the end for better wording.
89 names.remove("None")
90 names.append("None")
91 raise TypeError(
92 "{!r} must be an instance of {}, not a {}".format(
93 k,
94 ", ".join(names[:-1]) + " or " + names[-1]
95 if len(names) > 1 else names[0],
96 type_name(type(v))))
97
98
99def check_in_list(values, /, *, _print_supported_values=True, **kwargs):
100 """
101 For each *key, value* pair in *kwargs*, check that *value* is in *values*;
102 if not, raise an appropriate ValueError.
103
104 Parameters
105 ----------
106 values : iterable
107 Sequence of values to check on.
108 _print_supported_values : bool, default: True
109 Whether to print *values* when raising ValueError.
110 **kwargs : dict
111 *key, value* pairs as keyword arguments to find in *values*.
112
113 Raises
114 ------
115 ValueError
116 If any *value* in *kwargs* is not found in *values*.
117
118 Examples
119 --------
120 >>> _api.check_in_list(["foo", "bar"], arg=arg, other_arg=other_arg)
121 """
122 if not kwargs:
123 raise TypeError("No argument to check!")
124 for key, val in kwargs.items():
125 if val not in values:
126 msg = f"{val!r} is not a valid value for {key}"
127 if _print_supported_values:
128 msg += f"; supported values are {', '.join(map(repr, values))}"
129 raise ValueError(msg)
130
131
132def check_shape(shape, /, **kwargs):
133 """
134 For each *key, value* pair in *kwargs*, check that *value* has the shape *shape*;
135 if not, raise an appropriate ValueError.
136
137 *None* in the shape is treated as a "free" size that can have any length.
138 e.g. (None, 2) -> (N, 2)
139
140 The values checked must be numpy arrays.
141
142 Examples
143 --------
144 To check for (N, 2) shaped arrays
145
146 >>> _api.check_shape((None, 2), arg=arg, other_arg=other_arg)
147 """
148 for k, v in kwargs.items():
149 data_shape = v.shape
150
151 if (len(data_shape) != len(shape)
152 or any(s != t and t is not None for s, t in zip(data_shape, shape))):
153 dim_labels = iter(itertools.chain(
154 'NMLKJIH',
155 (f"D{i}" for i in itertools.count())))
156 text_shape = ", ".join([str(n) if n is not None else next(dim_labels)
157 for n in shape[::-1]][::-1])
158 if len(shape) == 1:
159 text_shape += ","
160
161 raise ValueError(
162 f"{k!r} must be {len(shape)}D with shape ({text_shape}), "
163 f"but your input has shape {v.shape}"
164 )
165
166
167def check_getitem(mapping, /, **kwargs):
168 """
169 *kwargs* must consist of a single *key, value* pair. If *key* is in
170 *mapping*, return ``mapping[value]``; else, raise an appropriate
171 ValueError.
172
173 Examples
174 --------
175 >>> _api.check_getitem({"foo": "bar"}, arg=arg)
176 """
177 if len(kwargs) != 1:
178 raise ValueError("check_getitem takes a single keyword argument")
179 (k, v), = kwargs.items()
180 try:
181 return mapping[v]
182 except KeyError:
183 raise ValueError(
184 f"{v!r} is not a valid value for {k}; supported values are "
185 f"{', '.join(map(repr, mapping))}") from None
186
187
188def caching_module_getattr(cls):
189 """
190 Helper decorator for implementing module-level ``__getattr__`` as a class.
191
192 This decorator must be used at the module toplevel as follows::
193
194 @caching_module_getattr
195 class __getattr__: # The class *must* be named ``__getattr__``.
196 @property # Only properties are taken into account.
197 def name(self): ...
198
199 The ``__getattr__`` class will be replaced by a ``__getattr__``
200 function such that trying to access ``name`` on the module will
201 resolve the corresponding property (which may be decorated e.g. with
202 ``_api.deprecated`` for deprecating module globals). The properties are
203 all implicitly cached. Moreover, a suitable AttributeError is generated
204 and raised if no property with the given name exists.
205 """
206
207 assert cls.__name__ == "__getattr__"
208 # Don't accidentally export cls dunders.
209 props = {name: prop for name, prop in vars(cls).items()
210 if isinstance(prop, property)}
211 instance = cls()
212
213 @functools.cache
214 def __getattr__(name):
215 if name in props:
216 return props[name].__get__(instance)
217 raise AttributeError(
218 f"module {cls.__module__!r} has no attribute {name!r}")
219
220 return __getattr__
221
222
223def define_aliases(alias_d, cls=None):
224 """
225 Class decorator for defining property aliases.
226
227 Use as ::
228
229 @_api.define_aliases({"property": ["alias", ...], ...})
230 class C: ...
231
232 For each property, if the corresponding ``get_property`` is defined in the
233 class so far, an alias named ``get_alias`` will be defined; the same will
234 be done for setters. If neither the getter nor the setter exists, an
235 exception will be raised.
236
237 The alias map is stored as the ``_alias_map`` attribute on the class and
238 can be used by `.normalize_kwargs` (which assumes that higher priority
239 aliases come last).
240 """
241 if cls is None: # Return the actual class decorator.
242 return functools.partial(define_aliases, alias_d)
243
244 def make_alias(name): # Enforce a closure over *name*.
245 @functools.wraps(getattr(cls, name))
246 def method(self, *args, **kwargs):
247 return getattr(self, name)(*args, **kwargs)
248 return method
249
250 for prop, aliases in alias_d.items():
251 exists = False
252 for prefix in ["get_", "set_"]:
253 if prefix + prop in vars(cls):
254 exists = True
255 for alias in aliases:
256 method = make_alias(prefix + prop)
257 method.__name__ = prefix + alias
258 method.__doc__ = f"Alias for `{prefix + prop}`."
259 setattr(cls, prefix + alias, method)
260 if not exists:
261 raise ValueError(
262 f"Neither getter nor setter exists for {prop!r}")
263
264 def get_aliased_and_aliases(d):
265 return {*d, *(alias for aliases in d.values() for alias in aliases)}
266
267 preexisting_aliases = getattr(cls, "_alias_map", {})
268 conflicting = (get_aliased_and_aliases(preexisting_aliases)
269 & get_aliased_and_aliases(alias_d))
270 if conflicting:
271 # Need to decide on conflict resolution policy.
272 raise NotImplementedError(
273 f"Parent class already defines conflicting aliases: {conflicting}")
274 cls._alias_map = {**preexisting_aliases, **alias_d}
275 return cls
276
277
278def select_matching_signature(funcs, *args, **kwargs):
279 """
280 Select and call the function that accepts ``*args, **kwargs``.
281
282 *funcs* is a list of functions which should not raise any exception (other
283 than `TypeError` if the arguments passed do not match their signature).
284
285 `select_matching_signature` tries to call each of the functions in *funcs*
286 with ``*args, **kwargs`` (in the order in which they are given). Calls
287 that fail with a `TypeError` are silently skipped. As soon as a call
288 succeeds, `select_matching_signature` returns its return value. If no
289 function accepts ``*args, **kwargs``, then the `TypeError` raised by the
290 last failing call is re-raised.
291
292 Callers should normally make sure that any ``*args, **kwargs`` can only
293 bind a single *func* (to avoid any ambiguity), although this is not checked
294 by `select_matching_signature`.
295
296 Notes
297 -----
298 `select_matching_signature` is intended to help implementing
299 signature-overloaded functions. In general, such functions should be
300 avoided, except for back-compatibility concerns. A typical use pattern is
301 ::
302
303 def my_func(*args, **kwargs):
304 params = select_matching_signature(
305 [lambda old1, old2: locals(), lambda new: locals()],
306 *args, **kwargs)
307 if "old1" in params:
308 warn_deprecated(...)
309 old1, old2 = params.values() # note that locals() is ordered.
310 else:
311 new, = params.values()
312 # do things with params
313
314 which allows *my_func* to be called either with two parameters (*old1* and
315 *old2*) or a single one (*new*). Note that the new signature is given
316 last, so that callers get a `TypeError` corresponding to the new signature
317 if the arguments they passed in do not match any signature.
318 """
319 # Rather than relying on locals() ordering, one could have just used func's
320 # signature (``bound = inspect.signature(func).bind(*args, **kwargs);
321 # bound.apply_defaults(); return bound``) but that is significantly slower.
322 for i, func in enumerate(funcs):
323 try:
324 return func(*args, **kwargs)
325 except TypeError:
326 if i == len(funcs) - 1:
327 raise
328
329
330def nargs_error(name, takes, given):
331 """Generate a TypeError to be raised by function calls with wrong arity."""
332 return TypeError(f"{name}() takes {takes} positional arguments but "
333 f"{given} were given")
334
335
336def kwarg_error(name, kw):
337 """
338 Generate a TypeError to be raised by function calls with wrong kwarg.
339
340 Parameters
341 ----------
342 name : str
343 The name of the calling function.
344 kw : str or Iterable[str]
345 Either the invalid keyword argument name, or an iterable yielding
346 invalid keyword arguments (e.g., a ``kwargs`` dict).
347 """
348 if not isinstance(kw, str):
349 kw = next(iter(kw))
350 return TypeError(f"{name}() got an unexpected keyword argument '{kw}'")
351
352
353def recursive_subclasses(cls):
354 """Yield *cls* and direct and indirect subclasses of *cls*."""
355 yield cls
356 for subcls in cls.__subclasses__():
357 yield from recursive_subclasses(subcls)
358
359
360def warn_external(message, category=None):
361 """
362 `warnings.warn` wrapper that sets *stacklevel* to "outside Matplotlib".
363
364 The original emitter of the warning can be obtained by patching this
365 function back to `warnings.warn`, i.e. ``_api.warn_external =
366 warnings.warn`` (or ``functools.partial(warnings.warn, stacklevel=2)``,
367 etc.).
368 """
369 frame = sys._getframe()
370 for stacklevel in itertools.count(1):
371 if frame is None:
372 # when called in embedded context may hit frame is None
373 break
374 if not re.match(r"\A(matplotlib|mpl_toolkits)(\Z|\.(?!tests\.))",
375 # Work around sphinx-gallery not setting __name__.
376 frame.f_globals.get("__name__", "")):
377 break
378 frame = frame.f_back
379 # preemptively break reference cycle between locals and the frame
380 del frame
381 warnings.warn(message, category, stacklevel)