steer-core 0.1.19__tar.gz → 0.1.20__tar.gz

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.
Files changed (45) hide show
  1. {steer_core-0.1.19 → steer_core-0.1.20}/PKG-INFO +1 -1
  2. {steer_core-0.1.19 → steer_core-0.1.20}/steer_core/Data/database.db +0 -0
  3. {steer_core-0.1.19 → steer_core-0.1.20}/steer_core/Decorators/Electrochemical.py +3 -7
  4. {steer_core-0.1.19 → steer_core-0.1.20}/steer_core/Decorators/General.py +1 -0
  5. steer_core-0.1.20/steer_core/Mixins/Dunder.py +164 -0
  6. {steer_core-0.1.19 → steer_core-0.1.20}/steer_core/Mixins/TypeChecker.py +26 -3
  7. steer_core-0.1.20/steer_core/__init__.py +1 -0
  8. {steer_core-0.1.19 → steer_core-0.1.20}/steer_core.egg-info/PKG-INFO +1 -1
  9. {steer_core-0.1.19 → steer_core-0.1.20}/steer_core.egg-info/SOURCES.txt +3 -3
  10. steer_core-0.1.20/test/test_validation_mixin.py +97 -0
  11. steer_core-0.1.19/steer_core/__init__.py +0 -1
  12. steer_core-0.1.19/test/test_compound_components.py +0 -339
  13. steer_core-0.1.19/test/test_compound_components_clean.py +0 -0
  14. steer_core-0.1.19/test/test_slider_controls.py +0 -286
  15. {steer_core-0.1.19 → steer_core-0.1.20}/README.md +0 -0
  16. {steer_core-0.1.19 → steer_core-0.1.20}/setup.cfg +0 -0
  17. {steer_core-0.1.19 → steer_core-0.1.20}/setup.py +0 -0
  18. {steer_core-0.1.19 → steer_core-0.1.20}/steer_core/Apps/Components/MaterialSelectors.py +0 -0
  19. {steer_core-0.1.19 → steer_core-0.1.20}/steer_core/Apps/Components/RangeSliderComponents.py +0 -0
  20. {steer_core-0.1.19 → steer_core-0.1.20}/steer_core/Apps/Components/SliderComponents.py +0 -0
  21. {steer_core-0.1.19 → steer_core-0.1.20}/steer_core/Apps/Components/__init__.py +0 -0
  22. {steer_core-0.1.19 → steer_core-0.1.20}/steer_core/Apps/ContextManagers.py +0 -0
  23. {steer_core-0.1.19 → steer_core-0.1.20}/steer_core/Apps/Performance/CallbackTimer.py +0 -0
  24. {steer_core-0.1.19 → steer_core-0.1.20}/steer_core/Apps/Performance/__init__.py +0 -0
  25. {steer_core-0.1.19 → steer_core-0.1.20}/steer_core/Apps/Utils/SliderControls.py +0 -0
  26. {steer_core-0.1.19 → steer_core-0.1.20}/steer_core/Apps/Utils/__init__.py +0 -0
  27. {steer_core-0.1.19 → steer_core-0.1.20}/steer_core/Apps/__init__.py +0 -0
  28. {steer_core-0.1.19 → steer_core-0.1.20}/steer_core/Constants/Units.py +0 -0
  29. {steer_core-0.1.19 → steer_core-0.1.20}/steer_core/Constants/Universal.py +0 -0
  30. {steer_core-0.1.19 → steer_core-0.1.20}/steer_core/Constants/__init__.py +0 -0
  31. {steer_core-0.1.19 → steer_core-0.1.20}/steer_core/ContextManagers/__init__.py +0 -0
  32. {steer_core-0.1.19 → steer_core-0.1.20}/steer_core/Data/__init__.py +0 -0
  33. {steer_core-0.1.19 → steer_core-0.1.20}/steer_core/DataManager.py +0 -0
  34. {steer_core-0.1.19 → steer_core-0.1.20}/steer_core/Decorators/Coordinates.py +0 -0
  35. {steer_core-0.1.19 → steer_core-0.1.20}/steer_core/Decorators/Objects.py +0 -0
  36. {steer_core-0.1.19 → steer_core-0.1.20}/steer_core/Decorators/__init__.py +0 -0
  37. {steer_core-0.1.19 → steer_core-0.1.20}/steer_core/Mixins/Colors.py +0 -0
  38. {steer_core-0.1.19 → steer_core-0.1.20}/steer_core/Mixins/Coordinates.py +0 -0
  39. {steer_core-0.1.19 → steer_core-0.1.20}/steer_core/Mixins/Data.py +0 -0
  40. {steer_core-0.1.19 → steer_core-0.1.20}/steer_core/Mixins/Plotter.py +0 -0
  41. {steer_core-0.1.19 → steer_core-0.1.20}/steer_core/Mixins/__init__.py +0 -0
  42. /steer_core-0.1.19/steer_core/Mixins/Serializer.py → /steer_core-0.1.20/steer_core/Mixins/serializer.py +0 -0
  43. {steer_core-0.1.19 → steer_core-0.1.20}/steer_core.egg-info/dependency_links.txt +0 -0
  44. {steer_core-0.1.19 → steer_core-0.1.20}/steer_core.egg-info/requires.txt +0 -0
  45. {steer_core-0.1.19 → steer_core-0.1.20}/steer_core.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: steer-core
3
- Version: 0.1.19
3
+ Version: 0.1.20
4
4
  Summary: Modelling energy storage from cell to site - STEER OpenCell Design
5
5
  Home-page: https://github.com/nicholas9182/steer-core/
6
6
  Author: Nicholas Siemons
@@ -1,19 +1,16 @@
1
1
  from functools import wraps
2
2
 
3
-
4
3
  def calculate_half_cell_curve(func):
5
4
  """
6
5
  Decorator to recalculate half-cell curve properties after a method call.
7
6
  This is useful for methods that modify the half-cell curve data.
8
7
  """
