Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/python-package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ jobs:
mypy src
- name: Test with pytest
run: |
pytest
pytest --cov --cov-fail-under=95

publish:
needs: test
Expand Down
9 changes: 1 addition & 8 deletions src/spellbind/float_values.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]

Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/spellbind/functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}")
5 changes: 0 additions & 5 deletions src/spellbind/int_values.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 3 additions & 3 deletions src/spellbind/str_values.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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:
Expand Down
8 changes: 4 additions & 4 deletions src/spellbind/values.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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)
Expand Down
8 changes: 8 additions & 0 deletions tests/test_events/test_events.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
39 changes: 37 additions & 2 deletions tests/test_values/test_bool_values/test_bool_values.py
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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
26 changes: 26 additions & 0 deletions tests/test_values/test_int_values/test_add_int_values.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
14 changes: 14 additions & 0 deletions tests/test_values/test_int_values/test_int_values.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)"
14 changes: 14 additions & 0 deletions tests/test_values/test_int_values/test_int_values_arithmatic.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
10 changes: 10 additions & 0 deletions tests/test_values/test_simple_variable.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
57 changes: 54 additions & 3 deletions tests/test_values/test_str_values/test_to_str_values.py
Original file line number Diff line number Diff line change
@@ -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
12 changes: 12 additions & 0 deletions tests/test_version.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import re
import sys
from unittest.mock import patch

import spellbind

Expand All @@ -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"