syd 0.1.5__py3-none-any.whl → 0.1.7__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.
@@ -1,11 +1,12 @@
1
1
  from typing import List, Any, Callable, Dict, Tuple, Union, Optional
2
- from functools import wraps
2
+ from functools import wraps, partial
3
+ import inspect
3
4
  from contextlib import contextmanager
4
- from abc import ABC, abstractmethod
5
5
  from matplotlib.figure import Figure
6
6
 
7
7
  from .parameters import (
8
8
  ParameterType,
9
+ ActionType,
9
10
  Parameter,
10
11
  ParameterAddError,
11
12
  ParameterUpdateError,
@@ -22,16 +23,20 @@ class _NoUpdate:
22
23
  cls._instance = super().__new__(cls)
23
24
  return cls._instance
24
25
 
26
+ def __eq__(self, other):
27
+ """This makes sure all comparisons of _NoUpdate objects return True"""
28
+ return isinstance(other, _NoUpdate)
29
+
25
30
 
26
31
  # Create the singleton instance
27
32
  _NO_UPDATE = _NoUpdate()
28
33
 
29
34
 
30
35
  def validate_parameter_operation(
31
- operation: str, parameter_type: ParameterType
36
+ operation: str, parameter_type: Union[ParameterType, ActionType]
32
37
  ) -> Callable:
33
38
  """
34
- Decorator that validates parameter operations for the InteractiveViewer class.
39
+ Decorator that validates parameter operations for the viewer class.
35
40
 
36
41
  This decorator ensures that:
37
42
  1. The operation type matches the method name (add/update)
@@ -64,14 +69,14 @@ def validate_parameter_operation(
64
69
  "Incorrect use of validate_parameter_operation decorator. Must be called with 'add' or 'update' as the first argument."
65
70
  )
66
71
 
67
- @wraps(func)
68
- def wrapper(self: "InteractiveViewer", name: Any, *args, **kwargs):
69
- # Validate operation matches method name (add/update)
70
- if not func.__name__.startswith(operation):
71
- raise ValueError(
72
- f"Invalid operation type specified ({operation}) for method {func.__name__}"
73
- )
72
+ # Validate operation matches method name (add/update)
73
+ if not func.__name__.startswith(operation):
74
+ raise ValueError(
75
+ f"Invalid operation type specified ({operation}) for method {func.__name__}"
76
+ )
74
77
 
78
+ @wraps(func)
79
+ def wrapper(self: "Viewer", name: Any, *args, **kwargs):
75
80
  # Validate parameter name is a string
76
81
  if not isinstance(name, str):
77
82
  if operation == "add":
@@ -103,17 +108,17 @@ def validate_parameter_operation(
103
108
  parameter_type.name,
104
109
  "Parameter not found - you can only update registered parameters!",
105
110
  )
106
- if type(self.parameters[name]) != parameter_type.value:
107
- msg = f"Parameter called {name} was found but is registered as a different parameter type ({type(self.parameters[name])})"
111
+ if not isinstance(self.parameters[name], parameter_type.value):
112
+ msg = f"Parameter called {name} was found but is registered as a different parameter type ({type(self.parameters[name])}). Expecting {parameter_type.value}."
108
113
  raise ParameterUpdateError(name, parameter_type.name, msg)
109
114
 
110
115
  try:
111
116
  return func(self, name, *args, **kwargs)
112
117
  except Exception as e:
113
118
  if operation == "add":
114
- raise ParameterAddError(name, parameter_type.name, str(e)) from e
119
+ raise ParameterAddError(name, parameter_type.name, str(e))
115
120
  elif operation == "update":
116
- raise ParameterUpdateError(name, parameter_type.name, str(e)) from e
121
+ raise ParameterUpdateError(name, parameter_type.name, str(e))
117
122
  else:
118
123
  raise e
119
124
 
@@ -122,7 +127,7 @@ def validate_parameter_operation(
122
127
  return decorator
123
128
 
124
129
 
125
- class InteractiveViewer(ABC):
130
+ class Viewer:
126
131
  """
127
132
  Base class for creating interactive matplotlib figures with GUI controls.
128
133
 
@@ -137,7 +142,7 @@ class InteractiveViewer(ABC):
137
142
 
138
143
  Examples
139
144
  --------
140
- >>> class MyViewer(InteractiveViewer):
145
+ >>> class MyViewer(Viewer):
141
146
  ... def plot(self, state: Dict[str, Any]):
142
147
  ... fig = plt.figure()
143
148
  ... plt.plot([0, state['x']])
@@ -153,7 +158,6 @@ class InteractiveViewer(ABC):
153
158
 
154
159
  parameters: Dict[str, Parameter]
155
160
  callbacks: Dict[str, List[Callable]]
156
- state: Dict[str, Any]
157
161
  _app_deployed: bool
158
162
  _in_callbacks: bool
159
163
 
@@ -161,15 +165,16 @@ class InteractiveViewer(ABC):
161
165
  instance = super().__new__(cls)
162
166
  instance.parameters = {}
163
167
  instance.callbacks = {}
164
- instance.state = {}
165
168
  instance._app_deployed = False
166
169
  instance._in_callbacks = False
167
170
  return instance
168
171
 
169
- def get_state(self) -> Dict[str, Any]:
172
+ @property
173
+ def state(self) -> Dict[str, Any]:
170
174
  """
171
175
  Get the current values of all parameters.
172
176
 
177
+
173
178
  Returns
174
179
  -------
175
180
  dict
@@ -179,18 +184,38 @@ class InteractiveViewer(ABC):
179
184
  --------
180
185
  >>> viewer.add_float('x', value=1.0, min_value=0, max_value=10)
181
186
  >>> viewer.add_text('label', value='data')
182
- >>> viewer.get_state()
187
+ >>> viewer.state
183
188
  {'x': 1.0, 'label': 'data'}
184
189
  """
185
- return {name: param.value for name, param in self.parameters.items()}
190
+ return {
191
+ name: param.value
192
+ for name, param in self.parameters.items()
193
+ if not param._is_action
194
+ }
195
+
196
+ def plot(self, state: Dict[str, Any]) -> Figure:
197
+ """Create and return a matplotlib figure.
198
+
199
+ This is a placeholder. You must either:
200
+
201
+ 1. Call set_plot() with your plotting function
202
+ This will look like this:
203
+ >>> def plot(state):
204
+ >>> ... generate figure, plot stuff ...
205
+ >>> return fig
206
+ >>> viewer.set_plot(plot))
207
+
208
+ 2. Subclass Viewer and override this method
209
+ This will look like this:
210
+ >>> class YourViewer(Viewer):
211
+ >>> def plot(self, state):
212
+ >>> ... generate figure, plot stuff ...
213
+ >>> return fig
186
214
 
187
- @abstractmethod
188
- def plot(self, **kwargs) -> Figure:
189
- """
190
- Create and return a matplotlib figure.
191
-
192
- This method must be implemented in your subclass. It should create a new
193
- figure using the current parameter values from self.parameters.
215
+ Parameters
216
+ ----------
217
+ state : dict
218
+ Current parameter values
194
219
 
195
220
  Returns
196
221
  -------
@@ -200,27 +225,56 @@ class InteractiveViewer(ABC):
200
225
  Notes
201
226
  -----
202
227
  - Create a new figure each time, don't reuse old ones
203
- - Access parameter values using self.parameters['name'].value
204
- - Return the figure object, don't call plt.show()
228
+ - Access parameter values using state['param_name']
229
+ - Access your viewer class using self (or viewer for the set_plot() method)
230
+ - Return the figure object, don't call plt.show()!
205
231
  """
206
- raise NotImplementedError("Subclasses must implement the plot method")
232
+ raise NotImplementedError(
233
+ "Plot method not implemented. Either subclass "
234
+ "Viewer and override plot(), or use "
235
+ "set_plot() to provide a plotting function."
236
+ )
237
+
238
+ def set_plot(self, func: Callable) -> None:
239
+ """Set the plot method for the viewer"""
240
+ self.plot = self._prepare_function(func, context="Setting plot:")
207
241
 
208
242
  def deploy(self, env: str = "notebook", **kwargs):
209
243
  """Deploy the app in a notebook or standalone environment"""
210
244
  if env == "notebook":
211
- from .notebook_deploy import NotebookDeployment
245
+ from .notebook_deployment import NotebookDeployer
212
246
 
213
- deployer = NotebookDeployment(self, **kwargs)
247
+ deployer = NotebookDeployer(self, **kwargs)
214
248
  deployer.deploy()
249
+ return self
250
+
251
+ elif env == "plotly":
252
+ from .plotly_deployment import PlotlyDeployer
215
253
 
216
- return deployer
254
+ deployer = PlotlyDeployer(self, **kwargs)
255
+ deployer.deploy(mode="server")
256
+ return self
257
+
258
+ elif env == "plotly-inline":
259
+ from .plotly_deployment import PlotlyDeployer
260
+
261
+ deployer = PlotlyDeployer(self, **kwargs)
262
+ deployer.deploy(mode="notebook")
263
+ return self
264
+
265
+ elif env == "flask":
266
+ from .flask_deployment import FlaskDeployer
267
+
268
+ deployer = FlaskDeployer(self, **kwargs)
269
+ deployer.deploy()
270
+ return self
217
271
  else:
218
272
  raise ValueError(
219
- f"Unsupported environment: {env}, only 'notebook' is supported right now."
273
+ f"Unsupported environment: {env}, only 'notebook', 'plotly', 'plotly-inline', and 'flask' are supported right now."
220
274
  )
221
275
 
222
276
  @contextmanager
223
- def deploy_app(self):
277
+ def _deploy_app(self):
224
278
  """Internal context manager to control app deployment state"""
225
279
  self._app_deployed = True
226
280
  try:
@@ -228,6 +282,128 @@ class InteractiveViewer(ABC):
228
282
  finally:
229
283
  self._app_deployed = False
230
284
 
285
+ def _prepare_function(
286
+ self,
287
+ func: Callable,
288
+ context: Optional[str] = "",
289
+ ) -> Callable:
290
+ # Check if func is Callable
291
+ if not callable(func):
292
+ raise ValueError(f"Function {func} is not callable")
293
+
294
+ # Handle partial functions
295
+ if isinstance(func, partial):
296
+ get_self = (
297
+ lambda func: hasattr(func.func, "__self__") and func.func.__self__
298
+ )
299
+ get_name = lambda func: func.func.__name__
300
+ else:
301
+ get_self = lambda func: hasattr(func, "__self__") and func.__self__
302
+ get_name = lambda func: func.__name__
303
+
304
+ # Get function signature
305
+ try:
306
+ params = list(inspect.signature(func).parameters.values())
307
+ except ValueError:
308
+ # Handle built-ins or other objects without signatures
309
+ raise ValueError(context + f"Cannot inspect function signature for {func}")
310
+
311
+ # Look through params and check if there are two positional parameters (including self for bound methods)
312
+ bound_method = get_self(func) is self
313
+ positional_params = 0
314
+ required_kwargs = 0
315
+ optional_part = ""
316
+ for param in params:
317
+ # Check if it's a positional parameter. If it is, count it.
318
+ # We need at least 1 positional parameter. When we already have 1,
319
+ # we need to make sure any other positional parameters have defaults.
320
+ if param.kind in (
321
+ inspect.Parameter.POSITIONAL_OR_KEYWORD,
322
+ inspect.Parameter.POSITIONAL_ONLY,
323
+ ):
324
+ if positional_params < 1:
325
+ positional_params += 1
326
+ else:
327
+ if param.default == inspect.Parameter.empty:
328
+ positional_params += 1
329
+ else:
330
+ optional_part += f", {param.name}={param.default!r}"
331
+ elif param.kind == inspect.Parameter.VAR_KEYWORD:
332
+ optional_part += f", **{param.name}"
333
+ elif param.kind == inspect.Parameter.KEYWORD_ONLY:
334
+ optional_part += (
335
+ f", {param.name}={param.default!r}"
336
+ if param.default != inspect.Parameter.empty
337
+ else f""
338
+ )
339
+ if param.default == inspect.Parameter.empty:
340
+ required_kwargs += 1
341
+
342
+ if positional_params != 1 or required_kwargs != 0:
343
+ func_name = get_name(func)
344
+ if isinstance(func, partial):
345
+ func_sig = str(inspect.signature(func))
346
+ if bound_method:
347
+ func_sig = "(" + "self, " + func_sig[1:]
348
+ msg = (
349
+ context
350
+ + "\n"
351
+ + f"Your partial function '{func_name}' has an incorrect signature.\n"
352
+ "Partial functions must have exactly one positional parameter\n"
353
+ "which corresponds to a dictionary of the current state of the viewer.\n"
354
+ "\nYour partial function effectivelylooks like this:\n"
355
+ f"def {func_name}{func_sig}:\n"
356
+ " ... your function code ..."
357
+ )
358
+ raise ValueError(msg)
359
+
360
+ if bound_method:
361
+ original_method = getattr(get_self(func).__class__, get_name(func))
362
+ func_sig = str(inspect.signature(original_method))
363
+
364
+ msg = (
365
+ context + "\n"
366
+ f"Your bound method '{func_name}{func_sig}' has an incorrect signature.\n"
367
+ "Bound methods must have exactly one positional parameter in addition to self.\n"
368
+ "The first parameter should be self (required for bound methods).\n"
369
+ "The second parameter should be state -- a dictionary of the current state of the viewer.\n"
370
+ "\nYour method looks like this:\n"
371
+ "class YourViewer(Viewer):\n"
372
+ f" def {func_name}{func_sig}:\n"
373
+ " ... your function code ...\n"
374
+ "\nIt should look like this:\n"
375
+ "class YourViewer(Viewer):\n"
376
+ f" def {func_name}(self, state{optional_part}):\n"
377
+ " ... your function code ..."
378
+ )
379
+ raise ValueError(msg)
380
+ else:
381
+ func_sig = str(inspect.signature(func))
382
+ bound_elsewhere = get_self(func) and get_self(func) is not self
383
+ if bound_elsewhere:
384
+ func_name = f"self.{func_name}"
385
+ func_sig = f"(self, {func_sig[1:]})"
386
+ add_self = True
387
+ else:
388
+ add_self = False
389
+ msg = (
390
+ context + "\n"
391
+ f"Your function '{func_name}{func_sig}' has an incorrect signature.\n"
392
+ "Functions must have exactly one positional parameter\n"
393
+ "which corresponds to a dictionary of the current state of the viewer.\n"
394
+ "\nYour function looks like this:\n"
395
+ f"def {func_name}{func_sig}:\n"
396
+ " ... your function code ...\n"
397
+ "\nIt should look like this:\n"
398
+ f"def {func_name}({'self, ' if add_self else ''}state{optional_part}):\n"
399
+ " ... your function code ..."
400
+ )
401
+ raise ValueError(msg)
402
+
403
+ # If we've made it here, the function has exactly one required positional parameter
404
+ # which means it's callable by the viewer.
405
+ return func
406
+
231
407
  def perform_callbacks(self, name: str) -> bool:
232
408
  """Perform callbacks for all parameters that have changed"""
233
409
  if self._in_callbacks:
@@ -235,7 +411,7 @@ class InteractiveViewer(ABC):
235
411
  try:
236
412
  self._in_callbacks = True
237
413
  if name in self.callbacks:
238
- state = self.get_state()
414
+ state = self.state
239
415
  for callback in self.callbacks[name]:
240
416
  callback(state)
241
417
  finally:
@@ -266,6 +442,11 @@ class InteractiveViewer(ABC):
266
442
  if isinstance(parameter_name, str):
267
443
  parameter_name = [parameter_name]
268
444
 
445
+ callback = self._prepare_function(
446
+ callback,
447
+ context="Setting on_change callback:",
448
+ )
449
+
269
450
  for param_name in parameter_name:
270
451
  if param_name not in self.parameters:
271
452
  raise ValueError(f"Parameter '{param_name}' is not registered!")
@@ -320,7 +501,7 @@ class InteractiveViewer(ABC):
320
501
  Examples
321
502
  --------
322
503
  >>> viewer.add_text('title', value='My Plot')
323
- >>> viewer.get_state()['title']
504
+ >>> viewer.state['title']
324
505
  'My Plot'
325
506
  """
326
507
  try:
@@ -348,7 +529,7 @@ class InteractiveViewer(ABC):
348
529
  Examples
349
530
  --------
350
531
  >>> viewer.add_boolean('show_grid', value=True)
351
- >>> viewer.get_state()['show_grid']
532
+ >>> viewer.state['show_grid']
352
533
  True
353
534
  """
354
535
  try:
@@ -379,7 +560,7 @@ class InteractiveViewer(ABC):
379
560
  --------
380
561
  >>> viewer.add_selection('color', value='red',
381
562
  ... options=['red', 'green', 'blue'])
382
- >>> viewer.get_state()['color']
563
+ >>> viewer.state['color']
383
564
  'red'
384
565
  """
385
566
  try:
@@ -414,7 +595,7 @@ class InteractiveViewer(ABC):
414
595
  >>> viewer.add_multiple_selection('toppings',
415
596
  ... value=['cheese'],
416
597
  ... options=['cheese', 'pepperoni', 'mushrooms'])
417
- >>> viewer.get_state()['toppings']
598
+ >>> viewer.state['toppings']
418
599
  ['cheese']
419
600
  """
420
601
  try:
@@ -454,10 +635,10 @@ class InteractiveViewer(ABC):
454
635
  Examples
455
636
  --------
456
637
  >>> viewer.add_integer('count', value=5, min_value=0, max_value=10)
457
- >>> viewer.get_state()['count']
638
+ >>> viewer.state['count']
458
639
  5
459
640
  >>> viewer.update_integer('count', value=15) # Will be clamped to 10
460
- >>> viewer.get_state()['count']
641
+ >>> viewer.state['count']
461
642
  10
462
643
  """
463
644
  try:
@@ -468,7 +649,7 @@ class InteractiveViewer(ABC):
468
649
  max_value,
469
650
  )