9
-
10
8
  @wraps(func)
11
9
  def wrapper(self, *args, **kwargs):
12
10
  result = func(self, *args, **kwargs)
13
- if hasattr(self, "_update_properties") and self._update_properties:
11
+ if hasattr(self, '_update_properties') and self._update_properties:
14
12
  self._calculate_half_cell_curve()
15
13
  return result
16
-
17
14
  return wrapper
18
15
 
19
16
 
@@ -22,12 +19,11 @@ def calculate_half_cell_curves_properties(func):
22
19
  Decorator to recalculate half-cell curves properties after a method call.
23
20
  This is useful for methods that modify the half-cell curves data.
24
21
  """
25
-
26
22
  @wraps(func)
27
23
  def wrapper(self, *args, **kwargs):
28
24
  result = func(self, *args, **kwargs)
29
- if hasattr(self, "_update_properties") and self._update_properties:
25
+ if hasattr(self, '_update_properties') and self._update_properties:
30
26
  self._calculate_half_cell_curves_properties()
31
27
  return result
32
-
33
28
  return wrapper
29
+
@@ -31,3 +31,4 @@ def calculate_all_properties(func):
31
31
  return result
32
32
 
33
33
  return wrapper
34
+
@@ -0,0 +1,164 @@
1
+
2
+
3
+ import numpy as np
4
+
5
+
6
+ class DunderMixin:
7
+
8
+ def _get_comparable_properties(self):
9
+ """Get all comparable properties from the class hierarchy."""
10
+ properties = []
11
+ for cls in self.__class__.__mro__:
12
+ for name, value in cls.__dict__.items():
13
+ if isinstance(value, property):
14
+ if not self._should_exclude_property(name):
15
+ properties.append(name)
16
+ return properties
17
+
18
+ def _should_exclude_property(self, name):
19
+ """Check if a property should be excluded from comparison."""
20
+ return (
21
+ name.endswith('_trace') or
22
+ name.endswith('_range') or
23
+ name in {'last_updated', 'properties'}
24
+ )
25
+
26
+ def _is_plotly_trace(self, obj):
27
+ """Check if object is a Plotly trace object."""
28
+ return (
29
+ hasattr(obj, '__module__') and
30
+ obj.__module__ and
31
+ obj.__module__.startswith('plotly.graph_objs')
32
+ )
33
+
34
+ def _compare_none_values(self, self_value, other_value):
35
+ """Compare None values. Returns (should_continue, result)."""
36
+ if self_value is None and other_value is None:
37
+ return True, True # Continue, values are equal
38
+ elif self_value is None or other_value is None:
39
+ return False, False # Stop, values are not equal
40
+ return True, None # Continue, not None values
41
+
42
+ def _compare_plotly_traces(self, self_value, other_value):
43
+ """Compare Plotly trace objects. Returns (should_continue, result)."""
44
+ if self._is_plotly_trace(self_value) or self._is_plotly_trace(other_value):
45
+ return True, True # Skip Plotly traces, continue
46
+ return True, None # Continue, not Plotly traces
47
+
48
+ def _compare_numpy_arrays(self, self_value, other_value):
49
+ """Compare NumPy arrays. Returns (should_continue, result)."""
50
+ if isinstance(self_value, np.ndarray) and isinstance(other_value, np.ndarray):
51
+ return False, np.array_equal(self_value, other_value, equal_nan=True)
52
+ elif isinstance(self_value, np.ndarray) or isinstance(other_value, np.ndarray):
53
+ return False, False # One is numpy array, other is not
54
+ return True, None # Continue, not numpy arrays
55
+
56
+ def _compare_dataframes(self, self_value, other_value):
57
+ """Compare pandas DataFrames/Series. Returns (should_continue, result)."""
58
+ if hasattr(self_value, 'equals') and hasattr(other_value, 'equals'):
59
+ return False, self_value.equals(other_value)
60
+ elif hasattr(self_value, 'equals') or hasattr(other_value, 'equals'):
61
+ return False, False # Only one is a DataFrame/Series
62
+ return True, None # Continue, not DataFrames
63
+
64
+ def _compare_dictionaries(self, self_value, other_value):
65
+ """Compare dictionaries by comparing keys and values separately. Returns (should_continue, result)."""
66
+ if isinstance(self_value, dict) and isinstance(other_value, dict):
67
+ # Compare keys first (order-independent)
68
+ if list(self_value.keys()) == list(other_value.keys()) and list(self_value.values()) == list(other_value.values()):
69
+ return False, True # Quick path: both keys and values match in order
70
+
71
+ # Compare values for each key
72
+ for key in self_value.keys():
73
+ if self_value[key] != other_value[key]:
74
+ return False, False
75
+
76
+ return False, True # Dictionaries are equal
77
+ elif isinstance(self_value, dict) or isinstance(other_value, dict):
78
+ return False, False # One is dict, other is not
79
+ return True, None # Continue, not dictionaries
80
+
81
+ def _compare_sequences(self, self_value, other_value):
82
+ """Compare lists and tuples. Returns (should_continue, result)."""
83
+ if isinstance(self_value, (list, tuple)) and isinstance(other_value, (list, tuple)):
84
+ return False, type(self_value) == type(other_value) and self_value == other_value
85
+ elif isinstance(self_value, (list, tuple)) or isinstance(other_value, (list, tuple)):
86
+ return False, False # One is sequence, other is not
87
+ return True, None # Continue, not sequences
88
+
89
+ def _compare_other_types(self, self_value, other_value):
90
+ """Compare all other types. Returns (should_continue, result)."""
91
+ return False, self_value == other_value
92
+
93
+ def __eq__(self, other):
94
+ """
95
+ Compare two instances based on all their @property decorated attributes.
96
+
97
+ Returns True if all properties have equal values, False otherwise.
98
+ Returns False if other is not an instance of the same class.
99
+ """
100
+ # Quick identity check first (performance optimization)
101
+ if self is other:
102
+ return True
103
+
104
+ # Check if other is the same type
105
+ if type(other) != type(self):
106
+ return False
107
+
108
+ # Cache properties to avoid repeated computation (performance optimization)
109
+ if not hasattr(self, '_cached_properties'):
110
+ self._cached_properties = self._get_comparable_properties()
111
+
112
+ # Define comparison methods in order of priority/frequency
113
+ comparison_methods = [
114
+ self._compare_none_values,
115
+ self._compare_plotly_traces,
116
+ self._compare_numpy_arrays,
117
+ self._compare_dataframes,
118
+ self._compare_dictionaries,
119
+ self._compare_sequences,
120
+ self._compare_other_types,
121
+ ]
122
+
123
+ # Compare all property values
124
+ for prop_name in self._cached_properties:
125
+
126
+ try:
127
+ self_value = getattr(self, prop_name)
128
+ other_value = getattr(other, prop_name)
129
+
130
+ # Execute comparison methods until one handles the values
131
+ for method in comparison_methods:
132
+ should_continue, result = method(self_value, other_value)
133
+ if not should_continue:
134
+ if not result:
135
+ return False
136
+ break # Values are equal, continue to next property
137
+
138
+ except (AttributeError, Exception):
139
+ # If property doesn't exist or comparison fails
140
+ return False
141
+
142
+ return True
143
+
144
+ def __hash__(self):
145
+ """
146
+ Simple, robust hash based on object identity.
147
+
148
+ Uses id() for a fast, guaranteed-unique hash that won't fail.
149
+ Objects are only equal if they're the same instance.
150
+ """
151
+ return hash(id(self))
152
+
153
+ def __str__(self):
154
+ """
155
+ String representation of the instance showing all @property decorated attributes and their values.
156
+ """
157
+ return f"{self.__class__.__name__}, {self.__name__}"
158
+
159
+ def __repr__(self):
160
+ """
161
+ Official string representation of the instance.
162
+ """
163
+ return self.__str__()
164
+
@@ -1,12 +1,37 @@
1
1
  from typing import Type
2
2
  import pandas as pd
3
3
  import numpy as np
4
+ import plotly.graph_objects as go
4
5
 
5
6
 
6
7
  ALLOWED_REFERENCE = ["Na/Na+", "Li/Li+"]
7
8
 
8
9
 
9
10
  class ValidationMixin:
11
+
12
+ @staticmethod
13
+ def validate_plotly_trace(value: object, name: str) -> None:
14
+ """
15
+ Validate that a value is a Plotly trace object.
16
+
17
+ Parameters
18
+ ----------
19
+ value : object
20
+ The value to validate.
21
+ name : str
22
+ The name of the parameter for error messages.
23
+
24
+ Raises
25
+ ------
26
+ TypeError
27
+ If the value is not a Plotly trace object.
28
+ """
29
+ return (
30
+ hasattr(value, '__module__') and
31
+ value.__module__ and
32
+ value.__module__.startswith('plotly.graph_objs')
33
+ )
34
+
10
35
  @staticmethod
11
36
  def validate_type(value: Type, expected_type: Type, name: str) -> None:
12
37
  """
