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.
syd/interactive_viewer.py CHANGED
@@ -1,12 +1,36 @@
1
- from typing import List, Any, Callable, Dict, Tuple
2
- from functools import wraps
1
+ from typing import List, Any, Callable, Dict, Tuple, Union, Optional
2
+ from functools import wraps, partial
3
+ import inspect
3
4
  from contextlib import contextmanager
4
5
  from matplotlib.figure import Figure
5
6
 
6
- from .parameters import ParameterType, Parameter
7
+ from .parameters import (
8
+ ParameterType,
9
+ ActionType,
10
+ Parameter,
11
+ ParameterAddError,
12
+ ParameterUpdateError,
13
+ )
7
14
 
8
15
 
9
- def validate_parameter_operation(operation: str, parameter_type: ParameterType) -> Callable:
16
+ class _NoUpdate:
17
+ """Singleton class to represent a non-update in parameter operations."""
18
+
19
+ _instance = None
20
+
21
+ def __new__(cls):
22
+ if cls._instance is None:
23
+ cls._instance = super().__new__(cls)
24
+ return cls._instance
25
+
26
+
27
+ # Create the singleton instance
28
+ _NO_UPDATE = _NoUpdate()
29
+
30
+
31
+ def validate_parameter_operation(
32
+ operation: str, parameter_type: Union[ParameterType, ActionType]
33
+ ) -> Callable:
10
34
  """
11
35
  Decorator that validates parameter operations for the InteractiveViewer class.
12
36
 
@@ -36,25 +60,63 @@ def validate_parameter_operation(operation: str, parameter_type: ParameterType)
36
60
  """
37
61
 
38
62
  def decorator(func: Callable) -> Callable:
63
+ if operation not in ["add", "update"]:
64
+ raise ValueError(
65
+ "Incorrect use of validate_parameter_operation decorator. Must be called with 'add' or 'update' as the first argument."
66
+ )
67
+
68
+ # Validate operation matches method name (add/update)
69
+ if not func.__name__.startswith(operation):
70
+ raise ValueError(
71
+ f"Invalid operation type specified ({operation}) for method {func.__name__}"
72
+ )
73
+
39
74
  @wraps(func)
40
- def wrapper(self: "InteractiveViewer", name: str, *args, **kwargs):
41
- # Validate operation matches method name (add/update)
42
- if not func.__name__.startswith(operation):
43
- raise ValueError(f"Invalid operation type specified ({operation}) for method {func.__name__}")
75
+ def wrapper(self: "InteractiveViewer", name: Any, *args, **kwargs):
76
+ # Validate parameter name is a string
77
+ if not isinstance(name, str):
78
+ if operation == "add":
79
+ raise ParameterAddError(
80
+ name, parameter_type.name, "Parameter name must be a string"
81
+ )
82
+ elif operation == "update":
83
+ raise ParameterUpdateError(
84
+ name, parameter_type.name, "Parameter name must be a string"
85
+ )
44
86
 
45
87
  # Validate deployment state
46
88
  if operation == "add" and self._app_deployed:
47
- raise RuntimeError("The app is currently deployed, cannot add a new parameter right now.")
89
+ raise RuntimeError(
90
+ "The app is currently deployed, cannot add a new parameter right now."
91
+ )
92
+
93
+ if operation == "add":
94
+ if name in self.parameters:
95
+ raise ParameterAddError(
96
+ name, parameter_type.name, "Parameter already exists!"
97
+ )
48
98
 
49
99
  # For updates, validate parameter existence and type
50
100
  if operation == "update":
51
101
  if name not in self.parameters:
52
- raise ValueError(f"Parameter called {name} not found - you can only update registered parameters!")
53
- if type(self.parameters[name]) != parameter_type:
54
- msg = f"Parameter called {name} was found but is registered as a different parameter type ({type(self.parameters[name])})"
55
- raise TypeError(msg)
102
+ raise ParameterUpdateError(
103
+ name,
104
+ parameter_type.name,
105
+ "Parameter not found - you can only update registered parameters!",
106
+ )
107
+ if not isinstance(self.parameters[name], parameter_type.value):
108
+ msg = f"Parameter called {name} was found but is registered as a different parameter type ({type(self.parameters[name])}). Expecting {parameter_type.value}."
109
+ raise ParameterUpdateError(name, parameter_type.name, msg)
56
110
 
57
- return func(self, name, *args, **kwargs)
111
+ try:
112
+ return func(self, name, *args, **kwargs)
113
+ except Exception as e:
114
+ if operation == "add":
115
+ raise ParameterAddError(name, parameter_type.name, str(e)) from e
116
+ elif operation == "update":
117
+ raise ParameterUpdateError(name, parameter_type.name, str(e)) from e
118
+ else:
119
+ raise e
58
120
 
59
121
  return wrapper
60
122
 
@@ -62,28 +124,129 @@ def validate_parameter_operation(operation: str, parameter_type: ParameterType)
62
124
 
63
125
 
64
126
  class InteractiveViewer:
127
+ """
128
+ Base class for creating interactive matplotlib figures with GUI controls.
129
+
130
+ This class helps you create interactive visualizations by adding GUI elements
131
+ (like sliders, dropdowns, etc.) that update your plot in real-time. To use it:
132
+
133
+ 1. Create a subclass and implement the plot() method
134
+ 2. Add parameters using add_* methods before deploying
135
+ 3. Use on_change() to make parameters update the plot
136
+ 4. Use update_* methods to update parameter values and properties
137
+ 5. Deploy the app to show the interactive figure
138
+
139
+ Examples
140
+ --------
141
+ >>> class MyViewer(InteractiveViewer):
142
+ ... def plot(self, state: Dict[str, Any]):
143
+ ... fig = plt.figure()
144
+ ... plt.plot([0, state['x']])
145
+ ... return fig
146
+ ...
147
+ ... def update_based_on_x(self, state: Dict[str, Any]):
148
+ ... self.update_float('x', value=state['x'])
149
+ ...
150
+ >>> viewer = MyViewer()
151
+ >>> viewer.add_float('x', value=1.0, min_value=0, max_value=10)
152
+ >>> viewer.on_change('x', viewer.update_based_on_x)
153
+ """
154
+
155
+ parameters: Dict[str, Parameter]
156
+ callbacks: Dict[str, List[Callable]]
157
+ state: Dict[str, Any]
158
+ _app_deployed: bool
159
+ _in_callbacks: bool
160
+
65
161
  def __new__(cls, *args, **kwargs):
66
162
  instance = super().__new__(cls)
67
163
  instance.parameters = {}
68
164
  instance.callbacks = {}
69
165
  instance.state = {}
70
166
  instance._app_deployed = False
167
+ instance._in_callbacks = False
71
168
  return instance
72
169
 
73
- def __init__(self):
74
- self.parameters: Dict[str, Parameter] = {}
75
- self.callbacks: Dict[str, List[Callable]] = {}
76
- self.state = {}
77
- self._app_deployed = False
78
-
79
170
  def get_state(self) -> Dict[str, Any]:
171
+ """
172
+ Get the current values of all parameters.
173
+
174
+ Returns
175
+ -------
176
+ dict
177
+ Dictionary mapping parameter names to their current values
178
+
179
+ Examples
180
+ --------
181
+ >>> viewer.add_float('x', value=1.0, min_value=0, max_value=10)
182
+ >>> viewer.add_text('label', value='data')
183
+ >>> viewer.get_state()
184
+ {'x': 1.0, 'label': 'data'}
185
+ """
80
186
  return {name: param.value for name, param in self.parameters.items()}
81
187
 