470
651
  except Exception as e:
471
- raise ParameterAddError(name, "number", str(e)) from e
652
+ raise ParameterAddError(name, "number", str(e))
472
653
  else:
473
654
  self.parameters[name] = new_param
474
655
 
@@ -480,11 +661,12 @@ class InteractiveViewer(ABC):
480
661
  value: Union[float, int],
481
662
  min_value: Union[float, int],
482
663
  max_value: Union[float, int],
483
- step: float = 0.1,
664
+ step: float = 0.01,
484
665
  ) -> None:
485
666
  """
486
667
  Add a decimal number parameter to the viewer.
487
668
 
669
+
488
670
  Creates a slider in the GUI that lets users select numbers between
489
671
  min_value and max_value. Values will be rounded to the nearest step
490
672
  and clamped to stay within bounds.
@@ -500,16 +682,16 @@ class InteractiveViewer(ABC):
500
682
  max_value : float
501
683
  Maximum allowed value
502
684
  step : float, optional
503
- Size of each increment (default: 0.1)
685
+ Size of each increment (default: 0.01)
504
686
 
505
687
  Examples
506
688
  --------
507
689
  >>> viewer.add_float('temperature', value=20.0,
508
690
  ... min_value=0.0, max_value=100.0, step=0.5)
509
- >>> viewer.get_state()['temperature']
691
+ >>> viewer.state['temperature']
510
692
  20.0
511
693
  >>> viewer.update_float('temperature', value=20.7) # Will round to 20.5
512
- >>> viewer.get_state()['temperature']
694
+ >>> viewer.state['temperature']
513
695
  20.5
514
696
  """
