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.
syd/parameters.py CHANGED
@@ -1,9 +1,10 @@
1
- from typing import List, Any, Tuple, Generic, TypeVar, Optional, Dict, Callable
2
- from dataclasses import dataclass
3
- from abc import ABC, abstractmethod
1
+ from typing import List, Any, Tuple, Generic, TypeVar, Optional, Dict, Callable, Union
2
+ from dataclasses import dataclass, field
3
+ from abc import ABC, ABCMeta, abstractmethod
4
4
  from enum import Enum
5
5
  from copy import deepcopy
6
6
  from warnings import warn
7
+ import numpy as np
7
8
 
8
9
  T = TypeVar("T")
9
10
 
@@ -55,6 +56,29 @@ class ParameterUpdateError(Exception):
55
56
  )
56
57
 
57
58
 
59
+ class ParameterUpdateWarning(Warning):
60
+ """
61
+ Warning raised when there is a non-critical issue updating a parameter.
62
+
63
+ Parameters
64
+ ----------
65
+ parameter_name : str
66
+ Name of the parameter that had the warning
67
+ parameter_type : str
68
+ Type of the parameter
69
+ message : str, optional
70
+ Additional warning details
71
+ """
72
+
73
+ def __init__(self, parameter_name: str, parameter_type: str, message: str = None):
74
+ self.parameter_name = parameter_name
75
+ self.parameter_type = parameter_type
76
+ super().__init__(
77
+ f"Warning updating {parameter_type} parameter '{parameter_name}'"
78
+ + (f": {message}" if message else "")
79
+ )
80
+
81
+
58
82
  def get_parameter_attributes(param_class) -> List[str]:
59
83
  """
60
84
  Get all valid attributes for a parameter class.
@@ -82,8 +106,39 @@ def get_parameter_attributes(param_class) -> List[str]:
82
106
  return attributes
83
107
 
84
108
 
109
+ class ParameterMeta(ABCMeta):
110
+ _parameter_types = {}
111
+ _parameter_ids = {} # Store unique identifiers for our parameter types
112
+
113
+ def __new__(cls, name, bases, namespace):
114
+ parameter_class = super().__new__(cls, name, bases, namespace)
115
+ if name != "Parameter":
116
+ # Generate a unique ID for this parameter type
117
+ type_id = f"syd.parameters.{name}" # Using fully qualified name
118
+ cls._parameter_ids[name] = type_id
119
+
120
+ # Add ID to the class
121
+ if not hasattr(parameter_class, "_parameter_type_id"):
122
+ setattr(parameter_class, "_parameter_type_id", type_id)
123
+ else:
124
+ if getattr(parameter_class, "_parameter_type_id") != type_id:
125
+ raise ValueError(
126
+ f"Parameter type {name} has multiple IDs: {type_id} and {getattr(parameter_class, '_parameter_type_id')}"
127
+ )
128
+ cls._parameter_types[name] = parameter_class
129
+ return parameter_class
130
+
131
+ def __instancecheck__(cls, instance):
132
+ type_id = cls._parameter_ids.get(cls.__name__)
133
+ if not type_id:
134
+ return False
135
+
136
+ # Check if instance has our type ID
137
+ return getattr(instance.__class__, "_parameter_type_id", None) == type_id
138
+
139
+
85
140
  @dataclass
86
- class Parameter(Generic[T], ABC):
141
+ class Parameter(Generic[T], ABC, metaclass=ParameterMeta):
87
142
  """
88
143
  Base class for all parameter types. Parameters are the building blocks
89
144
  for creating interactive GUI elements.
@@ -106,6 +161,7 @@ class Parameter(Generic[T], ABC):
106
161
 
107
162
  name: str
108
163
  value: T
164
+ _is_action: bool = False
109
165
 
110
166
  @abstractmethod
111
167
  def __init__(self, name: str, value: T):
@@ -229,8 +285,8 @@ class TextParameter(Parameter[str]):
229
285
  Parameter for text input.
230
286
 
231
287
  Creates a text box in the GUI that accepts any string input.
232
- See :meth:`~syd.interactive_viewer.InteractiveViewer.add_text` and
233
- :meth:`~syd.interactive_viewer.InteractiveViewer.update_text` for usage.
288
+ See :meth:`~syd.viewer.Viewer.add_text` and
289
+ :meth:`~syd.viewer.Viewer.update_text` for usage.
234
290
 
235
291
  Parameters
236
292
  ----------
@@ -272,8 +328,8 @@ class BooleanParameter(Parameter[bool]):
272
328
  Parameter for boolean values.
273
329
 
274
330
  Creates a checkbox in the GUI that can be toggled on/off.
275
- See :meth:`~syd.interactive_viewer.InteractiveViewer.add_boolean` and
276
- :meth:`~syd.interactive_viewer.InteractiveViewer.update_boolean` for usage.
331
+ See :meth:`~syd.viewer.Viewer.add_boolean` and
332
+ :meth:`~syd.viewer.Viewer.update_boolean` for usage.
277
333
 
278
334
  Parameters
279
335
  ----------
@@ -315,8 +371,8 @@ class SelectionParameter(Parameter[Any]):
315
371
  Parameter for single selection from a list of options.
316
372
 
317
373
  Creates a dropdown menu in the GUI where users can select one option.
