Coverage for /usr/lib/python3/dist-packages/matplotlib/_api/deprecation.py: 69%

173 statements  

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

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 removal = f"in {removal}" if removal else "two minor releases later" 

33 if not message: 

34 message = ( 

35 ("The %(name)s %(obj_type)s" if obj_type else "%(name)s") 

36 + (" will be deprecated in a future version" 

37 if pending else 

38 (" was deprecated in Matplotlib %(since)s" 

39 + (" and will be removed %(removal)s" if removal else ""))) 

40 + "." 

41 + (" Use %(alternative)s instead." if alternative else "") 

42 + (" %(addendum)s" if addendum else "")) 

43 warning_cls = (PendingDeprecationWarning if pending 

44 else MatplotlibDeprecationWarning) 

45 return warning_cls(message % dict( 

46 func=name, name=name, obj_type=obj_type, since=since, removal=removal, 

47 alternative=alternative, addendum=addendum)) 

48 

49 

50def warn_deprecated( 

51 since, *, message='', name='', alternative='', pending=False, 

52 obj_type='', addendum='', removal=''): 

53 """ 

54 Display a standardized deprecation. 

55 

56 Parameters 

57 ---------- 

58 since : str 

59 The release at which this API became deprecated. 

60 message : str, optional 

61 Override the default deprecation message. The ``%(since)s``, 

62 ``%(name)s``, ``%(alternative)s``, ``%(obj_type)s``, ``%(addendum)s``, 

63 and ``%(removal)s`` format specifiers will be replaced by the values 

64 of the respective arguments passed to this function. 

65 name : str, optional 

66 The name of the deprecated object. 

67 alternative : str, optional 

68 An alternative API that the user may use in place of the deprecated 

69 API. The deprecation warning will tell the user about this alternative 

70 if provided. 

71 pending : bool, optional 

72 If True, uses a PendingDeprecationWarning instead of a 

73 DeprecationWarning. Cannot be used together with *removal*. 

74 obj_type : str, optional 

75 The object type being deprecated. 

76 addendum : str, optional 

77 Additional text appended directly to the final message. 

78 removal : str, optional 

79 The expected removal version. With the default (an empty string), a 

80 removal version is automatically computed from *since*. Set to other 

81 Falsy values to not schedule a removal date. Cannot be used together 

82 with *pending*. 

83 

84 Examples 

85 -------- 

86 :: 

87 

88 # To warn of the deprecation of "matplotlib.name_of_module" 

89 warn_deprecated('1.4.0', name='matplotlib.name_of_module', 

90 obj_type='module') 

91 """ 

92 warning = _generate_deprecation_warning( 

93 since, message, name, alternative, pending, obj_type, addendum, 

94 removal=removal) 

95 from . import warn_external 

96 warn_external(warning, category=MatplotlibDeprecationWarning) 

97 

98 

99def deprecated(since, *, message='', name='', alternative='', pending=False, 

100 obj_type=None, addendum='', removal=''): 

101 """ 

102 Decorator to mark a function, a class, or a property as deprecated. 

103 

104 When deprecating a classmethod, a staticmethod, or a property, the 

105 ``@deprecated`` decorator should go *under* ``@classmethod`` and 

106 ``@staticmethod`` (i.e., `deprecated` should directly decorate the 

107 underlying callable), but *over* ``@property``. 

108 

109 When deprecating a class ``C`` intended to be used as a base class in a 

110 multiple inheritance hierarchy, ``C`` *must* define an ``__init__`` method 

111 (if ``C`` instead inherited its ``__init__`` from its own base class, then 

112 ``@deprecated`` would mess up ``__init__`` inheritance when installing its 

113 own (deprecation-emitting) ``C.__init__``). 

114 

115 Parameters are the same as for `warn_deprecated`, except that *obj_type* 

116 defaults to 'class' if decorating a class, 'attribute' if decorating a 

117 property, and 'function' otherwise. 

118 

119 Examples 

120 -------- 

121 :: 

122 

123 @deprecated('1.4.0') 

124 def the_function_to_deprecate(): 

125 pass 

126 """ 