82
- def plot(self, **kwargs) -> Figure:
83
- raise NotImplementedError("Subclasses must implement the plot method")
188
+ def plot(self, state: Dict[str, Any]) -> Figure:
189
+ """Create and return a matplotlib figure.
190
+
191
+ This is a placeholder. You must either:
192
+
193
+ 1. Call set_plot() with your plotting function
194
+ This will look like this:
195
+ >>> def plot(viewer, state):
196
+ >>> ... generate figure, plot stuff ...
197
+ >>> return fig
198
+ >>> viewer.set_plot(plot))
199
+
200
+ 2. Subclass InteractiveViewer and override this method
201
+ This will look like this:
202
+ >>> class YourViewer(InteractiveViewer):
203
+ >>> def plot(self, state):
204
+ >>> ... generate figure, plot stuff ...
205
+ >>> return fig
206
+
207
+ Parameters
208
+ ----------
209
+ state : dict
210
+ Current parameter values
211
+
212
+ Returns
213
+ -------
214
+ matplotlib.figure.Figure
215
+ The figure to display
216
+
217
+ Notes
218
+ -----
219
+ - Create a new figure each time, don't reuse old ones
220
+ - Access parameter values using state['param_name']
221
+ - Access your viewer class using self (or viewer for the set_plot() method)
222
+ - Return the figure object, don't call plt.show()!
223
+ """
224
+ raise NotImplementedError(
225
+ "Plot method not implemented. Either subclass "
226
+ "InteractiveViewer and override plot(), or use "
227
+ "set_plot() to provide a plotting function."
228
+ )
229
+
230
+ def set_plot(self, func: Callable) -> None:
231
+ """Set the plot method for the viewer"""
232
+ self.plot = self._prepare_function(func, context="Setting plot:")
233
+
234
+ def deploy(self, env: str = "notebook", **kwargs):
235
+ """Deploy the app in a notebook or standalone environment"""
236
+ if env == "notebook":
237
+ from .notebook_deployment import NotebookDeployment
238
+
239
+ deployer = NotebookDeployment(self, **kwargs)
240
+ deployer.deploy()
241
+
242
+ return self
243
+ else:
244
+ raise ValueError(
245
+ f"Unsupported environment: {env}, only 'notebook' is supported right now."
246
+ )
84
247
 
85
248
  @contextmanager
86
- def deploy_app(self):
249
+ def _deploy_app(self):
87
250
  """Internal context manager to control app deployment state"""
88
251
  self._app_deployed = True
89
252
  try:
@@ -91,23 +254,208 @@ class InteractiveViewer:
91
254
  finally:
92
255
  self._app_deployed = False
93
256
 
257
+ def _prepare_function(
258
+ self,
259
+ func: Callable,
260
+ context: Optional[str] = "",
261
+ ) -> Callable:
262
+ # Check if func is Callable
263
+ if not callable(func):
264
+ raise ValueError(f"Function {func} is not callable")
265
+
266
+ # Handle partial functions
267
+ if isinstance(func, partial):
268
+ # Create new partial with self as first arg if not already there
269
+ get_self = (
270
+ lambda func: hasattr(func.func, "__self__") and func.func.__self__
271
+ )
272
+ get_name = lambda func: func.func.__name__
273
+ else:
274
+ get_self = lambda func: hasattr(func, "__self__") and func.__self__
275
+ get_name = lambda func: func.__name__
276
+
277
+ # Check if it's a class method
278
+ class_method = get_self(func) is self.__class__
279
+ if class_method:
280
+ raise ValueError(context + "Class methods are not supported.")
281
+
282
+ # Check if it's a bound method to another instance other than this one
283
+ if get_self(func) and get_self(func) is not self:
284
+ raise ValueError(
285
+ context + "Bound methods to other instances are not supported."
286
+ )
287
+
288
+ # Get function signature
289
+ try:
290
+ params = list(inspect.signature(func).parameters.values())
291
+ except ValueError:
292
+ # Handle built-ins or other objects without signatures
293
+ raise ValueError(context + f"Cannot inspect function signature for {func}")
294
+
295
+ # Look through params and check if there are two positional parameters (including self for bound methods)
296
+ bound_method = get_self(func) is self
297
+ positional_params = 0 + bound_method
298
+ optional_part = ""
299
+ for param in params:
300
+ # Check if it's a positional parameter. If it is, count it.
301
+ # As long as we have less than 2 positional parameters, we're good.
302
+ # When we already have 2 positional parameters, we need to make sure any other positional parameters have defaults.
303
+ if param.kind in (
304
+ inspect.Parameter.POSITIONAL_OR_KEYWORD,
305
+ inspect.Parameter.POSITIONAL_ONLY,
306
+ ):
307
+ if positional_params < 2:
308
+ positional_params += 1
309
+ else:
310
+ if param.default == inspect.Parameter.empty:
311
+ positional_params += 1
312
+ else:
313
+ optional_part += f", {param.name}={param.default!r}"
314
+ elif param.kind == inspect.Parameter.VAR_KEYWORD:
315
+ optional_part += f", **{param.name}"
316
+ elif param.kind == inspect.Parameter.KEYWORD_ONLY:
317
+ optional_part += (
318
+ f", {param.name}={param.default!r}"
319
+ if param.default != inspect.Parameter.empty
320
+ else f", {param.name}"
321
+ )
322
+
323
+ if positional_params != 2:
324
+ func_name = get_name(func)
325
+ if isinstance(func, partial):
326
+ func_sig = str(inspect.signature(func))
327
+ if bound_method:
328
+ func_sig = "(" + "self, " + func_sig[1:]
329
+ msg = (
330
+ context
331
+ + "\n"
332
+ + f"Your partial function '{func_name}' has an incorrect signature.\n"
333
+ "Partial functions must have exactly two positional parameters.\n"
334
+ "The first parameter should be self -- it corresponds to the viewer.\n"
335
+ "The second parameter should be state -- a dictionary of the current state of the viewer.\n"
336
+ "\nYour partial function effectivelylooks like this:\n"
337
+ f"def {func_name}{func_sig}:\n"
338
+ " ... your function code ..."
339
+ )
340
+ raise ValueError(msg)
341
+
342
+ if bound_method:
343
+ original_method = getattr(get_self(func).__class__, get_name(func))
344
+ func_sig = str(inspect.signature(original_method))
345
+
346
+ msg = (
347
+ context + "\n"
348
+ f"Your bound method '{func_name}{func_sig}' has an incorrect signature.\n"
349
+ "Bound methods must have exactly one positional parameter in addition to self.\n"
350
+ "The first parameter should be self -- it corresponds to the viewer.\n"
351
+ "The second parameter should be state -- a dictionary of the current state of the viewer.\n"
352
+ "\nYour method looks like this:\n"
353
+ "class YourViewer(InteractiveViewer):\n"
354
+ f" def {func_name}{func_sig}:\n"
355
+ " ... your function code ...\n"
356
+ "\nIt should look like this:\n"
357
+ "class YourViewer(InteractiveViewer):\n"
358
+ f" def {func_name}(self, state{optional_part}):\n"
359
+ " ... your function code ..."
360
+ )
361
+ raise ValueError(msg)
362
+ else:
363
+ func_sig = str(inspect.signature(func))
364
+
365
+ msg = (
366
+ context + "\n"
367
+ f"Your function '{func_name}{func_sig}' has an incorrect signature.\n"
368
+ "External functions must have exactly two positional parameters.\n"
369
+ "The first parameter should be viewer -- it corresponds to the viewer.\n"
370
+ "The second parameter should be state -- a dictionary of the current state of the viewer.\n"
371
+ "\nYour function looks like this:\n"
372
+ f"def {func_name}{func_sig}:\n"
373
+ " ... your function code ...\n"
374
+ "\nIt should look like this:\n"
375
+ f"def {func_name}(viewer, state{optional_part}):\n"
376
+ " ... your function code ..."
377
+ )
378
+ raise ValueError(msg)
379
+
380
+ # If not a bound method, wrap it to inject self when called with just the state
381
+ if bound_method:
382
+ return func
383
+
384
+ @wraps(func)
385
+ def func_with_self(*args, **kwargs):
386
+ return func(self, *args, **kwargs)
387
+
388
+ return func_with_self
389
+
94
390
  def perform_callbacks(self, name: str) -> bool:
95
391
  """Perform callbacks for all parameters that have changed"""
96
- if name in self.callbacks:
97
- state = self.get_state()
98
- for callback in self.callbacks[name]:
99
- callback(state)
100
-
101
- def on_change(self, parameter_name: str, callback: Callable):
102
- """Register a function to be called when a parameter changes."""
103
- if parameter_name not in self.parameters:
104
- raise ValueError(f"Parameter '{parameter_name}' is not registered!")
105
- if parameter_name not in self.callbacks:
106
- self.callbacks[parameter_name] = []
107
- self.callbacks[parameter_name].append(callback)
392
+ if self._in_callbacks:
393
+ return
394
+ try:
395
+ self._in_callbacks = True
396
+ if name in self.callbacks:
397
+ state = self.get_state()
398
+ for callback in self.callbacks[name]:
399
+ callback(state)
400
+ finally:
401
+ self._in_callbacks = False
402
+
403
+ def on_change(self, parameter_name: Union[str, List[str]], callback: Callable):
404
+ """
405
+ Register a function to run when parameters change.
406
+
407
+ The callback function will receive a dictionary of all current parameter
408
+ values whenever any of the specified parameters change.
409
+
410
+ Parameters
411
+ ----------
412
+ parameter_name : str or list of str
413
+ Name(s) of parameters to watch for changes
414
+ callback : callable
415
+ Function to call when changes occur. Should accept a single dict argument
416
+ containing the current state.
417
+
418
+ Examples
419
+ --------
420
+ >>> def update_plot(state):
421
+ ... print(f"x changed to {state['x']}")
422
+ >>> viewer.on_change('x', update_plot)
423
+ >>> viewer.on_change(['x', 'y'], lambda s: viewer.plot()) # Update on either change
424
+ """
425
+ if isinstance(parameter_name, str):
426
+ parameter_name = [parameter_name]
427
+
428
+ callback = self._prepare_function(
429
+ callback,
430
+ context="Setting on_change callback:",
431
+ )
432
+
433
+ for param_name in parameter_name:
434
+ if param_name not in self.parameters:
435
+ raise ValueError(f"Parameter '{param_name}' is not registered!")
436
+ if param_name not in self.callbacks:
437
+ self.callbacks[param_name] = []
438
+ self.callbacks[param_name].append(callback)
108
439
 
109
440
  def set_parameter_value(self, name: str, value: Any) -> None:
110
- """Set a parameter value and trigger dependency updates"""
441
+ """
442
+ Update a parameter's value and trigger any callbacks.
443
+
444
+ This is a lower-level method - usually you'll want to use the update_*
445
+ methods instead (e.g., update_float, update_text, etc.).
446
+
447
+ Parameters
448
+ ----------
449
+ name : str
450
+ Name of the parameter to update
451
+ value : Any
452
+ New value for the parameter
453
+
454
+ Raises
455
+ ------
456
+ ValueError
457
+ If the parameter doesn't exist or the value is invalid
458
+ """
111
459
  if name not in self.parameters:
112
460
  raise ValueError(f"Parameter {name} not found")
113
461
 
@@ -119,92 +467,971 @@ class InteractiveViewer:
119
467
 
120
468
  # -------------------- parameter registration methods --------------------
121
469
  @validate_parameter_operation("add", ParameterType.text)
122
- def add_text(self, name: str, default: str = "") -> None:
123
- self.parameters[name] = ParameterType.text.value(name, default)
470
+ def add_text(self, name: str, *, value: str) -> None:
471
+ """
472
+ Add a text input parameter to the viewer.
473
+
474
+ Creates a text box in the GUI that accepts any string input.
475
+ See :class:`~syd.parameters.TextParameter` for details.
476
+
477
+ Parameters
478
+ ----------
479
+ name : str
480
+ Name of the parameter (used as label in GUI)
481
+ value : str
482
+ Initial text value
483
+
484
+ Examples
485
+ --------
486
+ >>> viewer.add_text('title', value='My Plot')
487
+ >>> viewer.get_state()['title']
488
+ 'My Plot'
489
+ """
490
+ try:
491
+ new_param = ParameterType.text.value(name, value)
492
+ except Exception as e:
493
+ raise ParameterAddError(name, "text", str(e)) from e
494
+ else:
495
+ self.parameters[name] = new_param
496
+
497
+ @validate_parameter_operation("add", ParameterType.boolean)
498
+ def add_boolean(self, name: str, *, value: bool) -> None:
499
+ """
500
+ Add a boolean parameter to the viewer.
501
+
502
+ Creates a checkbox in the GUI that can be toggled on/off.
503
+ See :class:`~syd.parameters.BooleanParameter` for details.
504
+
505
+ Parameters
506
+ ----------
507
+ name : str
508
+ Name of the parameter (used as label in GUI)
509
+ value : bool
510
+ Initial state (True=checked, False=unchecked)
511
+
512
+ Examples
513
+ --------
514
+ >>> viewer.add_boolean('show_grid', value=True)
515
+ >>> viewer.get_state()['show_grid']
516
+ True
517
+ """
518
+ try:
519
+ new_param = ParameterType.boolean.value(name, value)
520
+ except Exception as e:
521
+ raise ParameterAddError(name, "boolean", str(e)) from e
522
+ else:
523
+ self.parameters[name] = new_param
124
524
 
125
525
  @validate_parameter_operation("add", ParameterType.selection)
126
- def add_selection(self, name: str, options: List[Any], default: Any = None) -> None:
127
- self.parameters[name] = ParameterType.selection.value(name, options, default)
526
+ def add_selection(self, name: str, *, value: Any, options: List[Any]) -> None:
527
+ """
528
+ Add a single-selection parameter to the viewer.
529
+
530
+ Creates a dropdown menu in the GUI where users can select one option.
531
+ See :class:`~syd.parameters.SelectionParameter` for details.
532
+
533
+ Parameters
534
+ ----------
535
+ name : str
536
+ Name of the parameter (used as label in GUI)
537
+ value : Any
538
+ Initially selected value (must be one of the options)
539
+ options : list
540
+ List of values that can be selected
541
+
542
+ Examples
543
+ --------
544
+ >>> viewer.add_selection('color', value='red',
545
+ ... options=['red', 'green', 'blue'])
546
+ >>> viewer.get_state()['color']
547
+ 'red'
548
+ """
549
+ try:
550
+ new_param = ParameterType.selection.value(name, value, options)
551
+ except Exception as e:
552
+ raise ParameterAddError(name, "selection", str(e)) from e
553
+ else:
554
+ self.parameters[name] = new_param
128
555
 
129
556
  @validate_parameter_operation("add", ParameterType.multiple_selection)
