Coverage for colour/utilities/callback.py: 100%
33 statements
« prev ^ index » next coverage.py v7.11.0, created at 2025-11-15 19:01 +1300
« prev ^ index » next coverage.py v7.11.0, created at 2025-11-15 19:01 +1300
1"""
2Callback Management
3===================
5Provide callback management functionality for event-driven systems.
6"""
8from __future__ import annotations
10import typing
11from collections import defaultdict
12from dataclasses import dataclass
14if typing.TYPE_CHECKING:
15 from colour.hints import Any, Callable, List
17__author__ = "Colour Developers"
18__copyright__ = "Copyright 2013 Colour Developers"
19__license__ = "BSD-3-Clause - https://opensource.org/licenses/BSD-3-Clause"
20__maintainer__ = "Colour Developers"
21__email__ = "colour-developers@colour-science.org"
22__status__ = "Production"
24__all__ = [
25 "Callback",
26 "MixinCallback",
27]
30@dataclass
31class Callback:
32 """
33 Represent a named callback with its associated callable function.
35 This dataclass encapsulates a callback's identifying name and its
36 callable function for use in event-driven callback systems.
38 Parameters
39 ----------
40 name
41 Callback identifier used for registration and management.
42 function
43 Callable object to execute when the callback is triggered.
44 """
46 name: str
47 function: Callable
50class MixinCallback:
51 """
52 Provide callback support for attribute changes in classes.
54 This mixin extends class functionality to enable callback registration,
55 allowing automatic invocation when specified attributes are modified.
56 Callbacks can transform or validate attribute values before they are set.
58 Attributes
59 ----------
60 - :attr:`~colour.utilities.MixinCallback.callbacks`
61 - :attr:`~colour.utilities.MixinCallback.__setattr__`
63 Methods
64 -------
65 - :meth:`~colour.utilities.MixinCallback.register_callback`
66 - :meth:`~colour.utilities.MixinCallback.unregister_callback`
68 Examples
69 --------
70 >>> class WithCallback(MixinCallback):
71 ... def __init__(self):
72 ... super().__init__()
73 ... self.attribute_a = "a"
74 >>> with_callback = WithCallback()
75 >>> def _on_attribute_a_changed(self, name: str, value: str) -> str:
76 ... return value.upper()
77 >>> with_callback.register_callback(
78 ... "attribute_a", "on_attribute_a_changed", _on_attribute_a_changed
79 ... )
80 >>> with_callback.attribute_a = "a"
81 >>> with_callback.attribute_a
82 'A'
83 """
85 def __init__(self) -> None:
86 super().__init__()
88 self._callbacks: defaultdict[str, List[Callback]] = defaultdict(list)
90 @property
91 def callbacks(self) -> defaultdict[str, List[Callback]]:
92 """
93 Getter for the event callbacks dictionary.
95 Returns
96 -------
97 :class:`defaultdict`
98 Dictionary mapping event names to lists of callback functions.
99 Each key represents an event identifier, and each value contains
100 the registered callbacks for that event.
101 """
103 return self._callbacks
105 def __setattr__(self, name: str, value: Any) -> None:
106 """
107 Set the specified value to the attribute with the specified name.
109 Parameters
110 ----------
111 name
112 Name of the attribute to set.
113 value
114 Value to set the attribute with.
115 """
117 if hasattr(self, "_callbacks"):
118 for callback in self._callbacks.get(name, []):
119 value = callback.function(self, name, value)
121 super().__setattr__(name, value)
123 def register_callback(self, attribute: str, name: str, function: Callable) -> None:
124 """
125 Register a callback with the specified name for the specified
126 attribute.
128 Parameters
129 ----------
130 attribute
131 Attribute to register the callback for.
132 name
133 Callback name.
134 function
135 Callback callable.
137 Examples
138 --------
139 >>> class WithCallback(MixinCallback):
140 ... def __init__(self):
141 ... super().__init__()
142 ... self.attribute_a = "a"
143 >>> with_callback = WithCallback()
144 >>> with_callback.register_callback(
145 ... "attribute_a", "callback", lambda *args: None
146 ... )
147 >>> with_callback.callbacks # doctest: +SKIP
148 defaultdict(<class 'list'>, {'attribute_a': \
149[Callback(name='callback', function=<function <lambda> at 0x...>)]})
150 """
152 self._callbacks[attribute].append(Callback(name, function))
154 def unregister_callback(self, attribute: str, name: str) -> None:
155 """
156 Unregister the callback with the specified name for the specified
157 attribute.
159 Parameters
160 ----------
161 attribute
162 Attribute to unregister the callback for.
163 name
164 Callback name.
166 Examples
167 --------
168 >>> class WithCallback(MixinCallback):
169 ... def __init__(self):
170 ... super().__init__()
171 ... self.attribute_a = "a"
172 >>> with_callback = WithCallback()
173 >>> with_callback.register_callback(
174 ... "attribute_a", "callback", lambda s, n, v: v
175 ... )
176 >>> with_callback.callbacks # doctest: +SKIP
177 defaultdict(<class 'list'>, {'attribute_a': \
178[Callback(name='callback', function=<function <lambda> at 0x...>)]})
179 >>> with_callback.unregister_callback("attribute_a", "callback")
180 >>> with_callback.callbacks
181 defaultdict(<class 'list'>, {})
182 """
184 if self._callbacks.get(attribute) is None: # pragma: no cover
185 return
187 self._callbacks[attribute] = [
188 callback
189 for callback in self._callbacks.get(attribute, [])
190 if callback.name != name
191 ]
193 if len(self._callbacks[attribute]) == 0:
194 self._callbacks.pop(attribute, None)