Coverage for utilities/callback.py: 33%

33 statements  

« prev     ^ index     » next       coverage.py v7.11.0, created at 2025-11-16 22:49 +1300

1""" 

2Callback Management 

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

4 

5Provide callback management functionality for event-driven systems. 

6""" 

7 

8from __future__ import annotations 

9 

10import typing 

11from collections import defaultdict 

12from dataclasses import dataclass 

13 

14if 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 

31class 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 

50class 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)