127 

128 def deprecate(obj, message=message, name=name, alternative=alternative, 

129 pending=pending, obj_type=obj_type, addendum=addendum): 

130 from matplotlib._api import classproperty 

131 

132 if isinstance(obj, type): 

133 if obj_type is None: 

134 obj_type = "class" 

135 func = obj.__init__ 

136 name = name or obj.__name__ 

137 old_doc = obj.__doc__ 

138 

139 def finalize(wrapper, new_doc): 

140 try: 

141 obj.__doc__ = new_doc 

142 except AttributeError: # Can't set on some extension objects. 

143 pass 

144 obj.__init__ = functools.wraps(obj.__init__)(wrapper) 

145 return obj 

146 

147 elif isinstance(obj, (property, classproperty)): 

148 if obj_type is None: 

149 obj_type = "attribute" 

150 func = None 

151 name = name or obj.fget.__name__ 

152 old_doc = obj.__doc__ 

153 

154 class _deprecated_property(type(obj)): 

155 def __get__(self, instance, owner=None): 

156 if instance is not None or owner is not None \ 

157 and isinstance(self, classproperty): 

158 emit_warning() 

159 return super().__get__(instance, owner) 

160 

161 def __set__(self, instance, value): 

162 if instance is not None: 

163 emit_warning() 

164 return super().__set__(instance, value) 

165 

166 def __delete__(self, instance): 

167 if instance is not None: 

168 emit_warning() 

169 return super().__delete__(instance) 

170 

171 def __set_name__(self, owner, set_name): 

172 nonlocal name 

173 if name == "<lambda>": 

174 name = set_name 

175 

176 def finalize(_, new_doc): 

177 return _deprecated_property( 

178 fget=obj.fget, fset=obj.fset, fdel=obj.fdel, doc=new_doc) 

179 

180 else: 

181 if obj_type is None: 

182 obj_type = "function" 

183 func = obj 

184 name = name or obj.__name__ 

185 old_doc = func.__doc__ 

186 

187 def finalize(wrapper, new_doc): 

188 wrapper = functools.wraps(func)(wrapper) 

189 wrapper.__doc__ = new_doc 

190 return wrapper 

191 

192 def emit_warning(): 

193 warn_deprecated( 

194 since, message=message, name=name, alternative=alternative, 

195 pending=pending, obj_type=obj_type, addendum=addendum, 

196 removal=removal) 

197 

198 def wrapper(*args, **kwargs): 

199 emit_warning() 

200 return func(*args, **kwargs) 

201 

202 old_doc = inspect.cleandoc(old_doc or '').strip('\n') 

203 

204 notes_header = '\nNotes\n-----' 

205 second_arg = ' '.join([t.strip() for t in 

206 (message, f"Use {alternative} instead." 

207 if alternative else "", addendum) if t]) 

208 new_doc = (f"[*Deprecated*] {old_doc}\n" 

209 f"{notes_header if notes_header not in old_doc else ''}\n" 

210 f".. deprecated:: {since}\n" 

211 f" {second_arg}") 

212 

213 if not old_doc: 

214 # This is to prevent a spurious 'unexpected unindent' warning from 

215 # docutils when the original docstring was blank. 

216 new_doc += r'\ ' 

217 

218 return finalize(wrapper, new_doc) 

219 

220 return deprecate 

221 

222 

223class deprecate_privatize_attribute: 

224 """ 

225 Helper to deprecate public access to an attribute (or method). 

226 

227 This helper should only be used at class scope, as follows:: 

228 

229 class Foo: 

230 attr = _deprecate_privatize_attribute(*args, **kwargs) 

231 

232 where *all* parameters are forwarded to `deprecated`. This form makes 

233 ``attr`` a property which forwards read and write access to ``self._attr`` 

234 (same name but with a leading underscore), with a deprecation warning. 

235 Note that the attribute name is derived from *the name this helper is 

236 assigned to*. This helper also works for deprecating methods. 

237 """ 