515
697
  try:
@@ -558,11 +740,11 @@ class InteractiveViewer(ABC):
558
740
  >>> viewer.add_integer_range('age_range',
559
741
  ... value=(25, 35),
560
742
  ... min_value=18, max_value=100)
561
- >>> viewer.get_state()['age_range']
743
+ >>> viewer.state['age_range']
562
744
  (25, 35)
563
745
  >>> # Values will be swapped if low > high
564
746
  >>> viewer.update_integer_range('age_range', value=(40, 30))
565
- >>> viewer.get_state()['age_range']
747
+ >>> viewer.state['age_range']
566
748
  (30, 40)
567
749
  """
568
750
  try:
@@ -585,7 +767,7 @@ class InteractiveViewer(ABC):
585
767
  value: Tuple[Union[float, int], Union[float, int]],
586
768
  min_value: Union[float, int],
587
769
  max_value: Union[float, int],
588
- step: float = 0.1,
770
+ step: float = 0.01,
589
771
  ) -> None:
590
772
  """
591
773
  Add a range parameter for decimal numbers to the viewer.
@@ -606,18 +788,18 @@ class InteractiveViewer(ABC):
606
788
  max_value : float
607
789
  Maximum allowed value for both low and high
608
790
  step : float, optional
609
- Size of each increment (default: 0.1)
791
+ Size of each increment (default: 0.01)
610
792
 