318
- See :meth:`~syd.interactive_viewer.InteractiveViewer.add_selection` and
319
- :meth:`~syd.interactive_viewer.InteractiveViewer.update_selection` for usage.
374
+ See :meth:`~syd.viewer.Viewer.add_selection` and
375
+ :meth:`~syd.viewer.Viewer.update_selection` for usage.
320
376
 
321
377
  Parameters
322
378
  ----------
@@ -324,8 +380,8 @@ class SelectionParameter(Parameter[Any]):
324
380
  The name of the parameter
325
381
  value : Any
326
382
  The initially selected value (must be one of the options)
327
- options : list
328
- List of valid choices that can be selected
383
+ options : sequence
384
+ List, tuple, or 1D numpy array of valid choices that can be selected
329
385
 
330
386
  Examples
331
387
  --------
@@ -336,15 +392,59 @@ class SelectionParameter(Parameter[Any]):
336
392
  >>> color.value
337
393
  'blue'
338
394
  >>> color.update({"value": "yellow"}) # This will raise an error
395
+ >>> # With numpy array
396
+ >>> import numpy as np
397
+ >>> numbers = SelectionParameter("number", 1, options=np.array([1, 2, 3]))
398
+ >>> numbers.value
399
+ 1
339
400
  """
340
401
 
341
402
  options: List[Any]
342
403
 
343
- def __init__(self, name: str, value: Any, options: List[Any]):
404
+ def __init__(self, name: str, value: Any, options: Union[List, Tuple]):
344
405
  self.name = name
345
- self.options = options
406
+ self.options = self._validate_options(options)
346
407
  self._value = self._validate(value)
347
408
 
409
+ def _validate_options(self, options: Any) -> List[Any]:
410
+ """
411
+ Validate options and convert to list if necessary.
412
+
413
+ Parameters
414
+ ----------
415
+ options : list or tuple
416
+ The options to validate
417
+
418
+ Returns
419
+ -------
420
+ list
421
+ Validated list of options
422
+
423
+ Raises
424
+ ------
425
+ TypeError
426
+ If options is not a list or tuple
427
+ ValueError
428
+ If any option is not hashable
429
+ """
430
+ if not isinstance(options, (list, tuple)):
431
+ raise TypeError(
432
+ f"Options for parameter {self.name} must be a list or tuple"
433
+ )
434
+
435
+ if not options:
436
+ raise ValueError(f"Options for parameter {self.name} must not be empty")
437
+
438
+ # Verify all options are hashable (needed for comparison)
439
+ try:
440
+ for opt in options:
441
+ hash(opt)
442
+ except TypeError as e:
443
+ raise ValueError(
444
+ f"All options for parameter {self.name} must be hashable: {str(e)}"
445
+ )
446
+ return list(options)
447
+
348
448
  def _validate(self, new_value: Any) -> Any:
349
449
  """
350
450
  Validate that value is one of the allowed options.
@@ -372,13 +472,14 @@ class SelectionParameter(Parameter[Any]):
372
472
  Raises:
373
473
  TypeError: If options is not a list or tuple
374
474
  """
375
- if not isinstance(self.options, (list, tuple)):
376
- raise TypeError(
377
- f"Options for parameter {self.name} are not a list or tuple: {self.options}"
378
- )
475
+ self.options = self._validate_options(self.options)
379
476
  if self.value not in self.options:
380
477
  warn(
381
- f"Value {self.value} not in options: {self.options}, setting to first option"
478
+ ParameterUpdateWarning(
479
+ self.name,
480
+ type(self).__name__,
481
+ f"Value {self.value} not in options, setting to first option ({self.options[0]})",
482
+ )
382
483
  )
383
484
  self.value = self.options[0]
384
485
 
@@ -389,8 +490,8 @@ class MultipleSelectionParameter(Parameter[List[Any]]):
389
490
  Parameter for multiple selections from a list of options.
390
491
 
391
492
  Creates a set of checkboxes or multi-select dropdown in the GUI.
392
- See :meth:`~syd.interactive_viewer.InteractiveViewer.add_multiple_selection` and
393
- :meth:`~syd.interactive_viewer.InteractiveViewer.update_multiple_selection` for usage.
493
+ See :meth:`~syd.viewer.Viewer.add_multiple_selection` and
494
+ :meth:`~syd.viewer.Viewer.update_multiple_selection` for usage.
394
495
 
395
496
  Parameters
396
497
  ----------
@@ -398,8 +499,8 @@ class MultipleSelectionParameter(Parameter[List[Any]]):
398
499
  The name of the parameter
399
500
  value : list
400
501
  List of initially selected values (must all be from options)
401
- options : list
402
- List of valid choices that can be selected
502
+ options : sequence
503
+ List, tuple, or 1D numpy array of valid choices that can be selected
403
504
 
404
505
  Examples
405
506
  --------
@@ -408,18 +509,61 @@ class MultipleSelectionParameter(Parameter[List[Any]]):
408
509
  ... options=["cheese", "mushrooms", "pepperoni", "olives"])
409
510
  >>> toppings.value
410
511
  ['cheese', 'mushrooms']
411
- >>> toppings.update({"value": ["cheese", "pepperoni"]})
412
- >>> toppings.value
413
- ['cheese', 'pepperoni']
512
+ >>> # With numpy array
513
+ >>> import numpy as np
514
+ >>> numbers = MultipleSelectionParameter("numbers",
515
+ ... value=[1, 3],
516
+ ... options=np.array([1, 2, 3, 4]))
517
+ >>> numbers.value
518
+ [1, 3]
414
519
  """
