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