611
793
  Examples
612
794
  --------
613
795
  >>> viewer.add_float_range('price_range',
614
796
  ... value=(10.0, 20.0),
615
797
  ... min_value=0.0, max_value=100.0, step=0.5)
616
- >>> viewer.get_state()['price_range']
798
+ >>> viewer.state['price_range']
617
799
  (10.0, 20.0)
618
800
  >>> # Values will be rounded to nearest step
619
801
  >>> viewer.update_float_range('price_range', value=(10.7, 19.2))
620
- >>> viewer.get_state()['price_range']
802
+ >>> viewer.state['price_range']
621
803
  (10.5, 19.0)
622
804
  """
623
805
  try:
@@ -639,15 +821,12 @@ class InteractiveViewer(ABC):
639
821
  name: str,
640
822
  *,
641
823
  value: Union[float, int],
642
- min_value: Optional[Union[float, int]] = None,
643
- max_value: Optional[Union[float, int]] = None,
644
824
  ) -> None:
645
825
  """
646
826
  Add an unbounded integer parameter to the viewer.
647
827
 
648
828
  Creates a text input box in the GUI for entering whole numbers. Unlike
649
- add_integer(), this allows very large numbers and optionally no minimum
650
- or maximum bounds.
829
+ add_integer(), this allows very large numbers without bounds.
651
830
  See :class:`~syd.parameters.UnboundedIntegerParameter` for details.
652
831
 
653
832
  Parameters