415
520
 
416
521
  options: List[Any]
417
522
 
418
- def __init__(self, name: str, value: List[Any], options: List[Any]):
523
+ def __init__(self, name: str, value: List[Any], options: Union[List, Tuple]):
419
524
  self.name = name
420
- self.options = options
525
+ self.options = self._validate_options(options)
421
526
  self._value = self._validate(value)
422
527
 
528
+ def _validate_options(self, options: Any) -> List[Any]:
529
+ """
530
+ Validate options and convert to list if necessary.
531
+
532
+ Parameters
533
+ ----------
534
+ options : list or tuple
535
+ The options to validate
536
+
537
+ Returns
538
+ -------
539
+ list
540
+ Validated list of options
541
+
542
+ Raises
543
+ ------
544
+ TypeError
545
+ If options is not a list or tuple
546
+ ValueError
547
+ If any option is not hashable
548
+ """
549
+ if not isinstance(options, (list, tuple)):
550
+ raise TypeError(
551
+ f"Options for parameter {self.name} must be a list or tuple, received {type(options)}"
552
+ )
553
+
554
+ if not options:
555
+ raise ValueError(f"Options for parameter {self.name} must not be empty")
556
+
557
+ # Verify all options are hashable (needed for comparison)
558
+ try:
559
+ for opt in options:
560
+ hash(opt)
561
+ except TypeError as e:
562
+ raise ValueError(
563
+ f"All options for parameter {self.name} must be hashable: {str(e)}"
564
+ )
565
+ return list(options)
566
+
423
567
  def _validate(self, new_value: Any) -> List[Any]:
424
568
  """
425
569
  Validate list of selected values against options.
@@ -446,19 +590,24 @@ class MultipleSelectionParameter(Parameter[List[Any]]):
446
590
  return [x for x in self.options if x in new_value]
447
591
 
448
592
  def _validate_update(self) -> None:
449
- if not isinstance(self.options, (list, tuple)):
450
- raise TypeError(
451
- f"Options for parameter {self.name} are not a list or tuple: {self.options}"
452
- )
593
+ self.options = self._validate_options(self.options)
453
594
  if not isinstance(self.value, (list, tuple)):
454
595
  warn(
455
- f"For parameter {self.name}, value {self.value} is not a list or tuple. Setting to empty list."
596
+ ParameterUpdateWarning(
597
+ self.name,
598
+ type(self).__name__,
599
+ f"For parameter {self.name}, value {self.value} is not a list or tuple. Setting to empty list.",
600
+ )
456
601
  )
457
602
  self.value = []
458
603
  if not all(val in self.options for val in self.value):
459
604
  invalid = [val for val in self.value if val not in self.options]
460
605
  warn(
461
- f"For parameter {self.name}, value {self.value} contains invalid options: {invalid}. Setting to empty list."
606
+ ParameterUpdateWarning(
607
+ self.name,
608
+ type(self).__name__,
609
+ f"For parameter {self.name}, value {self.value} contains invalid selections: {invalid}. Setting to empty list.",
610
+ )
462
611
  )
463
612
  self.value = []
464
613
  # Keep only unique values while preserving order based on self.options
@@ -472,8 +621,8 @@ class IntegerParameter(Parameter[int]):
472
621
  Parameter for bounded integer values.
473
622
 
474
623
  Creates a slider in the GUI for selecting whole numbers between bounds.
475
- See :meth:`~syd.interactive_viewer.InteractiveViewer.add_integer` and
476
- :meth:`~syd.interactive_viewer.InteractiveViewer.update_integer` for usage.
624
+ See :meth:`~syd.viewer.Viewer.add_integer` and
625
+ :meth:`~syd.viewer.Viewer.update_integer` for usage.
477
626
 
478
627
  Parameters
479
628
  ----------
@@ -535,10 +684,22 @@ class IntegerParameter(Parameter[int]):
535
684
 
536
685
  if compare_to_range:
537
686
  if new_value < self.min_value:
538
- warn(f"Value {new_value} below minimum {self.min_value}, clamping")
687
+ warn(
688
+ ParameterUpdateWarning(
689
+ self.name,
690
+ type(self).__name__,
691
+ f"Value {new_value} below minimum {self.min_value}, clamping",
692
+ )
693
+ )
539
694
  new_value = self.min_value
540
695
  if new_value > self.max_value:
541
- warn(f"Value {new_value} above maximum {self.max_value}, clamping")
696
+ warn(
697
+ ParameterUpdateWarning(
698
+ self.name,
699
+ type(self).__name__,
700
+ f"Value {new_value} above maximum {self.max_value}, clamping",
701
+ )
702
+ )
542
703
  new_value = self.max_value
543
704
  return int(new_value)
544
705
 
@@ -559,7 +720,13 @@ class IntegerParameter(Parameter[int]):
559
720
  "IntegerParameter must have both min_value and max_value bounds",
560
721
  )
561
722
  if self.min_value > self.max_value:
562
- warn(f"Min value greater than max value, swapping")
723
+ warn(
724
+ ParameterUpdateWarning(
725
+ self.name,
726
+ type(self).__name__,
727
+ f"Min value greater than max value, swapping",
728
+ )
729
+ )
563
730
  self.min_value, self.max_value = self.max_value, self.min_value
564
731
  self.value = self._validate(self.value)
565
732
 
@@ -570,8 +737,8 @@ class FloatParameter(Parameter[float]):
570
737
  Parameter for bounded decimal numbers.
571
738
 
572
739
  Creates a slider in the GUI for selecting numbers between bounds.
573
- See :meth:`~syd.interactive_viewer.InteractiveViewer.add_float` and
574
- :meth:`~syd.interactive_viewer.InteractiveViewer.update_float` for usage.
740
+ See :meth:`~syd.viewer.Viewer.add_float` and
741
+ :meth:`~syd.viewer.Viewer.update_float` for usage.
575
742
 
576
743
  Parameters
577
744
  ----------
@@ -584,7 +751,7 @@ class FloatParameter(Parameter[float]):
584
751
  max_value : float
585
752
  Maximum allowed value
586
753
  step : float, optional
587
- Size of each increment (default is 0.1)
754
+ Size of each increment (default is 0.001)
588
755
 
589
756
  Examples
590
757
  --------
@@ -617,7 +784,7 @@ class FloatParameter(Parameter[float]):
617
784
  value: float,
618
785
  min_value: float,
619
786
  max_value: float,
620
- step: float = 0.1,
787
+ step: float = 0.001,
621
788
  ):
622
789
  self.name = name
623
790
  self.step = step
@@ -651,10 +818,22 @@ class FloatParameter(Parameter[float]):
651
818
 
652
819
  if compare_to_range:
653
820
  if new_value < self.min_value:
654
- warn(f"Value {new_value} below minimum {self.min_value}, clamping")
821
+ warn(
822
+ ParameterUpdateWarning(
823
+ self.name,
824
+ type(self).__name__,
825
+ f"Value {new_value} below minimum {self.min_value}, clamping",
826
+ )
827
+ )
655
828
  new_value = self.min_value
656
829
  if new_value > self.max_value:
657
- warn(f"Value {new_value} above maximum {self.max_value}, clamping")
830
+ warn(
831
+ ParameterUpdateWarning(
832
+ self.name,
833
+ type(self).__name__,
834
+ f"Value {new_value} above maximum {self.max_value}, clamping",
835
+ )
836
+ )
658
837
  new_value = self.max_value
659
838
 
660
839
  return float(new_value)
@@ -676,7 +855,13 @@ class FloatParameter(Parameter[float]):
676
855
  "FloatParameter must have both min_value and max_value bounds",
677
856
  )
678
857
  if self.min_value > self.max_value:
679
- warn(f"Min value greater than max value, swapping")
858
+ warn(
859
+ ParameterUpdateWarning(
860
+ self.name,
861
+ type(self).__name__,
862
+ f"Min value greater than max value, swapping",
863
+ )
864
+ )
680
865
  self.min_value, self.max_value = self.max_value, self.min_value
681
866
  self.value = self._validate(self.value)
682
867
 
@@ -687,8 +872,8 @@ class IntegerRangeParameter(Parameter[Tuple[int, int]]):
687
872
  Parameter for a range of bounded integer values.
688
873
 
689
874
  Creates a range slider in the GUI for selecting a range of whole numbers.
690
- See :meth:`~syd.interactive_viewer.InteractiveViewer.add_integer_range` and
691
- :meth:`~syd.interactive_viewer.InteractiveViewer.update_integer_range` for usage.
875
+ See :meth:`~syd.viewer.Viewer.add_integer_range` and
876
+ :meth:`~syd.viewer.Viewer.update_integer_range` for usage.
692
877
 
693
878
  Parameters
694
879
  ----------
@@ -726,11 +911,11 @@ class IntegerRangeParameter(Parameter[Tuple[int, int]]):
726
911
  max_value: int,
727
912
  ):
728
913
  self.name = name
729
- self.min_value = self._validate_single(min_value)
730
- self.max_value = self._validate_single(max_value)
914
+ self.min_value = self._validate_single(min_value, context="min_value")
915
+ self.max_value = self._validate_single(max_value, context="max_value")
731
916
  self._value = self._validate(value)
732
917
 
733
- def _validate_single(self, new_value: Any) -> int:
918
+ def _validate_single(self, new_value: Any, context: Optional[str] = None) -> int:
734
919
  """
735
920
  Validate and convert a single numeric value.
736
921
 
@@ -748,8 +933,11 @@ class IntegerRangeParameter(Parameter[Tuple[int, int]]):
748
933
  """
749
934
  try:
750
935
  return int(new_value)
751
- except ValueError:
752
- raise ValueError(f"Value {new_value} cannot be converted to int")
936
+ except Exception:
937
+ msg = f"Value {new_value} cannot be converted to int"
938
+ if context:
939
+ msg += f" for {context}"
940
+ raise ValueError(msg)
753
941
 
754
942
  def _validate(self, new_value: Any) -> Tuple[int, int]:
755
943
  """
@@ -772,14 +960,32 @@ class IntegerRangeParameter(Parameter[Tuple[int, int]]):
772
960
  high = self._validate_single(new_value[1])
773
961
 
774
962
  if low > high:
775
- warn(f"Low value {low} greater than high value {high}, swapping")
963
+ warn(
964
+ ParameterUpdateWarning(
965
+ self.name,
966
+ type(self).__name__,
967
+ f"Low value {low} greater than high value {high}, swapping",
968
+ )
969
+ )
776
970
  low, high = high, low
777
971
 
778
972
  if low < self.min_value:
779
- warn(f"Low value {low} below minimum {self.min_value}, clamping")
973
+ warn(
974
+ ParameterUpdateWarning(
975
+ self.name,
976
+ type(self).__name__,
977
+ f"Low value {low} below minimum {self.min_value}, clamping",
978
+ )
979
+ )
780
980
  low = self.min_value
781
981
  if high > self.max_value:
782
- warn(f"High value {high} above maximum {self.max_value}, clamping")
982
+ warn(
983
+ ParameterUpdateWarning(
984
+ self.name,
985
+ type(self).__name__,
986
+ f"High value {high} above maximum {self.max_value}, clamping",
987
+ )
988
+ )
783
989
  high = self.max_value
784
990
 
785
991
  return (low, high)
@@ -801,7 +1007,13 @@ class IntegerRangeParameter(Parameter[Tuple[int, int]]):
801
1007
  "IntegerRangeParameter must have both min_value and max_value bounds",
802
1008
  )
803
1009
  if self.min_value > self.max_value:
804
- warn(f"Min value greater than max value, swapping")
1010
+ warn(
1011
+ ParameterUpdateWarning(
1012
+ self.name,
1013
+ type(self).__name__,
1014
+ f"Min value greater than max value, swapping",
1015
+ )
1016
+ )
805
1017
  self.min_value, self.max_value = self.max_value, self.min_value
806
1018
  self.value = self._validate(self.value)
807
1019
 
@@ -812,8 +1024,8 @@ class FloatRangeParameter(Parameter[Tuple[float, float]]):
812
1024
  Parameter for a range of bounded decimal numbers.
813
1025
 
814
1026
  Creates a range slider in the GUI for selecting a range of numbers.
815
- See :meth:`~syd.interactive_viewer.InteractiveViewer.add_float_range` and
816
- :meth:`~syd.interactive_viewer.InteractiveViewer.update_float_range` for usage.
1027
+ See :meth:`~syd.viewer.Viewer.add_float_range` and
1028
+ :meth:`~syd.viewer.Viewer.update_float_range` for usage.
817
1029
 
818
1030
  Parameters
819
1031
  ----------
@@ -826,7 +1038,7 @@ class FloatRangeParameter(Parameter[Tuple[float, float]]):
826
1038
  max_value : float
827
1039
  Maximum allowed value for both low and high
828
1040
  step : float, optional
829
- Size of each increment (default is 0.1)
1041
+ Size of each increment (default is 0.001)
830
1042
 
831
1043
  Examples
832
1044
  --------
@@ -859,15 +1071,15 @@ class FloatRangeParameter(Parameter[Tuple[float, float]]):
859
1071
  value: Tuple[float, float],
860
1072
  min_value: float,
861
1073
  max_value: float,
862
- step: float = 0.1,
1074
+ step: float = 0.001,
863
1075
  ):
864
1076
  self.name = name
865
1077
  self.step = step
866
- self.min_value = self._validate_single(min_value)
867
- self.max_value = self._validate_single(max_value)
1078
+ self.min_value = self._validate_single(min_value, context="min_value")
1079
+ self.max_value = self._validate_single(max_value, context="max_value")
868
1080
  self._value = self._validate(value)
869
1081
 
870
- def _validate_single(self, new_value: Any) -> float:
1082
+ def _validate_single(self, new_value: Any, context: Optional[str] = None) -> float:
871
1083
  """
872
1084
  Validate and convert a single numeric value.
873
1085
 
@@ -885,8 +1097,11 @@ class FloatRangeParameter(Parameter[Tuple[float, float]]):
885
1097
  """
886
1098
  try:
887
1099
  new_value = float(new_value)
888
- except ValueError:
889
- raise ValueError(f"Value {new_value} cannot be converted to float")
1100
+ except Exception:
1101
+ msg = f"Value {new_value} cannot be converted to float"
1102
+ if context:
1103
+ msg += f" for {context}"
1104
+ raise ValueError(msg)
890
1105
 
891
1106
  # Round to the nearest step
892
1107
  new_value = round(new_value / self.step) * self.step
@@ -913,14 +1128,32 @@ class FloatRangeParameter(Parameter[Tuple[float, float]]):
913
1128
  high = self._validate_single(new_value[1])
914
1129
 
915
1130
  if low > high:
916
- warn(f"Low value {low} greater than high value {high}, swapping")
1131
+ warn(
1132
+ ParameterUpdateWarning(
1133
+ self.name,
1134
+ type(self).__name__,
1135
+ f"Low value {low} greater than high value {high}, swapping",
1136
+ )
1137
+ )
917
1138
  low, high = high, low
918
1139
 
919
1140
  if low < self.min_value:
920
- warn(f"Low value {low} below minimum {self.min_value}, clamping")
1141
+ warn(
1142
+ ParameterUpdateWarning(
1143
+ self.name,
1144
+ type(self).__name__,
1145
+ f"Low value {low} below minimum {self.min_value}, clamping",
1146
+ )
1147
+ )
921
1148
  low = self.min_value
