Coverage for utilities/deprecation.py: 41%
111 statements
« prev ^ index » next coverage.py v7.11.0, created at 2025-11-16 22:49 +1300
« prev ^ index » next coverage.py v7.11.0, created at 2025-11-16 22:49 +1300
1"""
2Deprecation Utilities
3=====================
5Define deprecation management utilities for the Colour library.
6"""
8from __future__ import annotations
10import sys
11import typing
12from dataclasses import dataclass
13from importlib import import_module
14from operator import attrgetter
16if typing.TYPE_CHECKING:
17 from colour.hints import Any, ModuleType
19from colour.utilities import MixinDataclassIterable, attest, optional, usage_warning
21__author__ = "Colour Developers"
22__copyright__ = "Copyright 2013 Colour Developers"
23__license__ = "BSD-3-Clause - https://opensource.org/licenses/BSD-3-Clause"
24__maintainer__ = "Colour Developers"
25__email__ = "colour-developers@colour-science.org"
26__status__ = "Production"
28__all__ = [
29 "ObjectRenamed",
30 "ObjectRemoved",
31 "ObjectFutureRename",
32 "ObjectFutureRemove",
33 "ObjectFutureAccessChange",
34 "ObjectFutureAccessRemove",
35 "ModuleAPI",
36 "ArgumentRenamed",
37 "ArgumentRemoved",
38 "ArgumentFutureRename",
39 "ArgumentFutureRemove",
40 "get_attribute",
41 "build_API_changes",
42 "handle_arguments_deprecation",
43]
46@dataclass(frozen=True)
47class ObjectRenamed(MixinDataclassIterable):
48 """
49 Represent an object that has been renamed in the API.
51 Parameters
52 ----------
53 name
54 Object name that has been changed.
55 new_name
56 New object name.
57 """
59 name: str
60 new_name: str
62 def __str__(self) -> str:
63 """
64 Return a formatted string representation of the class.
66 Returns
67 -------
68 :class:`str`
69 Formatted string representation.
70 """
72 return f'"{self.name}" object has been renamed to "{self.new_name}".'
75@dataclass(frozen=True)
76class ObjectRemoved(MixinDataclassIterable):
77 """
78 Represent an object that has been removed from the API.
80 Parameters
81 ----------
82 name
83 Object name that has been removed.
84 """
86 name: str
88 def __str__(self) -> str:
89 """
90 Return a formatted string representation of the class.
92 Returns
93 -------
94 :class:`str`
95 Formatted string representation.
96 """
98 return f'"{self.name}" object has been removed from the API.'
101@dataclass(frozen=True)
102class ObjectFutureRename(MixinDataclassIterable):
103 """
104 Represent an object that will be renamed in a future release.
106 Parameters
107 ----------
108 name
109 Object name that will change in a future release.
110 new_name
111 New object name.
112 """
114 name: str
115 new_name: str
117 def __str__(self) -> str:
118 """
119 Return a formatted string representation of the class.
121 Returns
122 -------
123 :class:`str`
124 Formatted string representation.
125 """
127 return (
128 f'"{self.name}" object is deprecated and will be renamed to '
129 f'"{self.new_name}" in a future release.'
130 )
133@dataclass(frozen=True)
134class ObjectFutureRemove(MixinDataclassIterable):
135 """
136 Represent an object that will be removed in a future release.
138 Parameters
139 ----------
140 name
141 Object name that will be removed in a future release.
142 """
144 name: str
146 def __str__(self) -> str:
147 """
148 Return a formatted string representation of the class.
150 Returns
151 -------
152 :class:`str`
153 Formatted string representation.
154 """
156 return (
157 f'"{self.name}" object is deprecated and will be removed in '
158 f"a future release."
159 )
162@dataclass(frozen=True)
163class ObjectFutureAccessChange(MixinDataclassIterable):
164 """
165 Represent an object whose access pattern will change in a future
166 release.
168 Parameters
169 ----------
170 access
171 Object access that will change in a future release.
172 new_access
173 New object access pattern.
174 """
176 access: str
177 new_access: str
179 def __str__(self) -> str:
180 """
181 Return a formatted string representation of the class.
183 Returns
184 -------
185 :class:`str`
186 Formatted string representation.
187 """
189 return (
190 f'"{self.access}" object access is deprecated and will change '
191 f'to "{self.new_access}" in a future release.'
192 )
195@dataclass(frozen=True)
196class ObjectFutureAccessRemove(MixinDataclassIterable):
197 """
198 Represent an object whose access will be removed in a future release.
199 be removed in a future release.
201 Parameters
202 ----------
203 name
204 Object name whose access will be removed in a future release.
205 """
207 name: str
209 def __str__(self) -> str:
210 """
211 Return a formatted string representation of the class.
213 Returns
214 -------
215 :class:`str`
216 Formatted string representation.
217 """
219 return f'"{self.name}" object access will be removed in a future release.'
222@dataclass(frozen=True)
223class ArgumentRenamed(MixinDataclassIterable):
224 """
225 Represent an argument that has been renamed in the API.
227 Parameters
228 ----------
229 name
230 Argument name that has been changed.
231 new_name
232 New argument name.
233 """
235 name: str
236 new_name: str
238 def __str__(self) -> str:
239 """
240 Return a formatted string representation of the class.
242 Returns
243 -------
244 :class:`str`
245 Formatted string representation.
246 """
248 return f'"{self.name}" argument has been renamed to "{self.new_name}".'
251@dataclass(frozen=True)
252class ArgumentRemoved(MixinDataclassIterable):
253 """
254 Represent an argument that has been removed from the API.
256 Parameters
257 ----------
258 name
259 Argument name that has been removed.
260 """
262 name: str
264 def __str__(self) -> str:
265 """
266 Return a formatted string representation of the class.
268 Returns
269 -------
270 :class:`str`
271 Formatted string representation.
272 """
274 return f'"{self.name}" argument has been removed from the API.'
277@dataclass(frozen=True)
278class ArgumentFutureRename(MixinDataclassIterable):
279 """
280 Represent an argument that will be renamed in a future release.
281 change in a future release.
283 Parameters
284 ----------
285 name
286 Argument name that will change in a future release.
287 new_name
288 New argument name.
289 """
291 name: str
292 new_name: str
294 def __str__(self) -> str:
295 """
296 Return a formatted string representation of the class.
298 Returns
299 -------
300 :class:`str`
301 Formatted string representation.
302 """
304 return (
305 f'"{self.name}" argument is deprecated and will be renamed to '
306 f'"{self.new_name}" in a future release.'
307 )
310@dataclass(frozen=True)
311class ArgumentFutureRemove(MixinDataclassIterable):
312 """
313 Represent an argument that will be removed in a future release.
315 Parameters
316 ----------
317 name
318 Argument name that will be removed in a future release.
319 """
321 name: str
323 def __str__(self) -> str:
324 """
325 Return a formatted string representation of the class.
327 Returns
328 -------
329 :class:`str`
330 Formatted string representation.
331 """
333 return (
334 f'"{self.name}" argument is deprecated and will be removed in '
335 f"a future release."
336 )
339class ModuleAPI:
340 """
341 Define a class enabling customisation of module attribute access with
342 built-in deprecation management functionality.
344 Parameters
345 ----------
346 module
347 Module for which to customise attribute access behaviour.
349 Methods
350 -------
351 - :meth:`~colour.utilities.ModuleAPI.__init__`
352 - :meth:`~colour.utilities.ModuleAPI.__getattr__`
353 - :meth:`~colour.utilities.ModuleAPI.__dir__`
355 Examples
356 --------
357 >>> import sys
358 >>> sys.modules["colour"] = ModuleAPI(sys.modules["colour"])
359 ... # doctest: +SKIP
360 """
362 def __init__(self, module: ModuleType, changes: dict | None = None) -> None:
363 self._module = module
364 self._changes = optional(changes, {})
366 def __getattr__(self, attribute: str) -> Any:
367 """
368 Return the specified attribute value while handling deprecation.
370 Parameters
371 ----------
372 attribute
373 Attribute name.
375 Returns
376 -------
377 :class:`object`
378 Attribute value.
380 Raises
381 ------
382 AttributeError
383 If the attribute is not defined.
384 """
386 change = self._changes.get(attribute)
388 if change is not None:
389 if not isinstance(change, ObjectRemoved):
390 usage_warning(str(change))
392 return (
393 getattr(self._module, attribute)
394 if isinstance(change, ObjectFutureRemove)
395 else get_attribute(change.values[1])
396 )
398 raise AttributeError(str(change))
400 return getattr(self._module, attribute)
402 def __dir__(self) -> list:
403 """
404 Return the list of names in the module local scope filtered according
405 to the changes.
407 Returns
408 -------
409 :class:`list`
410 Filtered list of names in the module local scope.
411 """
413 return [
414 attribute
415 for attribute in dir(self._module)
416 if attribute not in self._changes
417 ]
420def get_attribute(attribute: str) -> Any:
421 """
422 Retrieve the value of the specified attribute from its namespace.
424 Parameters
425 ----------
426 attribute
427 Attribute to retrieve, ``attribute`` must have a namespace
428 module, e.g., *colour.models.oetf_inverse_BT2020*.
430 Returns
431 -------
432 :class:`object`
433 Retrieved attribute value.
435 Examples
436 --------
437 >>> get_attribute("colour.models.oetf_inverse_BT2020") # doctest: +ELLIPSIS
438 <function oetf_inverse_BT2020 at 0x...>
439 """
441 attest("." in attribute, '"{0}" attribute has no namespace!')
443 module_name, attribute = attribute.rsplit(".", 1)
445 module = optional(sys.modules.get(module_name), import_module(module_name))
447 attest(
448 module is not None,
449 f'"{module_name}" module does not exists or cannot be imported!',
450 )
452 return attrgetter(attribute)(module)
455def build_API_changes(changes: dict) -> dict:
456 """
457 Build effective API changes from specified API changes mapping.
459 Parameters
460 ----------
461 changes
462 Dictionary of desired API changes.
464 Returns
465 -------
466 :class:`dict`
467 API changes
469 Examples
470 --------
471 >>> from pprint import pprint
472 >>> changes = {
473 ... "ObjectRenamed": [
474 ... [
475 ... "module.object_1_name",
476 ... "module.object_1_new_name",
477 ... ]
478 ... ],
479 ... "ObjectFutureRename": [
480 ... [
481 ... "module.object_2_name",
482 ... "module.object_2_new_name",
483 ... ]
484 ... ],
485 ... "ObjectFutureAccessChange": [
486 ... [
487 ... "module.object_3_access",
488 ... "module.sub_module.object_3_new_access",
489 ... ]
490 ... ],
491 ... "ObjectRemoved": ["module.object_4_name"],
492 ... "ObjectFutureRemove": ["module.object_5_name"],
493 ... "ObjectFutureAccessRemove": ["module.object_6_access"],
494 ... }
495 >>> pprint(build_API_changes(changes)) # doctest: +SKIP
496 {'object_1_name': ObjectRenamed(name='module.object_1_name', \
497new_name='module.object_1_new_name'),
498 'object_2_name': ObjectFutureRename(name='module.object_2_name', \
499new_name='module.object_2_new_name'),
500 'object_3_access': ObjectFutureAccessChange(\
501access='module.object_3_access', \
502new_access='module.sub_module.object_3_new_access'),
503 'object_4_name': ObjectRemoved(name='module.object_4_name'),
504 'object_5_name': ObjectFutureRemove(name='module.object_5_name'),
505 'object_6_access': ObjectFutureAccessRemove(\
506name='module.object_6_access')}
507 """
509 for rename_type in (
510 ObjectRenamed,
511 ObjectFutureRename,
512 ObjectFutureAccessChange,
513 ArgumentRenamed,
514 ArgumentFutureRename,
515 ):
516 for change in changes.pop(rename_type.__name__, []):
517 changes[change[0].split(".")[-1]] = rename_type(*change)
519 for remove_type in (
520 ObjectRemoved,
521 ObjectFutureRemove,
522 ObjectFutureAccessRemove,
523 ArgumentRemoved,
524 ArgumentFutureRemove,
525 ):
526 for change in changes.pop(remove_type.__name__, []):
527 changes[change.split(".")[-1]] = remove_type(change)
529 return changes
532def handle_arguments_deprecation(changes: dict, **kwargs: Any) -> dict:
533 """
534 Handle argument deprecation according to the specified API changes
535 mapping.
537 Parameters
538 ----------
539 changes
540 Dictionary of specified API changes defining how arguments should
541 be handled during deprecation.
543 Other Parameters
544 ----------------
545 kwargs
546 Keyword arguments to process for deprecation handling.
548 Returns
549 -------
550 :class:`dict`
551 Processed keyword arguments with deprecation rules applied.
553 Examples
554 --------
555 >>> changes = {
556 ... "ArgumentRenamed": [
557 ... [
558 ... "argument_1_name",
559 ... "argument_1_new_name",
560 ... ]
561 ... ],
562 ... "ArgumentFutureRename": [
563 ... [
564 ... "argument_2_name",
565 ... "argument_2_new_name",
566 ... ]
567 ... ],
568 ... "ArgumentRemoved": ["argument_3_name"],
569 ... "ArgumentFutureRemove": ["argument_4_name"],
570 ... }
571 >>> handle_arguments_deprecation(
572 ... changes,
573 ... argument_1_name=True,
574 ... argument_2_name=True,
575 ... argument_4_name=True,
576 ... )
577 ... # doctest: +SKIP
578 {'argument_4_name': True, 'argument_1_new_name': True, \
579'argument_2_new_name': True}
580 """
582 changes = build_API_changes(changes)
584 for kwarg in kwargs.copy():
585 change = changes.get(kwarg)
587 if change is None:
588 continue
590 if not isinstance(change, ArgumentRemoved):
591 usage_warning(str(change))
593 if isinstance(change, ArgumentFutureRemove):
594 continue
595 kwargs[change.values[1]] = kwargs.pop(kwarg)
596 else:
597 kwargs.pop(kwarg)
598 usage_warning(str(change))
600 return kwargs