syd 0.1.4__py3-none-any.whl → 0.1.6__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,478 @@
1
+ from abc import ABC, abstractmethod
2
+ from typing import Any, Dict, Generic, List, TypeVar, Union, Callable
3
+ import ipywidgets as widgets
4
+
5
+ from ..parameters import (
6
+ Parameter,
7
+ TextParameter,
8
+ SelectionParameter,
9
+ MultipleSelectionParameter,
10
+ BooleanParameter,
11
+ IntegerParameter,
12
+ FloatParameter,
13
+ IntegerRangeParameter,
14
+ FloatRangeParameter,
15
+ UnboundedIntegerParameter,
16
+ UnboundedFloatParameter,
17
+ ButtonAction,
18
+ )
19
+
20
+ T = TypeVar("T", bound=Parameter[Any])
21
+ W = TypeVar("W", bound=widgets.Widget)
22
+
23
+
24
+ class BaseWidget(Generic[T, W], ABC):
25
+ """
26
+ Abstract base class for all parameter widgets.
27
+
28
+ This class defines the common interface and shared functionality
29
+ for widgets that correspond to different parameter types.
30
+ """
31
+
32
+ _widget: W
33
+ _callbacks: List[Dict[str, Union[Callable, Union[str, List[str]]]]]
34
+ _is_action: bool = False
35
+
36
+ def __init__(self, parameter: T, continuous: bool = False):
37
+ self._widget = self._create_widget(parameter, continuous)
38
+ self._updating = False # Flag to prevent circular updates
39
+ # List of callbacks to remember for quick disabling/enabling
40
+ self._callbacks = []
41
+
42
+ @abstractmethod
43
+ def _create_widget(self, parameter: T, continuous: bool) -> W:
44
+ """Create and return the appropriate ipywidget."""
45
+ pass
46
+
47
+ @property
48
+ def widget(self) -> W:
49
+ """Get the underlying ipywidget."""
50
+ return self._widget
51
+
52
+ @property
53
+ def value(self) -> Any:
54
+ """Get the current value of the widget."""
55
+ return self._widget.value
56
+
57
+ @value.setter
58
+ def value(self, new_value: Any) -> None:
59
+ """Set the value of the widget."""
60
+ self._widget.value = new_value
61
+
62
+ def matches_parameter(self, parameter: T) -> bool:
63
+ """Check if the widget matches the parameter."""
64
+ return self.value == parameter.value
65
+
66
+ def update_from_parameter(self, parameter: T) -> None:
67
+ """Update the widget from the parameter."""
68
+ try:
69
+ self._updating = True
70
+ self.disable_callbacks()
71
+ self.extra_updates_from_parameter(parameter)
72
+ self.value = parameter.value
73
+ finally:
74
+ self.reenable_callbacks()
75
+ self._updating = False
76
+
77
+ def extra_updates_from_parameter(self, parameter: T) -> None:
78
+ """Extra updates from the parameter."""
79
+ pass
80
+
81
+ def observe(self, callback: Callable) -> None:
82
+ """Observe the widget and call the callback when the value changes."""
83
+ full_callback = dict(handler=callback, names="value")
84
+ self._widget.observe(**full_callback)
85
+ self._callbacks.append(full_callback)
86
+
87
+ def unobserve(self, callback: Callable[[Any], None]) -> None:
88
+ """Unobserve the widget and stop calling the callback when the value changes."""
89
+ full_callback = dict(handler=callback, names="value")
90
+ self._widget.unobserve(**full_callback)
91
+ self._callbacks.remove(full_callback)
92
+
93
+ def reenable_callbacks(self) -> None:
94
+ """Reenable all callbacks from the widget."""
95
+ for callback in self._callbacks:
96
+ self._widget.observe(**callback)
97
+
98
+ def disable_callbacks(self) -> None:
99
+ """Disable all callbacks from the widget."""
100
+ for callback in self._callbacks:
101
+ self._widget.unobserve(**callback)
102
+
103
+
104
+ class TextWidget(BaseWidget[TextParameter, widgets.Text]):
105
+ """Widget for text parameters."""
106
+
107
+ def _create_widget(
108
+ self, parameter: TextParameter, continuous: bool
109
+ ) -> widgets.Text:
110
+ return widgets.Text(
111
+ value=parameter.value,
112
+ description=parameter.name,
113
+ continuous=continuous,
114
+ layout=widgets.Layout(width="95%"),
115
+ )
116
+
117
+
118
+ class BooleanWidget(BaseWidget[BooleanParameter, widgets.Checkbox]):
119
+ """Widget for boolean parameters."""
120
+
121
+ def _create_widget(
122
+ self, parameter: BooleanParameter, continuous: bool
123
+ ) -> widgets.Checkbox:
124
+ return widgets.Checkbox(value=parameter.value, description=parameter.name)
125
+
126
+
127
+ class SelectionWidget(BaseWidget[SelectionParameter, widgets.Dropdown]):
128
+ """Widget for single selection parameters."""
129
+
130
+ def _create_widget(
131
+ self, parameter: SelectionParameter, continuous: bool
132
+ ) -> widgets.Dropdown:
133
+ return widgets.Dropdown(
134
+ value=parameter.value,
135
+ options=parameter.options,
136
+ description=parameter.name,
137
+ layout=widgets.Layout(width="95%"),
138
+ )
139
+
140
+ def matches_parameter(self, parameter: SelectionParameter) -> bool:
141
+ """Check if the widget matches the parameter."""
142
+ return (
143
+ self.value == parameter.value and self._widget.options == parameter.options
144
+ )
145
+
146
+ def extra_updates_from_parameter(self, parameter: SelectionParameter) -> None:
147
+ """Extra updates from the parameter."""
148
+ new_options = parameter.options
149
+ current_value = self._widget.value
150
+ new_value = current_value if current_value in new_options else new_options[0]
151
+ self._widget.options = new_options
152
+ self._widget.value = new_value
153
+
154
+
155
+ class MultipleSelectionWidget(
156
+ BaseWidget[MultipleSelectionParameter, widgets.SelectMultiple]
157
+ ):
158
+ """Widget for multiple selection parameters."""
159
+
160
+ def _create_widget(
161
+ self, parameter: MultipleSelectionParameter, continuous: bool
162
+ ) -> widgets.SelectMultiple:
163
+ return widgets.SelectMultiple(
164
+ value=parameter.value,
165
+ options=parameter.options,
166
+ description=parameter.name,
167
+ rows=min(len(parameter.options), 4),
168
+ layout=widgets.Layout(width="95%"),
169
+ )
170
+
171
+ def matches_parameter(self, parameter: MultipleSelectionParameter) -> bool:
172
+ """Check if the widget matches the parameter."""
173
+ return (
174
+ self.value == parameter.value and self._widget.options == parameter.options
175
+ )
176
+
177
+ def extra_updates_from_parameter(
178
+ self, parameter: MultipleSelectionParameter
179
+ ) -> None:
180
+ """Extra updates from the parameter."""
181
+ new_options = parameter.options
182
+ current_values = set(self._widget.value)
183
+ new_values = [v for v in current_values if v in new_options]
184
+ self._widget.options = new_options
185
+ self._widget.value = new_values
186
+
187
+
188
+ class IntegerWidget(BaseWidget[IntegerParameter, widgets.IntSlider]):
189
+ """Widget for integer parameters."""
190
+
191
+ def _create_widget(
192
+ self, parameter: IntegerParameter, continuous: bool
193
+ ) -> widgets.IntSlider:
194
+ return widgets.IntSlider(
195
+ value=parameter.value,
196
+ min=parameter.min_value,
197
+ max=parameter.max_value,
198
+ description=parameter.name,
199
+ continuous=continuous,
200
+ layout=widgets.Layout(width="95%"),
201
+ style={"description_width": "initial"},
202
+ )
203
+
204
+ def matches_parameter(self, parameter: IntegerParameter) -> bool:
205
+ """Check if the widget matches the parameter."""
206
+ return (
207
+ self.value == parameter.value
208
+ and self._widget.min == parameter.min_value
209
+ and self._widget.max == parameter.max_value
210
+ )
211
+
212
+ def extra_updates_from_parameter(self, parameter: IntegerParameter) -> None:
213
+ """Extra updates from the parameter."""
214
+ current_value = self._widget.value
215
+ self._widget.min = parameter.min_value
216
+ self._widget.max = parameter.max_value
217
+ self.value = max(parameter.min_value, min(parameter.max_value, current_value))
218
+
219
+
220
+ class FloatWidget(BaseWidget[FloatParameter, widgets.FloatSlider]):
221
+ """Widget for float parameters."""
222
+
223
+ def _create_widget(
224
+ self, parameter: FloatParameter, continuous: bool
225
+ ) -> widgets.FloatSlider:
226
+ return widgets.FloatSlider(
227
+ value=parameter.value,
228
+ min=parameter.min_value,
229
+ max=parameter.max_value,
230
+ step=parameter.step,
231
+ description=parameter.name,
232
+ continuous=continuous,
233
+ layout=widgets.Layout(width="95%"),
234
+ style={"description_width": "initial"},
235
+ )
236
+
237
+ def matches_parameter(self, parameter: FloatParameter) -> bool:
238
+ """Check if the widget matches the parameter."""
239
+ return (
240
+ self.value == parameter.value
241
+ and self._widget.min == parameter.min_value
242
+ and self._widget.max == parameter.max_value
243
+ )
244
+
245
+ def extra_updates_from_parameter(self, parameter: FloatParameter) -> None:
246
+ """Extra updates from the parameter."""
247
+ current_value = self._widget.value
248
+ self._widget.min = parameter.min_value
249
+ self._widget.max = parameter.max_value
250
+ self._widget.step = parameter.step
251
+ self.value = max(parameter.min_value, min(parameter.max_value, current_value))
252
+
253
+
254
+ class IntegerRangeWidget(BaseWidget[IntegerRangeParameter, widgets.IntRangeSlider]):
255
+ """Widget for integer range parameters."""
256
+
257
+ def _create_widget(
258
+ self, parameter: IntegerRangeParameter, continuous: bool
259
+ ) -> widgets.IntRangeSlider:
260
+ return widgets.IntRangeSlider(
261
+ value=parameter.value,
262
+ min=parameter.min_value,
263
+ max=parameter.max_value,
264
+ description=parameter.name,
265
+ continuous=continuous,
266
+ layout=widgets.Layout(width="95%"),
267
+ style={"description_width": "initial"},
268
+ )
269
+
270
+ def matches_parameter(self, parameter: IntegerRangeParameter) -> bool:
271
+ """Check if the widget matches the parameter."""
272
+ return (
273
+ self.value == parameter.value
274
+ and self._widget.min == parameter.min_value
275
+ and self._widget.max == parameter.max_value
276
+ )
277
+
278
+ def extra_updates_from_parameter(self, parameter: IntegerRangeParameter) -> None:
279
+ """Extra updates from the parameter."""
280
+ current_value = self._widget.value
281
+ self._widget.min = parameter.min_value
282
+ self._widget.max = parameter.max_value
283
+ low, high = current_value
284
+ low = max(parameter.min_value, min(parameter.max_value, low))
285
+ high = max(parameter.min_value, min(parameter.max_value, high))
286
+ self.value = (low, high)
287
+
288
+
289
+ class FloatRangeWidget(BaseWidget[FloatRangeParameter, widgets.FloatRangeSlider]):
290
+ """Widget for float range parameters."""
291
+
292
+ def _create_widget(
293
+ self, parameter: FloatRangeParameter, continuous: bool
294
+ ) -> widgets.FloatRangeSlider:
295
+ return widgets.FloatRangeSlider(
296
+ value=parameter.value,
297
+ min=parameter.min_value,
298
+ max=parameter.max_value,
299
+ step=parameter.step,
300
+ description=parameter.name,
301
+ continuous=continuous,
302
+ layout=widgets.Layout(width="95%"),
303
+ style={"description_width": "initial"},
304
+ )
305
+
306
+ def matches_parameter(self, parameter: FloatRangeParameter) -> bool:
307
+ """Check if the widget matches the parameter."""
308
+ return (
309
+ self.value == parameter.value
310
+ and self._widget.min == parameter.min_value
311
+ and self._widget.max == parameter.max_value
312
+ )
313
+
314
+ def extra_updates_from_parameter(self, parameter: FloatRangeParameter) -> None:
315
+ """Extra updates from the parameter."""
316
+ current_value = self._widget.value
317
+ self._widget.min = parameter.min_value
318
+ self._widget.max = parameter.max_value
319
+ self._widget.step = parameter.step
320
+ low, high = current_value
321
+ low = max(parameter.min_value, min(parameter.max_value, low))
322
+ high = max(parameter.min_value, min(parameter.max_value, high))
323
+ self.value = (low, high)
324
+
325
+
326
+ class UnboundedIntegerWidget(
327
+ BaseWidget[UnboundedIntegerParameter, widgets.BoundedIntText]
328
+ ):
329
+ """Widget for unbounded integer parameters."""
330
+
331
+ def _create_widget(
332
+ self, parameter: UnboundedIntegerParameter, continuous: bool
333
+ ) -> widgets.BoundedIntText:
334
+ return widgets.BoundedIntText(
335
+ value=parameter.value,
336
+ min=parameter.min_value,
337
+ max=parameter.max_value,
338
+ description=parameter.name,
339
+ layout=widgets.Layout(width="95%"),
340
+ style={"description_width": "initial"},
341
+ )
342
+
343
+ def matches_parameter(self, parameter: UnboundedIntegerParameter) -> bool:
344
+ """Check if the widget matches the parameter."""
345
+ return self.value == parameter.value
346
+
347
+ def extra_updates_from_parameter(
348
+ self, parameter: UnboundedIntegerParameter
349
+ ) -> None:
350
+ """Extra updates from the parameter."""
351
+ if parameter.min_value is not None:
352
+ self._widget.min = parameter.min_value
353
+ if parameter.max_value is not None:
354
+ self._widget.max = parameter.max_value
355
+
356
+
357
+ class UnboundedFloatWidget(
358
+ BaseWidget[UnboundedFloatParameter, widgets.BoundedFloatText]
359
+ ):
360
+ """Widget for unbounded float parameters."""
361
+
362
+ def _create_widget(
363
+ self, parameter: UnboundedFloatParameter, continuous: bool
364
+ ) -> widgets.BoundedFloatText:
365
+ return widgets.BoundedFloatText(
366
+ value=parameter.value,
367
+ min=parameter.min_value,
368
+ max=parameter.max_value,
369
+ step=parameter.step,
370
+ description=parameter.name,
371
+ layout=widgets.Layout(width="95%"),
372
+ style={"description_width": "initial"},
373
+ )
374
+
375
+ def matches_parameter(self, parameter: UnboundedFloatParameter) -> bool:
376
+ """Check if the widget matches the parameter."""
377
+ return self.value == parameter.value
378
+
379
+ def extra_updates_from_parameter(self, parameter: UnboundedFloatParameter) -> None:
380
+ """Extra updates from the parameter."""
381
+ if parameter.min_value is not None:
382
+ self._widget.min = parameter.min_value
383
+ if parameter.max_value is not None:
384
+ self._widget.max = parameter.max_value
385
+ self._widget.step = parameter.step
386
+
387
+
388
+ class ButtonWidget(BaseWidget[ButtonAction, widgets.Button]):
389
+ """Widget for button parameters."""
390
+
391
+ _is_action: bool = True
392
+
393
+ def _create_widget(
394
+ self, parameter: ButtonAction, continuous: bool
395
+ ) -> widgets.Button:
396
+ button = widgets.Button(
397
+ description=parameter.label,
398
+ layout=widgets.Layout(width="auto"),
399
+ style={"description_width": "initial"},
400
+ )
401
+ button.on_click(parameter.callback)
402
+ return button
403
+
404
+ def matches_parameter(self, parameter: ButtonAction) -> bool:
405
+ """Check if the widget matches the parameter."""
406
+ return self._widget.description == parameter.label
407
+
408
+ def extra_updates_from_parameter(self, parameter: ButtonAction) -> None:
409
+ """Extra updates from the parameter."""
410
+ self._widget.description = parameter.label
411
+ # Update click handler
412
+ self._widget.on_click(parameter.callback, remove=True) # Remove old handler
413
+ self._widget.on_click(parameter.callback) # Add new handler
414
+
415
+ def observe(self, callback: Callable) -> None:
416
+ """Observe the widget and call the callback when the value changes."""
417
+ self._widget.on_click(callback)
418
+ self._callbacks.append(callback)
419
+
420
+ def unobserve(self, callback: Callable[[Any], None]) -> None:
421
+ """Unobserve the widget and stop calling the callback when the value changes."""
422
+ self._widget.on_click(None)
423
+ self._callbacks.remove(callback)
424
+
425
+ def reenable_callbacks(self) -> None:
426
+ """Reenable all callbacks from the widget."""
427
+ for callback in self._callbacks:
428
+ self._widget.on_click(callback)
429
+
430
+ def disable_callbacks(self) -> None:
431
+ """Disable all callbacks from the widget."""
432
+ for callback in self._callbacks:
433
+ self._widget.on_click(None)
434
+
435
+
436
+ def create_widget(
437
+ parameter: Union[Parameter[Any], ButtonAction],
438
+ continuous: bool = False,
439
+ ) -> BaseWidget[Union[Parameter[Any], ButtonAction], widgets.Widget]:
440
+ """Create and return the appropriate widget for the given parameter.
441
+
442
+ Args:
443
+ parameter: The parameter to create a widget for
444
+ continuous: Whether to update the widget value continuously during user interaction
445
+ """
446
+ widget_map = {
447
+ TextParameter: TextWidget,
448
+ SelectionParameter: SelectionWidget,
449
+ MultipleSelectionParameter: MultipleSelectionWidget,
450
+ BooleanParameter: BooleanWidget,
451
+ IntegerParameter: IntegerWidget,
452
+ FloatParameter: FloatWidget,
453
+ IntegerRangeParameter: IntegerRangeWidget,
454
+ FloatRangeParameter: FloatRangeWidget,
455
+ UnboundedIntegerParameter: UnboundedIntegerWidget,
456
+ UnboundedFloatParameter: UnboundedFloatWidget,
457
+ ButtonAction: ButtonWidget,
458
+ }
459
+
460
+ # Try direct type lookup first
461
+ widget_class = widget_map.get(type(parameter))
462
+
463
+ # If that fails, try matching by class name
464
+ if widget_class is None:
465
+ param_type_name = type(parameter).__name__
466
+ for key_class, value_class in widget_map.items():
467
+ if key_class.__name__ == param_type_name:
468
+ widget_class = value_class
469
+ break
470
+
471
+ if widget_class is None:
472
+ raise ValueError(
473
+ f"No widget implementation for parameter type: {type(parameter)}\n"
474
+ f"Parameter type name: {type(parameter).__name__}\n"
475
+ f"Available types: {[k.__name__ for k in widget_map.keys()]}"
476
+ )
477
+
478
+ return widget_class(parameter, continuous)