922
1149
  if high > self.max_value:
923
- warn(f"High value {high} above maximum {self.max_value}, clamping")
1150
+ warn(
1151
+ ParameterUpdateWarning(
1152
+ self.name,
1153
+ type(self).__name__,
1154
+ f"High value {high} above maximum {self.max_value}, clamping",
1155
+ )
1156
+ )
924
1157
  high = self.max_value
925
1158
 
926
1159
  return (low, high)
@@ -942,7 +1175,13 @@ class FloatRangeParameter(Parameter[Tuple[float, float]]):
942
1175
  "FloatRangeParameter must have both min_value and max_value bounds",
943
1176
  )
944
1177
  if self.min_value > self.max_value:
945
- warn(f"Min value greater than max value, swapping")
1178
+ warn(
1179
+ ParameterUpdateWarning(
1180
+ self.name,
1181
+ type(self).__name__,
1182
+ f"Min value greater than max value, swapping",
1183
+ )
1184
+ )
946
1185
  self.min_value, self.max_value = self.max_value, self.min_value
947
1186
  self.value = self._validate(self.value)
948
1187
 
@@ -953,8 +1192,8 @@ class UnboundedIntegerParameter(Parameter[int]):
953
1192
  Parameter for optionally bounded integer values.
954
1193
 
955
1194
  Creates a text input box in the GUI for entering whole numbers.
956
- See :meth:`~syd.interactive_viewer.InteractiveViewer.add_unbounded_integer` and
957
- :meth:`~syd.interactive_viewer.InteractiveViewer.update_unbounded_integer` for usage.
1195
+ See :meth:`~syd.viewer.Viewer.add_unbounded_integer` and
1196
+ :meth:`~syd.viewer.Viewer.update_unbounded_integer` for usage.
958
1197
 
959
1198
  Parameters
960
1199
  ----------
@@ -962,19 +1201,12 @@ class UnboundedIntegerParameter(Parameter[int]):
962
1201
  The name of the parameter
963
1202
  value : int
964
1203
  Initial value
965
- min_value : int, optional
966
- Minimum allowed value (or None for no minimum)
967
- max_value : int, optional
968
- Maximum allowed value (or None for no maximum)
969
1204
 
970
1205
  Examples
971
1206
  --------
972
- >>> count = UnboundedIntegerParameter("count", value=10, min_value=0)
1207
+ >>> count = UnboundedIntegerParameter("count", value=10)
973
1208
  >>> count.value
974
1209
  10
975
- >>> count.update({"value": -5}) # Will be clamped to min_value
976
- >>> count.value
977
- 0
978
1210
  >>> count.update({"value": 1000000}) # No maximum, so this is allowed
979
1211
  >>> count.value
980
1212
  1000000
@@ -982,43 +1214,24 @@ class UnboundedIntegerParameter(Parameter[int]):
982
1214
  Notes
983
1215
  -----
984
1216
  Use this instead of IntegerParameter when you:
985
- - Don't know a reasonable maximum value
986
- - Only want to enforce a minimum or maximum, but not both
1217
+ - Don't have any reason to bound the value
987
1218
  - Need to allow very large numbers that would be impractical with a slider
988
1219
  """
989
1220
 
990
- min_value: Optional[int]
991
- max_value: Optional[int]
992
-
993
1221
  def __init__(
994
1222
  self,
995
1223
  name: str,
996
1224
  value: int,
997
- min_value: Optional[int] = None,
998
- max_value: Optional[int] = None,
999
1225
  ):
1000
1226
  self.name = name
1001
- self.min_value = (
1002
- self._validate(min_value, compare_to_range=False)
1003
- if min_value is not None
1004
- else None
1005
- )
1006
- self.max_value = (
1007
- self._validate(max_value, compare_to_range=False)
1008
- if max_value is not None
1009
- else None
1010
- )
1011
1227
  self._value = self._validate(value)
1012
1228
 
1013
- def _validate(self, new_value: Any, compare_to_range: bool = True) -> int:
1229
+ def _validate(self, new_value: Any) -> int:
1014
1230
  """
1015
- Validate and convert value to integer, optionally checking bounds.
1016
-
1017
- Handles None min/max values by skipping those bound checks.
1231
+ Validate and convert value to integer.
1018
1232
 
1019
1233
  Args:
1020
1234
  new_value: Value to validate
1021
- compare_to_range: If True, clamps value to any defined min/max bounds
1022
1235
 
1023
1236
  Returns:
1024
1237
  Validated integer value
@@ -1031,32 +1244,15 @@ class UnboundedIntegerParameter(Parameter[int]):
1031
1244
  except ValueError:
1032
1245
  raise ValueError(f"Value {new_value} cannot be converted to int")
1033
1246
 
1034
- if compare_to_range:
1035
- if self.min_value is not None and new_value < self.min_value:
1036
- warn(f"Value {new_value} below minimum {self.min_value}, clamping")
1037
- new_value = self.min_value
1038
- if self.max_value is not None and new_value > self.max_value:
1039
- warn(f"Value {new_value} above maximum {self.max_value}, clamping")
1040
- new_value = self.max_value
1041
1247
  return int(new_value)
1042
1248
 
1043
1249
  def _validate_update(self) -> None:
1044
1250
  """
1045
1251
  Validate complete parameter state after updates.
1046
1252
 
1047
- Ensures min_value <= max_value, swapping if needed.
1048
- Re-validates current value against potentially updated bounds.
1049
-
1050
1253
  Raises:
1051
1254
  ParameterUpdateError: If bounds are invalid (e.g. None when required)
1052
1255
  """
1053
- if (
1054
- self.min_value is not None
1055
- and self.max_value is not None
1056
- and self.min_value > self.max_value
1057
- ):
1058
- warn(f"Min value greater than max value, swapping")
1059
- self.min_value, self.max_value = self.max_value, self.min_value
1060
1256
  self.value = self._validate(self.value)
1061
1257
 
1062
1258
 
@@ -1066,8 +1262,8 @@ class UnboundedFloatParameter(Parameter[float]):
1066
1262
  Parameter for optionally bounded decimal numbers.
1067
1263
 
1068
1264
  Creates a text input box in the GUI for entering numbers.
1069
- See :meth:`~syd.interactive_viewer.InteractiveViewer.add_unbounded_float` and
1070
- :meth:`~syd.interactive_viewer.InteractiveViewer.update_unbounded_float` for usage.
1265
+ See :meth:`~syd.viewer.Viewer.add_unbounded_float` and
1266
+ :meth:`~syd.viewer.Viewer.update_unbounded_float` for usage.
1071
1267
 
1072
1268
  Parameters
1073
1269
  ----------
@@ -1075,21 +1271,14 @@ class UnboundedFloatParameter(Parameter[float]):
1075
1271
  The name of the parameter
1076
1272
  value : float
1077
1273
  Initial value
1078
- min_value : float, optional
1079
- Minimum allowed value (or None for no minimum)
1080
- max_value : float, optional
1081
- Maximum allowed value (or None for no maximum)
1082
1274
  step : float, optional
1083
1275
  Size of each increment (default is None, meaning no rounding)
1084
1276
 
1085
1277
  Examples
1086
1278
  --------
1087
- >>> price = UnboundedFloatParameter("price", value=19.99, min_value=0.0, step=0.01)
1279
+ >>> price = UnboundedFloatParameter("price", value=19.99)
1088
1280
  >>> price.value
1089
1281
  19.99
1090
- >>> price.update({"value": -5.0}) # Will be clamped to min_value
1091
- >>> price.value
1092
- 0.0
1093
1282
  >>> price.update({"value": 19.987}) # Will be rounded to step
1094
1283
  >>> price.value
1095
1284
  19.99
@@ -1098,7 +1287,6 @@ class UnboundedFloatParameter(Parameter[float]):
1098
1287
  -----
1099
1288
  Use this instead of FloatParameter when you:
1100
1289
  - Don't know a reasonable maximum value
1101
- - Only want to enforce a minimum or maximum, but not both
1102
1290
  - Need to allow very large or precise numbers that would be impractical with a slider
1103
1291
 
1104
1292
  If step is provided, values will be rounded:
@@ -1107,42 +1295,25 @@ class UnboundedFloatParameter(Parameter[float]):
1107
1295
  - step=5.0 rounds to 0.0, 5.0, 10.0, etc.
1108
1296
  """
1109
1297
 
1110
- min_value: Optional[float]
1111
- max_value: Optional[float]
1112
- step: float
1298
+ step: Optional[float]
1113
1299
 
1114
1300
  def __init__(
1115
1301
  self,
1116
1302
  name: str,
1117
1303
  value: float,
1118
- min_value: Optional[float] = None,
1119
- max_value: Optional[float] = None,
1120
1304
  step: Optional[float] = None,
1121
1305
  ):
1122
1306
  self.name = name
1123
1307
  self.step = step
1124
- self.min_value = (
1125
- self._validate(min_value, compare_to_range=False)
1126
- if min_value is not None
1127
- else None
1128
- )
1129
- self.max_value = (
1130
- self._validate(max_value, compare_to_range=False)
1131
- if max_value is not None
1132
- else None
1133
- )
1134
1308
  self._value = self._validate(value)
1135
1309
 
1136
- def _validate(self, new_value: Any, compare_to_range: bool = True) -> float:
1310
+ def _validate(self, new_value: Any) -> float:
1137
1311
  """
1138
- Validate and convert value to float, optionally checking bounds.
1139
-
1140
- Handles None min/max values by skipping those bound checks.
1312
+ Validate and convert value to float.
1141
1313
  Only rounds to step if step is not None.
1142
1314
 
1143
1315
  Args:
1144
1316
  new_value: Value to validate
1145
- compare_to_range: If True, clamps value to any defined min/max bounds
1146
1317
 
1147
1318
  Returns:
1148
1319
  Validated and potentially rounded float value
@@ -1159,14 +1330,6 @@ class UnboundedFloatParameter(Parameter[float]):
1159
1330
  if self.step is not None:
1160
1331
  new_value = round(new_value / self.step) * self.step
1161
1332
 
1162
- if compare_to_range:
1163
- if self.min_value is not None and new_value < self.min_value:
1164
- warn(f"Value {new_value} below minimum {self.min_value}, clamping")
1165
- new_value = self.min_value
1166
- if self.max_value is not None and new_value > self.max_value:
1167
- warn(f"Value {new_value} above maximum {self.max_value}, clamping")
1168
- new_value = self.max_value
1169
-
1170
1333
  return float(new_value)