@@ -656,29 +835,17 @@ class InteractiveViewer(ABC):
656
835
  Name of the parameter (used as label in GUI)
657
836
  value : int
658
837
  Initial value
659
- min_value : int, optional
660
- Minimum allowed value (or None for no minimum)
661
- max_value : int, optional
662
- Maximum allowed value (or None for no maximum)
663
838
 
664
839
  Examples
665
840
  --------
666
- >>> viewer.add_unbounded_integer('population',
667
- ... value=1000000,
668
- ... min_value=0) # No maximum
669
- >>> viewer.get_state()['population']
841
+ >>> viewer.add_unbounded_integer('population', value=1000000)
842
+ >>> viewer.state['population']
670
843
  1000000
671
- >>> # Values below minimum will be clamped
672
- >>> viewer.update_unbounded_integer('population', value=-5)
673
- >>> viewer.get_state()['population']
674
- 0
675
844
  """
676
845
  try:
677
846
  new_param = ParameterType.unbounded_integer.value(
678
847
  name,
679
848
  value,
680
- min_value,
681
- max_value,
682
849
  )
683
850
  except Exception as e:
684
851
  raise ParameterAddError(name, "unbounded_integer", str(e)) from e
@@ -691,16 +858,14 @@ class InteractiveViewer(ABC):
691
858
  name: str,
692
859
  *,
693
860
  value: Union[float, int],
694
- min_value: Optional[Union[float, int]] = None,
695
- max_value: Optional[Union[float, int]] = None,
696
861
  step: Optional[float] = None,
697
862
  ) -> None:
698
863
  """
699
864
  Add an unbounded decimal number parameter to the viewer.
700
865
 
701
866
  Creates a text input box in the GUI for entering numbers. Unlike add_float(),
702
- this allows very large or precise numbers and optionally no minimum or
703
- maximum bounds. Values can optionally be rounded to a step size.
867
+ this allows very large or precise numbers without bounds. Values can optionally
868
+ be rounded to a step size.
704
869
  See :class:`~syd.parameters.UnboundedFloatParameter` for details.
705
870
 
706
871
  Parameters
@@ -709,32 +874,23 @@ class InteractiveViewer(ABC):
709
874
  Name of the parameter (used as label in GUI)
710
875
  value : float
711
876
  Initial value
712
- min_value : float, optional
713
- Minimum allowed value (or None for no minimum)
714
- max_value : float, optional
715
- Maximum allowed value (or None for no maximum)
716
877
  step : float, optional
717
878
  Size of each increment (or None for no rounding)
718
879
 
719
880
  Examples
720
881
  --------
721
- >>> viewer.add_unbounded_float('wavelength',
722
- ... value=550e-9, # Nanometers
723
- ... min_value=0.0,
724
- ... step=1e-9) # Round to nearest nanometer
725
- >>> viewer.get_state()['wavelength']
882
+ >>> viewer.add_unbounded_float('wavelength', value=550e-9, step=1e-9)
883
+ >>> viewer.state['wavelength']
726
884
  5.5e-07
727
885
  >>> # Values will be rounded if step is provided
728
886
  >>> viewer.update_unbounded_float('wavelength', value=550.7e-9)
729
- >>> viewer.get_state()['wavelength']
887
+ >>> viewer.state['wavelength']
730
888
  5.51e-07
731
889
  """
732
890
  try:
733
891
  new_param = ParameterType.unbounded_float.value(
734
892
  name,
735
893
  value,
736
- min_value,
737
- max_value,
738
894
  step,
739
895
  )
740
896
  except Exception as e:
@@ -742,7 +898,7 @@ class InteractiveViewer(ABC):
742
898
  else:
743
899
  self.parameters[name] = new_param
744
900
 
745
- @validate_parameter_operation("add", ParameterType.button)
901
+ @validate_parameter_operation("add", ActionType.button)
746
902
  def add_button(
747
903
  self,
748
904
  name: str,
@@ -764,21 +920,25 @@ class InteractiveViewer(ABC):
764
920
  label : str
765
921
  Text to display on the button
766
922
  callback : callable
767
- Function to call when the button is clicked (takes no arguments)
923
+ Function to call when the button is clicked (takes state as a single argument)
768
924
 
769
925
  Examples
770
926
  --------
771
- >>> def reset_plot():
927
+ >>> def reset_plot(state):
772
928
  ... print("Resetting plot...")
773
929
  >>> viewer.add_button('reset', label='Reset Plot', callback=reset_plot)
930
+
931
+ >>> def print_plot_info(state):
932
+ ... print(f"Current plot info: {state['plot_info']}")
933
+ >>> viewer.add_button('print_info', label='Print Plot Info', callback=print_plot_info)
774
934
  """
775
935
  try:
936
+ callback = self._prepare_function(
937
+ callback,
938
+ context="Setting button callback:",
939
+ )
776
940
 
777
- # Wrap the callback to include state as an input argument
778
- def wrapped_callback(button):
779
- callback(self.get_state())
780
-
781
- new_param = ParameterType.button.value(name, label, wrapped_callback)
941
+ new_param = ActionType.button.value(name, label, callback)
782
942
  except Exception as e:
783
943
  raise ParameterAddError(name, "button", str(e)) from e
784
944
  else:
@@ -792,7 +952,7 @@ class InteractiveViewer(ABC):
792
952
  """
793
953
  Update a text parameter's value.
794
954
 
795
- Updates a parameter created by :meth:`~syd.interactive_viewer.InteractiveViewer.add_text`.
955
+ Updates a parameter created by :meth:`~syd.viewer.Viewer.add_text`.
796
956
  See :class:`~syd.parameters.TextParameter` for details about value validation.
797
957
 
798
958
  Parameters
@@ -806,11 +966,11 @@ class InteractiveViewer(ABC):
806
966
  --------
807
967
  >>> viewer.add_text('title', value='Original Title')
808
968
  >>> viewer.update_text('title', value='New Title')
809
- >>> viewer.get_state()['title']
969
+ >>> viewer.state['title']
810
970
  'New Title'
811
971
  """