130
- def add_multiple_selection(self, name: str, options: List[Any], default: List[Any] = None) -> None:
131
- self.parameters[name] = ParameterType.multiple_selection.value(name, options, default)
557
+ def add_multiple_selection(
558
+ self, name: str, *, value: List[Any], options: List[Any]
559
+ ) -> None:
560
+ """
561
+ Add a multiple-selection parameter to the viewer.
132
562
 
133
- @validate_parameter_operation("add", ParameterType.boolean)
134
- def add_boolean(self, name: str, default: bool = True) -> None:
135
- self.parameters[name] = ParameterType.boolean.value(name, default)
563
+ Creates a set of checkboxes or a multi-select dropdown in the GUI where
564
+ users can select any number of options.
565
+ See :class:`~syd.parameters.MultipleSelectionParameter` for details.
566
+
567
+ Parameters
568
+ ----------
569
+ name : str
570
+ Name of the parameter (used as label in GUI)
571
+ value : list
572
+ Initially selected values (must all be in options)
573
+ options : list
574
+ List of values that can be selected
575
+
576
+ Examples
577
+ --------
578
+ >>> viewer.add_multiple_selection('toppings',
579
+ ... value=['cheese'],
580
+ ... options=['cheese', 'pepperoni', 'mushrooms'])
581
+ >>> viewer.get_state()['toppings']
582
+ ['cheese']
583
+ """
584
+ try:
585
+ new_param = ParameterType.multiple_selection.value(name, value, options)
586
+ except Exception as e:
587
+ raise ParameterAddError(name, "multiple_selection", str(e)) from e
588
+ else:
589
+ self.parameters[name] = new_param
136
590
 
137
591
  @validate_parameter_operation("add", ParameterType.integer)
138
- def add_integer(self, name: str, min_value: int = None, max_value: int = None, default: int = 0) -> None:
139
- self.parameters[name] = ParameterType.integer.value(name, min_value, max_value, default)
592
+ def add_integer(
593
+ self,
594
+ name: str,
595
+ *,
596
+ value: Union[float, int],
597
+ min_value: Union[float, int],
598
+ max_value: Union[float, int],
599
+ ) -> None:
600
+ """
601
+ Add an integer parameter to the viewer.
602
+
603
+ Creates a slider in the GUI that lets users select whole numbers between
604
+ min_value and max_value. Values will be clamped to stay within bounds.
605
+ See :class:`~syd.parameters.IntegerParameter` for details.
606
+
607
+ Parameters
608
+ ----------
609
+ name : str
610
+ Name of the parameter (used as label in GUI)
611
+ value : int
612
+ Initial value (will be clamped between min_value and max_value)
613
+ min_value : int
614
+ Minimum allowed value
615
+ max_value : int
616
+ Maximum allowed value
617
+
618
+ Examples
619
+ --------
620
+ >>> viewer.add_integer('count', value=5, min_value=0, max_value=10)
621
+ >>> viewer.get_state()['count']
622
+ 5
623
+ >>> viewer.update_integer('count', value=15) # Will be clamped to 10
624
+ >>> viewer.get_state()['count']
625
+ 10
626
+ """
627
+ try:
628
+ new_param = ParameterType.integer.value(
629
+ name,
630
+ value,
631
+ min_value,
632
+ max_value,
633
+ )
634
+ except Exception as e:
635
+ raise ParameterAddError(name, "number", str(e)) from e
636
+ else:
637
+ self.parameters[name] = new_param
140
638
 
141
639
  @validate_parameter_operation("add", ParameterType.float)
142
- def add_float(self, name: str, min_value: float = None, max_value: float = None, default: float = 0.0, step: float = 0.1) -> None:
143
- self.parameters[name] = ParameterType.float.value(name, min_value, max_value, default, step)
640
+ def add_float(
641
+ self,
642
+ name: str,
643
+ *,
644
+ value: Union[float, int],
645
+ min_value: Union[float, int],
646
+ max_value: Union[float, int],
647
+ step: float = 0.1,
648
+ ) -> None:
649
+ """
650
+ Add a decimal number parameter to the viewer.
651
+
652
+ Creates a slider in the GUI that lets users select numbers between
653
+ min_value and max_value. Values will be rounded to the nearest step
654
+ and clamped to stay within bounds.
655
+
656
+ Parameters
657
+ ----------
658
+ name : str
659
+ Name of the parameter (used as label in GUI)
660
+ value : float
661
+ Initial value (will be clamped between min_value and max_value)
662
+ min_value : float
663
+ Minimum allowed value
664
+ max_value : float
665
+ Maximum allowed value
666
+ step : float, optional
667
+ Size of each increment (default: 0.1)
144
668
 
145
- @validate_parameter_operation("add", ParameterType.integer_pair)
146
- def add_integer_pair(
669
+ Examples
670
+ --------
671
+ >>> viewer.add_float('temperature', value=20.0,
672
+ ... min_value=0.0, max_value=100.0, step=0.5)
673
+ >>> viewer.get_state()['temperature']
674
+ 20.0
675
+ >>> viewer.update_float('temperature', value=20.7) # Will round to 20.5
676
+ >>> viewer.get_state()['temperature']
677
+ 20.5
678
+ """
679
+ try:
680
+ new_param = ParameterType.float.value(
681
+ name,
682
+ value,
683
+ min_value,
684
+ max_value,
685
+ step,
686
+ )
687
+ except Exception as e:
688
+ raise ParameterAddError(name, "number", str(e)) from e
689
+ else:
690
+ self.parameters[name] = new_param
691
+
692
+ @validate_parameter_operation("add", ParameterType.integer_range)
693
+ def add_integer_range(
147
694
  self,
148
695
  name: str,
149
- default: Tuple[int, int],
150
- min_value: int = None,
151
- max_value: int = None,
696
+ *,
697
+ value: Tuple[Union[float, int], Union[float, int]],
698
+ min_value: Union[float, int],
699
+ max_value: Union[float, int],
152
700
  ) -> None:
153
- self.parameters[name] = ParameterType.integer_pair.value(name, default, min_value, max_value)
701
+ """
702
+ Add a range parameter for whole numbers to the viewer.
703
+
704
+ Creates a range slider in the GUI that lets users select a range of integers
705
+ between min_value and max_value. The range is specified as (low, high) and
706
+ both values will be clamped to stay within bounds.
707
+ See :class:`~syd.parameters.IntegerRangeParameter` for details.
708
+
709
+ Parameters
710
+ ----------
711
+ name : str
712
+ Name of the parameter (used as label in GUI)
713
+ value : tuple[int, int]
714
+ Initial (low, high) values
715
+ min_value : int
716
+ Minimum allowed value for both low and high
717
+ max_value : int
718
+ Maximum allowed value for both low and high
719
+
720
+ Examples
721
+ --------
722
+ >>> viewer.add_integer_range('age_range',
723
+ ... value=(25, 35),
724
+ ... min_value=18, max_value=100)
725
+ >>> viewer.get_state()['age_range']
726
+ (25, 35)
727
+ >>> # Values will be swapped if low > high
728
+ >>> viewer.update_integer_range('age_range', value=(40, 30))
729
+ >>> viewer.get_state()['age_range']
730
+ (30, 40)
731
+ """
732
+ try:
733
+ new_param = ParameterType.integer_range.value(
734
+ name,
735
+ value,
736
+ min_value,
737
+ max_value,
738
+ )
739
+ except Exception as e:
740
+ raise ParameterAddError(name, "integer_range", str(e)) from e
741
+ else:
742
+ self.parameters[name] = new_param
154
743
 
