steer-core 0.1.18__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.
- {steer_core-0.1.18 → steer_core-0.1.20}/PKG-INFO +1 -1
- {steer_core-0.1.18 → steer_core-0.1.20}/steer_core/Data/database.db +0 -0
- {steer_core-0.1.18 → steer_core-0.1.20}/steer_core/Decorators/Electrochemical.py +3 -7
- {steer_core-0.1.18 → steer_core-0.1.20}/steer_core/Decorators/General.py +1 -0
- steer_core-0.1.20/steer_core/Mixins/Dunder.py +164 -0
- {steer_core-0.1.18 → steer_core-0.1.20}/steer_core/Mixins/TypeChecker.py +26 -3
- steer_core-0.1.20/steer_core/__init__.py +1 -0
- {steer_core-0.1.18 → steer_core-0.1.20}/steer_core.egg-info/PKG-INFO +1 -1
- {steer_core-0.1.18 → steer_core-0.1.20}/steer_core.egg-info/SOURCES.txt +3 -3
- steer_core-0.1.20/test/test_validation_mixin.py +97 -0
- steer_core-0.1.18/steer_core/__init__.py +0 -1
- steer_core-0.1.18/test/test_compound_components.py +0 -339
- steer_core-0.1.18/test/test_compound_components_clean.py +0 -0
- steer_core-0.1.18/test/test_slider_controls.py +0 -286
- {steer_core-0.1.18 → steer_core-0.1.20}/README.md +0 -0
- {steer_core-0.1.18 → steer_core-0.1.20}/setup.cfg +0 -0
- {steer_core-0.1.18 → steer_core-0.1.20}/setup.py +0 -0
- {steer_core-0.1.18 → steer_core-0.1.20}/steer_core/Apps/Components/MaterialSelectors.py +0 -0
- {steer_core-0.1.18 → steer_core-0.1.20}/steer_core/Apps/Components/RangeSliderComponents.py +0 -0
- {steer_core-0.1.18 → steer_core-0.1.20}/steer_core/Apps/Components/SliderComponents.py +0 -0
- {steer_core-0.1.18 → steer_core-0.1.20}/steer_core/Apps/Components/__init__.py +0 -0
- {steer_core-0.1.18 → steer_core-0.1.20}/steer_core/Apps/ContextManagers.py +0 -0
- {steer_core-0.1.18 → steer_core-0.1.20}/steer_core/Apps/Performance/CallbackTimer.py +0 -0
- {steer_core-0.1.18 → steer_core-0.1.20}/steer_core/Apps/Performance/__init__.py +0 -0
- {steer_core-0.1.18 → steer_core-0.1.20}/steer_core/Apps/Utils/SliderControls.py +0 -0
- {steer_core-0.1.18 → steer_core-0.1.20}/steer_core/Apps/Utils/__init__.py +0 -0
- {steer_core-0.1.18 → steer_core-0.1.20}/steer_core/Apps/__init__.py +0 -0
- {steer_core-0.1.18 → steer_core-0.1.20}/steer_core/Constants/Units.py +0 -0
- {steer_core-0.1.18 → steer_core-0.1.20}/steer_core/Constants/Universal.py +0 -0
- {steer_core-0.1.18 → steer_core-0.1.20}/steer_core/Constants/__init__.py +0 -0
- {steer_core-0.1.18 → steer_core-0.1.20}/steer_core/ContextManagers/__init__.py +0 -0
- {steer_core-0.1.18 → steer_core-0.1.20}/steer_core/Data/__init__.py +0 -0
- {steer_core-0.1.18 → steer_core-0.1.20}/steer_core/DataManager.py +0 -0
- {steer_core-0.1.18 → steer_core-0.1.20}/steer_core/Decorators/Coordinates.py +0 -0
- {steer_core-0.1.18 → steer_core-0.1.20}/steer_core/Decorators/Objects.py +0 -0
- {steer_core-0.1.18 → steer_core-0.1.20}/steer_core/Decorators/__init__.py +0 -0
- {steer_core-0.1.18 → steer_core-0.1.20}/steer_core/Mixins/Colors.py +0 -0
- {steer_core-0.1.18 → steer_core-0.1.20}/steer_core/Mixins/Coordinates.py +0 -0
- {steer_core-0.1.18 → steer_core-0.1.20}/steer_core/Mixins/Data.py +0 -0
- {steer_core-0.1.18 → steer_core-0.1.20}/steer_core/Mixins/Plotter.py +0 -0
- {steer_core-0.1.18 → steer_core-0.1.20}/steer_core/Mixins/__init__.py +0 -0
- /steer_core-0.1.18/steer_core/Mixins/Serializer.py → /steer_core-0.1.20/steer_core/Mixins/serializer.py +0 -0
- {steer_core-0.1.18 → steer_core-0.1.20}/steer_core.egg-info/dependency_links.txt +0 -0
- {steer_core-0.1.18 → steer_core-0.1.20}/steer_core.egg-info/requires.txt +0 -0
- {steer_core-0.1.18 → steer_core-0.1.20}/steer_core.egg-info/top_level.txt +0 -0
|
Binary file
|
|
@@ -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,
|
|
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,
|
|
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
|
+
|
|
@@ -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"
|
|
@@ -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
|
-
|
|
39
|
-
test/
|
|
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.18"
|
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|