238 

239 def __init__(self, *args, **kwargs): 

240 self.deprecator = deprecated(*args, **kwargs) 

241 

242 def __set_name__(self, owner, name): 

243 setattr(owner, name, self.deprecator( 

244 property(lambda self: getattr(self, f"_{name}"), 

245 lambda self, value: setattr(self, f"_{name}", value)), 

246 name=name)) 

247 

248 

249# Used by _copy_docstring_and_deprecators to redecorate pyplot wrappers and 

250# boilerplate.py to retrieve original signatures. It may seem natural to store 

251# this information as an attribute on the wrapper, but if the wrapper gets 

252# itself functools.wraps()ed, then such attributes are silently propagated to 

253# the outer wrapper, which is not desired. 

254DECORATORS = {} 

255 

256 

257def rename_parameter(since, old, new, func=None): 

258 """ 

259 Decorator indicating that parameter *old* of *func* is renamed to *new*. 

260 

261 The actual implementation of *func* should use *new*, not *old*. If *old* 

262 is passed to *func*, a DeprecationWarning is emitted, and its value is 

263 used, even if *new* is also passed by keyword (this is to simplify pyplot 

264 wrapper functions, which always pass *new* explicitly to the Axes method). 

265 If *new* is also passed but positionally, a TypeError will be raised by the 

266 underlying function during argument binding. 

267 

268 Examples 

269 -------- 

270 :: 

271 

272 @_api.rename_parameter("3.1", "bad_name", "good_name") 

273 def func(good_name): ... 

274 """ 

275 

276 decorator = functools.partial(rename_parameter, since, old, new) 

277 

278 if func is None: 

279 return decorator 

280 

281 signature = inspect.signature(func) 

282 assert old not in signature.parameters, ( 

283 f"Matplotlib internal error: {old!r} cannot be a parameter for " 

284 f"{func.__name__}()") 

285 assert new in signature.parameters, ( 

286 f"Matplotlib internal error: {new!r} must be a parameter for " 

287 f"{func.__name__}()") 

288 

289 @functools.wraps(func) 

290 def wrapper(*args, **kwargs): 

291 if old in kwargs: 

292 warn_deprecated( 

293 since, message=f"The {old!r} parameter of {func.__name__}() " 

294 f"has been renamed {new!r} since Matplotlib {since}; support " 

295 f"for the old name will be dropped %(removal)s.") 

296 kwargs[new] = kwargs.pop(old) 

297 return func(*args, **kwargs) 

298 

299 # wrapper() must keep the same documented signature as func(): if we 

300 # instead made both *old* and *new* appear in wrapper()'s signature, they 

301 # would both show up in the pyplot function for an Axes method as well and 

302 # pyplot would explicitly pass both arguments to the Axes method. 

303 

304 DECORATORS[wrapper] = decorator 

305 return wrapper 

306 

307 

308class _deprecated_parameter_class: 

309 def __repr__(self): 

310 return "<deprecated parameter>" 

311 

312 

313_deprecated_parameter = _deprecated_parameter_class() 

314 

315 

316def delete_parameter(since, name, func=None, **kwargs): 

317 """ 

318 Decorator indicating that parameter *name* of *func* is being deprecated. 

319 

320 The actual implementation of *func* should keep the *name* parameter in its 

321 signature, or accept a ``**kwargs`` argument (through which *name* would be 

322 passed). 

323 

324 Parameters that come after the deprecated parameter effectively become 

325 keyword-only (as they cannot be passed positionally without triggering the 

326 DeprecationWarning on the deprecated parameter), and should be marked as 

327 such after the deprecation period has passed and the deprecated parameter 

328 is removed. 

329 

330 Parameters other than *since*, *name*, and *func* are keyword-only and 

331 forwarded to `.warn_deprecated`. 

332 

333 Examples 

334 -------- 

335 :: 

336 

337 @_api.delete_parameter("3.1", "unused") 

338 def func(used_arg, other_arg, unused, more_args): ... 

339 """ 

