From 2f5ddd4cec5213327206cdf10ef9ee581df5547e Mon Sep 17 00:00:00 2001 From: Georg Plaz Date: Thu, 12 Jun 2025 11:33:22 +0200 Subject: [PATCH] Raise test coverage to 100% pull overriding to_str method from ToStrValue up to super class StrValue Remove unused code Fix bug in MappedValue --- .github/workflows/python-package.yml | 2 +- src/spellbind/float_values.py | 9 +-- src/spellbind/functions.py | 2 +- src/spellbind/int_values.py | 5 -- src/spellbind/str_values.py | 6 +- src/spellbind/values.py | 8 +-- tests/test_events/test_events.py | 8 +++ .../test_bool_values/test_bool_values.py | 39 ++++++++++++- .../test_float_values_arithmatic.py | 14 +++++ .../test_int_values/test_add_int_values.py | 26 +++++++++ .../test_int_values/test_int_values.py | 14 +++++ .../test_int_values_arithmatic.py | 14 +++++ tests/test_values/test_simple_variable.py | 10 ++++ .../test_str_values/test_to_str_values.py | 57 ++++++++++++++++++- tests/test_version.py | 12 ++++ 15 files changed, 199 insertions(+), 27 deletions(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index ab6abdc..f712126 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -34,7 +34,7 @@ jobs: mypy src - name: Test with pytest run: | - pytest + pytest --cov --cov-fail-under=95 publish: needs: test diff --git a/src/spellbind/float_values.py b/src/spellbind/float_values.py index 00c41bf..3f09d3e 100644 --- a/src/spellbind/float_values.py +++ b/src/spellbind/float_values.py @@ -11,7 +11,7 @@ from spellbind.values import Value, SimpleVariable, DerivedValue, DerivedValueBase, Constant, CombinedTwoValues if TYPE_CHECKING: - from spellbind.int_values import IntValue, IntLike + from spellbind.int_values import IntValue, IntLike # pragma: no cover FloatLike = Value[int] | float | Value[float] @@ -119,13 +119,6 @@ class FloatVariable(SimpleVariable[float], FloatValue): pass -def _create_float_getter(value: float | Value[int] | Value[float]) -> Callable[[], float]: - if isinstance(value, Value): - return lambda: value.value - else: - return lambda: value - - def _get_float(value: float | Value[int] | Value[float]) -> float: if isinstance(value, Value): return value.value diff --git a/src/spellbind/functions.py b/src/spellbind/functions.py index 7332e74..53b48a5 100644 --- a/src/spellbind/functions.py +++ b/src/spellbind/functions.py @@ -28,6 +28,6 @@ def assert_parameter_max_count(callable_: Callable, max_count: int) -> None: elif hasattr(callable_, '__class__'): callable_name = callable_.__class__.__name__ else: - callable_name = str(callable_) + callable_name = str(callable_) # pragma: no cover raise ValueError(f"Callable {callable_name} has too many non-default parameters: " f"{count_non_default_parameters(callable_)} > {max_count}") diff --git a/src/spellbind/int_values.py b/src/spellbind/int_values.py index 8c7be2c..e30f0e4 100644 --- a/src/spellbind/int_values.py +++ b/src/spellbind/int_values.py @@ -181,11 +181,6 @@ def transform(self, *values: int) -> int: return result -class DivideIntValues(CombinedTwoValues[int, int, float], FloatValue): - def transform(self, left: int, right: int) -> float: - return left / right - - class FloorDivideIntValues(CombinedTwoValues[int, int, int], IntValue): def transform(self, left: int, right: int) -> int: return left // right diff --git a/src/spellbind/str_values.py b/src/spellbind/str_values.py index f1e023c..a0575fa 100644 --- a/src/spellbind/str_values.py +++ b/src/spellbind/str_values.py @@ -17,6 +17,9 @@ def __add__(self, other: StringLike) -> StrValue: def __radd__(self, other: StringLike) -> StrValue: return ConcatenateStrValues(other, self) + def to_str(self) -> StrValue: + return self + class MappedStrValue(Generic[_S], DerivedValue[_S, str], StrValue): def __init__(self, value: Value[_S], transform: Callable[[_S], str]) -> None: @@ -39,9 +42,6 @@ class ToStrValue(DerivedValue[Any, str], StrValue): def transform(self, value: Any) -> str: return str(value) - def to_str(self) -> StrValue: - return self - class ConcatenateStrValues(CombinedMixedValues[str, str], StrValue): def transform(self, *values: str) -> str: diff --git a/src/spellbind/values.py b/src/spellbind/values.py index 425e916..6397d9c 100644 --- a/src/spellbind/values.py +++ b/src/spellbind/values.py @@ -7,9 +7,9 @@ from spellbind.observables import ValueObservable, Observer, ValueObserver if TYPE_CHECKING: - from spellbind.str_values import StrValue - from spellbind.int_values import IntValue - from spellbind.bool_values import BoolValue + from spellbind.str_values import StrValue # pragma: no cover + from spellbind.int_values import IntValue # pragma: no cover + from spellbind.bool_values import BoolValue # pragma: no cover EMPTY_FROZEN_SET: frozenset = frozenset() @@ -227,9 +227,9 @@ def value(self) -> _T: class MappedValue(DerivedValue[_S, _T], Generic[_S, _T]): def __init__(self, of: Value[_S], transformer: Callable[[_S], _T]): - super().__init__(of) self._transformer = transformer self._of = of + super().__init__(of) def transform(self, value: _S) -> _T: return self._transformer(value) diff --git a/tests/test_events/test_events.py b/tests/test_events/test_events.py index 4083fc5..edd38ba 100644 --- a/tests/test_events/test_events.py +++ b/tests/test_events/test_events.py @@ -104,6 +104,14 @@ def test_event_observe_lambda_observer(): assert calls == [True] +def test_event_observe_lambda_observer_with_one_parameter_fails(): + event = Event() + calls = [] + + with pytest.raises(ValueError): + event.observe(lambda x: calls.append(True)) + + def test_event_observe_mock_observer_times_parameter_limits_calls(): event = Event() mock_observer = NoParametersObserver() diff --git a/tests/test_values/test_bool_values/test_bool_values.py b/tests/test_values/test_bool_values/test_bool_values.py index cb6cb3a..d9c2f09 100644 --- a/tests/test_values/test_bool_values/test_bool_values.py +++ b/tests/test_values/test_bool_values/test_bool_values.py @@ -1,9 +1,44 @@ -from spellbind.bool_values import BoolConstant +from spellbind.bool_values import BoolConstant, BoolVariable -def test_bool_constant_str(): +def test_bool_constant_true_to_str(): const = BoolConstant(True) assert str(const) == "True" + +def test_bool_constant_false_to_str(): const_false = BoolConstant(False) assert str(const_false) == "False" + + +def test_logical_not_variable_true(): + var = BoolVariable(True) + negated = var.logical_not() + assert not negated.value + + +def test_logical_not_variable_false(): + var = BoolVariable(False) + negated = var.logical_not() + assert negated.value + + +def test_logical_not_flip_flop(): + var = BoolVariable(True) + negated = var.logical_not() + assert not negated.value + + var.value = False + assert negated.value + + var.value = True + assert not negated.value + + +def test_logical_not_double_negation(): + var = BoolVariable(True) + double_negated = var.logical_not().logical_not() + assert double_negated.value + + var.value = False + assert not double_negated.value diff --git a/tests/test_values/test_float_values/test_float_values_arithmatic.py b/tests/test_values/test_float_values/test_float_values_arithmatic.py index 3ed0cb1..7a382e9 100644 --- a/tests/test_values/test_float_values/test_float_values_arithmatic.py +++ b/tests/test_values/test_float_values/test_float_values_arithmatic.py @@ -114,3 +114,17 @@ def test_modulo_int_by_float_value(): v1.value = 4.0 assert v2.value == 2.0 + + +def test_float_pos(): + var = FloatVariable(3.14) + result = +var + assert result is var + assert result.value == 3.14 + + +def test_float_pos_negative(): + var = FloatVariable(-2.5) + result = +var + assert result is var + assert result.value == -2.5 diff --git a/tests/test_values/test_int_values/test_add_int_values.py b/tests/test_values/test_int_values/test_add_int_values.py index 16dede7..ae21235 100644 --- a/tests/test_values/test_int_values/test_add_int_values.py +++ b/tests/test_values/test_int_values/test_add_int_values.py @@ -84,3 +84,29 @@ def test_add_int_values_garbage_collected(): v1.value = 4 # trigger removal of weak references assert len(v0._on_change._subscriptions) == 0 assert len(v1._on_change._subscriptions) == 0 + + +def test_bind_to_added_int_variables(): + v0 = IntVariable(1) + v1 = IntVariable(2) + + variable = IntVariable(42) + variable.bind_to(v0 + v1) + assert variable.value == 3 + + v0.value = 5 + + assert variable.value == 7 + + +def test_bind_and_unbind_to_added_int_variables(): + v0 = IntVariable(1) + v1 = IntVariable(2) + + variable = IntVariable(42) + variable.bind_to(v0 + v1) + v0.value = 5 + + variable.unbind() + v0.value = 10 + assert variable.value == 7 diff --git a/tests/test_values/test_int_values/test_int_values.py b/tests/test_values/test_int_values/test_int_values.py index 8113df2..1b5180c 100644 --- a/tests/test_values/test_int_values/test_int_values.py +++ b/tests/test_values/test_int_values/test_int_values.py @@ -133,3 +133,17 @@ def test_clamp_int_values_reactive_bounds_changes(): max_val.value = 12 assert clamped.value == 12 + + +def test_derived_int_values_map_to_list(): + value0 = IntConstant(2) + value1 = IntConstant(3) + added = value0 + value1 + mapped_value = added.map(lambda x: ["foo"]*x) + + assert mapped_value.value == ["foo", "foo", "foo", "foo", "foo"] + + +def test_int_const_repr(): + const = IntConstant(42) + assert repr(const) == "IntConstant(42)" diff --git a/tests/test_values/test_int_values/test_int_values_arithmatic.py b/tests/test_values/test_int_values/test_int_values_arithmatic.py index 24e73fd..2f014c6 100644 --- a/tests/test_values/test_int_values/test_int_values_arithmatic.py +++ b/tests/test_values/test_int_values/test_int_values_arithmatic.py @@ -56,3 +56,17 @@ def test_modulo_int_by_int_value(): v1.value = 4 assert v2.value == 2 + + +def test_int_pos(): + var = IntVariable(42) + result = +var + assert result is var + assert result.value == 42 + + +def test_int_pos_negative(): + var = IntVariable(-15) + result = +var + assert result is var + assert result.value == -15 diff --git a/tests/test_values/test_simple_variable.py b/tests/test_values/test_simple_variable.py index f612c69..fb85a82 100644 --- a/tests/test_values/test_simple_variable.py +++ b/tests/test_values/test_simple_variable.py @@ -83,6 +83,16 @@ def test_simple_variable_unobserve_lambda(): assert calls == [10] +def test_simple_variable_bind_twice_to_same(): + variable = SimpleVariable("test") + constant = Constant("value") + + variable.bind_to(constant) + variable.bind_to(constant, already_bound_ok=True) + + assert variable.value == "value" + + def test_simple_variable_bind_to_constant(): variable = SimpleVariable("old") constant = Constant("new") diff --git a/tests/test_values/test_str_values/test_to_str_values.py b/tests/test_values/test_str_values/test_to_str_values.py index daf1473..3e23943 100644 --- a/tests/test_values/test_str_values/test_to_str_values.py +++ b/tests/test_values/test_str_values/test_to_str_values.py @@ -1,12 +1,63 @@ +from spellbind.bool_values import BoolVariable +from spellbind.float_values import FloatVariable +from spellbind.int_values import IntVariable +from spellbind.str_values import StrVariable from spellbind.values import SimpleVariable -def test_to_str_of_int_42(): - value = SimpleVariable(42) +def test_to_str_of_list(): + value = SimpleVariable([1, 2, 3]) + to_str_value = value.to_str() + + assert to_str_value.value == "[1, 2, 3]" + + value.value = ["a", "b"] + + assert to_str_value.value == "['a', 'b']" + + +def test_to_str_of_int(): + value = IntVariable(42) to_str_value = value.to_str() assert to_str_value.value == "42" value.value = 100 - assert to_str_value.value == "100" + + +def test_to_str_of_float(): + value = FloatVariable(3.14) + to_str_value = value.to_str() + + assert to_str_value.value == "3.14" + + value.value = 2.718 + assert to_str_value.value == "2.718" + + +def test_to_str_of_bool_true(): + value = BoolVariable(True) + to_str_value = value.to_str() + + assert to_str_value.value == "True" + + value.value = False + assert to_str_value.value == "False" + + +def test_to_str_of_bool_false(): + value = BoolVariable(False) + to_str_value = value.to_str() + + assert to_str_value.value == "False" + + value.value = True + assert to_str_value.value == "True" + + +def test_to_str_of_str_returns_same_object(): + value = StrVariable("hello") + to_str_value = value.to_str() + + assert to_str_value is value diff --git a/tests/test_version.py b/tests/test_version.py index 75760b5..a2b25fa 100644 --- a/tests/test_version.py +++ b/tests/test_version.py @@ -1,4 +1,6 @@ import re +import sys +from unittest.mock import patch import spellbind @@ -13,3 +15,13 @@ def test_version_format(): # Should be semver-like: 0.1.0, 0.1.dev1+g123abc, 0.1.0.dev1+g123abc.d20250609, etc. pattern = r'^\d+\.\d+(?:\.\d+)?(?:\.dev\d+\+g[a-f0-9]+(?:\.d\d{8})?)?$' assert re.match(pattern, version), f"Version '{version}' doesn't match expected format" + + +def test_version_fallback_on_exception(): + if 'spellbind' in sys.modules: + del sys.modules['spellbind'] + + # Patch importlib.metadata.version to raise an exception + with patch('importlib.metadata.version', side_effect=Exception("Package not found")): + import spellbind + assert spellbind.__version__ == "unknown"