Coverage for /usr/lib/python3/dist-packages/matplotlib/_api/__init__.py: 80%

117 statements  

« prev     ^ index     » next       coverage.py v7.9.1, created at 2025-06-14 15:55 +0200

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 ( 

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 starts with an 

62# underscore because it is intended to be positional-only (e.g., so that 

63# `_api.check_isinstance([...], types=foo)` doesn't fail. 

64 

65def check_isinstance(_types, **kwargs): 

66 """ 

67 For each *key, value* pair in *kwargs*, check that *value* is an instance 

68 of one of *_types*; if not, raise an appropriate TypeError. 

69 

70 As a special case, a ``None`` entry in *_types* is treated as NoneType. 

71 

72 Examples 

73 -------- 

74 >>> _api.check_isinstance((SomeClass, None), arg=arg) 

75 """ 

76 types = _types 

77 none_type = type(None) 

78 types = ((types,) if isinstance(types, type) else 

79 (none_type,) if types is None else 

80 tuple(none_type if tp is None else tp for tp in types)) 

81 

82 def type_name(tp): 

83 return ("None" if tp is none_type 

84 else tp.__qualname__ if tp.__module__ == "builtins" 

85 else f"{tp.__module__}.{tp.__qualname__}") 

86 

87 for k, v in kwargs.items(): 

88 if not isinstance(v, types): 

89 names = [*map(type_name, types)] 

90 if "None" in names: # Move it to the end for better wording. 

91 names.remove("None") 

92 names.append("None") 

93 raise TypeError( 

94 "{!r} must be an instance of {}, not a {}".format( 

95 k, 

96 ", ".join(names[:-1]) + " or " + names[-1] 

97 if len(names) > 1 else names[0], 

98 type_name(type(v)))) 

99 

100 

101def check_in_list(_values, *, _print_supported_values=True, **kwargs): 

102 """ 

103 For each *key, value* pair in *kwargs*, check that *value* is in *_values*. 

104 

105 Parameters 

106 ---------- 

107 _values : iterable 

108 Sequence of values to check on. 

109 _print_supported_values : bool, default: True 

110 Whether to print *_values* when raising ValueError. 

111 **kwargs : dict 

112 *key, value* pairs as keyword arguments to find in *_values*. 

113 

114 Raises 

115 ------ 

116 ValueError 

117 If any *value* in *kwargs* is not found in *_values*. 

118 

119 Examples 

120 -------- 

121 >>> _api.check_in_list(["foo", "bar"], arg=arg, other_arg=other_arg) 

122 """ 

123 if not kwargs: 

124 raise TypeError("No argument to check!") 

125 values = _values 

126 for key, val in kwargs.items(): 

127 if val not in values: 

128 msg = f"{val!r} is not a valid value for {key}" 

129 if _print_supported_values: 

130 msg += f"; supported values are {', '.join(map(repr, values))}" 

131 raise ValueError(msg) 

132 

133 

134def check_shape(_shape, **kwargs): 

135 """ 

136 For each *key, value* pair in *kwargs*, check that *value* has the shape 

137 *_shape*, if not, raise an appropriate ValueError. 

138 

139 *None* in the shape is treated as a "free" size that can have any length. 

140 e.g. (None, 2) -> (N, 2) 

141 

142 The values checked must be numpy arrays. 

143 

144 Examples 

145 -------- 

146 To check for (N, 2) shaped arrays 

147 

148 >>> _api.check_shape((None, 2), arg=arg, other_arg=other_arg) 

149 """ 

150 target_shape = _shape 

151 for k, v in kwargs.items(): 

152 data_shape = v.shape 

153 

154 if len(target_shape) != len(data_shape) or any( 

155 t not in [s, None] 

156 for t, s in zip(target_shape, data_shape) 

157 ): 

158 dim_labels = iter(itertools.chain( 

159 'MNLIJKLH', 

160 (f"D{i}" for i in itertools.count()))) 

161 text_shape = ", ".join((str(n) 

162 if n is not None 

163 else next(dim_labels) 

164 for n in target_shape)) 

165 

166 raise ValueError( 

167 f"{k!r} must be {len(target_shape)}D " 

168 f"with shape ({text_shape}). " 

169 f"Your input has shape {v.shape}." 

170 ) 

171 

172 

173def check_getitem(_mapping, **kwargs): 

174 """ 

175 *kwargs* must consist of a single *key, value* pair. If *key* is in 

176 *_mapping*, return ``_mapping[value]``; else, raise an appropriate 

177 ValueError. 

178 

179 Examples 

180 -------- 

181 >>> _api.check_getitem({"foo": "bar"}, arg=arg) 

182 """ 

183 mapping = _mapping 

184 if len(kwargs) != 1: 

185 raise ValueError("check_getitem takes a single keyword argument") 

186 (k, v), = kwargs.items() 

187 try: 

188 return mapping[v] 

189 except KeyError: 

190 raise ValueError( 

191 "{!r} is not a valid value for {}; supported values are {}" 

192 .format(v, k, ', '.join(map(repr, mapping)))) from None 

193 

194 

195def caching_module_getattr(cls): 

196 """ 

197 Helper decorator for implementing module-level ``__getattr__`` as a class. 

198 

199 This decorator must be used at the module toplevel as follows:: 

200 

201 @caching_module_getattr 

202 class __getattr__: # The class *must* be named ``__getattr__``. 

203 @property # Only properties are taken into account. 

204 def name(self): ... 

205 

206 The ``__getattr__`` class will be replaced by a ``__getattr__`` 

207 function such that trying to access ``name`` on the module will 

208 resolve the corresponding property (which may be decorated e.g. with 

209 ``_api.deprecated`` for deprecating module globals). The properties are 

210 all implicitly cached. Moreover, a suitable AttributeError is generated 

211 and raised if no property with the given name exists. 

212 """ 

213 

214 assert cls.__name__ == "__getattr__" 

215 # Don't accidentally export cls dunders. 

216 props = {name: prop for name, prop in vars(cls).items() 

217 if isinstance(prop, property)} 

218 instance = cls() 

219 

220 @functools.lru_cache(None) 

221 def __getattr__(name): 

222 if name in props: 

223 return props[name].__get__(instance) 

224 raise AttributeError( 

225 f"module {cls.__module__!r} has no attribute {name!r}") 

226 

227 return __getattr__ 

228 

229 

230def define_aliases(alias_d, cls=None): 

231 """ 

232 Class decorator for defining property aliases. 

233 

234 Use as :: 

235 

236 @_api.define_aliases({"property": ["alias", ...], ...}) 

237 class C: ... 

238 

239 For each property, if the corresponding ``get_property`` is defined in the 

240 class so far, an alias named ``get_alias`` will be defined; the same will 

241 be done for setters. If neither the getter nor the setter exists, an 

242 exception will be raised. 

243 

244 The alias map is stored as the ``_alias_map`` attribute on the class and 

245 can be used by `.normalize_kwargs` (which assumes that higher priority 

246 aliases come last). 

247 """ 

248 if cls is None: # Return the actual class decorator. 

249 return functools.partial(define_aliases, alias_d) 

250 

251 def make_alias(name): # Enforce a closure over *name*. 

252 @functools.wraps(getattr(cls, name)) 

253 def method(self, *args, **kwargs): 

254 return getattr(self, name)(*args, **kwargs) 

255 return method 

256 

257 for prop, aliases in alias_d.items(): 

258 exists = False 

259 for prefix in ["get_", "set_"]: 

260 if prefix + prop in vars(cls): 

261 exists = True 

262 for alias in aliases: 

263 method = make_alias(prefix + prop) 

264 method.__name__ = prefix + alias 

265 method.__doc__ = "Alias for `{}`.".format(prefix + prop) 

266 setattr(cls, prefix + alias, method) 

267 if not exists: 

268 raise ValueError( 

269 "Neither getter nor setter exists for {!r}".format(prop)) 

270 

271 def get_aliased_and_aliases(d): 

272 return {*d, *(alias for aliases in d.values() for alias in aliases)} 

273 

274 preexisting_aliases = getattr(cls, "_alias_map", {}) 

275 conflicting = (get_aliased_and_aliases(preexisting_aliases) 

276 & get_aliased_and_aliases(alias_d)) 

277 if conflicting: 

278 # Need to decide on conflict resolution policy. 

279 raise NotImplementedError( 

280 f"Parent class already defines conflicting aliases: {conflicting}") 

281 cls._alias_map = {**preexisting_aliases, **alias_d} 

282 return cls 

283 

284 

285def select_matching_signature(funcs, *args, **kwargs): 

286 """ 

287 Select and call the function that accepts ``*args, **kwargs``. 

288 

289 *funcs* is a list of functions which should not raise any exception (other 

290 than `TypeError` if the arguments passed do not match their signature). 

291 

292 `select_matching_signature` tries to call each of the functions in *funcs* 

293 with ``*args, **kwargs`` (in the order in which they are given). Calls 

294 that fail with a `TypeError` are silently skipped. As soon as a call 

295 succeeds, `select_matching_signature` returns its return value. If no 

296 function accepts ``*args, **kwargs``, then the `TypeError` raised by the 

297 last failing call is re-raised. 

298 

299 Callers should normally make sure that any ``*args, **kwargs`` can only 

300 bind a single *func* (to avoid any ambiguity), although this is not checked 

301 by `select_matching_signature`. 

302 

303 Notes 

304 ----- 

305 `select_matching_signature` is intended to help implementing 

306 signature-overloaded functions. In general, such functions should be 

307 avoided, except for back-compatibility concerns. A typical use pattern is 

308 :: 

309 

310 def my_func(*args, **kwargs): 

311 params = select_matching_signature( 

312 [lambda old1, old2: locals(), lambda new: locals()], 

313 *args, **kwargs) 

314 if "old1" in params: 

315 warn_deprecated(...) 

316 old1, old2 = params.values() # note that locals() is ordered. 

317 else: 

318 new, = params.values() 

319 # do things with params 

320 

321 which allows *my_func* to be called either with two parameters (*old1* and 

322 *old2*) or a single one (*new*). Note that the new signature is given 

323 last, so that callers get a `TypeError` corresponding to the new signature 

324 if the arguments they passed in do not match any signature. 

325 """ 

326 # Rather than relying on locals() ordering, one could have just used func's 

327 # signature (``bound = inspect.signature(func).bind(*args, **kwargs); 

328 # bound.apply_defaults(); return bound``) but that is significantly slower. 

329 for i, func in enumerate(funcs): 

330 try: 

331 return func(*args, **kwargs) 

332 except TypeError: 

333 if i == len(funcs) - 1: 

334 raise 

335 

336 

337def recursive_subclasses(cls): 

338 """Yield *cls* and direct and indirect subclasses of *cls*.""" 

339 yield cls 

340 for subcls in cls.__subclasses__(): 

341 yield from recursive_subclasses(subcls) 

342 

343 

344def warn_external(message, category=None): 

345 """ 

346 `warnings.warn` wrapper that sets *stacklevel* to "outside Matplotlib". 

347 

348 The original emitter of the warning can be obtained by patching this 

349 function back to `warnings.warn`, i.e. ``_api.warn_external = 

350 warnings.warn`` (or ``functools.partial(warnings.warn, stacklevel=2)``, 

351 etc.). 

352 """ 

353 frame = sys._getframe() 

354 for stacklevel in itertools.count(1): # lgtm[py/unused-loop-variable] 

355 if frame is None: 

356 # when called in embedded context may hit frame is None 

357 break 

358 if not re.match(r"\A(matplotlib|mpl_toolkits)(\Z|\.(?!tests\.))", 

359 # Work around sphinx-gallery not setting __name__. 

360 frame.f_globals.get("__name__", "")): 

361 break 

362 frame = frame.f_back 

363 warnings.warn(message, category, stacklevel)