812
972
  updates = {}
813
- if value is not _NO_UPDATE:
973
+ if not value == _NO_UPDATE:
814
974
  updates["value"] = value
815
975
  if updates:
816
976
  self.parameters[name].update(updates)
@@ -822,7 +982,7 @@ class InteractiveViewer(ABC):
822
982
  """
823
983
  Update a boolean parameter's value.
824
984
 
825
- Updates a parameter created by :meth:`~syd.interactive_viewer.InteractiveViewer.add_boolean`.
985
+ Updates a parameter created by :meth:`~syd.viewer.Viewer.add_boolean`.
826
986
  See :class:`~syd.parameters.BooleanParameter` for details about value validation.
827
987
 
828
988
  Parameters
@@ -836,11 +996,11 @@ class InteractiveViewer(ABC):
836
996
  --------
837
997
  >>> viewer.add_boolean('show_grid', value=True)
838
998
  >>> viewer.update_boolean('show_grid', value=False)
839
- >>> viewer.get_state()['show_grid']
999
+ >>> viewer.state['show_grid']
840
1000
  False
841
1001
  """
842
1002
  updates = {}
843
- if value is not _NO_UPDATE:
1003
+ if not value == _NO_UPDATE:
844
1004
  updates["value"] = value
845
1005
  if updates:
846
1006
  self.parameters[name].update(updates)
@@ -856,7 +1016,7 @@ class InteractiveViewer(ABC):
856
1016
  """
857
1017
  Update a selection parameter's value and/or options.
858
1018
 
859
- Updates a parameter created by :meth:`~syd.interactive_viewer.InteractiveViewer.add_selection`.
1019
+ Updates a parameter created by :meth:`~syd.viewer.Viewer.add_selection`.
860
1020
  See :class:`~syd.parameters.SelectionParameter` for details about value validation.
861
1021
 
862
1022
  Parameters
@@ -880,9 +1040,9 @@ class InteractiveViewer(ABC):
880
1040
  ... value='purple')
881
1041
  """
882
1042
  updates = {}
883
- if value is not _NO_UPDATE:
1043
+ if not value == _NO_UPDATE:
884
1044
  updates["value"] = value
885
- if options is not _NO_UPDATE:
1045
+ if not options == _NO_UPDATE:
886
1046
  updates["options"] = options
887
1047
  if updates:
888
1048
  self.parameters[name].update(updates)
@@ -898,7 +1058,7 @@ class InteractiveViewer(ABC):
898
1058
  """
899
1059
  Update a multiple selection parameter's values and/or options.
900
1060
 
901
- Updates a parameter created by :meth:`~syd.interactive_viewer.InteractiveViewer.add_multiple_selection`.
1061
+ Updates a parameter created by :meth:`~syd.viewer.Viewer.add_multiple_selection`.
902
1062
  See :class:`~syd.parameters.MultipleSelectionParameter` for details about value validation.
903
1063
 
904
1064
  Parameters
@@ -924,9 +1084,9 @@ class InteractiveViewer(ABC):
924
1084
  ... value=['cheese', 'bacon'])
925
1085
  """
926
1086
  updates = {}
927
- if value is not _NO_UPDATE:
1087
+ if not value == _NO_UPDATE:
928
1088
  updates["value"] = value
929
- if options is not _NO_UPDATE:
1089
+ if not options == _NO_UPDATE:
930
1090
  updates["options"] = options
931
1091
  if updates:
932
1092
  self.parameters[name].update(updates)
@@ -943,7 +1103,7 @@ class InteractiveViewer(ABC):
943
1103
  """
944
1104
  Update an integer parameter's value and/or bounds.
945
1105
 
946
- Updates a parameter created by :meth:`~syd.interactive_viewer.InteractiveViewer.add_integer`.
1106
+ Updates a parameter created by :meth:`~syd.viewer.Viewer.add_integer`.
947
1107
  See :class:`~syd.parameters.IntegerParameter` for details about value validation.
948
1108
 
949
1109
  Parameters
@@ -966,11 +1126,11 @@ class InteractiveViewer(ABC):
966
1126
  >>> viewer.update_integer('count', min_value=7, max_value=15)
967
1127
  """
968
1128
  updates = {}
969
- if value is not _NO_UPDATE:
1129
+ if not value == _NO_UPDATE:
970
1130
  updates["value"] = value
971
- if min_value is not _NO_UPDATE:
1131
+ if not min_value == _NO_UPDATE:
972
1132
  updates["min_value"] = min_value
973
- if max_value is not _NO_UPDATE:
1133
+ if not max_value == _NO_UPDATE:
974
1134
  updates["max_value"] = max_value
975
1135
  if updates:
976
1136
  self.parameters[name].update(updates)
@@ -988,7 +1148,7 @@ class InteractiveViewer(ABC):
988
1148
  """
989
1149
  Update a float parameter's value, bounds, and/or step size.
990
1150
 
991
- Updates a parameter created by :meth:`~syd.interactive_viewer.InteractiveViewer.add_float`.
1151
+ Updates a parameter created by :meth:`~syd.viewer.Viewer.add_float`.
992
1152
  See :class:`~syd.parameters.FloatParameter` for details about value validation.
993
1153
 
994
1154
  Parameters
@@ -1015,13 +1175,13 @@ class InteractiveViewer(ABC):
1015
1175
  ... min_value=15.0, max_value=30.0, step=0.1)
1016
1176
  """