1171
1334
 
1172
1335
  def _validate_update(self) -> None:
@@ -1179,24 +1342,58 @@ class UnboundedFloatParameter(Parameter[float]):
1179
1342
  Raises:
1180
1343
  ParameterUpdateError: If bounds are invalid (e.g. None when required)
1181
1344
  """
1182
- if (
1183
- self.min_value is not None
1184
- and self.max_value is not None
1185
- and self.min_value > self.max_value
1186
- ):
1187
- warn(f"Min value greater than max value, swapping")
1188
- self.min_value, self.max_value = self.max_value, self.min_value
1189
1345
  self.value = self._validate(self.value)
1190
1346
 
1191
1347
 
1192
- class ButtonParameter(Parameter):
1193
- """A parameter that represents a clickable button."""
1348
+ @dataclass(init=False)
1349
+ class ButtonAction(Parameter[None]):
1350
+ """
1351
+ Parameter for creating clickable buttons with callbacks.
1352
+
1353
+ Creates a button in the GUI that executes a callback function when clicked.
1354
+ See :meth:`~syd.viewer.Viewer.add_button` and
1355
+ :meth:`~syd.viewer.Viewer.update_button` for usage.
1356
+
1357
+ Parameters
1358
+ ----------
1359
+ name : str
1360
+ The name of the parameter
1361
+ label : str
1362
+ Text to display on the button
1363
+ callback : callable
1364
+ Function to execute when the button is clicked
1365
+
1366
+ Examples
1367
+ --------
1368
+ >>> def print_hello():
1369
+ ... print("Hello!")
1370
+ >>> button = ButtonAction("greeting", label="Say Hello", callback=print_hello)
1371
+ >>> button.callback() # Simulates clicking the button
1372
+ Hello!
1373
+ >>> # Update the button's label and callback
1374
+ >>> def print_goodbye():
1375
+ ... print("Goodbye!")
1376
+ >>> button.update({"label": "Say Goodbye", "callback": print_goodbye})
1377
+ >>> button.callback()
1378
+ Goodbye!
1379
+
1380
+ Notes
1381
+ -----
1382
+ Unlike other Parameter types, ButtonAction:
1383
+ - Has no value (always None)
1384
+ - Is marked as an action (_is_action = True)
1385
+ - Executes code directly rather than storing state
1386
+ - Cannot be updated through the value property
1387
+ """
1194
1388
 
1195
- _is_button: bool = True
1389
+ label: str
1390
+ callback: Callable
1391
+ value: None = field(default=None, repr=False)
1392
+ _is_action: bool = field(default=True, repr=False)
1196
1393
 
1197
- def __init__(self, name: str, label: str, callback: Callable[[], None]):
1394
+ def __init__(self, name: str, label: str, callback: Callable):
1198
1395
  """
1199
- Initialize a button parameter.
1396
+ Initialize a button.
1200
1397
 
1201
1398
  Args:
1202
1399
  name: Internal name of the parameter
@@ -1206,32 +1403,28 @@ class ButtonParameter(Parameter):
1206
1403
  self.name = name
1207
1404
  self.label = label
1208
1405
  self.callback = callback
1209
- self._value = None # Buttons don't have a value in the traditional sense
1210
-
1211
- def update(self, updates: Dict[str, Any]) -> None:
1212
- """Update the button's label and/or callback."""
1213
- if "label" in updates:
1214
- self.label = updates["label"]
1215
- if "callback" in updates:
1216
- self.callback = updates["callback"]
1406
+ self._value = None
1217
1407
 
1218
- @property
1219
- def value(self) -> None:
1220
- """Buttons don't have a value, always returns None."""
1221
- return self._value
1222
-
1223
- @value.setter
1224
- def value(self, _: Any) -> None:
1225
- """Buttons don't store values."""
1226
- pass
1408
+ def _validate(self, new_value: Any) -> None:
1409
+ """Validate the button's value."""
1410
+ return None
1227
1411
 
1228
1412
  def _validate_update(self) -> None:
1229
- """Buttons don't need validation."""
1230
- pass
1231
-
1232
- def _validate(self, new_value: Any) -> None:
1233
- """Buttons don't need validation."""
1234
- pass
1413
+ """Validate the button's value after updates."""
1414
+ if not callable(self.callback):
1415
+ raise ParameterUpdateError(
1416
+ self.name,
1417
+ type(self).__name__,
1418
+ f"Callback {self.callback} is not callable",
1419
+ )
1420
+ try:
1421
+ str(self.label)
1422
+ except Exception:
1423
+ raise ParameterUpdateError(
1424
+ self.name,
1425
+ type(self).__name__,
1426
+ f"Label {self.label} doesn't have a string representation",
1427
+ )
1235
1428
 
1236
1429
 
1237
1430
  class ParameterType(Enum):
@@ -1247,4 +1440,7 @@ class ParameterType(Enum):
1247
1440
  float_range = FloatRangeParameter
1248
1441
  unbounded_integer = UnboundedIntegerParameter
1249
1442
  unbounded_float = UnboundedFloatParameter
1250
- button = ButtonParameter
1443
+
1444
+
1445
+ class ActionType(Enum):
1446
+ button = ButtonAction