340 

341 decorator = functools.partial(delete_parameter, since, name, **kwargs) 

342 

343 if func is None: 

344 return decorator 

345 

346 signature = inspect.signature(func) 

347 # Name of `**kwargs` parameter of the decorated function, typically 

348 # "kwargs" if such a parameter exists, or None if the decorated function 

349 # doesn't accept `**kwargs`. 

350 kwargs_name = next((param.name for param in signature.parameters.values() 

351 if param.kind == inspect.Parameter.VAR_KEYWORD), None) 

352 if name in signature.parameters: 

353 kind = signature.parameters[name].kind 

354 is_varargs = kind is inspect.Parameter.VAR_POSITIONAL 

355 is_varkwargs = kind is inspect.Parameter.VAR_KEYWORD 

356 if not is_varargs and not is_varkwargs: 

357 name_idx = ( 

358 # Deprecated parameter can't be passed positionally. 

359 math.inf if kind is inspect.Parameter.KEYWORD_ONLY 

360 # If call site has no more than this number of parameters, the 

361 # deprecated parameter can't have been passed positionally. 

362 else [*signature.parameters].index(name)) 

363 func.__signature__ = signature = signature.replace(parameters=[ 

364 param.replace(default=_deprecated_parameter) 

365 if param.name == name else param 

366 for param in signature.parameters.values()]) 

367 else: 

368 name_idx = -1 # Deprecated parameter can always have been passed. 

369 else: 

370 is_varargs = is_varkwargs = False 

371 # Deprecated parameter can't be passed positionally. 

372 name_idx = math.inf 

373 assert kwargs_name, ( 

374 f"Matplotlib internal error: {name!r} must be a parameter for " 

375 f"{func.__name__}()") 

376 

377 addendum = kwargs.pop('addendum', None) 

378 

379 @functools.wraps(func) 

380 def wrapper(*inner_args, **inner_kwargs): 

381 if len(inner_args) <= name_idx and name not in inner_kwargs: 

382 # Early return in the simple, non-deprecated case (much faster than 

383 # calling bind()). 

384 return func(*inner_args, **inner_kwargs) 

385 arguments = signature.bind(*inner_args, **inner_kwargs).arguments 

386 if is_varargs and arguments.get(name): 

387 warn_deprecated( 

388 since, message=f"Additional positional arguments to " 

389 f"{func.__name__}() are deprecated since %(since)s and " 

390 f"support for them will be removed %(removal)s.") 

391 elif is_varkwargs and arguments.get(name): 

392 warn_deprecated( 

393 since, message=f"Additional keyword arguments to " 

394 f"{func.__name__}() are deprecated since %(since)s and " 

395 f"support for them will be removed %(removal)s.") 

396 # We cannot just check `name not in arguments` because the pyplot 

397 # wrappers always pass all arguments explicitly. 

398 elif any(name in d and d[name] != _deprecated_parameter 

399 for d in [arguments, arguments.get(kwargs_name, {})]): 

400 deprecation_addendum = ( 

401 f"If any parameter follows {name!r}, they should be passed as " 

402 f"keyword, not positionally.") 

403 warn_deprecated( 

404 since, 

405 name=repr(name), 

406 obj_type=f"parameter of {func.__name__}()", 

407 addendum=(addendum + " " + deprecation_addendum) if addendum 

408 else deprecation_addendum, 

409 **kwargs) 

410 return func(*inner_args, **inner_kwargs) 

411 

412 DECORATORS[wrapper] = decorator 

413 return wrapper 

414 

415 