1017
1177
  updates = {}
1018
- if value is not _NO_UPDATE:
1178
+ if not value == _NO_UPDATE:
1019
1179
  updates["value"] = value
1020
- if min_value is not _NO_UPDATE:
1180
+ if not min_value == _NO_UPDATE:
1021
1181
  updates["min_value"] = min_value
1022
- if max_value is not _NO_UPDATE:
1182
+ if not max_value == _NO_UPDATE:
1023
1183
  updates["max_value"] = max_value
1024
- if step is not _NO_UPDATE:
1184
+ if not step == _NO_UPDATE:
1025
1185
  updates["step"] = step
1026
1186
  if updates:
1027
1187
  self.parameters[name].update(updates)
@@ -1038,7 +1198,7 @@ class InteractiveViewer(ABC):
1038
1198
  """
1039
1199
  Update an integer range parameter's values and/or bounds.
1040
1200
 
1041
- Updates a parameter created by :meth:`~syd.interactive_viewer.InteractiveViewer.add_integer_range`.
1201
+ Updates a parameter created by :meth:`~syd.viewer.Viewer.add_integer_range`.
1042
1202
  See :class:`~syd.parameters.IntegerRangeParameter` for details about value validation.
1043
1203
 
1044
1204
  Parameters
@@ -1063,11 +1223,11 @@ class InteractiveViewer(ABC):
1063
1223
  >>> viewer.update_integer_range('age_range', min_value=20, max_value=80)
1064
1224
  """
1065
1225
  updates = {}
1066
- if value is not _NO_UPDATE:
1226
+ if not value == _NO_UPDATE:
1067
1227
  updates["value"] = value
1068
- if min_value is not _NO_UPDATE:
1228
+ if not min_value == _NO_UPDATE:
1069
1229
  updates["min_value"] = min_value
1070
- if max_value is not _NO_UPDATE:
1230
+ if not max_value == _NO_UPDATE:
1071
1231
  updates["max_value"] = max_value
1072
1232
  if updates:
1073
1233
  self.parameters[name].update(updates)
@@ -1085,7 +1245,7 @@ class InteractiveViewer(ABC):
1085
1245
  """
1086
1246
  Update a float range parameter's values, bounds, and/or step size.
1087
1247
 
1088
- Updates a parameter created by :meth:`~syd.interactive_viewer.InteractiveViewer.add_float_range`.
1248
+ Updates a parameter created by :meth:`~syd.viewer.Viewer.add_float_range`.
1089
1249
  See :class:`~syd.parameters.FloatRangeParameter` for details about value validation.
1090
1250
 
1091
1251
  Parameters
@@ -1113,13 +1273,13 @@ class InteractiveViewer(ABC):
1113
1273
  ... min_value=5.0, max_value=50.0, step=0.1)
1114
1274
  """
1115
1275
  updates = {}
1116
- if value is not _NO_UPDATE:
1276
+ if not value == _NO_UPDATE:
1117
1277
  updates["value"] = value
1118
- if min_value is not _NO_UPDATE:
1278
+ if not min_value == _NO_UPDATE:
1119
1279
  updates["min_value"] = min_value
1120
- if max_value is not _NO_UPDATE:
1280
+ if not max_value == _NO_UPDATE:
1121
1281
  updates["max_value"] = max_value
1122
- if step is not _NO_UPDATE:
1282
+ if not step == _NO_UPDATE:
1123
1283
  updates["step"] = step
1124
1284
  if updates:
1125
1285
  self.parameters[name].update(updates)
@@ -1130,13 +1290,11 @@ class InteractiveViewer(ABC):
1130
1290
  name: str,
1131
1291
  *,
1132
1292
  value: Union[int, _NoUpdate] = _NO_UPDATE,
1133
- min_value: Union[Optional[int], _NoUpdate] = _NO_UPDATE,
1134
- max_value: Union[Optional[int], _NoUpdate] = _NO_UPDATE,
1135
1293
  ) -> None:
1136
1294
  """
1137
1295
  Update an unbounded integer parameter's value and/or bounds.
1138
1296
 
1139
- Updates a parameter created by :meth:`~syd.interactive_viewer.InteractiveViewer.add_unbounded_integer`.
1297
+ Updates a parameter created by :meth:`~syd.viewer.Viewer.add_unbounded_integer`.
1140
1298
  See :class:`~syd.parameters.UnboundedIntegerParameter` for details about value validation.
1141
1299
 
1142
1300
  Parameters
@@ -1144,31 +1302,17 @@ class InteractiveViewer(ABC):
1144
1302
  name : str
1145
1303
  Name of the unbounded integer parameter to update
1146
1304
  value : int, optional
1147
- New value (will be clamped to any bounds) (if not provided, no change)
1148
- min_value : int or None, optional
1149
- New minimum value, or None for no minimum (if not provided, no change)
1150
- max_value : int or None, optional
1151
- New maximum value, or None for no maximum (if not provided, no change)
1305
+ New value (if not provided, no change)
1152
1306
 
1153
1307
  Examples
1154
1308
  --------
1155
- >>> viewer.add_unbounded_integer('population',
1156
- ... value=1000000,
1157
- ... min_value=0) # No maximum
1309
+ >>> viewer.add_unbounded_integer('population', value=1000000)
1158
1310
  >>> # Update just the value
1159
1311
  >>> viewer.update_unbounded_integer('population', value=2000000)
1160
- >>> # Add a maximum bound (current value will be clamped if needed)
1161
- >>> viewer.update_unbounded_integer('population', max_value=1500000)
1162
- >>> # Remove the minimum bound
1163
- >>> viewer.update_unbounded_integer('population', min_value=None)
1164
1312
  """