@@ -27,9 +52,7 @@ class ValidationMixin:
27
52
  If the value is not of the expected type.
28
53
  """
29
54
  if not isinstance(value, expected_type):
30
- raise TypeError(
31
- f"{name} must be of type {expected_type.__name__}. Provided: {type(value).__name__}."
32
- )
55
+ raise TypeError(f"{name} must be of type {expected_type.__name__}. Provided: {type(value).__name__}.")
33
56
 
34
57
  @staticmethod
35
58
  def validate_percentage(value: float, name: str) -> None:
@@ -0,0 +1 @@
1
+ __version__ = "0.1.20"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: steer-core
3
- Version: 0.1.19
3
+ Version: 0.1.20
4
4
  Summary: Modelling energy storage from cell to site - STEER OpenCell Design
5
5
  Home-page: https://github.com/nicholas9182/steer-core/
6
6
  Author: Nicholas Siemons
@@ -31,10 +31,10 @@ steer_core/Decorators/__init__.py
31
31
  steer_core/Mixins/Colors.py
32
32
  steer_core/Mixins/Coordinates.py
33
33
  steer_core/Mixins/Data.py
34
+ steer_core/Mixins/Dunder.py
34
35
  steer_core/Mixins/Plotter.py
35
36
  steer_core/Mixins/Serializer.py
36
37
  steer_core/Mixins/TypeChecker.py
37
38
  steer_core/Mixins/__init__.py
38
- test/test_compound_components.py
39
- test/test_compound_components_clean.py
40
- test/test_slider_controls.py
39
+ steer_core/Mixins/serializer.py
40
+ test/test_validation_mixin.py
@@ -0,0 +1,97 @@
1
+ import unittest
2
+ import plotly.graph_objects as go
3
+ from steer_core.Mixins.TypeChecker import ValidationMixin
4
+
5
+
6
+ class TestValidationMixin(unittest.TestCase):
7
+ """Test cases for ValidationMixin validation methods."""
8
+
9
+ def test_validate_plotly_trace_with_valid_traces(self):
10
+ """Test validate_plotly_trace with valid Plotly trace objects."""
11
+ # Test various Plotly trace types
12
+ valid_traces = [
13
+ go.Scatter(x=[1, 2, 3], y=[1, 2, 3]),
14
+ go.Bar(x=['A', 'B', 'C'], y=[1, 2, 3]),
15
+ go.Histogram(x=[1, 2, 3, 4, 5]),
16
+ go.Box(y=[1, 2, 3, 4, 5]),
17
+ go.Heatmap(z=[[1, 2], [3, 4]]),
18
+ go.Pie(values=[1, 2, 3], labels=['A', 'B', 'C']),
19
+ go.Scatter3d(x=[1, 2], y=[1, 2], z=[1, 2]),
20
+ ]
21
+
22
+ for trace in valid_traces:
23
+ with self.subTest(trace=type(trace).__name__):
24
+ result = ValidationMixin.validate_plotly_trace(trace, "test_trace")
25
+ self.assertTrue(result, f"Should return True for {type(trace).__name__}")
26
+
27
+ def test_validate_plotly_trace_with_invalid_objects(self):
28
+ """Test validate_plotly_trace with non-Plotly objects."""
29
+ invalid_objects = [
30
+ "string",
31
+ 123,
32
+ [1, 2, 3],
33
+ {"key": "value"},
34
+ None,
35
+ object(),
36
+ ]
37
+
38
+ for obj in invalid_objects:
39
+ with self.subTest(obj=type(obj).__name__):
40
+ # Should return False for non-Plotly objects
41
+ result = ValidationMixin.validate_plotly_trace(obj, "test_object")
42
+ self.assertFalse(result, f"Should return False for {type(obj).__name__}")
43
+
44
+ def test_validate_plotly_trace_with_objects_without_module(self):
45
+ """Test validate_plotly_trace with objects that don't have __module__ attribute."""
46
+ # Create an object without __module__ using a custom class
47
+ class MockObjectNoModule:
48
+ def __getattribute__(self, name):
49
+ if name == '__module__':
50
+ raise AttributeError(f"'{type(self).__name__}' object has no attribute '__module__'")
51
+ return super().__getattribute__(name)
52
+
53
+ mock_obj = MockObjectNoModule()
54
+
55
+ # Verify that hasattr returns False for __module__
56
+ self.assertFalse(hasattr(mock_obj, '__module__'), "Mock object should not have __module__ attribute")
57
+
58
+ result = ValidationMixin.validate_plotly_trace(mock_obj, "mock_object")
59
+ self.assertFalse(result, "Should return False for objects without __module__")
60
+
61
+ def test_validate_plotly_trace_with_wrong_module(self):
62
+ """Test validate_plotly_trace with objects from different modules."""
63
+ import pandas as pd
64
+ import numpy as np
65
+
66
+ # Test with objects from other modules
67
+ other_module_objects = [
68
+ pd.DataFrame({'x': [1, 2, 3]}),
69
+ np.array([1, 2, 3]),
70
+ ]
71
+
72
+ for obj in other_module_objects:
73
+ with self.subTest(obj=type(obj).__name__):
74
+ result = ValidationMixin.validate_plotly_trace(obj, "other_module_object")
75
+ self.assertFalse(result, f"Should return False for {type(obj).__name__} from different module")
76
+
77
+ def test_validate_plotly_trace_edge_cases(self):
78
+ """Test validate_plotly_trace with edge cases."""
79
+ # Test with object that has __module__ but it's None
80
+ class MockObjectWithNoneModule:
81
+ __module__ = None
82
+
83
+ mock_obj = MockObjectWithNoneModule()
84
+ result = ValidationMixin.validate_plotly_trace(mock_obj, "mock_object_none_module")
85
+ self.assertFalse(result, "Should return False when __module__ is None")
86
+
87
+ # Test with object that has __module__ but it's empty string
88
+ class MockObjectWithEmptyModule:
89
+ __module__ = ""
90
+
91
+ mock_obj_empty = MockObjectWithEmptyModule()
92
+ result = ValidationMixin.validate_plotly_trace(mock_obj_empty, "mock_object_empty_module")
93
+ self.assertFalse(result, "Should return False when __module__ is empty string")
94
+
95
+
96
+ if __name__ == '__main__':
97
+ unittest.main()
@@ -1 +0,0 @@
1
- __version__ = "0.1.19"
@@ -1,339 +0,0 @@
1
- """
2
- Unit tests for SliderWithTextInput component with built-in callback logic.
3
-
4
- Tests cover component initialization, layout generation, callback registration,
5
- and synchronization behavior.
6
- """
7
-
8
- import unittest
9
- from unittest.mock import Mock, patch
10
- import dash
11
- from dash import html, dcc
12
- import numpy as np
13
-
14
- # Import the component
15
- from steer_core.Apps.Components.SliderComponents import SliderWithTextInput
16
-
17
-
18
- class TestSliderWithTextInput(unittest.TestCase):
19
- """Test cases for SliderWithTextInput component."""
20
-
21
- def setUp(self):
22
- """Set up test fixtures."""
23
-
24
- self.test_id_base = {"type": "test", "index": 0}
25
-
26
- self.component = SliderWithTextInput(
27
- id_base=self.test_id_base,
28
- min_val=0.0,
29
- max_val=100.0,
30
- step=1.0,
31
- mark_interval=10.0,
32
- property_name="test_prop",
33
- title="Test Component",
34
- default_val=50.0,
35
- )
36
-
37
- def test_initialization(self):
38
- """Test component initialization and attribute assignment."""
39
- self.assertEqual(self.component.id_base, self.test_id_base)
40
- self.assertEqual(self.component.min_val, 0.0)
41
- self.assertEqual(self.component.max_val, 100.0)
42
- self.assertEqual(self.component.step, 1.0)
43
- self.assertEqual(self.component.mark_interval, 10.0)
44
- self.assertEqual(self.component.property_name, "test_prop")
45
- self.assertEqual(self.component.title, "Test Component")
46
- self.assertEqual(self.component.default_val, 50.0)
47
- self.assertTrue(self.component.with_slider_titles)
48
- self.assertFalse(self.component.slider_disable)
49
- self.assertEqual(self.component.div_width, "calc(90%)")
50
-
51
- def test_id_generation(self):
52
- """Test ID generation for component elements."""
53
- expected_slider_id = {
54
- **self.test_id_base,
55
- "subtype": "slider",
56
- "property": "test_prop",
57
- }
58
- expected_input_id = {
59
- **self.test_id_base,
60
- "subtype": "input",
61
- "property": "test_prop",
62
- }
63
-
64
- self.assertEqual(self.component.slider_id, expected_slider_id)
65
- self.assertEqual(self.component.input_id, expected_input_id)
66
-
67
- def test_make_id_method(self):
68
- """Test the _make_id private method."""
69
- result = self.component._make_id("custom_subtype")
70
- expected = {
71
- **self.test_id_base,
72
- "subtype": "custom_subtype",
73
- "property": "test_prop",
74
- }
75
- self.assertEqual(result, expected)
76
-
77
- def test_make_slider(self):
78
- """Test slider component creation."""
79
- slider = self.component._make_slider()
80
-
81
- self.assertIsInstance(slider, dcc.Slider)
82
- self.assertEqual(slider.id, self.component.slider_id)
83
- self.assertEqual(slider.min, 0.0)
84
- self.assertEqual(slider.max, 100.0)
85
- self.assertEqual(slider.value, 50.0)
86
- self.assertEqual(slider.step, 1.0)
87
- self.assertFalse(slider.disabled)
88
- self.assertEqual(slider.updatemode, "mouseup")
89
-
90
- # Test marks generation
91
- expected_marks = {int(i): "" for i in np.arange(0.0, 110.0, 10.0)}
92
- self.assertEqual(slider.marks, expected_marks)
93
-
94
- def test_make_input(self):
95
- """Test input component creation."""
96
- input_comp = self.component._make_input()
97
-
98
- self.assertIsInstance(input_comp, dcc.Input)
99
- self.assertEqual(input_comp.id, self.component.input_id)
100
- self.assertEqual(input_comp.type, "number")
101
- self.assertEqual(input_comp.value, 50.0)
102
- self.assertEqual(input_comp.step, 1.0)
103
- self.assertFalse(input_comp.disabled)
104
- self.assertEqual(input_comp.style, {"margin-left": "20px"})
105
-
106
- def test_call_method(self):
107
- """Test the __call__ method that generates the complete layout."""
108
- layout = self.component()
109
-
110
- self.assertIsInstance(layout, html.Div)
111
- self.assertEqual(len(layout.children), 5) # P, Div, Input, Br, Br
112
-
113
- # Check title paragraph
114
- title_p = layout.children[0]
115
- self.assertIsInstance(title_p, html.P)
116
- self.assertEqual(title_p.children, "Test Component")
117
-
118
- def test_call_method_without_title(self):
119
- """Test layout generation with titles disabled."""
120
- self.component.with_slider_titles = False
121
- layout = self.component()
122
-
123
- title_p = layout.children[0]
124
- self.assertEqual(title_p.children, "\u00A0") # Non-breaking space
125
-
126
- def test_components_property(self):
127
- """Test the components property."""
128
- components = self.component.components
129
-
130
- expected = {
131
- "slider": self.component.slider_id,
132
- "input": self.component.input_id,
133
- }
134
- self.assertEqual(components, expected)
135
-
136
- def test_validate_and_clamp_value(self):
137
- """Test value validation and clamping."""
138
- # Test normal value
139
- self.assertEqual(self.component._validate_and_clamp_value(50.0), 50.0)
140
-
141
- # Test value below minimum
142
- self.assertEqual(self.component._validate_and_clamp_value(-10.0), 0.0)
143
-
144
- # Test value above maximum
145
- self.assertEqual(self.component._validate_and_clamp_value(150.0), 100.0)
146
-
147
- # Test None value
148
- self.assertEqual(self.component._validate_and_clamp_value(None), 50.0)
149
-
150
- # Test invalid string
151
- self.assertEqual(self.component._validate_and_clamp_value("invalid"), 50.0)
152
-
153
- def test_validate_and_clamp_value_no_default(self):
154
- """Test value validation when no default value is set."""
155
- component = SliderWithTextInput(
156
- id_base=self.test_id_base,
157
- min_val=0.0,
158
- max_val=100.0,
159
- step=1.0,
160
- mark_interval=10.0,
161
- property_name="test_prop",
162
- title="Test Component",
163
- default_val=None,
164
- )
165
-
166
- # Should return min_val when no default and invalid input
167
- self.assertEqual(component._validate_and_clamp_value(None), 0.0)
168
- self.assertEqual(component._validate_and_clamp_value("invalid"), 0.0)
169
-
170
- def test_no_automatic_callbacks(self):
171
- """Test that component no longer has automatic callback registration methods."""
172
- # These methods should no longer exist
173
- with self.assertRaises(AttributeError):
174
- self.component.register_callbacks()
175
-
176
- with self.assertRaises(AttributeError):
177
- self.component.register_clientside_callbacks()
178
-
179
- with self.assertRaises(AttributeError):
180
- SliderWithTextInput.with_sync(
181
- id_base=self.test_id_base,
182
- min_val=0.0,
183
- max_val=100.0,
184
- step=1.0,
185
- mark_interval=10.0,
186
- property_name="test",
187
- title="Test",
188
- )
189
-
190
- # Store methods should also no longer exist
191
- with self.assertRaises(AttributeError):
192
- self.component.get_store_input()
193
-
194
- with self.assertRaises(AttributeError):
195
- self.component.get_store_output()
196
-
197
- # Store ID should no longer exist
198
- with self.assertRaises(AttributeError):
199
- self.component.store_id
200
-
201
- def test_disabled_components(self):
202
- """Test component creation with disabled state."""
203
- disabled_component = SliderWithTextInput(
204
- id_base=self.test_id_base,
205
- min_val=0.0,
206
- max_val=100.0,
207
- step=1.0,
208
- mark_interval=10.0,
209
- property_name="disabled_test",
210
- title="Disabled Test",
211
- slider_disable=True,
212
- )
213
-
214
- slider = disabled_component._make_slider()
215
- input_comp = disabled_component._make_input()
216
-
217
- self.assertTrue(slider.disabled)
218
- self.assertTrue(input_comp.disabled)
219
-
220
- def test_custom_div_width(self):
221
- """Test component with custom div width."""
222
- custom_component = SliderWithTextInput(
223
- id_base=self.test_id_base,
224
- min_val=0.0,
225
- max_val=100.0,
226
- step=1.0,
227
- mark_interval=10.0,
228
- property_name="width_test",
229
- title="Width Test",
230
- div_width="50%",
231
- )
232
-
233
- layout = custom_component()
234
- self.assertEqual(layout.style["width"], "50%")
235
-
236
- def test_get_value_inputs(self):
237
- """Test the get_value_inputs helper method."""
238
- inputs = self.component.get_value_inputs()
239
-
240
- # Should return a list with two Input objects
241
- self.assertEqual(len(inputs), 2)
242
-
243
- # Check slider input
244
- slider_input = inputs[0]
245
- self.assertEqual(slider_input.component_id, self.component.slider_id)
246
- self.assertEqual(slider_input.component_property, "value")
247
-
248
- # Check input input
249
- input_input = inputs[1]
250
- self.assertEqual(input_input.component_id, self.component.input_id)
251
- self.assertEqual(input_input.component_property, "value")
252
-
253
- def test_get_value_outputs(self):
254
- """Test the get_value_outputs helper method."""
255
- outputs = self.component.get_value_outputs()
256
-
257
- # Should return a list with two Output objects
258
- self.assertEqual(len(outputs), 2)
259
-
260
- # Check slider output
261
- slider_output = outputs[0]
262
- self.assertEqual(slider_output.component_id, self.component.slider_id)
263
- self.assertEqual(slider_output.component_property, "value")
264
-
265
- # Check input output
266
- input_output = outputs[1]
267
- self.assertEqual(input_output.component_id, self.component.input_id)
268
- self.assertEqual(input_output.component_property, "value")
269
-
270
- def test_get_pattern_matching_value_inputs(self):
271
- """Test the get_pattern_matching_value_inputs helper method."""
272
- inputs = self.component.get_pattern_matching_value_inputs("temperature")
273
-
274
- # Should return a list with two Input objects
275
- self.assertEqual(len(inputs), 2)
276
-
277
- # Check pattern structure
278
- slider_pattern = inputs[0].component_id
279
- self.assertEqual(slider_pattern["type"], "parameter")
280
- self.assertEqual(slider_pattern["subtype"], "slider")
281
- self.assertEqual(slider_pattern["property"], "temperature")
282
-
283
- input_pattern = inputs[1].component_id
284
- self.assertEqual(input_pattern["type"], "parameter")
285
- self.assertEqual(input_pattern["subtype"], "input")
286
- self.assertEqual(input_pattern["property"], "temperature")
287
-
288
- def test_get_pattern_matching_value_outputs(self):
289
- """Test the get_pattern_matching_value_outputs helper method."""
290
- outputs = self.component.get_pattern_matching_value_outputs("ALL")
291
-
292
- # Should return a list with two Output objects
293
- self.assertEqual(len(outputs), 2)
294
-
295
- # Check pattern structure
296
- slider_pattern = outputs[0].component_id
297
- self.assertEqual(slider_pattern["type"], "parameter")
298
- self.assertEqual(slider_pattern["subtype"], "slider")
299
- self.assertEqual(slider_pattern["property"], "ALL")
300
-
301
- input_pattern = outputs[1].component_id
302
- self.assertEqual(input_pattern["type"], "parameter")
303
- self.assertEqual(input_pattern["subtype"], "input")
304
- self.assertEqual(input_pattern["property"], "ALL")
305
-
306
-
307
- class TestSliderWithTextInputIntegration(unittest.TestCase):
308
- """Integration tests for SliderWithTextInput component."""
309
-
310
- def setUp(self):
311
- """Set up test fixtures."""
312
- self.app = dash.Dash(__name__)
313
- self.component = SliderWithTextInput(
314
- id_base={"type": "test", "index": 0},
315
- min_val=0.0,
316
- max_val=100.0,
317
- step=1.0,
318
- mark_interval=10.0,
319
- property_name="integration_test",
320
- title="Integration Test Component",
321
- )
322
-
323
- def test_component_creation(self):
324
- """Test that component creates correctly without auto-sync."""
325
- self.assertIsInstance(self.component, SliderWithTextInput)
326
- self.assertEqual(self.component.min_val, 0.0)
327
- self.assertEqual(self.component.max_val, 100.0)
328
- self.assertEqual(self.component.property_name, "integration_test")
329
-
330
- def test_component_layout_generation(self):
331
- """Test that the component generates proper layout."""
332
- layout = self.component()
333
- self.assertIsInstance(layout, html.Div)
334
- # Should have title, slider div, input, and two breaks
335
- self.assertEqual(len(layout.children), 5)
336
-
337
-
338
- if __name__ == "__main__":
339
- unittest.main()
File without changes
@@ -1,286 +0,0 @@
1
- """
2
- Unit tests for SliderControls utility functions.
3
- """
4
-
5
- import unittest
6
- import math
7
- from steer_core.Apps.Utils.SliderControls import (
8
- calculate_slider_steps,
9
- calculate_mark_intervals,
10
- create_slider_config,
11
- snap_to_slider_grid,
12
- )
13
-
14
-
15
- class TestSliderControls(unittest.TestCase):
16
- """Test cases for slider control utility functions."""
17
-
18
- def test_calculate_slider_steps_basic(self):
19
- """Test basic step calculation functionality."""
20
- min_vals = [0, 0, 0]
21
- max_vals = [10, 100, 1000]
22
- steps = calculate_slider_steps(min_vals, max_vals)
23
-
24
- # Should have 3 steps
25
- self.assertEqual(len(steps), 3)
26
-
27
- # Steps should be reasonable for their ranges
28
- self.assertLessEqual(steps[0], 1.0) # 10 range
29
- self.assertLessEqual(steps[1], 10.0) # 100 range
30
- self.assertLessEqual(steps[2], 100.0) # 1000 range
31
-
32
- # All steps should be positive
33
- for step in steps:
34
- self.assertGreater(step, 0)
35
-
36
- def test_calculate_slider_steps_edge_cases(self):
37
- """Test edge cases for step calculation."""
38
- # Zero range
39
- steps = calculate_slider_steps([5], [5])
40
- self.assertEqual(steps[0], 0.001)
41
-
42
- # Very small range
43
- steps = calculate_slider_steps([0], [0.001])
44
- self.assertGreater(steps[0], 0)
45
- self.assertLess(steps[0], 0.001)
46
-
47
- # Negative ranges
48
- steps = calculate_slider_steps([-100], [100])
49
- self.assertGreater(steps[0], 0)
50
-
51
- def test_calculate_slider_steps_validation(self):
52
- """Test input validation for step calculation."""
53
- # Mismatched lengths
54
- with self.assertRaises(ValueError):
55
- calculate_slider_steps([0, 1], [10])
56
-
57
- # Max < min
58
- with self.assertRaises(ValueError):
59
- calculate_slider_steps([10], [5])
60
-
61
- def test_calculate_mark_intervals(self):
62
- """Test mark interval calculation."""
63
- min_vals = [0, 0]
64
- max_vals = [100, 1000]
65
- intervals = calculate_mark_intervals(min_vals, max_vals)
66
-
67
- self.assertEqual(len(intervals), 2)
68
-
69
- # Intervals should be reasonable
70
- self.assertGreater(intervals[0], 0)
71
- self.assertGreater(intervals[1], 0)
72
-
73
- # Should be larger than steps for same ranges
74
- steps = calculate_slider_steps(min_vals, max_vals)
75
- for interval, step in zip(intervals, steps):
76
- self.assertGreaterEqual(interval, step)
77
-
78
- def test_create_slider_config(self):
79
- """Test complete slider configuration creation."""
80
- min_vals = [0, 20]
81
- max_vals = [100, 80]
82
-
83
- config = create_slider_config(min_vals, max_vals)
84
-
85
- # Check required keys
86
- required_keys = [
87
- "min_vals",
88
- "max_vals",
89
- "step_vals",
90
- "input_step_vals",
91
- "mark_vals",
92
- ]
93
- for key in required_keys:
94
- self.assertIn(key, config)
95
-
96
- # Check list lengths
97
- self.assertEqual(len(config["min_vals"]), 2)
98
- self.assertEqual(len(config["max_vals"]), 2)
99
- self.assertEqual(len(config["step_vals"]), 2)
100
- self.assertEqual(len(config["input_step_vals"]), 2)
101
- self.assertEqual(len(config["mark_vals"]), 2)
102
-
103
- # Check values - min/max should be grid-snapped but close to originals
104
- # Grid-snapped min should be <= original min
105
- for i, (grid_min, orig_min) in enumerate(zip(config["min_vals"], min_vals)):
106
- self.assertLessEqual(grid_min, orig_min)
107
- self.assertAlmostEqual(grid_min, orig_min, delta=config["step_vals"][i])
108
-
109
- # Grid-snapped max should be >= original max
110
- for i, (grid_max, orig_max) in enumerate(zip(config["max_vals"], max_vals)):
111
- self.assertGreaterEqual(grid_max, orig_max)
112
- self.assertAlmostEqual(grid_max, orig_max, delta=config["step_vals"][i])
113
-
114
- # Check that step values are positive
115
- for step in config["step_vals"]:
116
- self.assertGreater(step, 0)
117
-
118
- # Check that input step values are positive and slider steps are 10x larger than input steps
119
- # Also check minimum step sizes: slider min 0.1, input min 0.01
120
- for i, (slider_step, input_step) in enumerate(
121
- zip(config["step_vals"], config["input_step_vals"])
122
- ):
123
- self.assertGreater(input_step, 0)
124
- self.assertGreaterEqual(slider_step, 0.1) # Minimum step size for sliders
125
- self.assertGreaterEqual(input_step, 0.01) # Minimum step size for inputs
126
- # When both are above minimum, slider should be 10x larger than input
127
- if input_step > 0.01:
128
- self.assertAlmostEqual(slider_step, input_step * 10.0, places=10)
129
-
130
- # Check that mark_vals contains dictionaries
131
- for marks in config["mark_vals"]:
132
- self.assertIsInstance(marks, dict)
133
- self.assertGreater(len(marks), 0)
134
-
135
- def test_create_slider_config_with_property_values(self):
136
- """Test slider configuration with property values snapping to grid."""
137
- min_vals = [0, 20]
138
- max_vals = [100, 80]
139
- property_vals = [23.7, 45.3]
140
-
141
- config = create_slider_config(min_vals, max_vals, property_vals)
142
-
143
- # Check that both grid value types are present
144
- self.assertIn("grid_slider_vals", config)
145
- self.assertIn("grid_input_vals", config)
146
- self.assertEqual(len(config["grid_slider_vals"]), 2)
147
- self.assertEqual(len(config["grid_input_vals"]), 2)
148
-
149
- # Grid values should be properly snapped to grid (note: no longer constrained to slider range)
150
- for i, (slider_grid_val, input_grid_val) in enumerate(
151
- zip(config["grid_slider_vals"], config["grid_input_vals"])
152
- ):
153
- # Values should be snapped to their respective grids
154
- slider_step = config["step_vals"][i]
155
- input_step = config["input_step_vals"][i]
156
-
157
- # Check grid alignment (values should be on grid)
158
- slider_offset = (slider_grid_val - config["min_vals"][i]) % slider_step
159
- input_offset = (input_grid_val - config["min_vals"][i]) % input_step
160
-
161
- # Due to floating point precision, offset might be very close to 0 or step size
162
- self.assertTrue(
163
- abs(slider_offset) < 1e-10 or abs(slider_offset - slider_step) < 1e-10,
164
- f"Slider value not on grid: offset={slider_offset}",
165
- )
166
- self.assertTrue(
167
- abs(input_offset) < 1e-10 or abs(input_offset - input_step) < 1e-10,
168
- f"Input value not on grid: offset={input_offset}",
169
- )
170
-
171
- def test_create_slider_config_out_of_range_values(self):
172
- """Test that property values outside slider range are preserved (not clamped)."""
173
- min_vals = [0, 10]
174
- max_vals = [100, 50]
175
- # Property values outside the ranges
176
- property_vals = [150, 5] # 150 > 100, 5 < 10
177
-
178
- config = create_slider_config(min_vals, max_vals, property_vals)
179
-
180
- # Values should be preserved (not clamped to range)
181
- self.assertEqual(config["grid_slider_vals"][0], 150.0) # Not clamped to 100
182
- self.assertEqual(config["grid_slider_vals"][1], 5.0) # Not clamped to 10
183
- self.assertEqual(config["grid_input_vals"][0], 150.0) # Not clamped to 100
184
- self.assertEqual(config["grid_input_vals"][1], 5.0) # Not clamped to 10
185
-
186
- # Values should still be on grid
187
- for i, (slider_val, input_val) in enumerate(
188
- zip(config["grid_slider_vals"], config["grid_input_vals"])
189
- ):
190
- slider_step = config["step_vals"][i]
191
- input_step = config["input_step_vals"][i]
192
-
193
- # Check grid alignment
194
- slider_offset = (slider_val - config["min_vals"][i]) % slider_step
195
- input_offset = (input_val - config["min_vals"][i]) % input_step
196
-
197
- # Due to floating point precision, offset might be very close to 0 or step size
198
- self.assertTrue(
199
- abs(slider_offset) < 1e-10 or abs(slider_offset - slider_step) < 1e-10
200
- )
201
- self.assertTrue(
202
- abs(input_offset) < 1e-10 or abs(input_offset - input_step) < 1e-10
203
- )
204
-
205
- def test_create_slider_config_validation(self):
206
- """Test validation for slider configuration creation."""
207
- # Mismatched lengths for min/max
208
- with self.assertRaises(ValueError):
209
- config = create_slider_config([0], [100, 200])
210
-
211
- # Wrong property values length
212
- with self.assertRaises(ValueError):
213
- config = create_slider_config([0], [100], [25, 50])
214
-
215
- def test_create_slider_config_marks(self):
216
- """Test that mark_vals are properly structured dictionaries with interval-based marks."""
217
- min_vals = [0, 10]
218
- max_vals = [100, 50]
219
-
220
- config = create_slider_config(min_vals, max_vals)
221
-
222
- # Check mark_vals structure
223
- self.assertEqual(len(config["mark_vals"]), 2)
224
-
225
- for i, marks in enumerate(config["mark_vals"]):
226
- self.assertIsInstance(marks, dict)
227
-
228
- # Check that all mark keys are floats and values are empty strings (no labels)
229
- for mark_pos, mark_label in marks.items():
230
- self.assertIsInstance(mark_pos, (int, float))
231
- self.assertEqual(mark_label, "") # Empty string, no labels
232
- self.assertGreaterEqual(mark_pos, config["min_vals"][i])
233
- self.assertLessEqual(mark_pos, config["max_vals"][i])
234
-
235
- # Check that marks follow interval-based logic
236
- # First slider: range 0-100 (interval=100.0), should have marks at multiples of 100 within range
237
- marks1 = config["mark_vals"][0]
238
- # Range 100 gets interval 100.0, so marks should be at 0, 100
239
- expected_positions1 = [0.0, 100.0]
240
- self.assertEqual(sorted(marks1.keys()), expected_positions1)
241
-
242
- # Second slider: range 10-50 (range=40, interval=10.0), should have marks at multiples of 10 within range
243
- marks2 = config["mark_vals"][1]
244
- expected_positions2 = [10.0, 20.0, 30.0, 40.0, 50.0]
245
- self.assertEqual(sorted(marks2.keys()), expected_positions2)
246
-
247
- def test_snap_to_slider_grid(self):
248
- """Test snapping values to slider grid."""
249
- # Value already on grid
250
- result = snap_to_slider_grid(23.7, 0, 100, 0.1)
251
- self.assertAlmostEqual(result, 23.7, places=5)
252
-
253
- # Value between grid points
254
- result = snap_to_slider_grid(23.75, 0, 100, 0.1)
255
- self.assertAlmostEqual(result, 23.8, places=5)
256
-
257
- # Value below minimum
258
- result = snap_to_slider_grid(-10, 0, 100, 1.0)
259
- self.assertEqual(result, 0.0)
260
-
261
- # Value above maximum
262
- result = snap_to_slider_grid(150, 0, 100, 1.0)
263
- self.assertEqual(result, 100.0)
264
- # Integer step
265
- result = snap_to_slider_grid(23.7, 0, 100, 1.0)
266
- self.assertEqual(result, 24.0)
267
-
268
- def test_realistic_scenarios(self):
269
- """Test with realistic parameter scenarios."""
270
- # Temperature control scenario
271
- min_vals = [20, 0, 0.1]
272
- max_vals = [100, 50, 10.0]
273
- steps = calculate_slider_steps(min_vals, max_vals)
274
-
275
- # Temperature (80°C range) - should have fine control
276
- self.assertLessEqual(steps[0], 1.0)
277
-
278
- # Percentage (50% range) - should have reasonable control
279
- self.assertLessEqual(steps[1], 1.0)
280
-
281
- # Flow rate (9.9 L/min range) - should have fine control
282
- self.assertLessEqual(steps[2], 0.1)
283
-
284
-
285
- if __name__ == "__main__":
286
- unittest.main()
File without changes
File without changes
File without changes