416def make_keyword_only(since, name, func=None): 

417 """ 

418 Decorator indicating that passing parameter *name* (or any of the following 

419 ones) positionally to *func* is being deprecated. 

420 

421 When used on a method that has a pyplot wrapper, this should be the 

422 outermost decorator, so that :file:`boilerplate.py` can access the original 

423 signature. 

424 """ 

425 

426 decorator = functools.partial(make_keyword_only, since, name) 

427 

428 if func is None: 

429 return decorator 

430 

431 signature = inspect.signature(func) 

432 POK = inspect.Parameter.POSITIONAL_OR_KEYWORD 

433 KWO = inspect.Parameter.KEYWORD_ONLY 

434 assert (name in signature.parameters 

435 and signature.parameters[name].kind == POK), ( 

436 f"Matplotlib internal error: {name!r} must be a positional-or-keyword " 

437 f"parameter for {func.__name__}()") 

438 names = [*signature.parameters] 

439 name_idx = names.index(name) 

440 kwonly = [name for name in names[name_idx:] 

441 if signature.parameters[name].kind == POK] 

442 

443 @functools.wraps(func) 

444 def wrapper(*args, **kwargs): 

445 # Don't use signature.bind here, as it would fail when stacked with 

446 # rename_parameter and an "old" argument name is passed in 

447 # (signature.bind would fail, but the actual call would succeed). 

448 if len(args) > name_idx: 

449 warn_deprecated( 

450 since, message="Passing the %(name)s %(obj_type)s " 

451 "positionally is deprecated since Matplotlib %(since)s; the " 

452 "parameter will become keyword-only %(removal)s.", 

453 name=name, obj_type=f"parameter of {func.__name__}()") 

454 return func(*args, **kwargs) 

455 

456 # Don't modify *func*'s signature, as boilerplate.py needs it. 

457 wrapper.__signature__ = signature.replace(parameters=[ 

458 param.replace(kind=KWO) if param.name in kwonly else param 

459 for param in signature.parameters.values()]) 

460 DECORATORS[wrapper] = decorator 

461 return wrapper 

462 

463 

464def deprecate_method_override(method, obj, *, allow_empty=False, **kwargs): 

465 """ 

466 Return ``obj.method`` with a deprecation if it was overridden, else None. 

467 

468 Parameters 

469 ---------- 

470 method 

471 An unbound method, i.e. an expression of the form 

472 ``Class.method_name``. Remember that within the body of a method, one 

473 can always use ``__class__`` to refer to the class that is currently 

474 being defined. 

475 obj 

476 Either an object of the class where *method* is defined, or a subclass 

477 of that class. 

478 allow_empty : bool, default: False 

479 Whether to allow overrides by "empty" methods without emitting a 

480 warning. 

481 **kwargs 

482 Additional parameters passed to `warn_deprecated` to generate the 

483 deprecation warning; must at least include the "since" key. 

484 """ 

485 

486 def empty(): pass 

487 def empty_with_docstring(): """doc""" 

488 

489 name = method.__name__ 

490 bound_child = getattr(obj, name) 

491 bound_base = ( 

492 method # If obj is a class, then we need to use unbound methods. 

493 if isinstance(bound_child, type(empty)) and isinstance(obj, type) 

494 else method.__get__(obj)) 

495 if (bound_child != bound_base 

496 and (not allow_empty 

497 or (getattr(getattr(bound_child, "__code__", None), 

498 "co_code", None) 

499 not in [empty.__code__.co_code, 

500 empty_with_docstring.__code__.co_code]))): 

501 warn_deprecated(**{"name": name, "obj_type": "method", **kwargs}) 

502 return bound_child 

503 return None 

504 

505 

506@contextlib.contextmanager 

507def suppress_matplotlib_deprecation_warning(): 

508 with warnings.catch_warnings(): 

509 warnings.simplefilter("ignore", MatplotlibDeprecationWarning) 

510 yield