1165
1313
  updates = {}
1166
- if value is not _NO_UPDATE:
1314
+ if not value == _NO_UPDATE:
1167
1315
  updates["value"] = value
1168
- if min_value is not _NO_UPDATE:
1169
- updates["min_value"] = min_value
1170
- if max_value is not _NO_UPDATE:
1171
- updates["max_value"] = max_value
1172
1316
  if updates:
1173
1317
  self.parameters[name].update(updates)
1174
1318
 
@@ -1178,14 +1322,12 @@ class InteractiveViewer(ABC):
1178
1322
  name: str,
1179
1323
  *,
1180
1324
  value: Union[float, _NoUpdate] = _NO_UPDATE,
1181
- min_value: Union[Optional[float], _NoUpdate] = _NO_UPDATE,
1182
- max_value: Union[Optional[float], _NoUpdate] = _NO_UPDATE,
1183
1325
  step: Union[Optional[float], _NoUpdate] = _NO_UPDATE,
1184
1326
  ) -> None:
1185
1327
  """
1186
1328
  Update an unbounded float parameter's value, bounds, and/or step size.
1187
1329
 
1188
- Updates a parameter created by :meth:`~syd.interactive_viewer.InteractiveViewer.add_unbounded_float`.
1330
+ Updates a parameter created by :meth:`~syd.viewer.Viewer.add_unbounded_float`.
1189
1331
  See :class:`~syd.parameters.UnboundedFloatParameter` for details about value validation.
1190
1332
 
1191
1333
  Parameters
@@ -1194,41 +1336,28 @@ class InteractiveViewer(ABC):
1194
1336
  Name of the unbounded float parameter to update
1195
1337
  value : float, optional
1196
1338
  New value (will be rounded if step is set) (if not provided, no change)
1197
- min_value : float or None, optional
1198
- New minimum value, or None for no minimum (if not provided, no change)
1199
- max_value : float or None, optional
1200
- New maximum value, or None for no maximum (if not provided, no change)
1201
1339
  step : float or None, optional
1202
1340
  New step size for rounding, or None for no rounding (if not provided, no change)
1203
1341
 
1204
1342
  Examples
1205
1343
  --------
1206
- >>> viewer.add_unbounded_float('wavelength',
1207
- ... value=550e-9, # Nanometers
1208
- ... min_value=0.0,
1209
- ... step=1e-9)
1344
+ >>> viewer.add_unbounded_float('wavelength', value=550e-9, step=1e-9)
1210
1345
  >>> # Update value (will be rounded if step is set)
1211
- >>> viewer.update_unbounded_float('wavelength', value=632.8e-9) # HeNe laser
1212
- >>> # Change step size and add maximum
1213
- >>> viewer.update_unbounded_float('wavelength',
1214
- ... step=0.1e-9, # Finer control
1215
- ... max_value=1000e-9) # Infrared limit
1346
+ >>> viewer.update_unbounded_float('wavelength', value=632.8e-9)
1347
+ >>> # Change step size
1348
+ >>> viewer.update_unbounded_float('wavelength', step=0.1e-9)
1216
1349
  >>> # Remove step size (allow any precision)
1217
1350
  >>> viewer.update_unbounded_float('wavelength', step=None)
1218
1351
  """
1219
1352
  updates = {}
1220
- if value is not _NO_UPDATE:
1353
+ if not value == _NO_UPDATE:
1221
1354
  updates["value"] = value
1222
- if min_value is not _NO_UPDATE:
1223
- updates["min_value"] = min_value
1224
- if max_value is not _NO_UPDATE:
1225
- updates["max_value"] = max_value
1226
- if step is not _NO_UPDATE:
1355
+ if not step == _NO_UPDATE:
1227
1356
  updates["step"] = step
1228
1357
  if updates:
1229
1358
  self.parameters[name].update(updates)
1230
1359
 
1231
- @validate_parameter_operation("update", ParameterType.button)
1360
+ @validate_parameter_operation("update", ActionType.button)
1232
1361
  def update_button(
1233
1362
  self,
1234
1363
  name: str,
@@ -1239,8 +1368,8 @@ class InteractiveViewer(ABC):
1239
1368
  """
1240
1369
  Update a button parameter's label and/or callback function.
1241
1370
 
1242
- Updates a parameter created by :meth:`~syd.interactive_viewer.InteractiveViewer.add_button`.
1243
- See :class:`~syd.parameters.ButtonParameter` for details.
1371
+ Updates a parameter created by :meth:`~syd.viewer.Viewer.add_button`.
1372
+ See :class:`~syd.parameters.ButtonAction` for details.
1244
1373
 
1245
1374
  Parameters
1246
1375
  ----------
@@ -1253,16 +1382,20 @@ class InteractiveViewer(ABC):
1253
1382
 
1254
1383
  Examples
1255
1384
  --------
1256
- >>> def new_callback():
1385
+ >>> def new_callback(state):
1257
1386
  ... print("New action...")
1258
1387
  >>> viewer.update_button('reset',
1259
- ... label='Clear Plot',
1388
+ ... label='New Action!',
1260
1389
  ... callback=new_callback)
1261
1390
  """
1262
1391
  updates = {}
1263
- if label is not _NO_UPDATE:
1392
+ if not label == _NO_UPDATE:
1264
1393
  updates["label"] = label
1265
- if callback is not _NO_UPDATE:
1394
+ if not callback == _NO_UPDATE:
1395
+ callback = self._prepare_function(
1396
+ callback,
1397
+ context="Updating button callback:",
1398
+ )
1266
1399
  updates["callback"] = callback
1267
1400
  if updates:
1268
1401
  self.parameters[name].update(updates)