Coverage for pyWebLayout/core/callback_registry.py: 92%

75 statements  

« prev     ^ index     » next       coverage.py v7.11.2, created at 2025-11-12 12:02 +0000

1""" 

2Callback Registry for managing interactable elements and their callbacks. 

3 

4This module provides a registry system for tracking interactive elements (links, buttons, forms) 

5and managing their callbacks. Supports multiple binding strategies: 

6- HTML id attributes for HTML-generated content 

7- Auto-generated ids for programmatic construction 

8- Type-based batch operations 

9""" 

10 

11from typing import Dict, List, Optional, Callable 

12from pyWebLayout.core.base import Interactable 

13 

14 

15class CallbackRegistry: 

16 """ 

17 Registry for managing interactable callbacks with multiple binding strategies. 

18 

19 Supports: 

20 - Direct references by object id 

21 - HTML id attributes (from parsed HTML) 

22 - Type-based queries (all buttons, all links, etc.) 

23 - Auto-generated ids for programmatic construction 

24 

25 This enables flexible callback binding for both HTML-generated content 

26 and manually constructed UIs. 

27 """ 

28 

29 def __init__(self): 

30 """Initialize an empty callback registry.""" 

31 self._by_reference: Dict[int, Interactable] = {} # id(obj) -> obj 

32 self._by_id: Dict[str, Interactable] = {} # HTML id or auto id -> obj 

33 self._by_type: Dict[str, List[Interactable]] = {} # type name -> [objs] 

34 self._auto_counter: int = 0 

35 

36 def register(self, obj: Interactable, html_id: Optional[str] = None) -> str: 

37 """ 

38 Register an interactable object with optional HTML id. 

39 

40 The object is always registered by reference (using Python's id()). 

41 If an html_id is provided, it's also registered by that id. 

42 If no html_id is provided, an auto-generated id is created. 

43 

44 Args: 

45 obj: The interactable object to register 

46 html_id: Optional HTML id attribute value (e.g., from <button id="save-btn">) 

47 

48 Returns: 

49 The id used for registration (either html_id or auto-generated) 

50 

51 Example: 

52 >>> button = ButtonText(...) 

53 >>> registry.register(button, html_id="save-btn") 

54 'save-btn' 

55 >>> registry.register(other_button) # No html_id 

56 'auto_button_0' 

57 """ 

58 # Always register by Python object id for direct lookups 

59 obj_id = id(obj) 

60 self._by_reference[obj_id] = obj 

61 

62 # Determine type name and register by type 

63 type_name = self._get_type_name(obj) 

64 if type_name not in self._by_type: 

65 self._by_type[type_name] = [] 

66 self._by_type[type_name].append(obj) 

67 

68 # Register by HTML id or generate auto id 

69 if html_id: 

70 # Use provided HTML id 

71 self._by_id[html_id] = obj 

72 return html_id 

73 else: 

74 # Generate automatic id 

75 auto_id = f"auto_{type_name}_{self._auto_counter}" 

76 self._auto_counter += 1 

77 self._by_id[auto_id] = obj 

78 return auto_id 

79 

80 def get_by_id(self, identifier: str) -> Optional[Interactable]: 

81 """ 

82 Get an interactable by its id (HTML id or auto-generated id). 

83 

84 Args: 

85 identifier: The id to lookup (e.g., "save-btn" or "auto_button_0") 

86 

87 Returns: 

88 The interactable object, or None if not found 

89 

90 Example: 

91 >>> button = registry.get_by_id("save-btn") 

92 >>> if button: 

93 ... button._callback = my_save_function 

94 """ 

95 return self._by_id.get(identifier) 

96 

97 def get_by_type(self, type_name: str) -> List[Interactable]: 

98 """ 

99 Get all interactables of a specific type. 

100 

101 Args: 

102 type_name: The type name (e.g., "link", "button", "form_field") 

103 

104 Returns: 

105 List of interactable objects of that type (may be empty) 

106 

107 Example: 

108 >>> all_buttons = registry.get_by_type("button") 

109 >>> for button in all_buttons: 

110 ... print(button.text) 

111 """ 

112 return self._by_type.get(type_name, []).copy() 

113 

114 def get_all_ids(self) -> List[str]: 

115 """ 

116 Get all registered ids (both HTML ids and auto-generated ids). 

117 

118 Returns: 

119 List of all ids in the registry 

120 

121 Example: 

122 >>> ids = registry.get_all_ids() 

123 >>> print(ids) 

124 ['save-btn', 'cancel-btn', 'auto_link_0', 'auto_button_1'] 

125 """ 

126 return list(self._by_id.keys()) 

127 

128 def get_all_types(self) -> List[str]: 

