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
« 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.
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"""
11from typing import Dict, List, Optional, Callable
12from pyWebLayout.core.base import Interactable
15class CallbackRegistry:
16 """
17 Registry for managing interactable callbacks with multiple binding strategies.
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
25 This enables flexible callback binding for both HTML-generated content
26 and manually constructed UIs.
27 """
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
36 def register(self, obj: Interactable, html_id: Optional[str] = None) -> str:
37 """
38 Register an interactable object with optional HTML id.
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.
44 Args:
45 obj: The interactable object to register
46 html_id: Optional HTML id attribute value (e.g., from <button id="save-btn">)
48 Returns:
49 The id used for registration (either html_id or auto-generated)
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
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)
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
80 def get_by_id(self, identifier: str) -> Optional[Interactable]:
81 """
82 Get an interactable by its id (HTML id or auto-generated id).
84 Args:
85 identifier: The id to lookup (e.g., "save-btn" or "auto_button_0")
87 Returns:
88 The interactable object, or None if not found
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)
97 def get_by_type(self, type_name: str) -> List[Interactable]:
98 """
99 Get all interactables of a specific type.
101 Args:
102 type_name: The type name (e.g., "link", "button", "form_field")
104 Returns:
105 List of interactable objects of that type (may be empty)
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()
114 def get_all_ids(self) -> List[str]:
115 """
116 Get all registered ids (both HTML ids and auto-generated ids).
118 Returns:
119 List of all ids in the registry
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())
128 def get_all_types(self) -> List[str]:
129 """
130 Get all registered type names.
132 Returns:
133 List of type names that have registered objects
135 Example:
136 >>> types = registry.get_all_types()
137 >>> print(types)
138 ['link', 'button', 'form_field']
139 """
140 return list(self._by_type.keys())
142 def set_callback(self, identifier: str, callback: Callable) -> bool:
143 """
144 Set the callback for an interactable by its id.
146 Args:
147 identifier: The id of the interactable
148 callback: The callback function to set
150 Returns:
151 True if the interactable was found and callback set, False otherwise
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
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.
169 Useful for batch operations like setting a default click sound
170 for all buttons, or a default link handler for all links.
172 Args:
173 type_name: The type name (e.g., "button", "link")
174 callback: The callback function to set
176 Returns:
177 Number of objects that had their callback set
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)
190 def unregister(self, identifier: str) -> bool:
191 """
192 Unregister an interactable by its id.
194 Args:
195 identifier: The id of the interactable to unregister
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)
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
213 return True
214 return False
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
223 def count(self) -> int:
224 """
225 Get the total number of registered interactables.
227 Returns:
228 Total count of registered objects
229 """
230 return len(self._by_id)
232 def count_by_type(self, type_name: str) -> int:
233 """
234 Get the count of interactables of a specific type.
236 Args:
237 type_name: The type name to count
239 Returns:
240 Number of objects of that type
241 """
242 return len(self._by_type.get(type_name, []))
244 def _get_type_name(self, obj: Interactable) -> str:
245 """
246 Get a normalized type name for an interactable object.
248 Args:
249 obj: The interactable object
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
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()
267 def __len__(self) -> int:
268 """Support len() to get count of registered interactables."""
269 return self.count()
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
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})"