Coverage for utilities/callback.py: 33%

1"""

2 Callback Management

3===================

4

5 Provide callback management functionality for event-driven systems.

6"""

7

8 from __future__ import annotations

9

10 import typing

11 from collections import defaultdict

12 from dataclasses import dataclass

13

14 if typing.TYPE_CHECKING:

15 from colour.hints import Any, Callable, List

16

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"

23

24 __all__ = [

25 "Callback",

26 "MixinCallback",

27]

28

29

30@dataclass

31 class Callback:

32 """

33 Represent a named callback with its associated callable function.

34

35 This dataclass encapsulates a callback's identifying name and its

36 callable function for use in event-driven callback systems.

37

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 """

45

46 name: str

47 function: Callable

48

49

50 class MixinCallback:

51 """

52 Provide callback support for attribute changes in classes.

53

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.

57

58 Attributes

59 ----------

60 - :attr:`~colour.utilities.MixinCallback.callbacks`

61 - :attr:`~colour.utilities.MixinCallback.__setattr__`

62

63 Methods

64 -------

65 - :meth:`~colour.utilities.MixinCallback.register_callback`

66 - :meth:`~colour.utilities.MixinCallback.unregister_callback`

67

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 """

84

85 def __init__(self) -> None:

86 super().__init__()

87

88 self._callbacks: defaultdict[str, List[Callback]] = defaultdict(list)

89

90 @property

91 def callbacks(self) -> defaultdict[str, List[Callback]]:

92 """

93 Getter for the event callbacks dictionary.

94

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 """

102

103 return self._callbacks

104

105 def __setattr__(self, name: str, value: Any) -> None:

106 """

107 Set the specified value to the attribute with the specified name.

108

109 Parameters

110 ----------

111 name

112 Name of the attribute to set.

113 value

114 Value to set the attribute with.

115 """

116

117 if hasattr(self, "_callbacks"):

118 for callback in self._callbacks.get(name, []):

119 value = callback.function(self, name, value)

120

121 super().__setattr__(name, value)

122

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.

127

128 Parameters

129 ----------

130 attribute

131 Attribute to register the callback for.

132 name

133 Callback name.

134 function

135 Callback callable.

136

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 """

151

152 self._callbacks[attribute].append(Callback(name, function))

153

154 def unregister_callback(self, attribute: str, name: str) -> None:

155 """

156 Unregister the callback with the specified name for the specified

157 attribute.

158

159 Parameters

160 ----------

161 attribute

162 Attribute to unregister the callback for.

163 name

164 Callback name.

165

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 """

183

184 if self._callbacks.get(attribute) is None: # pragma: no cover

185 return

186

187 self._callbacks[attribute] = [

188 callback

189 for callback in self._callbacks.get(attribute, [])

190 if callback.name != name

191 ]

192

193 if len(self._callbacks[attribute]) == 0:

194 self._callbacks.pop(attribute, None)