129 """ 

130 Get all registered type names. 

131 

132 Returns: 

133 List of type names that have registered objects 

134 

135 Example: 

136 >>> types = registry.get_all_types() 

137 >>> print(types) 

138 ['link', 'button', 'form_field'] 

139 """ 

140 return list(self._by_type.keys()) 

141 

142 def set_callback(self, identifier: str, callback: Callable) -> bool: 

143 """ 

144 Set the callback for an interactable by its id. 

145 

146 Args: 

147 identifier: The id of the interactable 

148 callback: The callback function to set 

149 

150 Returns: 

151 True if the interactable was found and callback set, False otherwise 

152 

153 Example: 

154 >>> def on_save(point): 

155 ... print("Save clicked!") 

156 >>> registry.set_callback("save-btn", on_save) 

157 True 

158 """ 

159 obj = self.get_by_id(identifier) 

160 if obj: 

161 obj._callback = callback 

162 return True 

163 return False 

164 

165 def set_callbacks_by_type(self, type_name: str, callback: Callable) -> int: 

166 """ 

167 Set the callback for all interactables of a specific type. 

168 

169 Useful for batch operations like setting a default click sound 

170 for all buttons, or a default link handler for all links. 

171 

172 Args: 

173 type_name: The type name (e.g., "button", "link") 

174 callback: The callback function to set 

175 

176 Returns: 

177 Number of objects that had their callback set 

178 

179 Example: 

180 >>> def play_click_sound(point): 

181 ... audio.play("click.wav") 

182 >>> count = registry.set_callbacks_by_type("button", play_click_sound) 

183 >>> print(f"Set callback for {count} buttons") 

184 """ 

185 objects = self.get_by_type(type_name) 

186 for obj in objects: 

187 obj._callback = callback 

188 return len(objects) 

189 

190 def unregister(self, identifier: str) -> bool: 

191 """ 

192 Unregister an interactable by its id. 

193 

194 Args: 

195 identifier: The id of the interactable to unregister 

196 

197 Returns: 

198 True if the interactable was found and unregistered, False otherwise 

199 """ 

200 obj = self._by_id.pop(identifier, None) 

201 if obj: 201 ↛ 214line 201 didn't jump to line 214 because the condition on line 201 was always true

202 # Remove from reference map 

203 self._by_reference.pop(id(obj), None) 

204 

205 # Remove from type map 

206 type_name = self._get_type_name(obj) 

207 if type_name in self._by_type: 207 ↛ 213line 207 didn't jump to line 213 because the condition on line 207 was always true

208 try: 

209 self._by_type[type_name].remove(obj) 

210 except ValueError: 

211 pass 

212 

213 return True 

214 return False 

215 

216 def clear(self): 

217 """Clear all registered interactables.""" 

218 self._by_reference.clear() 

219 self._by_id.clear() 

220 self._by_type.clear() 

221 self._auto_counter = 0 

222 

223 def count(self) -> int: 

224 """ 

225 Get the total number of registered interactables. 

226 

227 Returns: 

228 Total count of registered objects 

229 """ 

230 return len(self._by_id) 

231 

232 def count_by_type(self, type_name: str) -> int: 

233 """ 

234 Get the count of interactables of a specific type. 

235 

236 Args: 

237 type_name: The type name to count 

238 

239 Returns: 

240 Number of objects of that type 

241 """ 

242 return len(self._by_type.get(type_name, [])) 

243 

244 def _get_type_name(self, obj: Interactable) -> str: 

245 """ 

246 Get a normalized type name for an interactable object. 

247 

248 Args: 

249 obj: The interactable object 

250 

251 Returns: 

252 Type name string (e.g., "link", "button", "form_field") 

253 """ 

254 # Import here to avoid circular imports 

255 from pyWebLayout.concrete.functional import LinkText, ButtonText, FormFieldText 

256 

257 if isinstance(obj, LinkText): 

258 return "link" 

259 elif isinstance(obj, ButtonText): 

260 return "button" 

261 elif isinstance(obj, FormFieldText): 261 ↛ 265line 261 didn't jump to line 265 because the condition on line 261 was always true

262 return "form_field" 

263 else: 

264 # Fallback to class name 

265 return obj.__class__.__name__.lower() 

266 

267 def __len__(self) -> int: 

268 """Support len() to get count of registered interactables.""" 

269 return self.count() 

270 

271 def __contains__(self, identifier: str) -> bool: 

272 """Support 'in' operator to check if an id is registered.""" 

273 return identifier in self._by_id 

274 

275 def __repr__(self) -> str: 

276 """String representation showing registry statistics.""" 

277 type_counts = {t: len(objs) for t, objs in self._by_type.items()} 

278 return f"CallbackRegistry(total={self.count()}, types={type_counts})"