155
- @validate_parameter_operation("add", ParameterType.float_pair)
156
- def add_float_pair(
744
+ @validate_parameter_operation("add", ParameterType.float_range)
745
+ def add_float_range(
157
746
  self,
158
747
  name: str,
159
- default: Tuple[float, float],
160
- min_value: float = None,
161
- max_value: float = None,
748
+ *,
749
+ value: Tuple[Union[float, int], Union[float, int]],
750
+ min_value: Union[float, int],
751
+ max_value: Union[float, int],
162
752
  step: float = 0.1,
163
753
  ) -> None:
164
- self.parameters[name] = ParameterType.float_pair.value(name, default, min_value, max_value, step)
754
+ """
755
+ Add a range parameter for decimal numbers to the viewer.
756
+
757
+ Creates a range slider in the GUI that lets users select a range of numbers
758
+ between min_value and max_value. The range is specified as (low, high) and
759
+ both values will be rounded to the nearest step and clamped to stay within bounds.
760
+ See :class:`~syd.parameters.FloatRangeParameter` for details.
761
+
762
+ Parameters
763
+ ----------
764
+ name : str
765
+ Name of the parameter (used as label in GUI)
766
+ value : tuple[float, float]
767
+ Initial (low, high) values
768
+ min_value : float
769
+ Minimum allowed value for both low and high
770
+ max_value : float
771
+ Maximum allowed value for both low and high
772
+ step : float, optional
773
+ Size of each increment (default: 0.1)
774
+
775
+ Examples
776
+ --------
777
+ >>> viewer.add_float_range('price_range',
778
+ ... value=(10.0, 20.0),
779
+ ... min_value=0.0, max_value=100.0, step=0.5)
780
+ >>> viewer.get_state()['price_range']
781
+ (10.0, 20.0)
782
+ >>> # Values will be rounded to nearest step
783
+ >>> viewer.update_float_range('price_range', value=(10.7, 19.2))
784
+ >>> viewer.get_state()['price_range']
785
+ (10.5, 19.0)
786
+ """
787
+ try:
788
+ new_param = ParameterType.float_range.value(
789
+ name,
790
+ value,
791
+ min_value,
792
+ max_value,
793
+ step,
794
+ )
795
+ except Exception as e:
796
+ raise ParameterAddError(name, "float_range", str(e)) from e
797
+ else:
798
+ self.parameters[name] = new_param
799
+
800
+ @validate_parameter_operation("add", ParameterType.unbounded_integer)
801
+ def add_unbounded_integer(
802
+ self,
803
+ name: str,
804
+ *,
805
+ value: Union[float, int],
806
+ min_value: Optional[Union[float, int]] = None,
807
+ max_value: Optional[Union[float, int]] = None,
808
+ ) -> None:
809
+ """
810
+ Add an unbounded integer parameter to the viewer.
811
+
812
+ Creates a text input box in the GUI for entering whole numbers. Unlike
813
+ add_integer(), this allows very large numbers and optionally no minimum
814
+ or maximum bounds.
815
+ See :class:`~syd.parameters.UnboundedIntegerParameter` for details.
816
+
817
+ Parameters
818
+ ----------
819
+ name : str
820
+ Name of the parameter (used as label in GUI)
821
+ value : int
822
+ Initial value
823
+ min_value : int, optional
824
+ Minimum allowed value (or None for no minimum)
825
+ max_value : int, optional
826
+ Maximum allowed value (or None for no maximum)
827
+
828
+ Examples
829
+ --------
830
+ >>> viewer.add_unbounded_integer('population',
831
+ ... value=1000000,
832
+ ... min_value=0) # No maximum
833
+ >>> viewer.get_state()['population']
834
+ 1000000
835
+ >>> # Values below minimum will be clamped
836
+ >>> viewer.update_unbounded_integer('population', value=-5)
837
+ >>> viewer.get_state()['population']
838
+ 0
839
+ """
840
+ try:
841
+ new_param = ParameterType.unbounded_integer.value(
842
+ name,
843
+ value,
844
+ min_value,
845
+ max_value,
846
+ )
847
+ except Exception as e:
848
+ raise ParameterAddError(name, "unbounded_integer", str(e)) from e
849
+ else:
850
+ self.parameters[name] = new_param
851
+
852
+ @validate_parameter_operation("add", ParameterType.unbounded_float)
853
+ def add_unbounded_float(
854
+ self,
855
+ name: str,
856
+ *,
857
+ value: Union[float, int],
858
+ min_value: Optional[Union[float, int]] = None,
859
+ max_value: Optional[Union[float, int]] = None,
860
+ step: Optional[float] = None,
861
+ ) -> None:
862
+ """
863
+ Add an unbounded decimal number parameter to the viewer.
864
+
865
+ Creates a text input box in the GUI for entering numbers. Unlike add_float(),
866
+ this allows very large or precise numbers and optionally no minimum or
867
+ maximum bounds. Values can optionally be rounded to a step size.
868
+ See :class:`~syd.parameters.UnboundedFloatParameter` for details.
869
+
870
+ Parameters
871
+ ----------
872
+ name : str
873
+ Name of the parameter (used as label in GUI)
874
+ value : float
875
+ Initial value
876
+ min_value : float, optional
877
+ Minimum allowed value (or None for no minimum)
878
+ max_value : float, optional
879
+ Maximum allowed value (or None for no maximum)
880
+ step : float, optional
881
+ Size of each increment (or None for no rounding)
882
+
883
+ Examples
884
+ --------
885
+ >>> viewer.add_unbounded_float('wavelength',
886
+ ... value=550e-9, # Nanometers
887
+ ... min_value=0.0,
888
+ ... step=1e-9) # Round to nearest nanometer
889
+ >>> viewer.get_state()['wavelength']
890
+ 5.5e-07
891
+ >>> # Values will be rounded if step is provided
892
+ >>> viewer.update_unbounded_float('wavelength', value=550.7e-9)
893
+ >>> viewer.get_state()['wavelength']
894
+ 5.51e-07
895
+ """
896
+ try:
897
+ new_param = ParameterType.unbounded_float.value(
898
+ name,
899
+ value,
900
+ min_value,
901
+ max_value,
902
+ step,
903
+ )
904
+ except Exception as e:
905
+ raise ParameterAddError(name, "unbounded_float", str(e)) from e
906
+ else:
907
+ self.parameters[name] = new_param
908
+
909
+ @validate_parameter_operation("add", ActionType.button)
910
+ def add_button(
911
+ self,
912
+ name: str,
913
+ *,
914
+ label: str,
915
+ callback: Callable[[], None],
916
+ ) -> None:
917
+ """
918
+ Add a button parameter to the viewer.
919
+
920
+ Creates a clickable button in the GUI that triggers the provided callback function
921
+ when clicked. The button's display text can be different from its parameter name.
922
+ See :class:`~syd.parameters.ButtonParameter` for details.
923
+
924
+ Parameters
925
+ ----------
926
+ name : str
927
+ Name of the parameter (internal identifier)
928
+ label : str
929
+ Text to display on the button
930
+ callback : callable
931
+ Function to call when the button is clicked (takes no arguments)
932
+
933
+ Examples
934
+ --------
935
+ >>> def reset_plot():
936
+ ... print("Resetting plot...")
937
+ >>> viewer.add_button('reset', label='Reset Plot', callback=reset_plot)
938
+ """
939
+ try:
940
+
941
+ callback = self._prepare_function(
942
+ callback,
943
+ context="Setting button callback:",
944
+ )
945
+
946
+ new_param = ActionType.button.value(name, label, callback)
947
+ except Exception as e:
948
+ raise ParameterAddError(name, "button", str(e)) from e
949
+ else:
950
+ self.parameters[name] = new_param
165
951
 
166
952
  # -------------------- parameter update methods --------------------
167
953
  @validate_parameter_operation("update", ParameterType.text)
168
- def update_text(self, name: str, default: str = "") -> None:
169
- self.parameters[name] = ParameterType.text.value(name, default)
954
+ def update_text(
955
+ self, name: str, *, value: Union[str, _NoUpdate] = _NO_UPDATE
956
+ ) -> None:
957
+ """
958
+ Update a text parameter's value.
959
+
960
+ Updates a parameter created by :meth:`~syd.interactive_viewer.InteractiveViewer.add_text`.
961
+ See :class:`~syd.parameters.TextParameter` for details about value validation.
962
+
963
+ Parameters
964
+ ----------
965
+ name : str
966
+ Name of the text parameter to update
967
+ value : str, optional
968
+ New text value (if not provided, no change)
969
+
970
+ Examples
971
+ --------
972
+ >>> viewer.add_text('title', value='Original Title')
973
+ >>> viewer.update_text('title', value='New Title')
974
+ >>> viewer.get_state()['title']
975
+ 'New Title'
976
+ """
977
+ updates = {}
978
+ if value is not _NO_UPDATE:
979
+ updates["value"] = value
980
+ if updates:
981
+ self.parameters[name].update(updates)
982
+
983
+ @validate_parameter_operation("update", ParameterType.boolean)
984
+ def update_boolean(
985
+ self, name: str, *, value: Union[bool, _NoUpdate] = _NO_UPDATE
986
+ ) -> None:
987
+ """
988
+ Update a boolean parameter's value.
989
+
990
+ Updates a parameter created by :meth:`~syd.interactive_viewer.InteractiveViewer.add_boolean`.
991
+ See :class:`~syd.parameters.BooleanParameter` for details about value validation.
992
+
993
+ Parameters
994
+ ----------
995
+ name : str
996
+ Name of the boolean parameter to update
997
+ value : bool, optional
998
+ New state (True/False) (if not provided, no change)
999
+
1000
+ Examples
1001
+ --------
1002
+ >>> viewer.add_boolean('show_grid', value=True)
1003
+ >>> viewer.update_boolean('show_grid', value=False)
1004
+ >>> viewer.get_state()['show_grid']
1005
+ False
1006
+ """
1007
+ updates = {}
1008
+ if value is not _NO_UPDATE:
1009
+ updates["value"] = value
1010
+ if updates:
1011
+ self.parameters[name].update(updates)
170
1012
 
171
1013
  @validate_parameter_operation("update", ParameterType.selection)
172
- def update_selection(self, name: str, options: List[Any], default: Any = None) -> None:
173
- self.parameters[name] = ParameterType.selection.value(name, options, default)
1014
+ def update_selection(
1015
+ self,
1016
+ name: str,
1017
+ *,
1018
+ value: Union[Any, _NoUpdate] = _NO_UPDATE,
1019
+ options: Union[List[Any], _NoUpdate] = _NO_UPDATE,
1020
+ ) -> None:
1021
+ """
1022
+ Update a selection parameter's value and/or options.
1023
+
1024
+ Updates a parameter created by :meth:`~syd.interactive_viewer.InteractiveViewer.add_selection`.
1025
+ See :class:`~syd.parameters.SelectionParameter` for details about value validation.
1026
+
1027
+ Parameters
1028
+ ----------
1029
+ name : str
1030
+ Name of the selection parameter to update
1031
+ value : Any, optional
1032
+ New selected value (must be in options) (if not provided, no change)
1033
+ options : list, optional
1034
+ New list of selectable options (if not provided, no change)
1035
+
1036
+ Examples
1037
+ --------
1038
+ >>> viewer.add_selection('color', value='red',
1039
+ ... options=['red', 'green', 'blue'])
1040
+ >>> # Update just the value
1041
+ >>> viewer.update_selection('color', value='blue')
1042
+ >>> # Update options and value together
1043
+ >>> viewer.update_selection('color',
1044
+ ... options=['purple', 'orange'],
1045
+ ... value='purple')
1046
+ """
1047
+ updates = {}
1048
+ if value is not _NO_UPDATE:
1049
+ updates["value"] = value
1050
+ if options is not _NO_UPDATE:
1051
+ updates["options"] = options
1052
+ if updates:
1053
+ self.parameters[name].update(updates)
174
1054
 
175
1055
  @validate_parameter_operation("update", ParameterType.multiple_selection)
176
- def update_multiple_selection(self, name: str, options: List[Any], default: List[Any] = None) -> None:
177
- self.parameters[name] = ParameterType.multiple_selection.value(name, options, default)
1056
+ def update_multiple_selection(
1057
+ self,
1058
+ name: str,
1059
+ *,
1060
+ value: Union[List[Any], _NoUpdate] = _NO_UPDATE,
1061
+ options: Union[List[Any], _NoUpdate] = _NO_UPDATE,
1062
+ ) -> None:
1063
+ """
1064
+ Update a multiple selection parameter's values and/or options.
178
1065
 
179
- @validate_parameter_operation("update", ParameterType.boolean)
180
- def update_boolean(self, name: str, default: bool = True) -> None:
181
- self.parameters[name] = ParameterType.boolean.value(name, default)
1066
+ Updates a parameter created by :meth:`~syd.interactive_viewer.InteractiveViewer.add_multiple_selection`.
1067
+ See :class:`~syd.parameters.MultipleSelectionParameter` for details about value validation.
1068
+
1069
+ Parameters
1070
+ ----------
1071
+ name : str
1072
+ Name of the multiple selection parameter to update
1073
+ value : list, optional
1074
+ New list of selected values (all must be in options) (if not provided, no change)
1075
+ options : list, optional
1076
+ New list of selectable options (if not provided, no change)
1077
+
1078
+ Examples
1079
+ --------
1080
+ >>> viewer.add_multiple_selection('toppings',
1081
+ ... value=['cheese'],
1082
+ ... options=['cheese', 'pepperoni', 'mushrooms'])
1083
+ >>> # Update selected values
1084
+ >>> viewer.update_multiple_selection('toppings',
1085
+ ... value=['cheese', 'mushrooms'])
1086
+ >>> # Update options (will reset value if current selections not in new options)
1087
+ >>> viewer.update_multiple_selection('toppings',
1088
+ ... options=['cheese', 'bacon', 'olives'],
1089
+ ... value=['cheese', 'bacon'])
1090
+ """
1091
+ updates = {}
1092
+ if value is not _NO_UPDATE:
1093
+ updates["value"] = value
1094
+ if options is not _NO_UPDATE:
1095
+ updates["options"] = options
1096
+ if updates:
1097
+ self.parameters[name].update(updates)
182
1098
 
183
1099
  @validate_parameter_operation("update", ParameterType.integer)
184
- def update_integer(self, name: str, min_value: int = None, max_value: int = None, default: int = 0) -> None:
185
- self.parameters[name] = ParameterType.integer.value(name, min_value, max_value, default)
1100
+ def update_integer(
1101
+ self,
1102
+ name: str,
1103
+ *,
1104
+ value: Union[int, _NoUpdate] = _NO_UPDATE,
1105
+ min_value: Union[int, _NoUpdate] = _NO_UPDATE,
1106
+ max_value: Union[int, _NoUpdate] = _NO_UPDATE,
1107
+ ) -> None:
1108
+ """
1109
+ Update an integer parameter's value and/or bounds.
1110
+
1111
+ Updates a parameter created by :meth:`~syd.interactive_viewer.InteractiveViewer.add_integer`.
1112
+ See :class:`~syd.parameters.IntegerParameter` for details about value validation.
1113
+
1114
+ Parameters
1115
+ ----------
1116
+ name : str
1117
+ Name of the integer parameter to update
1118
+ value : int, optional
1119
+ New value (will be clamped to bounds) (if not provided, no change)
1120
+ min_value : int, optional
1121
+ New minimum value (if not provided, no change)
1122
+ max_value : int, optional
1123
+ New maximum value (if not provided, no change)
1124
+
1125
+ Examples
1126
+ --------
1127
+ >>> viewer.add_integer('count', value=5, min_value=0, max_value=10)
1128
+ >>> # Update just the value
1129
+ >>> viewer.update_integer('count', value=8)
1130
+ >>> # Update bounds (current value will be clamped if needed)
1131
+ >>> viewer.update_integer('count', min_value=7, max_value=15)
1132
+ """
1133
+ updates = {}
1134
+ if value is not _NO_UPDATE:
1135
+ updates["value"] = value
1136
+ if min_value is not _NO_UPDATE:
1137
+ updates["min_value"] = min_value
1138
+ if max_value is not _NO_UPDATE:
1139
+ updates["max_value"] = max_value
1140
+ if updates:
1141
+ self.parameters[name].update(updates)
186
1142
 
187
1143
  @validate_parameter_operation("update", ParameterType.float)
188
- def update_float(self, name: str, min_value: float = None, max_value: float = None, default: float = 0.0, step: float = 0.1) -> None:
189
- self.parameters[name] = ParameterType.float.value(name, min_value, max_value, default, step)
1144
+ def update_float(
1145
+ self,
1146
+ name: str,
1147
+ *,
1148
+ value: Union[float, _NoUpdate] = _NO_UPDATE,
1149
+ min_value: Union[float, _NoUpdate] = _NO_UPDATE,
1150
+ max_value: Union[float, _NoUpdate] = _NO_UPDATE,
1151
+ step: Union[float, _NoUpdate] = _NO_UPDATE,
1152
+ ) -> None:
1153
+ """
1154
+ Update a float parameter's value, bounds, and/or step size.
1155
+
1156
+ Updates a parameter created by :meth:`~syd.interactive_viewer.InteractiveViewer.add_float`.
1157
+ See :class:`~syd.parameters.FloatParameter` for details about value validation.
1158
+
1159
+ Parameters
1160
+ ----------
1161
+ name : str
1162
+ Name of the float parameter to update
1163
+ value : float, optional
1164
+ New value (will be rounded and clamped) (if not provided, no change)
1165
+ min_value : float, optional
1166
+ New minimum value (if not provided, no change)
1167
+ max_value : float, optional
1168
+ New maximum value (if not provided, no change)
1169
+ step : float, optional
1170
+ New step size (if not provided, no change)
1171
+
1172
+ Examples
1173
+ --------
1174
+ >>> viewer.add_float('temperature', value=20.0,
1175
+ ... min_value=0.0, max_value=100.0, step=0.5)
1176
+ >>> # Update just the value (will round to step)
1177
+ >>> viewer.update_float('temperature', value=20.7) # Becomes 20.5
1178
+ >>> # Update bounds and step size
1179
+ >>> viewer.update_float('temperature',
1180
+ ... min_value=15.0, max_value=30.0, step=0.1)
1181
+ """
1182
+ updates = {}
1183
+ if value is not _NO_UPDATE:
1184
+ updates["value"] = value
1185
+ if min_value is not _NO_UPDATE:
1186
+ updates["min_value"] = min_value
1187
+ if max_value is not _NO_UPDATE:
1188
+ updates["max_value"] = max_value
1189
+ if step is not _NO_UPDATE:
1190
+ updates["step"] = step
1191
+ if updates:
1192
+ self.parameters[name].update(updates)
190
1193
 
191
- @validate_parameter_operation("update", ParameterType.integer_pair)
192
- def update_integer_pair(
1194
+ @validate_parameter_operation("update", ParameterType.integer_range)
1195
+ def update_integer_range(
193
1196
  self,
194
1197
  name: str,
195
- default: Tuple[int, int],
196
- min_value: int = None,
197
- max_value: int = None,
1198
+ *,
1199
+ value: Union[Tuple[int, int], _NoUpdate] = _NO_UPDATE,
1200
+ min_value: Union[int, _NoUpdate] = _NO_UPDATE,
1201
+ max_value: Union[int, _NoUpdate] = _NO_UPDATE,
198
1202
  ) -> None:
199
- self.parameters[name] = ParameterType.integer_pair.value(name, default, min_value, max_value)
1203
+ """
1204
+ Update an integer range parameter's values and/or bounds.
200
1205
 
201
- @validate_parameter_operation("update", ParameterType.float_pair)
202
- def update_float_pair(
1206
+ Updates a parameter created by :meth:`~syd.interactive_viewer.InteractiveViewer.add_integer_range`.
1207
+ See :class:`~syd.parameters.IntegerRangeParameter` for details about value validation.
1208
+
1209
+ Parameters
1210
+ ----------
1211
+ name : str
1212
+ Name of the integer range parameter to update
1213
+ value : tuple[int, int], optional
1214
+ New (low, high) values (will be clamped) (if not provided, no change)
1215
+ min_value : int, optional
1216
+ New minimum value for both low and high (if not provided, no change)
1217
+ max_value : int, optional
1218
+ New maximum value for both low and high (if not provided, no change)
1219
+
1220
+ Examples
1221
+ --------
1222
+ >>> viewer.add_integer_range('age_range',
1223
+ ... value=(25, 35),
1224
+ ... min_value=18, max_value=100)
1225
+ >>> # Update just the range (values will be swapped if needed)
1226
+ >>> viewer.update_integer_range('age_range', value=(40, 30)) # Becomes (30, 40)
1227
+ >>> # Update bounds (current values will be clamped if needed)
1228
+ >>> viewer.update_integer_range('age_range', min_value=20, max_value=80)
1229
+ """
1230
+ updates = {}
1231
+ if value is not _NO_UPDATE:
1232
+ updates["value"] = value
1233
+ if min_value is not _NO_UPDATE:
1234
+ updates["min_value"] = min_value
1235
+ if max_value is not _NO_UPDATE:
1236
+ updates["max_value"] = max_value
1237
+ if updates:
1238
+ self.parameters[name].update(updates)
1239
+
1240
+ @validate_parameter_operation("update", ParameterType.float_range)
1241
+ def update_float_range(
203
1242
  self,
204
1243
  name: str,
205
- default: Tuple[float, float],
206
- min_value: float = None,
207
- max_value: float = None,
208
- step: float = 0.1,
1244
+ *,
1245
+ value: Union[Tuple[float, float], _NoUpdate] = _NO_UPDATE,
1246
+ min_value: Union[float, _NoUpdate] = _NO_UPDATE,
1247
+ max_value: Union[float, _NoUpdate] = _NO_UPDATE,
1248
+ step: Union[float, _NoUpdate] = _NO_UPDATE,
1249
+ ) -> None:
1250
+ """
1251
+ Update a float range parameter's values, bounds, and/or step size.
1252
+
1253
+ Updates a parameter created by :meth:`~syd.interactive_viewer.InteractiveViewer.add_float_range`.
1254
+ See :class:`~syd.parameters.FloatRangeParameter` for details about value validation.
1255
+
1256
+ Parameters
1257
+ ----------
1258
+ name : str
1259
+ Name of the float range parameter to update
1260
+ value : tuple[float, float], optional
1261
+ New (low, high) values (will be rounded and clamped) (if not provided, no change)
1262
+ min_value : float, optional
1263
+ New minimum value for both low and high (if not provided, no change)
1264
+ max_value : float, optional
1265
+ New maximum value for both low and high (if not provided, no change)
1266
+ step : float, optional
1267
+ New step size for rounding values (if not provided, no change)
1268
+
1269
+ Examples
1270
+ --------
1271
+ >>> viewer.add_float_range('price_range',
1272
+ ... value=(10.0, 20.0),
1273
+ ... min_value=0.0, max_value=100.0, step=0.5)
1274
+ >>> # Update just the range (values will be rounded and swapped if needed)
1275
+ >>> viewer.update_float_range('price_range', value=(15.7, 14.2)) # Becomes (14.0, 15.5)
1276
+ >>> # Update bounds and step size
1277
+ >>> viewer.update_float_range('price_range',
1278
+ ... min_value=5.0, max_value=50.0, step=0.1)
1279
+ """
1280
+ updates = {}
1281
+ if value is not _NO_UPDATE:
1282
+ updates["value"] = value
1283
+ if min_value is not _NO_UPDATE:
1284
+ updates["min_value"] = min_value
1285
+ if max_value is not _NO_UPDATE:
1286
+ updates["max_value"] = max_value
1287
+ if step is not _NO_UPDATE:
1288
+ updates["step"] = step
1289
+ if updates:
1290
+ self.parameters[name].update(updates)
1291
+
1292
+ @validate_parameter_operation("update", ParameterType.unbounded_integer)
1293
+ def update_unbounded_integer(
1294
+ self,
1295
+ name: str,
1296
+ *,
1297
+ value: Union[int, _NoUpdate] = _NO_UPDATE,
1298
+ min_value: Union[Optional[int], _NoUpdate] = _NO_UPDATE,
1299
+ max_value: Union[Optional[int], _NoUpdate] = _NO_UPDATE,
209
1300
  ) -> None:
210
- self.parameters[name] = ParameterType.float_pair.value(name, default, min_value, max_value, step)
1301
+ """
1302
+ Update an unbounded integer parameter's value and/or bounds.
1303
+
1304
+ Updates a parameter created by :meth:`~syd.interactive_viewer.InteractiveViewer.add_unbounded_integer`.
1305
+ See :class:`~syd.parameters.UnboundedIntegerParameter` for details about value validation.
1306
+
1307
+ Parameters
1308
+ ----------
1309
+ name : str
1310
+ Name of the unbounded integer parameter to update
1311
+ value : int, optional
1312
+ New value (will be clamped to any bounds) (if not provided, no change)
1313
+ min_value : int or None, optional
1314
+ New minimum value, or None for no minimum (if not provided, no change)
1315
+ max_value : int or None, optional
1316
+ New maximum value, or None for no maximum (if not provided, no change)
1317
+
1318
+ Examples
1319
+ --------
1320
+ >>> viewer.add_unbounded_integer('population',
1321
+ ... value=1000000,
1322
+ ... min_value=0) # No maximum
1323
+ >>> # Update just the value
1324
+ >>> viewer.update_unbounded_integer('population', value=2000000)
1325
+ >>> # Add a maximum bound (current value will be clamped if needed)
1326
+ >>> viewer.update_unbounded_integer('population', max_value=1500000)
1327
+ >>> # Remove the minimum bound
1328
+ >>> viewer.update_unbounded_integer('population', min_value=None)
1329
+ """
1330
+ updates = {}
1331
+ if value is not _NO_UPDATE:
1332
+ updates["value"] = value
1333
+ if min_value is not _NO_UPDATE:
1334
+ updates["min_value"] = min_value
1335
+ if max_value is not _NO_UPDATE:
1336
+ updates["max_value"] = max_value
1337
+ if updates:
1338
+ self.parameters[name].update(updates)
1339
+
1340
+ @validate_parameter_operation("update", ParameterType.unbounded_float)
1341
+ def update_unbounded_float(
1342
+ self,
1343
+ name: str,
1344
+ *,
1345
+ value: Union[float, _NoUpdate] = _NO_UPDATE,
1346
+ min_value: Union[Optional[float], _NoUpdate] = _NO_UPDATE,
1347
+ max_value: Union[Optional[float], _NoUpdate] = _NO_UPDATE,
1348
+ step: Union[Optional[float], _NoUpdate] = _NO_UPDATE,
1349
+ ) -> None:
1350
+ """
1351
+ Update an unbounded float parameter's value, bounds, and/or step size.
1352
+
1353
+ Updates a parameter created by :meth:`~syd.interactive_viewer.InteractiveViewer.add_unbounded_float`.
1354
+ See :class:`~syd.parameters.UnboundedFloatParameter` for details about value validation.
1355
+
1356
+ Parameters
1357
+ ----------
1358
+ name : str
1359
+ Name of the unbounded float parameter to update
1360
+ value : float, optional
1361
+ New value (will be rounded if step is set) (if not provided, no change)
1362
+ min_value : float or None, optional
1363
+ New minimum value, or None for no minimum (if not provided, no change)
1364
+ max_value : float or None, optional
1365
+ New maximum value, or None for no maximum (if not provided, no change)
1366
+ step : float or None, optional
1367
+ New step size for rounding, or None for no rounding (if not provided, no change)
1368
+
1369
+ Examples
1370
+ --------
1371
+ >>> viewer.add_unbounded_float('wavelength',
1372
+ ... value=550e-9, # Nanometers
1373
+ ... min_value=0.0,
1374
+ ... step=1e-9)
1375
+ >>> # Update value (will be rounded if step is set)
1376
+ >>> viewer.update_unbounded_float('wavelength', value=632.8e-9) # HeNe laser
1377
+ >>> # Change step size and add maximum
1378
+ >>> viewer.update_unbounded_float('wavelength',
1379
+ ... step=0.1e-9, # Finer control
1380
+ ... max_value=1000e-9) # Infrared limit
1381
+ >>> # Remove step size (allow any precision)
1382
+ >>> viewer.update_unbounded_float('wavelength', step=None)
1383
+ """
1384
+ updates = {}
1385
+ if value is not _NO_UPDATE:
1386
+ updates["value"] = value
1387
+ if min_value is not _NO_UPDATE:
1388
+ updates["min_value"] = min_value
1389
+ if max_value is not _NO_UPDATE:
1390
+ updates["max_value"] = max_value
1391
+ if step is not _NO_UPDATE:
1392
+ updates["step"] = step
1393
+ if updates:
1394
+ self.parameters[name].update(updates)
1395
+
1396
+ @validate_parameter_operation("update", ActionType.button)
1397
+ def update_button(
1398
+ self,
1399
+ name: str,
1400
+ *,
1401
+ label: Union[str, _NoUpdate] = _NO_UPDATE,
1402
+ callback: Union[Callable[[], None], _NoUpdate] = _NO_UPDATE,
1403
+ ) -> None:
1404
+ """
1405
+ Update a button parameter's label and/or callback function.
1406
+
1407
+ Updates a parameter created by :meth:`~syd.interactive_viewer.InteractiveViewer.add_button`.
1408
+ See :class:`~syd.parameters.ButtonAction` for details.
1409
+
1410
+ Parameters
1411
+ ----------
1412
+ name : str
1413
+ Name of the button parameter to update
1414
+ label : str, optional
1415
+ New text to display on the button (if not provided, no change)
1416
+ callback : callable, optional
1417
+ New function to call when clicked (if not provided, no change)
1418
+
1419
+ Examples
1420
+ --------
1421
+ >>> def new_callback():
1422
+ ... print("New action...")
1423
+ >>> viewer.update_button('reset',
1424
+ ... label='Clear Plot',
1425
+ ... callback=new_callback)
1426
+ """
1427
+ updates = {}
1428
+ if label is not _NO_UPDATE:
1429
+ updates["label"] = label
1430
+ if callback is not _NO_UPDATE:
1431
+ callback = self._prepare_function(
1432
+ callback,
1433
+ context="Updating button callback:",
1434
+ )
1435
+ updates["callback"] = callback
1436
+ if updates:
1437
+ self.parameters[name].update(updates)