From 635bf132eea576129911a68189811d4970abd661 Mon Sep 17 00:00:00 2001 From: Georg Plaz Date: Fri, 27 Jun 2025 17:54:35 +0200 Subject: [PATCH 1/5] Combine Added/multiplied FloatValues --- src/spellbind/float_values.py | 215 +++++++++++++----- src/spellbind/values.py | 47 +++- ...oat_values.py => test_abs_float_values.py} | 41 ++-- .../test_add_float_values.py | 52 ++++- .../test_average_float_values.py | 47 ++++ .../test_clamp_float_values.py | 115 ++++++++++ .../test_compare_float_values.py | 1 - .../test_divide_float_values.py | 24 +- .../test_float_values/test_float_values.py | 131 +---------- .../test_float_values_arithmatic.py | 130 ----------- .../test_min_max_float_values.py | 96 ++++++++ .../test_mod_float_values.py | 81 +++++++ .../test_multiply_float_values.py | 52 ++++- .../test_negate_float_values.py | 34 +++ .../test_pos_float_values.py | 22 ++ .../test_pow_float_values.py | 80 +++++++ .../test_round_float_values.py | 3 + .../test_subtract_float_values.py | 33 ++- .../test_floor_divide_int_values.py | 51 +++++ 19 files changed, 906 insertions(+), 349 deletions(-) rename tests/test_values/test_float_values/{test_unary_float_values.py => test_abs_float_values.py} (50%) create mode 100644 tests/test_values/test_float_values/test_average_float_values.py create mode 100644 tests/test_values/test_float_values/test_clamp_float_values.py delete mode 100644 tests/test_values/test_float_values/test_float_values_arithmatic.py create mode 100644 tests/test_values/test_float_values/test_min_max_float_values.py create mode 100644 tests/test_values/test_float_values/test_mod_float_values.py create mode 100644 tests/test_values/test_float_values/test_negate_float_values.py create mode 100644 tests/test_values/test_float_values/test_pos_float_values.py create mode 100644 tests/test_values/test_float_values/test_pow_float_values.py create mode 100644 tests/test_values/test_int_values/test_floor_divide_int_values.py diff --git a/src/spellbind/float_values.py b/src/spellbind/float_values.py index 362cf0d..5bedc34 100644 --- a/src/spellbind/float_values.py +++ b/src/spellbind/float_values.py @@ -10,7 +10,7 @@ from spellbind.bool_values import BoolValue, BoolLike from spellbind.functions import clamp_float, multiply_all_floats from spellbind.values import Value, SimpleVariable, OneToOneValue, DerivedValueBase, Constant, TwoToOneValue, \ - SelectValue + SelectValue, NotConstantError if TYPE_CHECKING: from spellbind.int_values import IntValue, IntLike # pragma: no cover @@ -22,42 +22,51 @@ _U = TypeVar("_U") +_COMMUTATIVE_OPERATORS = { + operator.add, sum, multiply_all_floats, max, min +} + + +def _average_float(values: Sequence[float]) -> float: + return sum(values) / len(values) + + class FloatValue(Value[float], ABC): def __add__(self, other: FloatLike) -> FloatValue: - return AddFloatValues(self, other) + return FloatValue.derive_many(sum, self, other, is_associative=True) def __radd__(self, other: int | float) -> FloatValue: - return AddFloatValues(other, self) + return FloatValue.derive_many(sum, other, self, is_associative=True) def __sub__(self, other: FloatLike) -> FloatValue: - return SubtractFloatValues(self, other) + return FloatValue.derive_two(operator.sub, self, other) def __rsub__(self, other: int | float) -> FloatValue: - return SubtractFloatValues(other, self) + return FloatValue.derive_two(operator.sub, other, self) def __mul__(self, other: FloatLike) -> FloatValue: - return MultiplyFloatValues(self, other) + return FloatValue.derive_many(multiply_all_floats, self, other, is_associative=True) def __rmul__(self, other: int | float) -> FloatValue: - return MultiplyFloatValues(other, self) + return FloatValue.derive_many(multiply_all_floats, other, self, is_associative=True) def __truediv__(self, other: FloatLike) -> FloatValue: - return DivideValues(self, other) + return FloatValue.derive_two(operator.truediv, self, other) def __rtruediv__(self, other: int | float) -> FloatValue: - return DivideValues(other, self) + return FloatValue.derive_two(operator.truediv, other, self) def __pow__(self, other: FloatLike) -> FloatValue: - return PowerFloatValues(self, other) + return FloatValue.derive_two(operator.pow, self, other) def __rpow__(self, other: FloatLike) -> FloatValue: - return PowerFloatValues(other, self) + return FloatValue.derive_two(operator.pow, other, self) def __mod__(self, other: FloatLike) -> FloatValue: - return ModuloFloatValues(self, other) + return FloatValue.derive_two(operator.mod, self, other) def __rmod__(self, other: int | float) -> FloatValue: - return ModuloFloatValues(other, self) + return FloatValue.derive_two(operator.mod, other, self) def __abs__(self) -> FloatValue: return AbsFloatValue(self) @@ -101,7 +110,57 @@ def __pos__(self) -> Self: return self def clamp(self, min_value: FloatLike, max_value: FloatLike) -> FloatValue: - return ClampFloatValue(self, min_value, max_value) + return FloatValue.derive_three(clamp_float, self, min_value, max_value) + + def decompose_float_operands(self, operator_: Callable[[Sequence[float]], _S]) -> Sequence[FloatLike]: + return (self,) + + @classmethod + def min(cls, *values: FloatLike) -> FloatValue: + return cls.derive_many(min, *values, is_associative=True) + + @classmethod + def max(cls, *values: FloatLike) -> FloatValue: + return cls.derive_many(max, *values, is_associative=True) + + @classmethod + def average(cls, *values: FloatLike) -> FloatValue: + return cls.derive_many(_average_float, *values) + + @classmethod + def derive_two(cls, operator_: Callable[[float, float], float], first: FloatLike, second: FloatLike) -> FloatValue: + try: + constant_first = _get_constant_float(first) + constant_second = _get_constant_float(second) + except NotConstantError: + return TwoFloatsToFloatsValue(operator_, first, second) + else: + return FloatConstant.of(operator_(constant_first, constant_second)) + + @classmethod + def derive_three(cls, operator_: Callable[[float, float, float], float], + first: FloatLike, second: FloatLike, third: FloatLike) -> FloatValue: + try: + constant_first = _get_constant_float(first) + constant_second = _get_constant_float(second) + constant_third = _get_constant_float(third) + except NotConstantError: + return ThreeFloatToFloatValue(operator_, first, second, third) + else: + return FloatConstant.of(operator_(constant_first, constant_second, constant_third)) + + @classmethod + def derive_many(cls, operator_: Callable[[Sequence[float]], float], *values: FloatLike, is_associative: bool = False) -> FloatValue: + try: + constant_values = [_get_constant_float(v) for v in values] + except NotConstantError: + if is_associative: + flattened = [item for v in values for item in _decompose_float_operands(operator_, v)] + return ManyFloatsToFloatValue(operator_, *flattened) + else: + return ManyFloatsToFloatValue(operator_, *values) + else: + return FloatConstant.of(operator_(constant_values)) class OneToFloatValue(Generic[_S], OneToOneValue[_S, float], FloatValue): @@ -109,7 +168,27 @@ class OneToFloatValue(Generic[_S], OneToOneValue[_S, float], FloatValue): class FloatConstant(FloatValue, Constant[float]): - pass + _cache: dict[float, FloatConstant] = {} + + @classmethod + def of(cls, value: float) -> FloatConstant: + try: + return cls._cache[value] + except KeyError: + return FloatConstant(value) + + def __abs__(self): + if self.value >= 0: + return self + return FloatConstant.of(-self.value) + + def __neg__(self): + return FloatConstant.of(-self.value) + + +for _value in [*range(101)]: + FloatConstant._cache[_value] = FloatConstant(_value) + FloatConstant._cache[-_value] = FloatConstant(-_value) class FloatVariable(SimpleVariable[float], FloatValue): @@ -125,6 +204,7 @@ def _create_float_getter(value: float | Value[int] | Value[float]) -> Callable[[ class OneFloatToOneValue(DerivedValueBase[_S], Generic[_S]): def __init__(self, transformer: Callable[[float], _S], of: FloatLike): + self._of = of self._getter = _create_float_getter(of) self._transformer = transformer super().__init__(*[v for v in (of,) if isinstance(v, Value)]) @@ -137,21 +217,49 @@ def _calculate_value(self) -> _S: return self._transformer(self._getter()) -class ManyFloatToOneValue(DerivedValueBase[_S], Generic[_S]): +class OneFloatToFloatValue(OneFloatToOneValue[float], FloatValue): + pass + + +def _get_constant_float(value: FloatLike) -> float: + if isinstance(value, Value): + return value.constant_value_or_raise + return value + + +def _decompose_float_operands(operator_: Callable, value: FloatLike) -> Sequence[FloatLike]: + if isinstance(value, Value): + if isinstance(value, FloatValue): + return value.decompose_float_operands(operator_) + return value.decompose_operands(operator_) + return (value,) + + +class ManyFloatsToOneValue(DerivedValueBase[_S], Generic[_S]): def __init__(self, transformer: Callable[[Sequence[float]], _S], *values: FloatLike): - self._value_getters = [_create_float_getter(v) for v in values] + self._input_values = tuple(values) + self._value_getters = [_create_float_getter(v) for v in self._input_values] self._transformer = transformer - super().__init__(*[v for v in values if isinstance(v, Value)]) + super().__init__(*[v for v in self._input_values if isinstance(v, Value)]) def _calculate_value(self) -> _S: gotten_values = [getter() for getter in self._value_getters] return self._transformer(gotten_values) -class TwoFloatToOneValue(DerivedValueBase[_S], Generic[_S]): +class ManyFloatsToFloatValue(ManyFloatsToOneValue[float], FloatValue): + def decompose_float_operands(self, operator_: Callable) -> Sequence[FloatLike]: + if self._transformer == operator_: + return self._input_values + return (self,) + + +class TwoFloatsToOneValue(DerivedValueBase[_S], Generic[_S]): def __init__(self, transformer: Callable[[float, float], _S], first: FloatLike, second: FloatLike): self._transformer = transformer + self._of_first = first + self._of_second = second self._first_getter = _create_float_getter(first) self._second_getter = _create_float_getter(second) super().__init__(*[v for v in (first, second) if isinstance(v, Value)]) @@ -160,10 +268,20 @@ def _calculate_value(self) -> _S: return self._transformer(self._first_getter(), self._second_getter()) +class TwoFloatsToFloatsValue(TwoFloatsToOneValue[float], FloatValue): + def decompose_float_operands(self, operator_: Callable) -> Sequence[FloatLike]: + if self._transformer == operator_: + return self._of_first, self._of_second + return (self,) + + class ThreeFloatToOneValue(DerivedValueBase[_S], Generic[_S]): def __init__(self, transformer: Callable[[float, float, float], _S], first: FloatLike, second: FloatLike, third: FloatLike): self._transformer = transformer + self._of_first = first + self._of_second = second + self._of_third = third self._first_getter = _create_float_getter(first) self._second_getter = _create_float_getter(second) self._third_getter = _create_float_getter(third) @@ -173,29 +291,11 @@ def _calculate_value(self) -> _S: return self._transformer(self._first_getter(), self._second_getter(), self._third_getter()) -class MaxFloatValues(ManyFloatToOneValue[float], FloatValue): - def __init__(self, *values: FloatLike): - super().__init__(max, *values) - - -class MinFloatValues(ManyFloatToOneValue[float], FloatValue): - def __init__(self, *values: FloatLike): - super().__init__(min, *values) - - -class AddFloatValues(ManyFloatToOneValue[float], FloatValue): - def __init__(self, *values: FloatLike): - super().__init__(sum, *values) - - -class SubtractFloatValues(TwoFloatToOneValue[float], FloatValue): - def __init__(self, left: FloatLike, right: FloatLike): - super().__init__(operator.sub, left, right) - - -class MultiplyFloatValues(ManyFloatToOneValue[float], FloatValue): - def __init__(self, *values: FloatLike): - super().__init__(multiply_all_floats, *values) +class ThreeFloatToFloatValue(ThreeFloatToOneValue[float], FloatValue): + def decompose_float_operands(self, operator_: Callable) -> Sequence[FloatLike]: + if self._transformer == operator_: + return self._of_first, self._of_second, self._of_third + return (self,) class RoundFloatValue(TwoToOneValue[float, int, float], FloatValue): @@ -203,41 +303,30 @@ def __init__(self, value: FloatValue, ndigits: IntLike): super().__init__(round, value, ndigits) -class DivideValues(TwoFloatToOneValue[float], FloatValue): - def __init__(self, left: FloatLike, right: FloatLike): - super().__init__(operator.truediv, left, right) - - -class ModuloFloatValues(TwoFloatToOneValue[float], FloatValue): - def __init__(self, left: FloatLike, right: FloatLike): - super().__init__(operator.mod, left, right) - - class AbsFloatValue(OneFloatToOneValue[float], FloatValue): def __init__(self, value: FloatLike): super().__init__(abs, value) - -class PowerFloatValues(TwoFloatToOneValue[float], FloatValue): - def __init__(self, left: FloatLike, right: FloatLike): - super().__init__(operator.pow, left, right) + def __abs__(self) -> Self: + return self -class NegateFloatValue(OneFloatToOneValue[float], FloatValue): +class NegateFloatValue(OneFloatToFloatValue, FloatValue): def __init__(self, value: FloatLike): super().__init__(operator.neg, value) + def __neg__(self) -> FloatValue: + of = self._of + if isinstance(of, FloatValue): + return of + return super().__neg__() + -class CompareNumbersValues(TwoFloatToOneValue[bool], BoolValue): +class CompareNumbersValues(TwoFloatsToOneValue[bool], BoolValue): def __init__(self, left: FloatLike, right: FloatLike, op: Callable[[float, float], bool]): super().__init__(op, left, right) -class ClampFloatValue(ThreeFloatToOneValue[float], FloatValue): - def __init__(self, value: FloatLike, min_value: FloatLike, max_value: FloatLike): - super().__init__(clamp_float, value, min_value, max_value) - - class SelectFloatValue(SelectValue[float], FloatValue): def __init__(self, condition: BoolLike, if_true: float | Value[float], if_false: float | Value[float]): super().__init__(condition, if_true, if_false) diff --git a/src/spellbind/values.py b/src/spellbind/values.py index 2e5d6e1..236732a 100644 --- a/src/spellbind/values.py +++ b/src/spellbind/values.py @@ -27,6 +27,10 @@ def _create_value_getter(value: Value[_S] | _S) -> Callable[[], _S]: return lambda: value +class NotConstantError(Exception): + pass + + class Value(ValueObservable[_S], Generic[_S], ABC): @property @abstractmethod @@ -83,6 +87,13 @@ def map_to_bool(self, transformer: Callable[[_S], bool]) -> BoolValue: from spellbind.bool_values import OneToBoolValue return OneToBoolValue(transformer, self) + @property + def constant_value_or_raise(self) -> _S: + raise NotConstantError + + def decompose_operands(self, operator_: Callable) -> Sequence[Value[_S] | _S]: + return (self,) + class Variable(Value[_S], Generic[_S], ABC): @property @@ -194,6 +205,10 @@ def unobserve(self, observer: Observer | ValueObserver[_S]) -> None: def derived_from(self) -> frozenset[Value[_S]]: return EMPTY_FROZEN_SET + @property + def constant_value_or_raise(self) -> _S: + return self._value + class DerivedValueBase(Value[_S], Generic[_S], ABC): def __init__(self, *derived_from: Value): @@ -234,6 +249,7 @@ class OneToOneValue(DerivedValueBase[_T], Generic[_S, _T]): def __init__(self, transformer: Callable[[_S], _T], of: Value[_S]): self._getter = _create_value_getter(of) + self._of = of self._transformer = transformer super().__init__(*[v for v in (of,) if isinstance(v, Value)]) @@ -243,19 +259,29 @@ def _calculate_value(self) -> _T: class ManyToOneValue(DerivedValueBase[_T], Generic[_S, _T]): def __init__(self, transformer: Callable[[Sequence[_S]], _T], *values: _S | Value[_S]): - self._value_getters = [_create_value_getter(v) for v in values] + self._input_values = tuple(values) + self._value_getters = [_create_value_getter(v) for v in self._input_values] self._transformer = transformer - super().__init__(*[v for v in values if isinstance(v, Value)]) + super().__init__(*[v for v in self._input_values if isinstance(v, Value)]) def _calculate_value(self) -> _T: gotten_values = [getter() for getter in self._value_getters] return self._transformer(gotten_values) +class ManyToSameValue(ManyToOneValue[_S, _S], Generic[_S]): + def decompose_operands(self, operator_: Callable) -> Sequence[Value[_S] | _S]: + if operator_ == self._transformer: + return self._input_values + return (self,) + + class TwoToOneValue(DerivedValueBase[_U], Generic[_S, _T, _U]): def __init__(self, transformer: Callable[[_S, _T], _U], first: Value[_S] | _S, second: Value[_T] | _T): self._transformer = transformer + self._of_first = first + self._of_second = second self._first_getter = _create_value_getter(first) self._second_getter = _create_value_getter(second) super().__init__(*[v for v in (first, second) if isinstance(v, Value)]) @@ -264,10 +290,20 @@ def _calculate_value(self) -> _U: return self._transformer(self._first_getter(), self._second_getter()) +class TwoToSameValue(TwoToOneValue[_S, _S, _S], Generic[_S]): + def decompose_operands(self, operator_: Callable) -> Sequence[Value[_S] | _S]: + if operator_ == self._transformer: + return self._of_first, self._of_second + return (self,) + + class ThreeToOneValue(DerivedValueBase[_V], Generic[_S, _T, _U, _V]): def __init__(self, transformer: Callable[[_S, _T, _U], _V], first: Value[_S] | _S, second: Value[_T] | _T, third: Value[_U] | _U): self._transformer = transformer + self._of_first = first + self._of_second = second + self._of_third = third self._first_getter = _create_value_getter(first) self._second_getter = _create_value_getter(second) self._third_getter = _create_value_getter(third) @@ -277,6 +313,13 @@ def _calculate_value(self) -> _V: return self._transformer(self._first_getter(), self._second_getter(), self._third_getter()) +class ThreeToSameValue(ThreeToOneValue[_S, _S, _S, _S], Generic[_S]): + def decompose_operands(self, operator_: Callable) -> Sequence[Value[_S] | _S]: + if operator_ == self._transformer: + return self._of_first, self._of_second, self._of_third + return (self,) + + class SelectValue(ThreeToOneValue[bool, _S, _S, _S], Generic[_S]): def __init__(self, condition: BoolLike, if_true: Value[_S] | _S, if_false: Value[_S] | _S): super().__init__(lambda b, t, f: t if b else f, condition, if_true, if_false) diff --git a/tests/test_values/test_float_values/test_unary_float_values.py b/tests/test_values/test_float_values/test_abs_float_values.py similarity index 50% rename from tests/test_values/test_float_values/test_unary_float_values.py rename to tests/test_values/test_float_values/test_abs_float_values.py index 78ad1bf..f116c6c 100644 --- a/tests/test_values/test_float_values/test_unary_float_values.py +++ b/tests/test_values/test_float_values/test_abs_float_values.py @@ -1,22 +1,4 @@ -from spellbind.float_values import FloatVariable - - -def test_negate_float_value(): - v0 = FloatVariable(5.5) - v1 = -v0 - assert v1.value == -5.5 - - v0.value = -3.2 - assert v1.value == 3.2 - - -def test_negate_float_value_zero(): - v0 = FloatVariable(0.0) - v1 = -v0 - assert v1.value == 0.0 - - v0.value = 7.8 - assert v1.value == -7.8 +from spellbind.float_values import FloatVariable, FloatConstant def test_abs_float_value_positive(): @@ -44,3 +26,24 @@ def test_abs_float_value_zero(): v0.value = -7.2 assert v1.value == 7.2 + + +def test_abs_of_abs_value_is_itself(): + v0 = FloatVariable(-5.5) + v1 = abs(v0) + v2 = abs(v1) + assert v2 is v1 + + +def test_abs_of_constant_is_constant(): + v0 = FloatConstant(-5.5) + v1 = abs(v0) + assert v1.value == 5.5 + assert isinstance(v1, FloatConstant) + + +def test_abs_of_positive_constant_is_itself(): + v0 = FloatConstant(5.5) + v1 = abs(v0) + assert v1 is v0 + assert v1.value == 5.5 diff --git a/tests/test_values/test_float_values/test_add_float_values.py b/tests/test_values/test_float_values/test_add_float_values.py index 738a31f..61e510e 100644 --- a/tests/test_values/test_float_values/test_add_float_values.py +++ b/tests/test_values/test_float_values/test_add_float_values.py @@ -1,4 +1,4 @@ -from spellbind.float_values import FloatVariable +from spellbind.float_values import FloatVariable, ManyFloatsToFloatValue, FloatConstant from spellbind.int_values import IntVariable @@ -56,3 +56,53 @@ def test_add_int_plus_float_value(): v1.value = 3.5 assert v2.value == 5.5 + + +def test_add_many_values_waterfall_style_are_combined(): + v0 = FloatVariable(1.5) + v1 = FloatVariable(2.5) + v2 = FloatVariable(3.5) + v3 = FloatVariable(4.5) + + v4 = v0 + v1 + v2 + v3 + assert v4.value == 12.0 + + assert isinstance(v4, ManyFloatsToFloatValue) + assert v4._input_values == (v0, v1, v2, v3) + + +def test_add_many_values_grouped_are_combined(): + v0 = FloatVariable(1.5) + v1 = FloatVariable(2.5) + v2 = FloatVariable(3.5) + v3 = FloatVariable(4.5) + + v4 = (v0 + v1) + (v2 + v3) + assert v4.value == 12.0 + + assert isinstance(v4, ManyFloatsToFloatValue) + assert v4._input_values == (v0, v1, v2, v3) + + +def test_add_constant_to_literal_is_constant(): + v0 = FloatConstant(1.5) + v1 = 2.5 + v2 = v0 + v1 + assert v2.value == 4.0 + assert isinstance(v2, FloatConstant) + + +def test_add_constant_to_constant_is_constant(): + v0 = FloatConstant(1.5) + v1 = FloatConstant(2.5) + v2 = v0 + v1 + assert v2.value == 4.0 + assert isinstance(v2, FloatConstant) + + +def test_add_literal_to_constant_is_constant(): + v0 = 1.5 + v1 = FloatConstant(2.5) + v2 = v0 + v1 + assert v2.value == 4.0 + assert isinstance(v2, FloatConstant) diff --git a/tests/test_values/test_float_values/test_average_float_values.py b/tests/test_values/test_float_values/test_average_float_values.py new file mode 100644 index 0000000..41c812a --- /dev/null +++ b/tests/test_values/test_float_values/test_average_float_values.py @@ -0,0 +1,47 @@ +from spellbind.float_values import FloatValue, FloatVariable, FloatConstant + + +def test_min_float_values(): + v0 = FloatVariable(1.) + v1 = FloatVariable(2.) + v2 = FloatVariable(3.) + + average_val = FloatValue.average(v0, v1, v2) + assert average_val.value == 2. + + v2.value = 6. + assert average_val.value == 3. + + +def test_min_float_values_with_literals(): + v0 = FloatVariable(1.) + + average_val = FloatValue.average(v0, 2., 3.) + assert average_val.value == 2. + + v0.value = 4. + assert average_val.value == 3. + + +def test_min_int_constants_is_constant(): + v0 = FloatConstant(1) + v1 = FloatConstant(2) + v2 = FloatConstant(3) + + average_val = FloatValue.average(v0, v1, v2) + assert isinstance(average_val, FloatConstant) + + +def test_sum_averaged_float_values(): + v0 = FloatVariable(1.) + v1 = FloatVariable(2.) + v2 = FloatVariable(3.) + + average_val_0 = FloatValue.average(v0, v1, v2) + + v3 = FloatVariable(4.) + v4 = FloatVariable(5.) + average_val_1 = FloatValue.average(v3, v4) + summed_average = average_val_0 + average_val_1 + + assert summed_average.value == (1. + 2. + 3.) / 3. + (4. + 5.) / 2. diff --git a/tests/test_values/test_float_values/test_clamp_float_values.py b/tests/test_values/test_float_values/test_clamp_float_values.py new file mode 100644 index 0000000..fa30623 --- /dev/null +++ b/tests/test_values/test_float_values/test_clamp_float_values.py @@ -0,0 +1,115 @@ +from spellbind.float_values import FloatVariable, FloatConstant + + +def test_clamp_float_values_in_range(): + value = FloatVariable(15.5) + min_val = FloatVariable(10.0) + max_val = FloatVariable(20.0) + + clamped = value.clamp(min_val, max_val) + assert clamped.value == 15.5 + + +def test_clamp_float_values_below_min(): + value = FloatVariable(5.2) + min_val = FloatVariable(10.0) + max_val = FloatVariable(20.0) + + clamped = value.clamp(min_val, max_val) + assert clamped.value == 10.0 + + +def test_clamp_float_values_above_max(): + value = FloatVariable(25.8) + min_val = FloatVariable(10.0) + max_val = FloatVariable(20.0) + + clamped = value.clamp(min_val, max_val) + assert clamped.value == 20.0 + + +def test_clamp_float_values_with_literals_in_range(): + value = FloatVariable(15.5) + + clamped = value.clamp(10.0, 20.0) + assert clamped.value == 15.5 + + +def test_clamp_float_values_with_literals_below_min(): + value = FloatVariable(5.2) + + clamped = value.clamp(10.0, 20.0) + assert clamped.value == 10.0 + + +def test_clamp_float_values_with_literals_above_max(): + value = FloatVariable(25.8) + + clamped = value.clamp(10.0, 20.0) + assert clamped.value == 20.0 + + +def test_clamp_float_values_reactive_value_changes(): + value = FloatVariable(15.5) + min_val = FloatVariable(10.0) + max_val = FloatVariable(20.0) + + clamped = value.clamp(min_val, max_val) + assert clamped.value == 15.5 + + value.value = 5.2 + assert clamped.value == 10.0 + + value.value = 25.8 + assert clamped.value == 20.0 + + value.value = 12.3 + assert clamped.value == 12.3 + + +def test_clamp_float_values_reactive_bounds_changes(): + value = FloatVariable(15.5) + min_val = FloatVariable(10.0) + max_val = FloatVariable(20.0) + + clamped = value.clamp(min_val, max_val) + assert clamped.value == 15.5 + + min_val.value = 18.0 + assert clamped.value == 18.0 + + min_val.value = 11.0 + assert clamped.value == 15.5 + + max_val.value = 12.0 + assert clamped.value == 12.0 + + +def test_clamp_middle_three_constants_is_constant(): + value = FloatConstant(15.5) + min_val = FloatConstant(10.0) + max_val = FloatConstant(20.0) + + clamped = value.clamp(min_val, max_val) + assert isinstance(clamped, FloatConstant) + assert clamped.value == 15.5 + + +def test_clamp_lower_three_constants_is_constant(): + value = FloatConstant(5.2) + min_val = FloatConstant(10.0) + max_val = FloatConstant(20.0) + + clamped = value.clamp(min_val, max_val) + assert isinstance(clamped, FloatConstant) + assert clamped.value == 10.0 + + +def test_clamp_upper_three_constants_is_constant(): + value = FloatConstant(25.8) + min_val = FloatConstant(10.0) + max_val = FloatConstant(20.0) + + clamped = value.clamp(min_val, max_val) + assert isinstance(clamped, FloatConstant) + assert clamped.value == 20.0 diff --git a/tests/test_values/test_float_values/test_compare_float_values.py b/tests/test_values/test_float_values/test_compare_float_values.py index eb9a74f..e9406a9 100644 --- a/tests/test_values/test_float_values/test_compare_float_values.py +++ b/tests/test_values/test_float_values/test_compare_float_values.py @@ -1,4 +1,3 @@ -# Float Comparison Tests - Less Than from spellbind.float_values import FloatVariable from spellbind.int_values import IntVariable diff --git a/tests/test_values/test_float_values/test_divide_float_values.py b/tests/test_values/test_float_values/test_divide_float_values.py index a381c1b..49ad559 100644 --- a/tests/test_values/test_float_values/test_divide_float_values.py +++ b/tests/test_values/test_float_values/test_divide_float_values.py @@ -1,4 +1,4 @@ -from spellbind.float_values import FloatVariable +from spellbind.float_values import FloatVariable, FloatConstant from spellbind.int_values import IntVariable @@ -56,3 +56,25 @@ def test_truediv_float_values(): v0.value = 15.0 assert v2.value == 3.75 + + +def test_truediv_constant_constant_is_constant(): + v0 = FloatConstant(10.0) + v1 = FloatConstant(4.0) + v2 = v0 / v1 + assert v2.value == 2.5 + assert isinstance(v2, FloatConstant) + + +def test_truediv_literal_constant_is_constant(): + v0 = FloatConstant(10.0) + v2 = v0 / 4.0 + assert v2.value == 2.5 + assert isinstance(v2, FloatConstant) + + +def test_truediv_constant_literal_is_constant(): + v0 = FloatConstant(4.0) + v2 = 10.0 / v0 + assert v2.value == 2.5 + assert isinstance(v2, FloatConstant) diff --git a/tests/test_values/test_float_values/test_float_values.py b/tests/test_values/test_float_values/test_float_values.py index 9dd5d6a..6401752 100644 --- a/tests/test_values/test_float_values/test_float_values.py +++ b/tests/test_values/test_float_values/test_float_values.py @@ -1,7 +1,6 @@ import gc -from spellbind.float_values import FloatConstant, MaxFloatValues, MinFloatValues, FloatVariable -from spellbind.values import SimpleVariable +from spellbind.float_values import FloatConstant, FloatVariable def test_float_constant_str(): @@ -9,50 +8,6 @@ def test_float_constant_str(): assert str(const) == "3.14" -def test_max_float_values(): - a = SimpleVariable(10.5) - b = SimpleVariable(20.3) - c = SimpleVariable(5.7) - - max_val = MaxFloatValues(a, b, c) - assert max_val.value == 20.3 - - a.value = 30.1 - assert max_val.value == 30.1 - - -def test_max_float_values_with_literals(): - a = SimpleVariable(10.5) - - max_val = MaxFloatValues(a, 25.7, 15.2) - assert max_val.value == 25.7 - - a.value = 30.1 - assert max_val.value == 30.1 - - -def test_min_float_values(): - a = SimpleVariable(10.5) - b = SimpleVariable(20.3) - c = SimpleVariable(5.7) - - min_val = MinFloatValues(a, b, c) - assert min_val.value == 5.7 - - c.value = 2.1 - assert min_val.value == 2.1 - - -def test_min_float_values_with_literals(): - a = SimpleVariable(10.5) - - min_val = MinFloatValues(a, 25.7, 15.2) - assert min_val.value == 10.5 - - a.value = 5.1 - assert min_val.value == 5.1 - - def test_add_float_values_keeps_reference(): v0 = FloatVariable(1.5) v1 = FloatVariable(2.5) @@ -76,87 +31,3 @@ def test_add_int_values_garbage_collected(): v1.value = 4.5 # trigger removal of weak references assert len(v0._on_change._subscriptions) == 0 assert len(v1._on_change._subscriptions) == 0 - - -def test_clamp_float_values_in_range(): - value = FloatVariable(15.5) - min_val = FloatVariable(10.0) - max_val = FloatVariable(20.0) - - clamped = value.clamp(min_val, max_val) - assert clamped.value == 15.5 - - -def test_clamp_float_values_below_min(): - value = FloatVariable(5.2) - min_val = FloatVariable(10.0) - max_val = FloatVariable(20.0) - - clamped = value.clamp(min_val, max_val) - assert clamped.value == 10.0 - - -def test_clamp_float_values_above_max(): - value = FloatVariable(25.8) - min_val = FloatVariable(10.0) - max_val = FloatVariable(20.0) - - clamped = value.clamp(min_val, max_val) - assert clamped.value == 20.0 - - -def test_clamp_float_values_with_literals_in_range(): - value = FloatVariable(15.5) - - clamped = value.clamp(10.0, 20.0) - assert clamped.value == 15.5 - - -def test_clamp_float_values_with_literals_below_min(): - value = FloatVariable(5.2) - - clamped = value.clamp(10.0, 20.0) - assert clamped.value == 10.0 - - -def test_clamp_float_values_with_literals_above_max(): - value = FloatVariable(25.8) - - clamped = value.clamp(10.0, 20.0) - assert clamped.value == 20.0 - - -def test_clamp_float_values_reactive_value_changes(): - value = FloatVariable(15.5) - min_val = FloatVariable(10.0) - max_val = FloatVariable(20.0) - - clamped = value.clamp(min_val, max_val) - assert clamped.value == 15.5 - - value.value = 5.2 - assert clamped.value == 10.0 - - value.value = 25.8 - assert clamped.value == 20.0 - - value.value = 12.3 - assert clamped.value == 12.3 - - -def test_clamp_float_values_reactive_bounds_changes(): - value = FloatVariable(15.5) - min_val = FloatVariable(10.0) - max_val = FloatVariable(20.0) - - clamped = value.clamp(min_val, max_val) - assert clamped.value == 15.5 - - min_val.value = 18.0 - assert clamped.value == 18.0 - - min_val.value = 11.0 - assert clamped.value == 15.5 - - max_val.value = 12.0 - assert clamped.value == 12.0 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 deleted file mode 100644 index 7a382e9..0000000 --- a/tests/test_values/test_float_values/test_float_values_arithmatic.py +++ /dev/null @@ -1,130 +0,0 @@ -from spellbind.float_values import FloatVariable -from spellbind.int_values import IntVariable - - -# Power Tests -def test_power_float_values(): - v0 = FloatVariable(2.0) - v1 = FloatVariable(3.0) - v2 = v0 ** v1 - assert v2.value == 8.0 - - v0.value = 3.0 - assert v2.value == 27.0 - - -def test_power_float_value_to_float(): - v0 = FloatVariable(2.0) - v2 = v0 ** 3.0 - assert v2.value == 8.0 - - v0.value = 3.0 - assert v2.value == 27.0 - - -def test_power_float_value_to_int(): - v0 = FloatVariable(2.5) - v2 = v0 ** 2 - assert v2.value == 6.25 - - v0.value = 3.0 - assert v2.value == 9.0 - - -def test_power_float_value_to_int_value(): - v0 = FloatVariable(2.0) - v1 = IntVariable(3) - v2 = v0 ** v1 - assert v2.value == 8.0 - - v0.value = 3.0 - assert v2.value == 27.0 - - -def test_power_float_to_float_value(): - v1 = FloatVariable(3.0) - v2 = 2.0 ** v1 - assert v2.value == 8.0 - - v1.value = 4.0 - assert v2.value == 16.0 - - -def test_power_int_to_float_value(): - v1 = FloatVariable(3.0) - v2 = 2 ** v1 - assert v2.value == 8.0 - - v1.value = 4.0 - assert v2.value == 16.0 - - -# Modulo Tests -def test_modulo_float_values(): - v0 = FloatVariable(10.5) - v1 = FloatVariable(3.0) - v2 = v0 % v1 - assert v2.value == 1.5 - - v0.value = 15.5 - assert v2.value == 0.5 - - -def test_modulo_float_value_by_float(): - v0 = FloatVariable(10.5) - v2 = v0 % 3.0 - assert v2.value == 1.5 - - v0.value = 15.5 - assert v2.value == 0.5 - - -def test_modulo_float_value_by_int(): - v0 = FloatVariable(10.5) - v2 = v0 % 3 - assert v2.value == 1.5 - - v0.value = 15.5 - assert v2.value == 0.5 - - -def test_modulo_float_value_by_int_value(): - v0 = FloatVariable(10.5) - v1 = IntVariable(3) - v2 = v0 % v1 - assert v2.value == 1.5 - - v0.value = 15.5 - assert v2.value == 0.5 - - -def test_modulo_float_by_float_value(): - v1 = FloatVariable(3.0) - v2 = 10.5 % v1 - assert v2.value == 1.5 - - v1.value = 4.0 - assert v2.value == 2.5 - - -def test_modulo_int_by_float_value(): - v1 = FloatVariable(3.0) - v2 = 10 % v1 - assert v2.value == 1.0 - - 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_float_values/test_min_max_float_values.py b/tests/test_values/test_float_values/test_min_max_float_values.py new file mode 100644 index 0000000..a6a33ab --- /dev/null +++ b/tests/test_values/test_float_values/test_min_max_float_values.py @@ -0,0 +1,96 @@ +from spellbind.float_values import FloatConstant, FloatValue, FloatVariable, ManyFloatsToFloatValue +from spellbind.values import SimpleVariable + + +def test_min_float_values(): + v0 = SimpleVariable(10.5) + v1 = SimpleVariable(20.3) + v2 = SimpleVariable(5.7) + + min_val = FloatValue.min(v0, v1, v2) + assert min_val.value == 5.7 + + v2.value = 2.1 + assert min_val.value == 2.1 + + +def test_min_float_values_with_literals(): + v0 = SimpleVariable(10.5) + + min_val = FloatValue.min(v0, 25.7, 15.2) + assert min_val.value == 10.5 + + v0.value = 5.1 + assert min_val.value == 5.1 + + +def test_min_int_constants_is_constant(): + v0 = FloatConstant(10.5) + v1 = FloatConstant(20.3) + v2 = FloatConstant(5.7) + + min_val = FloatValue.min(v0, v1, v2) + assert isinstance(min_val, FloatConstant) + + +def test_max_float_values(): + v0 = SimpleVariable(10.5) + v1 = SimpleVariable(20.3) + v2 = SimpleVariable(5.7) + + max_val = FloatValue.max(v0, v1, v2) + assert max_val.value == 20.3 + + v0.value = 30.1 + assert max_val.value == 30.1 + + +def test_max_float_values_with_literals(): + v0 = SimpleVariable(10.5) + + max_val = FloatValue.max(v0, 25.7, 15.2) + assert max_val.value == 25.7 + + v0.value = 30.1 + assert max_val.value == 30.1 + + +def test_max_int_constants_is_constant(): + v0 = FloatConstant(10.5) + v1 = FloatConstant(20.3) + v2 = FloatConstant(5.7) + + max_val = FloatValue.max(v0, v1, v2) + assert isinstance(max_val, FloatConstant) + + +def test_flattens_min_values(): + v0 = FloatVariable(10.5) + v1 = FloatVariable(20.3) + v2 = FloatVariable(5.7) + + min_val_0 = FloatValue.min(v0, v1, v2) + + v3 = FloatVariable(15.0) + v4 = FloatVariable(25.0) + min_val_1 = FloatValue.min(v3, v4) + flattened_min_val = FloatValue.min(min_val_0, min_val_1) + assert flattened_min_val.value == 5.7 + assert isinstance(flattened_min_val, ManyFloatsToFloatValue) + assert flattened_min_val._input_values == (v0, v1, v2, v3, v4) + + +def test_flattens_max_values(): + v0 = FloatVariable(10.5) + v1 = FloatVariable(20.3) + v2 = FloatVariable(5.7) + + max_val_0 = FloatValue.max(v0, v1, v2) + + v3 = FloatVariable(15.0) + v4 = FloatVariable(25.0) + max_val_1 = FloatValue.max(v3, v4) + flattened_max_val = FloatValue.max(max_val_0, max_val_1) + assert flattened_max_val.value == 25.0 + assert isinstance(flattened_max_val, ManyFloatsToFloatValue) + assert flattened_max_val._input_values == (v0, v1, v2, v3, v4) diff --git a/tests/test_values/test_float_values/test_mod_float_values.py b/tests/test_values/test_float_values/test_mod_float_values.py new file mode 100644 index 0000000..67b5d09 --- /dev/null +++ b/tests/test_values/test_float_values/test_mod_float_values.py @@ -0,0 +1,81 @@ +from spellbind.float_values import FloatVariable, FloatConstant +from spellbind.int_values import IntVariable + + +def test_modulo_float_values(): + v0 = FloatVariable(10.5) + v1 = FloatVariable(3.0) + v2 = v0 % v1 + assert v2.value == 1.5 + + v0.value = 15.5 + assert v2.value == 0.5 + + +def test_modulo_float_value_by_float(): + v0 = FloatVariable(10.5) + v2 = v0 % 3.0 + assert v2.value == 1.5 + + v0.value = 15.5 + assert v2.value == 0.5 + + +def test_modulo_float_value_by_int(): + v0 = FloatVariable(10.5) + v2 = v0 % 3 + assert v2.value == 1.5 + + v0.value = 15.5 + assert v2.value == 0.5 + + +def test_modulo_float_value_by_int_value(): + v0 = FloatVariable(10.5) + v1 = IntVariable(3) + v2 = v0 % v1 + assert v2.value == 1.5 + + v0.value = 15.5 + assert v2.value == 0.5 + + +def test_modulo_float_by_float_value(): + v1 = FloatVariable(3.0) + v2 = 10.5 % v1 + assert v2.value == 1.5 + + v1.value = 4.0 + assert v2.value == 2.5 + + +def test_modulo_int_by_float_value(): + v1 = FloatVariable(3.0) + v2 = 10 % v1 + assert v2.value == 1.0 + + v1.value = 4.0 + assert v2.value == 2.0 + + +def test_modulo_constant_constant_is_constant(): + v0 = FloatConstant(10.5) + v1 = FloatConstant(3.0) + v2 = v0 % v1 + assert v2.value == 1.5 + assert isinstance(v2, FloatConstant) + + +def test_modulo_constant_literal_is_constant(): + v0 = FloatConstant(10.5) + v2 = v0 % 3.0 + assert v2.value == 1.5 + assert isinstance(v2, FloatConstant) + + +def test_modulo_literal_const_is_constant(): + v0 = 10.5 + v1 = FloatConstant(3.0) + v2 = v0 % v1 + assert v2.value == 1.5 + assert isinstance(v2, FloatConstant) diff --git a/tests/test_values/test_float_values/test_multiply_float_values.py b/tests/test_values/test_float_values/test_multiply_float_values.py index d0205e3..8aeb8a9 100644 --- a/tests/test_values/test_float_values/test_multiply_float_values.py +++ b/tests/test_values/test_float_values/test_multiply_float_values.py @@ -1,4 +1,4 @@ -from spellbind.float_values import FloatVariable +from spellbind.float_values import FloatVariable, ManyFloatsToFloatValue, FloatConstant from spellbind.int_values import IntVariable @@ -56,3 +56,53 @@ def test_multiply_int_times_float_value(): v1.value = 4.0 assert v2.value == 8.0 + + +def test_multiply_many_values_waterfall_style_are_combined(): + v0 = FloatVariable(1.5) + v1 = FloatVariable(2.5) + v2 = FloatVariable(3.5) + v3 = FloatVariable(4.5) + + v4 = v0 * v1 * v2 * v3 + assert v4.value == 59.0625 + + assert isinstance(v4, ManyFloatsToFloatValue) + assert v4._input_values == (v0, v1, v2, v3) + + +def test_multiply_many_values_grouped_are_combined(): + v0 = FloatVariable(1.5) + v1 = FloatVariable(2.5) + v2 = FloatVariable(3.5) + v3 = FloatVariable(4.5) + + v4 = (v0 * v1) * (v2 * v3) + assert v4.value == 59.0625 + + assert isinstance(v4, ManyFloatsToFloatValue) + assert v4._input_values == (v0, v1, v2, v3) + + +def test_multiply_constant_by_literal_is_constant(): + v0 = FloatConstant(1.5) + v1 = 2.5 + v2 = v0 * v1 + assert v2.value == 3.75 + assert isinstance(v2, FloatConstant) + + +def test_multiply_constant_by_constant_is_constant(): + v0 = FloatConstant(1.5) + v1 = FloatConstant(2.5) + v2 = v0 * v1 + assert v2.value == 3.75 + assert isinstance(v2, FloatConstant) + + +def test_multiply_literal_by_constant_is_constant(): + v0 = 1.5 + v1 = FloatConstant(2.5) + v2 = v0 * v1 + assert v2.value == 3.75 + assert isinstance(v2, FloatConstant) diff --git a/tests/test_values/test_float_values/test_negate_float_values.py b/tests/test_values/test_float_values/test_negate_float_values.py new file mode 100644 index 0000000..5e96d4e --- /dev/null +++ b/tests/test_values/test_float_values/test_negate_float_values.py @@ -0,0 +1,34 @@ +from spellbind.float_values import FloatVariable, FloatConstant + + +def test_negate_float_value(): + v0 = FloatVariable(5.5) + v1 = -v0 + assert v1.value == -5.5 + + v0.value = -3.2 + assert v1.value == 3.2 + + +def test_negate_float_value_zero(): + v0 = FloatVariable(0.0) + v1 = -v0 + assert v1.value == 0.0 + + v0.value = 7.8 + assert v1.value == -7.8 + + +def test_negate_float_constant_is_constant(): + v0 = FloatConstant(5.5) + v1 = -v0 + assert v1.value == -5.5 + + assert isinstance(v0, FloatConstant) + + +def test_negate_variable_twice_is_same(): + v0 = FloatVariable(5.5) + v1 = -v0 + v2 = -v1 + assert v0 is v2 diff --git a/tests/test_values/test_float_values/test_pos_float_values.py b/tests/test_values/test_float_values/test_pos_float_values.py new file mode 100644 index 0000000..1621f9e --- /dev/null +++ b/tests/test_values/test_float_values/test_pos_float_values.py @@ -0,0 +1,22 @@ +from spellbind.float_values import FloatVariable, FloatConstant + + +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 + + +def test_float_pos_constant_is_constant(): + const = FloatConstant(3.14) + result = +const + assert result is const + assert result.value == 3.14 diff --git a/tests/test_values/test_float_values/test_pow_float_values.py b/tests/test_values/test_float_values/test_pow_float_values.py new file mode 100644 index 0000000..99c4011 --- /dev/null +++ b/tests/test_values/test_float_values/test_pow_float_values.py @@ -0,0 +1,80 @@ +from spellbind.float_values import FloatVariable, FloatConstant +from spellbind.int_values import IntVariable + + +def test_power_float_to_float_value(): + v1 = FloatVariable(3.0) + v2 = 2.0 ** v1 + assert v2.value == 8.0 + + v1.value = 4.0 + assert v2.value == 16.0 + + +def test_power_float_values(): + v0 = FloatVariable(2.0) + v1 = FloatVariable(3.0) + v2 = v0 ** v1 + assert v2.value == 8.0 + + v0.value = 3.0 + assert v2.value == 27.0 + + +def test_power_float_value_to_float(): + v0 = FloatVariable(2.0) + v2 = v0 ** 3.0 + assert v2.value == 8.0 + + v0.value = 3.0 + assert v2.value == 27.0 + + +def test_power_float_value_to_int(): + v0 = FloatVariable(2.5) + v2 = v0 ** 2 + assert v2.value == 6.25 + + v0.value = 3.0 + assert v2.value == 9.0 + + +def test_power_float_value_to_int_value(): + v0 = FloatVariable(2.0) + v1 = IntVariable(3) + v2 = v0 ** v1 + assert v2.value == 8.0 + + v0.value = 3.0 + assert v2.value == 27.0 + + +def test_power_int_to_float_value(): + v1 = FloatVariable(3.0) + v2 = 2 ** v1 + assert v2.value == 8.0 + + v1.value = 4.0 + assert v2.value == 16.0 + + +def test_power_constant_to_constant(): + v0 = FloatConstant(2.0) + v1 = FloatConstant(3.0) + v2 = v0 ** v1 + assert v2.value == 8.0 + assert isinstance(v2, FloatConstant) + + +def test_power_literal_to_constant(): + v0 = FloatConstant(2.0) + v2 = 3.0 ** v0 + assert v2.value == 9.0 + assert isinstance(v2, FloatConstant) + + +def test_power_constant_to_literal(): + v0 = FloatConstant(2.0) + v2 = v0 ** 3.0 + assert v2.value == 8.0 + assert isinstance(v2, FloatConstant) diff --git a/tests/test_values/test_float_values/test_round_float_values.py b/tests/test_values/test_float_values/test_round_float_values.py index 2fe7297..7e4d752 100644 --- a/tests/test_values/test_float_values/test_round_float_values.py +++ b/tests/test_values/test_float_values/test_round_float_values.py @@ -56,4 +56,7 @@ def test_round_float_change_comma_doesnt_change_int_value(): rounded.observe(observer) assert rounded.value == 3 + v0.value = 3.3 + assert rounded.value == 3 + observer.assert_not_called() diff --git a/tests/test_values/test_float_values/test_subtract_float_values.py b/tests/test_values/test_float_values/test_subtract_float_values.py index 1a7ba0a..4158cc8 100644 --- a/tests/test_values/test_float_values/test_subtract_float_values.py +++ b/tests/test_values/test_float_values/test_subtract_float_values.py @@ -1,4 +1,4 @@ -from spellbind.float_values import FloatVariable +from spellbind.float_values import FloatVariable, FloatConstant from spellbind.int_values import IntVariable @@ -56,3 +56,34 @@ def test_subtract_int_minus_float_value(): v1.value = 1.5 assert v2.value == 3.5 + + +def test_subtract_constant_from_constant_is_constant(): + v0 = FloatConstant(5.5) + v1 = FloatConstant(2.5) + v2 = v0 - v1 + assert v2.value == 3.0 + assert isinstance(v2, FloatConstant) + + +def test_subtract_literal_from_constant_is_constant(): + v0 = FloatConstant(5.5) + v2 = v0 - 2.5 + assert v2.value == 3.0 + assert isinstance(v2, FloatConstant) + + +def test_subtract_constant_from_literal_is_constant(): + v0 = FloatConstant(5.5) + v2 = 8.0 - v0 + assert v2.value == 2.5 + assert isinstance(v2, FloatConstant) + + +def test_subtract_twice(): + v0 = FloatVariable(5.5) + v1 = FloatVariable(2.5) + v2 = FloatVariable(1.0) + + v3 = (v0 - v1) - v2 + assert v3.value == 2.0 diff --git a/tests/test_values/test_int_values/test_floor_divide_int_values.py b/tests/test_values/test_int_values/test_floor_divide_int_values.py new file mode 100644 index 0000000..826cd04 --- /dev/null +++ b/tests/test_values/test_int_values/test_floor_divide_int_values.py @@ -0,0 +1,51 @@ +from spellbind.int_values import IntVariable, IntConstant + + +def test_floordiv_int_values(): + v0 = IntVariable(10) + v1 = IntVariable(3) + v2 = v0 // v1 + assert v2.value == 3 + + v0.value = 15 + assert v2.value == 5 + + +def test_floordiv_int_value_by_int(): + v0 = IntVariable(10) + v2 = v0 // 3 + assert v2.value == 3 + + v0.value = 15 + assert v2.value == 5 + + +def test_floordiv_int_divided_by_int_value(): + v1 = IntVariable(3) + v2 = 10 // v1 + assert v2.value == 3 + + v1.value = 4 + assert v2.value == 2 + + +def test_floordiv_constant_constant_is_constant(): + v0 = IntConstant(10) + v1 = IntConstant(3) + v2 = v0 // v1 + assert v2.value == 3 + assert isinstance(v2, IntConstant) + + +def test_floordiv_literal_constant_is_constant(): + v0 = IntConstant(10) + v2 = v0 // 3 + assert v2.value == 3 + assert isinstance(v2, IntConstant) + + +def test_floordiv_constant_literal_is_constant(): + v0 = IntConstant(3) + v2 = 10 // v0 + assert v2.value == 3 + assert isinstance(v2, IntConstant) From 98a72fe50b83dfdcab6f326d334f12b637bbe4c5 Mon Sep 17 00:00:00 2001 From: Georg Plaz Date: Sun, 29 Jun 2025 10:00:29 +0200 Subject: [PATCH 2/5] Combine Added/multiplied IntValues --- src/spellbind/bool_values.py | 13 +- src/spellbind/float_values.py | 47 +++-- src/spellbind/int_values.py | 197 +++++++++++------- src/spellbind/values.py | 14 -- .../test_float_values/test_float_constant.py | 19 ++ .../test_float_values/test_float_values.py | 13 +- .../test_round_float_values.py | 8 +- .../test_int_values/test_abs_int_values.py | 49 +++++ .../test_int_values/test_add_int_values.py | 52 ++++- .../test_int_values/test_clamp_int_values.py | 115 ++++++++++ .../test_compare_int_values.py | 2 - .../test_int_values/test_divide_int_values.py | 46 ++-- .../test_int_values/test_int_constants.py | 13 ++ .../test_int_values/test_int_values.py | 146 ++----------- .../test_int_values_arithmatic.py | 72 ------- .../test_min_max_int_values.py | 65 ++++++ .../test_int_values/test_mod_int_values.py | 52 +++++ .../test_multiply_int_values.py | 52 ++++- .../test_int_values/test_negate_int_values.py | 34 +++ .../test_int_values/test_pos_int_values.py | 22 ++ .../test_int_values/test_pow_int_values.py | 51 +++++ .../test_subtract_int_values.py | 24 ++- .../test_int_values/test_unary_int_values.py | 46 ---- 23 files changed, 765 insertions(+), 387 deletions(-) create mode 100644 tests/test_values/test_float_values/test_float_constant.py create mode 100644 tests/test_values/test_int_values/test_abs_int_values.py create mode 100644 tests/test_values/test_int_values/test_clamp_int_values.py create mode 100644 tests/test_values/test_int_values/test_int_constants.py delete mode 100644 tests/test_values/test_int_values/test_int_values_arithmatic.py create mode 100644 tests/test_values/test_int_values/test_min_max_int_values.py create mode 100644 tests/test_values/test_int_values/test_mod_int_values.py create mode 100644 tests/test_values/test_int_values/test_negate_int_values.py create mode 100644 tests/test_values/test_int_values/test_pos_int_values.py create mode 100644 tests/test_values/test_int_values/test_pow_int_values.py delete mode 100644 tests/test_values/test_int_values/test_unary_int_values.py diff --git a/src/spellbind/bool_values.py b/src/spellbind/bool_values.py index a245962..fccc416 100644 --- a/src/spellbind/bool_values.py +++ b/src/spellbind/bool_values.py @@ -102,7 +102,18 @@ def __init__(self, left: BoolLike, right: BoolLike): class BoolConstant(BoolValue, Constant[bool]): - pass + @classmethod + def of(cls, value: bool) -> BoolConstant: + if value: + return TRUE + return FALSE + + def logical_not(self) -> BoolConstant: + return BoolConstant.of(not self.value) + + @property + def constant_value_or_raise(self) -> bool: + return self.value class BoolVariable(SimpleVariable[bool], BoolValue): diff --git a/src/spellbind/float_values.py b/src/spellbind/float_values.py index 5bedc34..9fb7e96 100644 --- a/src/spellbind/float_values.py +++ b/src/spellbind/float_values.py @@ -1,5 +1,6 @@ from __future__ import annotations +import math import operator from abc import ABC from typing import Generic, Callable, Sequence, TypeVar, overload @@ -9,8 +10,8 @@ from spellbind.bool_values import BoolValue, BoolLike from spellbind.functions import clamp_float, multiply_all_floats -from spellbind.values import Value, SimpleVariable, OneToOneValue, DerivedValueBase, Constant, TwoToOneValue, \ - SelectValue, NotConstantError +from spellbind.values import Value, SimpleVariable, OneToOneValue, DerivedValueBase, Constant, SelectValue, \ + NotConstantError if TYPE_CHECKING: from spellbind.int_values import IntValue, IntLike # pragma: no cover @@ -72,12 +73,14 @@ def __abs__(self) -> FloatValue: return AbsFloatValue(self) def floor(self) -> IntValue: - from spellbind.int_values import FloorFloatValue - return FloorFloatValue(self) + from spellbind.int_values import IntValue + floor_fun: Callable[[float], int] = math.floor + return IntValue.derive_one(floor_fun, self) def ceil(self) -> IntValue: - from spellbind.int_values import CeilFloatValue - return CeilFloatValue(self) + from spellbind.int_values import IntValue + ceil_fun: Callable[[float], int] = math.ceil + return IntValue.derive_one(ceil_fun, self) @overload def round(self) -> IntValue: ... @@ -87,9 +90,11 @@ def round(self, ndigits: IntLike) -> FloatValue: ... def round(self, ndigits: IntLike | None = None) -> FloatValue | IntValue: if ndigits is None: - from spellbind.int_values import RoundFloatToIntValue - return RoundFloatToIntValue(self) - return RoundFloatValue(self, ndigits) + from spellbind.int_values import IntValue + round_to_int_fun: Callable[[float], int] = round + return IntValue.derive_one(round_to_int_fun, self) + round_fun: Callable[[float, int], float] = round + return FloatValue.derive_two(round_fun, self, ndigits) def __lt__(self, other: FloatLike) -> BoolValue: return CompareNumbersValues(self, other, operator.lt) @@ -128,7 +133,24 @@ def average(cls, *values: FloatLike) -> FloatValue: return cls.derive_many(_average_float, *values) @classmethod - def derive_two(cls, operator_: Callable[[float, float], float], first: FloatLike, second: FloatLike) -> FloatValue: + def derive_one(cls, transformer: Callable[[float], float], of: FloatLike) -> FloatValue: + try: + constant_value = _get_constant_float(of) + except NotConstantError: + return OneFloatToFloatValue(transformer, of) + else: + return FloatConstant.of(transformer(constant_value)) + + @classmethod + @overload + def derive_two(cls, operator_: Callable[[float, int], float], first: FloatLike, second: IntLike) -> FloatValue: ... + + @classmethod + @overload + def derive_two(cls, operator_: Callable[[float, float], float], first: FloatLike, second: FloatLike) -> FloatValue: ... + + @classmethod + def derive_two(cls, operator_, first, second) -> FloatValue: try: constant_first = _get_constant_float(first) constant_second = _get_constant_float(second) @@ -298,11 +320,6 @@ def decompose_float_operands(self, operator_: Callable) -> Sequence[FloatLike]: return (self,) -class RoundFloatValue(TwoToOneValue[float, int, float], FloatValue): - def __init__(self, value: FloatValue, ndigits: IntLike): - super().__init__(round, value, ndigits) - - class AbsFloatValue(OneFloatToOneValue[float], FloatValue): def __init__(self, value: FloatLike): super().__init__(abs, value) diff --git a/src/spellbind/int_values.py b/src/spellbind/int_values.py index eef1ff6..315d442 100644 --- a/src/spellbind/int_values.py +++ b/src/spellbind/int_values.py @@ -1,24 +1,25 @@ from __future__ import annotations -import math import operator from abc import ABC -from typing import overload, Generic +from typing import overload, Generic, Callable, Sequence from typing_extensions import Self, TypeVar from spellbind.bool_values import BoolValue, BoolLike -from spellbind.float_values import FloatValue, MultiplyFloatValues, DivideValues, SubtractFloatValues, \ - AddFloatValues, CompareNumbersValues -from spellbind.functions import clamp_int, multiply_all_ints -from spellbind.values import Value, ManyToOneValue, SimpleVariable, TwoToOneValue, OneToOneValue, Constant, \ - ThreeToOneValue, SelectValue +from spellbind.float_values import FloatValue, \ + CompareNumbersValues +from spellbind.functions import clamp_int, multiply_all_ints, multiply_all_floats +from spellbind.values import Value, SimpleVariable, TwoToOneValue, OneToOneValue, Constant, \ + ThreeToOneValue, SelectValue, NotConstantError, ManyToSameValue IntLike = int | Value[int] FloatLike = IntLike | float | FloatValue _S = TypeVar('_S') +_T = TypeVar('_T') +_U = TypeVar('_U') class IntValue(Value[int], ABC): @@ -30,8 +31,8 @@ def __add__(self, other: float | FloatValue) -> FloatValue: ... def __add__(self, other: FloatLike) -> IntValue | FloatValue: if isinstance(other, (float, FloatValue)): - return AddFloatValues(self, other) - return AddIntValues(self, other) + return FloatValue.derive_many(sum, self, other, is_associative=True) + return IntValue.derive_many(sum, self, other, is_associative=True) @overload def __radd__(self, other: int) -> IntValue: ... @@ -41,8 +42,8 @@ def __radd__(self, other: float) -> FloatValue: ... def __radd__(self, other: int | float) -> IntValue | FloatValue: if isinstance(other, float): - return AddFloatValues(other, self) - return AddIntValues(other, self) + return FloatValue.derive_many(sum, other, self, is_associative=True) + return IntValue.derive_many(sum, other, self, is_associative=True) @overload def __sub__(self, other: IntLike) -> IntValue: ... @@ -52,8 +53,8 @@ def __sub__(self, other: float | FloatValue) -> FloatValue: ... def __sub__(self, other: FloatLike) -> IntValue | FloatValue: if isinstance(other, (float, FloatValue)): - return SubtractFloatValues(self, other) - return SubtractIntValues(self, other) + return FloatValue.derive_two(operator.sub, self, other) + return IntValue.derive_two(operator.sub, self, other) @overload def __rsub__(self, other: int) -> IntValue: ... @@ -63,8 +64,8 @@ def __rsub__(self, other: float) -> FloatValue: ... def __rsub__(self, other: int | float) -> IntValue | FloatValue: if isinstance(other, float): - return SubtractFloatValues(other, self) - return SubtractIntValues(other, self) + return FloatValue.derive_two(operator.sub, other, self) + return IntValue.derive_two(operator.sub, other, self) @overload def __mul__(self, other: IntLike) -> IntValue: ... @@ -74,8 +75,8 @@ def __mul__(self, other: float | FloatValue) -> FloatValue: ... def __mul__(self, other: FloatLike) -> IntValue | FloatValue: if isinstance(other, (float, FloatValue)): - return MultiplyFloatValues(self, other) - return MultiplyIntValues(self, other) + return FloatValue.derive_many(multiply_all_floats, self, other, is_associative=True) + return IntValue.derive_many(multiply_all_ints, self, other, is_associative=True) @overload def __rmul__(self, other: int) -> IntValue: ... @@ -85,32 +86,32 @@ def __rmul__(self, other: float) -> FloatValue: ... def __rmul__(self, other: int | float) -> IntValue | FloatValue: if isinstance(other, float): - return MultiplyFloatValues(other, self) - return MultiplyIntValues(other, self) + return FloatValue.derive_many(multiply_all_floats, other, self, is_associative=True) + return IntValue.derive_many(multiply_all_ints, other, self, is_associative=True) def __truediv__(self, other: FloatLike) -> FloatValue: - return DivideValues(self, other) + return FloatValue.derive_two(operator.truediv, self, other) def __rtruediv__(self, other: int | float) -> FloatValue: - return DivideValues(other, self) + return FloatValue.derive_two(operator.truediv, other, self) def __floordiv__(self, other: IntLike) -> IntValue: - return FloorDivideIntValues(self, other) + return IntValue.derive_two(operator.floordiv, self, other) def __rfloordiv__(self, other: int) -> IntValue: - return FloorDivideIntValues(other, self) + return IntValue.derive_two(operator.floordiv, other, self) def __pow__(self, other: IntLike) -> IntValue: - return PowerIntValues(self, other) + return IntValue.derive_two(operator.pow, self, other) def __rpow__(self, other: int) -> IntValue: - return PowerIntValues(other, self) + return IntValue.derive_two(operator.pow, other, self) def __mod__(self, other: IntLike) -> IntValue: - return ModuloIntValues(self, other) + return IntValue.derive_two(operator.mod, self, other) def __rmod__(self, other: int) -> IntValue: - return ModuloIntValues(other, self) + return IntValue.derive_two(operator.mod, other, self) def __abs__(self) -> IntValue: return AbsIntValue(self) @@ -134,89 +135,137 @@ def __pos__(self) -> Self: return self def clamp(self, min_value: IntLike, max_value: IntLike) -> IntValue: - return ClampIntValue(self, min_value, max_value) + return IntValue.derive_three(clamp_int, self, min_value, max_value) + + @classmethod + def min(cls, *values: IntLike) -> IntValue: + return IntValue.derive_many(min, *values, is_associative=True) + + @classmethod + def max(cls, *values: IntLike) -> IntValue: + return IntValue.derive_many(max, *values, is_associative=True) + + @classmethod + def derive_one(cls, operator_: Callable[[_S], int], value: _S | Value[_S]) -> IntValue: + if not isinstance(value, Value): + return IntConstant.of(operator_(value)) + try: + constant_value = value.constant_value_or_raise + except NotConstantError: + return OneToIntValue(operator_, value) + else: + return IntConstant.of(operator_(constant_value)) + + @classmethod + def derive_two(cls, operator_: Callable[[int, int], int], left: IntLike, right: IntLike) -> IntValue: + try: + left_value = _get_constant(left) + right_value = _get_constant(right) + except NotConstantError: + return TwoToIntValue(operator_, left, right) + else: + return IntConstant.of(operator_(left_value, right_value)) + + @classmethod + def derive_three(cls, operator_: Callable[[int, int, int], int], + first: IntLike, second: IntLike, third: IntLike) -> IntValue: + try: + constant_first = _get_constant(first) + constant_second = _get_constant(second) + constant_third = _get_constant(third) + except NotConstantError: + return ThreeToIntValue(operator_, first, second, third) + else: + return IntConstant.of(operator_(constant_first, constant_second, constant_third)) + + @classmethod + def derive_many(cls, operator_: Callable[[Sequence[int]], int], *values: IntLike, is_associative: bool = False) -> IntValue: + try: + constant_values = [_get_constant(v) for v in values] + except NotConstantError: + if is_associative: + flattened = tuple(item for v in values for item in _decompose_operands(operator_, v)) + return ManyIntsToIntValue(operator_, *flattened) + else: + return ManyIntsToIntValue(operator_, *values) + else: + return IntConstant.of(operator_(constant_values)) class OneToIntValue(Generic[_S], OneToOneValue[_S, int], IntValue): pass -class IntConstant(IntValue, Constant[int]): +class TwoToIntValue(Generic[_S, _T], TwoToOneValue[_S, _T, int], IntValue): pass -class IntVariable(SimpleVariable[int], IntValue): +class ThreeToIntValue(Generic[_S, _T, _U], ThreeToOneValue[_S, _T, _U, int], IntValue): pass -class MaxIntValues(ManyToOneValue[int, int], IntValue): - def __init__(self, *values: IntLike): - super().__init__(max, *values) - +class IntConstant(IntValue, Constant[int]): + _cache: dict[int, IntConstant] = {} -class MinIntValues(ManyToOneValue[int, int], IntValue): - def __init__(self, *values: IntLike): - super().__init__(min, *values) + @classmethod + def of(cls, value: int) -> IntConstant: + try: + return cls._cache[value] + except KeyError: + return IntConstant(value) + def __abs__(self): + if self.value >= 0: + return self + return IntConstant.of(-self.value) -class AddIntValues(ManyToOneValue[int, int], IntValue): - def __init__(self, *values: IntLike): - super().__init__(sum, *values) + def __neg__(self): + return IntConstant.of(-self.value) -class SubtractIntValues(TwoToOneValue[int, int, int], IntValue): - def __init__(self, left: IntLike, right: IntLike): - super().__init__(operator.sub, left, right) +for _value in [*range(101)]: + IntConstant._cache[_value] = IntConstant(_value) + IntConstant._cache[-_value] = IntConstant(-_value) -class MultiplyIntValues(ManyToOneValue[int, int], IntValue): - def __init__(self, *values: IntLike): - super().__init__(multiply_all_ints, *values) +def _get_constant(value: _S | Value[_S]) -> _S: + if isinstance(value, Value): + return value.constant_value_or_raise + return value -class FloorDivideIntValues(TwoToOneValue[int, int, int], IntValue): - def __init__(self, left: IntLike, right: IntLike): - super().__init__(operator.floordiv, left, right) +def _decompose_operands(operator_: Callable, value: _S | Value[_S]) -> Sequence[_S | Value[_S]]: + if isinstance(value, Value): + return value.decompose_operands(operator_) + return (value,) -class PowerIntValues(TwoToOneValue[int, int, int], IntValue): - def __init__(self, left: IntLike, right: IntLike): - super().__init__(operator.pow, left, right) +class IntVariable(SimpleVariable[int], IntValue): + pass -class ModuloIntValues(TwoToOneValue[int, int, int], IntValue): - def __init__(self, left: IntLike, right: IntLike): - super().__init__(operator.mod, left, right) +class ManyIntsToIntValue(ManyToSameValue[int], IntValue): + def __init__(self, operator_: Callable[[Sequence[int]], int], *values: IntLike): + super().__init__(operator_, *values) class AbsIntValue(OneToOneValue[int, int], IntValue): def __init__(self, value: Value[int]): super().__init__(abs, value) + def __abs__(self) -> Self: + return self + class NegateIntValue(OneToOneValue[int, int], IntValue): def __init__(self, value: Value[int]): super().__init__(operator.neg, value) - -class FloorFloatValue(OneToOneValue[float, int], IntValue): - def __init__(self, value: Value[float]): - super().__init__(math.floor, value) - - -class CeilFloatValue(OneToOneValue[float, int], IntValue): - def __init__(self, value: Value[float]): - super().__init__(math.ceil, value) - - -class RoundFloatToIntValue(OneToOneValue[float, int], IntValue): - def __init__(self, value: Value[float]): - super().__init__(round, value) - - -class ClampIntValue(ThreeToOneValue[int, int, int, int], IntValue): - def __init__(self, value: IntLike, min_value: IntLike, max_value: IntLike) -> None: - super().__init__(clamp_int, value, min_value, max_value) + def __neg__(self) -> IntValue: + of = self._of + if isinstance(of, IntValue): + return of + return super().__neg__() class SelectIntValue(SelectValue[int], IntValue): diff --git a/src/spellbind/values.py b/src/spellbind/values.py index 236732a..4271ca2 100644 --- a/src/spellbind/values.py +++ b/src/spellbind/values.py @@ -290,13 +290,6 @@ def _calculate_value(self) -> _U: return self._transformer(self._first_getter(), self._second_getter()) -class TwoToSameValue(TwoToOneValue[_S, _S, _S], Generic[_S]): - def decompose_operands(self, operator_: Callable) -> Sequence[Value[_S] | _S]: - if operator_ == self._transformer: - return self._of_first, self._of_second - return (self,) - - class ThreeToOneValue(DerivedValueBase[_V], Generic[_S, _T, _U, _V]): def __init__(self, transformer: Callable[[_S, _T, _U], _V], first: Value[_S] | _S, second: Value[_T] | _T, third: Value[_U] | _U): @@ -313,13 +306,6 @@ def _calculate_value(self) -> _V: return self._transformer(self._first_getter(), self._second_getter(), self._third_getter()) -class ThreeToSameValue(ThreeToOneValue[_S, _S, _S, _S], Generic[_S]): - def decompose_operands(self, operator_: Callable) -> Sequence[Value[_S] | _S]: - if operator_ == self._transformer: - return self._of_first, self._of_second, self._of_third - return (self,) - - class SelectValue(ThreeToOneValue[bool, _S, _S, _S], Generic[_S]): def __init__(self, condition: BoolLike, if_true: Value[_S] | _S, if_false: Value[_S] | _S): super().__init__(lambda b, t, f: t if b else f, condition, if_true, if_false) diff --git a/tests/test_values/test_float_values/test_float_constant.py b/tests/test_values/test_float_values/test_float_constant.py new file mode 100644 index 0000000..12dc4dd --- /dev/null +++ b/tests/test_values/test_float_values/test_float_constant.py @@ -0,0 +1,19 @@ +from spellbind.float_values import FloatConstant + + +def test_get_cached_constants_are_same(): + v0 = FloatConstant.of(12.) + v1 = FloatConstant.of(12.) + assert v0 is v1 + + +def test_get_non_cached_constant_are_different(): + v0 = FloatConstant.of(123456.) + v1 = FloatConstant.of(123456.) + assert v0 is not v1 + + +def test_float_constant_of_int_and_float_are_same(): + v0 = FloatConstant.of(12) + v1 = FloatConstant.of(12.) + assert v0 is v1 diff --git a/tests/test_values/test_float_values/test_float_values.py b/tests/test_values/test_float_values/test_float_values.py index 6401752..4362d37 100644 --- a/tests/test_values/test_float_values/test_float_values.py +++ b/tests/test_values/test_float_values/test_float_values.py @@ -1,6 +1,6 @@ import gc -from spellbind.float_values import FloatConstant, FloatVariable +from spellbind.float_values import FloatConstant, FloatVariable, FloatValue def test_float_constant_str(): @@ -31,3 +31,14 @@ def test_add_int_values_garbage_collected(): v1.value = 4.5 # trigger removal of weak references assert len(v0._on_change._subscriptions) == 0 assert len(v1._on_change._subscriptions) == 0 + + +def test_derive_float_constant_returns_constant(): + v0 = FloatConstant(4.5) + derived = FloatValue.derive_one(lambda x: x + 1.0, v0) + assert derived.constant_value_or_raise == 5.5 + + +def test_derive_float_literal_returns_constant(): + derived = FloatValue.derive_one(lambda x: x + 1.0, 4.5) + assert derived.constant_value_or_raise == 5.5 diff --git a/tests/test_values/test_float_values/test_round_float_values.py b/tests/test_values/test_float_values/test_round_float_values.py index 7e4d752..79e634f 100644 --- a/tests/test_values/test_float_values/test_round_float_values.py +++ b/tests/test_values/test_float_values/test_round_float_values.py @@ -1,5 +1,5 @@ from conftest import OneParameterObserver -from spellbind.float_values import FloatVariable +from spellbind.float_values import FloatVariable, FloatConstant from spellbind.int_values import IntVariable @@ -60,3 +60,9 @@ def test_round_float_change_comma_doesnt_change_int_value(): assert rounded.value == 3 observer.assert_not_called() + + +def test_round_constant_float_is_constant_value(): + v0 = FloatConstant(3.14159) + v1 = v0.round(2) + assert isinstance(v1, FloatConstant) diff --git a/tests/test_values/test_int_values/test_abs_int_values.py b/tests/test_values/test_int_values/test_abs_int_values.py new file mode 100644 index 0000000..1e96219 --- /dev/null +++ b/tests/test_values/test_int_values/test_abs_int_values.py @@ -0,0 +1,49 @@ +from spellbind.int_values import IntVariable, IntConstant + + +def test_abs_int_value_positive(): + v0 = IntVariable(5) + v1 = abs(v0) + assert v1.value == 5 + + v0.value = 10 + assert v1.value == 10 + + +def test_abs_int_value_negative(): + v0 = IntVariable(-5) + v1 = abs(v0) + assert v1.value == 5 + + v0.value = -10 + assert v1.value == 10 + + +def test_abs_int_value_zero(): + v0 = IntVariable(0) + v1 = abs(v0) + assert v1.value == 0 + + v0.value = -7 + assert v1.value == 7 + + +def test_abs_of_abs_value_is_itself(): + v0 = IntVariable(-5) + v1 = abs(v0) + v2 = abs(v1) + assert v2 is v1 + + +def test_abs_of_constant_is_constant(): + v0 = IntConstant(-5) + v1 = abs(v0) + assert v1.value == 5 + assert isinstance(v1, IntConstant) + + +def test_abs_of_positive_constant_is_itself(): + v0 = IntConstant(5) + v1 = abs(v0) + assert v1 is v0 + assert v1.value == 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 ae21235..4428362 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 @@ -1,7 +1,7 @@ import gc from spellbind.float_values import FloatVariable -from spellbind.int_values import IntVariable +from spellbind.int_values import IntVariable, ManyIntsToIntValue, IntConstant def test_add_int_values_values(): @@ -110,3 +110,53 @@ def test_bind_and_unbind_to_added_int_variables(): variable.unbind() v0.value = 10 assert variable.value == 7 + + +def test_add_many_values_waterfall_style_are_combined(): + v0 = IntVariable(1) + v1 = IntVariable(2) + v2 = IntVariable(3) + v3 = IntVariable(4) + + v4 = v0 + v1 + v2 + v3 + assert v4.value == 10.0 + + assert isinstance(v4, ManyIntsToIntValue) + assert v4._input_values == (v0, v1, v2, v3) + + +def test_add_many_values_grouped_are_combined(): + v0 = IntVariable(1) + v1 = IntVariable(2) + v2 = IntVariable(3) + v3 = IntVariable(4) + + v4 = (v0 + v1) + (v2 + v3) + assert v4.value == 10.0 + + assert isinstance(v4, ManyIntsToIntValue) + assert v4._input_values == (v0, v1, v2, v3) + + +def test_add_constant_to_literal_is_constant(): + v0 = IntConstant(1) + v1 = 2 + v2 = v0 + v1 + assert v2.value == 3 + assert isinstance(v2, IntConstant) + + +def test_add_constant_to_constant_is_constant(): + v0 = IntConstant(1) + v1 = IntConstant(2) + v2 = v0 + v1 + assert v2.value == 3 + assert isinstance(v2, IntConstant) + + +def test_add_literal_to_constant_is_constant(): + v0 = 1 + v1 = IntConstant(2) + v2 = v0 + v1 + assert v2.value == 3.0 + assert isinstance(v2, IntConstant) diff --git a/tests/test_values/test_int_values/test_clamp_int_values.py b/tests/test_values/test_int_values/test_clamp_int_values.py new file mode 100644 index 0000000..ec01ed5 --- /dev/null +++ b/tests/test_values/test_int_values/test_clamp_int_values.py @@ -0,0 +1,115 @@ +from spellbind.int_values import IntVariable, IntConstant + + +def test_clamp_int_values_in_range(): + value = IntVariable(15) + min_val = IntVariable(10) + max_val = IntVariable(20) + + clamped = value.clamp(min_val, max_val) + assert clamped.value == 15 + + +def test_clamp_int_values_below_min(): + value = IntVariable(5) + min_val = IntVariable(10) + max_val = IntVariable(20) + + clamped = value.clamp(min_val, max_val) + assert clamped.value == 10 + + +def test_clamp_int_values_above_max(): + value = IntVariable(25) + min_val = IntVariable(10) + max_val = IntVariable(20) + + clamped = value.clamp(min_val, max_val) + assert clamped.value == 20 + + +def test_clamp_int_values_with_literals_in_range(): + value = IntVariable(15) + + clamped = value.clamp(10, 20) + assert clamped.value == 15 + + +def test_clamp_int_values_with_literals_below_min(): + value = IntVariable(5) + + clamped = value.clamp(10, 20) + assert clamped.value == 10 + + +def test_clamp_int_values_with_literals_above_max(): + value = IntVariable(25) + + clamped = value.clamp(10, 20) + assert clamped.value == 20 + + +def test_clamp_int_values_reactive_value_changes(): + value = IntVariable(15) + min_val = IntVariable(10) + max_val = IntVariable(20) + + clamped = value.clamp(min_val, max_val) + assert clamped.value == 15 + + value.value = 5 + assert clamped.value == 10 + + value.value = 25 + assert clamped.value == 20 + + value.value = 12 + assert clamped.value == 12 + + +def test_clamp_int_values_reactive_bounds_changes(): + value = IntVariable(15) + min_val = IntVariable(10) + max_val = IntVariable(20) + + clamped = value.clamp(min_val, max_val) + assert clamped.value == 15 + + min_val.value = 18 + assert clamped.value == 18 + + min_val.value = 11 + assert clamped.value == 15 + + max_val.value = 12 + assert clamped.value == 12 + + +def test_clamp_middle_three_constants_is_constant(): + value = IntConstant(15) + min_val = IntConstant(10) + max_val = IntConstant(20) + + clamped = value.clamp(min_val, max_val) + assert isinstance(clamped, IntConstant) + assert clamped.value == 15 + + +def test_clamp_lower_three_constants_is_constant(): + value = IntConstant(5) + min_val = IntConstant(10) + max_val = IntConstant(20) + + clamped = value.clamp(min_val, max_val) + assert isinstance(clamped, IntConstant) + assert clamped.value == 10 + + +def test_clamp_upper_three_constants_is_constant(): + value = IntConstant(25) + min_val = IntConstant(10) + max_val = IntConstant(20) + + clamped = value.clamp(min_val, max_val) + assert isinstance(clamped, IntConstant) + assert clamped.value == 20 diff --git a/tests/test_values/test_int_values/test_compare_int_values.py b/tests/test_values/test_int_values/test_compare_int_values.py index 527b144..9e2920c 100644 --- a/tests/test_values/test_int_values/test_compare_int_values.py +++ b/tests/test_values/test_int_values/test_compare_int_values.py @@ -2,7 +2,6 @@ from spellbind.int_values import IntVariable -# Comparison Tests - Less Than def test_less_than_int_values(): v0 = IntVariable(3) v1 = IntVariable(5) @@ -119,7 +118,6 @@ def test_greater_than_int_value_and_float_value(): assert not v2.value -# Comparison Tests - Greater Than or Equal def test_greater_than_or_equal_int_values(): v0 = IntVariable(5) v1 = IntVariable(5) diff --git a/tests/test_values/test_int_values/test_divide_int_values.py b/tests/test_values/test_int_values/test_divide_int_values.py index 9c6df19..812d838 100644 --- a/tests/test_values/test_int_values/test_divide_int_values.py +++ b/tests/test_values/test_int_values/test_divide_int_values.py @@ -1,5 +1,5 @@ -from spellbind.float_values import FloatVariable -from spellbind.int_values import IntVariable +from spellbind.float_values import FloatVariable, FloatConstant +from spellbind.int_values import IntVariable, IntConstant def test_truediv_int_values(): @@ -40,25 +40,6 @@ def test_truediv_int_value_by_float_value(): assert v2.value == 3.75 -def test_floordiv_int_values(): - v0 = IntVariable(10) - v1 = IntVariable(3) - v2 = v0 // v1 - assert v2.value == 3 - - v0.value = 15 - assert v2.value == 5 - - -def test_floordiv_int_value_by_int(): - v0 = IntVariable(10) - v2 = v0 // 3 - assert v2.value == 3 - - v0.value = 15 - assert v2.value == 5 - - def test_truediv_int_divided_by_int_value(): v1 = IntVariable(4) v2 = 10 / v1 @@ -77,10 +58,23 @@ def test_truediv_float_divided_by_int_value(): assert v2.value == 2.0 -def test_floordiv_int_divided_by_int_value(): - v1 = IntVariable(3) - v2 = 10 // v1 - assert v2.value == 3 +def test_truediv_constant_constant_is_constant(): + v0 = IntConstant(10) + v1 = IntConstant(5) + v2 = v0 / v1 + assert v2.value == 2 + assert isinstance(v2, FloatConstant) + + +def test_truediv_literal_constant_is_constant(): + v0 = IntConstant(10) + v2 = v0 / 5 + assert v2.value == 2 + assert isinstance(v2, FloatConstant) + - v1.value = 4 +def test_truediv_constant_literal_is_constant(): + v0 = IntConstant(5) + v2 = 10 / v0 assert v2.value == 2 + assert isinstance(v2, FloatConstant) diff --git a/tests/test_values/test_int_values/test_int_constants.py b/tests/test_values/test_int_values/test_int_constants.py new file mode 100644 index 0000000..57cf451 --- /dev/null +++ b/tests/test_values/test_int_values/test_int_constants.py @@ -0,0 +1,13 @@ +from spellbind.int_values import IntConstant + + +def test_get_cached_constants_are_same(): + v0 = IntConstant.of(12) + v1 = IntConstant.of(12) + assert v0 is v1 + + +def test_get_non_cached_constant_are_different(): + v0 = IntConstant.of(123456) + v1 = IntConstant.of(123456) + assert v0 is not v1 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 1b5180c..8d9a9a2 100644 --- a/tests/test_values/test_int_values/test_int_values.py +++ b/tests/test_values/test_int_values/test_int_values.py @@ -1,5 +1,4 @@ -from spellbind.int_values import IntConstant, MaxIntValues, MinIntValues, IntVariable -from spellbind.values import SimpleVariable +from spellbind.int_values import IntConstant, IntVariable, IntValue def test_int_constant_str(): @@ -7,136 +6,8 @@ def test_int_constant_str(): assert str(const) == "42" -def test_max_int_values(): - a = SimpleVariable(10) - b = SimpleVariable(20) - c = SimpleVariable(5) - - max_val = MaxIntValues(a, b, c) - assert max_val.value == 20 - - a.value = 30 - assert max_val.value == 30 - - -def test_max_int_values_with_literals(): - a = SimpleVariable(10) - - max_val = MaxIntValues(a, 25, 15) - assert max_val.value == 25 - - a.value = 30 - assert max_val.value == 30 - - -def test_min_int_values(): - a = SimpleVariable(10) - b = SimpleVariable(20) - c = SimpleVariable(5) - - min_val = MinIntValues(a, b, c) - assert min_val.value == 5 - - c.value = 2 - assert min_val.value == 2 - - -def test_min_int_values_with_literals(): - a = SimpleVariable(10) - - min_val = MinIntValues(a, 25, 15) - assert min_val.value == 10 - - a.value = 5 - assert min_val.value == 5 - - -def test_clamp_int_values_in_range(): - value = IntVariable(15) - min_val = IntVariable(10) - max_val = IntVariable(20) - - clamped = value.clamp(min_val, max_val) - assert clamped.value == 15 - - -def test_clamp_int_values_below_min(): - value = IntVariable(5) - min_val = IntVariable(10) - max_val = IntVariable(20) - - clamped = value.clamp(min_val, max_val) - assert clamped.value == 10 - - -def test_clamp_int_values_above_max(): - value = IntVariable(25) - min_val = IntVariable(10) - max_val = IntVariable(20) - - clamped = value.clamp(min_val, max_val) - assert clamped.value == 20 - - -def test_clamp_int_values_with_literals_in_range(): - value = IntVariable(15) - - clamped = value.clamp(10, 20) - assert clamped.value == 15 - - -def test_clamp_int_values_with_literals_below_min(): - value = IntVariable(5) - - clamped = value.clamp(10, 20) - assert clamped.value == 10 - - -def test_clamp_int_values_with_literals_above_max(): - value = IntVariable(25) - - clamped = value.clamp(10, 20) - assert clamped.value == 20 - - -def test_clamp_int_values_reactive_value_changes(): - value = IntVariable(15) - min_val = IntVariable(10) - max_val = IntVariable(20) - - clamped = value.clamp(min_val, max_val) - assert clamped.value == 15 - - value.value = 5 - assert clamped.value == 10 - - value.value = 25 - assert clamped.value == 20 - - value.value = 12 - assert clamped.value == 12 - - -def test_clamp_int_values_reactive_bounds_changes(): - value = IntVariable(15) - min_val = IntVariable(10) - max_val = IntVariable(20) - - clamped = value.clamp(min_val, max_val) - assert clamped.value == 15 - - min_val.value = 18 - assert clamped.value == 18 - - min_val.value = 11 - assert clamped.value == 15 - - max_val.value = 12 - assert clamped.value == 12 - - -def test_derived_int_values_map_to_list(): - value0 = IntConstant(2) +def test_map_derived_int_value_to_list(): + value0 = IntVariable(2) value1 = IntConstant(3) added = value0 + value1 mapped_value = added.map(lambda x: ["foo"]*x) @@ -147,3 +18,14 @@ def test_derived_int_values_map_to_list(): def test_int_const_repr(): const = IntConstant(42) assert repr(const) == "IntConstant(42)" + + +def test_derive_int_constant_returns_constant(): + v0 = IntConstant(4) + derived = IntValue.derive_one(lambda x: x + 1, v0) + assert derived.constant_value_or_raise == 5 + + +def test_derive_int_literal_returns_constant(): + derived = IntValue.derive_one(lambda x: x + 1, 4) + assert derived.constant_value_or_raise == 5 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 deleted file mode 100644 index 2f014c6..0000000 --- a/tests/test_values/test_int_values/test_int_values_arithmatic.py +++ /dev/null @@ -1,72 +0,0 @@ -from spellbind.int_values import IntVariable - - -def test_power_int_values(): - v0 = IntVariable(2) - v1 = IntVariable(3) - v2 = v0 ** v1 - assert v2.value == 8 - - v0.value = 3 - assert v2.value == 27 - - -def test_power_int_value_to_int(): - v0 = IntVariable(2) - v2 = v0 ** 3 - assert v2.value == 8 - - v0.value = 3 - assert v2.value == 27 - - -def test_power_int_to_int_value(): - v1 = IntVariable(3) - v2 = 2 ** v1 - assert v2.value == 8 - - v1.value = 4 - assert v2.value == 16 - - -# Modulo Tests -def test_modulo_int_values(): - v0 = IntVariable(10) - v1 = IntVariable(3) - v2 = v0 % v1 - assert v2.value == 1 - - v0.value = 15 - assert v2.value == 0 - - -def test_modulo_int_value_by_int(): - v0 = IntVariable(10) - v2 = v0 % 3 - assert v2.value == 1 - - v0.value = 15 - assert v2.value == 0 - - -def test_modulo_int_by_int_value(): - v1 = IntVariable(3) - v2 = 10 % v1 - assert v2.value == 1 - - 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_int_values/test_min_max_int_values.py b/tests/test_values/test_int_values/test_min_max_int_values.py new file mode 100644 index 0000000..b1994d0 --- /dev/null +++ b/tests/test_values/test_int_values/test_min_max_int_values.py @@ -0,0 +1,65 @@ +from spellbind.float_values import FloatValue +from spellbind.int_values import IntConstant, IntValue +from spellbind.values import SimpleVariable + + +def test_min_int_values(): + a = SimpleVariable(10) + b = SimpleVariable(20) + c = SimpleVariable(5) + + min_val = FloatValue.min(a, b, c) + assert min_val.value == 5 + + c.value = 2 + assert min_val.value == 2 + + +def test_min_int_values_with_literals(): + a = SimpleVariable(10) + + min_val = FloatValue.min(a, 25, 15) + assert min_val.value == 10 + + a.value = 5 + assert min_val.value == 5 + + +def test_min_int_constants_is_constant(): + a = IntConstant(10) + b = IntConstant(20) + c = IntConstant(5) + + min_val = IntValue.min(a, b, c) + assert isinstance(min_val, IntConstant) + + +def test_max_int_values(): + a = SimpleVariable(10) + b = SimpleVariable(20) + c = SimpleVariable(5) + + max_val = FloatValue.max(a, b, c) + assert max_val.value == 20 + + a.value = 30 + assert max_val.value == 30 + + +def test_max_int_values_with_literals(): + a = SimpleVariable(10) + + max_val = FloatValue.max(a, 25, 15) + assert max_val.value == 25 + + a.value = 30 + assert max_val.value == 30 + + +def test_max_int_constants_is_constant(): + a = IntConstant(10) + b = IntConstant(20) + c = IntConstant(5) + + max_val = IntValue.max(a, b, c) + assert isinstance(max_val, IntConstant) diff --git a/tests/test_values/test_int_values/test_mod_int_values.py b/tests/test_values/test_int_values/test_mod_int_values.py new file mode 100644 index 0000000..70a1aa2 --- /dev/null +++ b/tests/test_values/test_int_values/test_mod_int_values.py @@ -0,0 +1,52 @@ +from spellbind.int_values import IntVariable, IntConstant + + +def test_modulo_int_values(): + v0 = IntVariable(10) + v1 = IntVariable(3) + v2 = v0 % v1 + assert v2.value == 1 + + v0.value = 15 + assert v2.value == 0 + + +def test_modulo_int_value_by_int(): + v0 = IntVariable(10) + v2 = v0 % 3 + assert v2.value == 1 + + v0.value = 15 + assert v2.value == 0 + + +def test_modulo_int_by_int_value(): + v1 = IntVariable(3) + v2 = 10 % v1 + assert v2.value == 1 + + v1.value = 4 + assert v2.value == 2 + + +def test_modulo_constant_constant_is_constant(): + v0 = IntConstant(10) + v1 = IntConstant(3) + v2 = v0 % v1 + assert v2.value == 1 + assert isinstance(v2, IntConstant) + + +def test_modulo_constant_literal_is_constant(): + v0 = IntConstant(10) + v2 = v0 % 3 + assert v2.value == 1 + assert isinstance(v2, IntConstant) + + +def test_modulo_literal_const_is_constant(): + v0 = 10 + v1 = IntConstant(3) + v2 = v0 % v1 + assert v2.value == 1 + assert isinstance(v2, IntConstant) diff --git a/tests/test_values/test_int_values/test_multiply_int_values.py b/tests/test_values/test_int_values/test_multiply_int_values.py index 203968c..b2ba6c5 100644 --- a/tests/test_values/test_int_values/test_multiply_int_values.py +++ b/tests/test_values/test_int_values/test_multiply_int_values.py @@ -1,5 +1,5 @@ from spellbind.float_values import FloatVariable -from spellbind.int_values import IntVariable +from spellbind.int_values import IntVariable, ManyIntsToIntValue, IntConstant def test_multiply_int_value_times_int(): @@ -56,3 +56,53 @@ def test_multiply_int_values(): v0.value = 5 assert v2.value == 20 + + +def test_multiply_many_values_waterfall_style_are_combined(): + v0 = IntVariable(1) + v1 = IntVariable(2) + v2 = IntVariable(3) + v3 = IntVariable(4) + + v4 = v0 * v1 * v2 * v3 + assert v4.value == 24 + + assert isinstance(v4, ManyIntsToIntValue) + assert v4._input_values == (v0, v1, v2, v3) + + +def test_multiply_many_values_grouped_are_combined(): + v0 = IntVariable(1) + v1 = IntVariable(2) + v2 = IntVariable(3) + v3 = IntVariable(4) + + v4 = (v0 * v1) * (v2 * v3) + assert v4.value == 24 + + assert isinstance(v4, ManyIntsToIntValue) + assert v4._input_values == (v0, v1, v2, v3) + + +def test_multiply_constant_by_literal_is_constant(): + v0 = IntConstant(2) + v1 = 3 + v2 = v0 * v1 + assert v2.value == 6 + assert isinstance(v2, IntConstant) + + +def test_multiply_constant_by_constant_is_constant(): + v0 = IntConstant(2) + v1 = IntConstant(3) + v2 = v0 * v1 + assert v2.value == 6 + assert isinstance(v2, IntConstant) + + +def test_multiply_literal_by_constant_is_constant(): + v0 = 2 + v1 = IntConstant(3) + v2 = v0 * v1 + assert v2.value == 6 + assert isinstance(v2, IntConstant) diff --git a/tests/test_values/test_int_values/test_negate_int_values.py b/tests/test_values/test_int_values/test_negate_int_values.py new file mode 100644 index 0000000..7c87742 --- /dev/null +++ b/tests/test_values/test_int_values/test_negate_int_values.py @@ -0,0 +1,34 @@ +from spellbind.int_values import IntVariable, IntConstant + + +def test_negate_int_value(): + v0 = IntVariable(5) + v1 = -v0 + assert v1.value == -5 + + v0.value = -3 + assert v1.value == 3 + + +def test_negate_int_value_zero(): + v0 = IntVariable(0) + v1 = -v0 + assert v1.value == 0 + + v0.value = 7 + assert v1.value == -7 + + +def test_negate_int_constant_is_constant(): + v0 = IntConstant(5) + v1 = -v0 + assert v1.value == -5 + + assert isinstance(v0, IntConstant) + + +def test_negate_variable_twice_is_same(): + v0 = IntVariable(5) + v1 = -v0 + v2 = -v1 + assert v0 is v2 diff --git a/tests/test_values/test_int_values/test_pos_int_values.py b/tests/test_values/test_int_values/test_pos_int_values.py new file mode 100644 index 0000000..2dd2d36 --- /dev/null +++ b/tests/test_values/test_int_values/test_pos_int_values.py @@ -0,0 +1,22 @@ +from spellbind.int_values import IntVariable, IntConstant + + +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 + + +def test_float_pos_constant_is_constant(): + const = IntConstant(3) + result = +const + assert result is const + assert result.value == 3 diff --git a/tests/test_values/test_int_values/test_pow_int_values.py b/tests/test_values/test_int_values/test_pow_int_values.py new file mode 100644 index 0000000..e05c5d1 --- /dev/null +++ b/tests/test_values/test_int_values/test_pow_int_values.py @@ -0,0 +1,51 @@ +from spellbind.int_values import IntVariable, IntConstant + + +def test_power_int_values(): + v0 = IntVariable(2) + v1 = IntVariable(3) + v2 = v0 ** v1 + assert v2.value == 8 + + v0.value = 3 + assert v2.value == 27 + + +def test_power_int_value_to_int(): + v0 = IntVariable(2) + v2 = v0 ** 3 + assert v2.value == 8 + + v0.value = 3 + assert v2.value == 27 + + +def test_power_int_to_int_value(): + v1 = IntVariable(3) + v2 = 2 ** v1 + assert v2.value == 8 + + v1.value = 4 + assert v2.value == 16 + + +def test_power_constant_to_constant(): + v0 = IntConstant(2) + v1 = IntConstant(3) + v2 = v0 ** v1 + assert v2.value == 8 + assert isinstance(v2, IntConstant) + + +def test_power_literal_to_constant(): + v0 = IntConstant(2) + v2 = 3 ** v0 + assert v2.value == 9 + assert isinstance(v2, IntConstant) + + +def test_power_constant_to_literal(): + v0 = IntConstant(2) + v2 = v0 ** 3 + assert v2.value == 8 + assert isinstance(v2, IntConstant) diff --git a/tests/test_values/test_int_values/test_subtract_int_values.py b/tests/test_values/test_int_values/test_subtract_int_values.py index a97324c..c09ba82 100644 --- a/tests/test_values/test_int_values/test_subtract_int_values.py +++ b/tests/test_values/test_int_values/test_subtract_int_values.py @@ -1,5 +1,5 @@ from spellbind.float_values import FloatVariable -from spellbind.int_values import IntVariable +from spellbind.int_values import IntVariable, IntConstant def test_subtract_int_values(): @@ -56,3 +56,25 @@ def test_subtract_float_minus_int_value(): v1.value = 3 assert v2.value == 2.5 + + +def test_subtract_constant_from_constant_returns_constant(): + v0 = IntConstant(5) + v1 = IntConstant(2) + v2 = v0 - v1 + assert v2.value == 3 + assert isinstance(v2, IntConstant) + + +def test_subtract_literal_from_constant_returns_constant(): + v0 = IntConstant(5) + v2 = v0 - 2 + assert v2.value == 3 + assert isinstance(v2, IntConstant) + + +def test_subtract_constant_from_literal_returns_constant(): + v0 = IntConstant(5) + v2 = 7 - v0 + assert v2.value == 2 + assert isinstance(v2, IntConstant) diff --git a/tests/test_values/test_int_values/test_unary_int_values.py b/tests/test_values/test_int_values/test_unary_int_values.py deleted file mode 100644 index be6bd71..0000000 --- a/tests/test_values/test_int_values/test_unary_int_values.py +++ /dev/null @@ -1,46 +0,0 @@ -from spellbind.int_values import IntVariable - - -def test_negate_int_value(): - v0 = IntVariable(5) - v1 = -v0 - assert v1.value == -5 - - v0.value = -3 - assert v1.value == 3 - - -def test_negate_int_value_zero(): - v0 = IntVariable(0) - v1 = -v0 - assert v1.value == 0 - - v0.value = 7 - assert v1.value == -7 - - -def test_abs_int_value_positive(): - v0 = IntVariable(5) - v1 = abs(v0) - assert v1.value == 5 - - v0.value = 10 - assert v1.value == 10 - - -def test_abs_int_value_negative(): - v0 = IntVariable(-5) - v1 = abs(v0) - assert v1.value == 5 - - v0.value = -10 - assert v1.value == 10 - - -def test_abs_int_value_zero(): - v0 = IntVariable(0) - v1 = abs(v0) - assert v1.value == 0 - - v0.value = -7 - assert v1.value == 7 From 89f685f124caead3cbc6971cdcf5eaef5bd98e1e Mon Sep 17 00:00:00 2001 From: Georg Plaz Date: Sun, 29 Jun 2025 12:42:07 +0200 Subject: [PATCH 3/5] Combine concatenated string values --- src/spellbind/int_values.py | 28 ++---- src/spellbind/str_values.py | 86 ++++++++++++++++--- src/spellbind/values.py | 12 +++ .../test_concatenate_str_values.py | 40 ++++++++- .../test_str_values/test_str_values.py | 25 +++++- 5 files changed, 155 insertions(+), 36 deletions(-) diff --git a/src/spellbind/int_values.py b/src/spellbind/int_values.py index 315d442..e495974 100644 --- a/src/spellbind/int_values.py +++ b/src/spellbind/int_values.py @@ -11,7 +11,7 @@ CompareNumbersValues from spellbind.functions import clamp_int, multiply_all_ints, multiply_all_floats from spellbind.values import Value, SimpleVariable, TwoToOneValue, OneToOneValue, Constant, \ - ThreeToOneValue, SelectValue, NotConstantError, ManyToSameValue + ThreeToOneValue, SelectValue, NotConstantError, ManyToSameValue, get_constant_of_generic_like, decompose_operands_of_generic_like IntLike = int | Value[int] FloatLike = IntLike | float | FloatValue @@ -159,8 +159,8 @@ def derive_one(cls, operator_: Callable[[_S], int], value: _S | Value[_S]) -> In @classmethod def derive_two(cls, operator_: Callable[[int, int], int], left: IntLike, right: IntLike) -> IntValue: try: - left_value = _get_constant(left) - right_value = _get_constant(right) + left_value = get_constant_of_generic_like(left) + right_value = get_constant_of_generic_like(right) except NotConstantError: return TwoToIntValue(operator_, left, right) else: @@ -170,9 +170,9 @@ def derive_two(cls, operator_: Callable[[int, int], int], left: IntLike, right: def derive_three(cls, operator_: Callable[[int, int, int], int], first: IntLike, second: IntLike, third: IntLike) -> IntValue: try: - constant_first = _get_constant(first) - constant_second = _get_constant(second) - constant_third = _get_constant(third) + constant_first = get_constant_of_generic_like(first) + constant_second = get_constant_of_generic_like(second) + constant_third = get_constant_of_generic_like(third) except NotConstantError: return ThreeToIntValue(operator_, first, second, third) else: @@ -181,10 +181,10 @@ def derive_three(cls, operator_: Callable[[int, int, int], int], @classmethod def derive_many(cls, operator_: Callable[[Sequence[int]], int], *values: IntLike, is_associative: bool = False) -> IntValue: try: - constant_values = [_get_constant(v) for v in values] + constant_values = [get_constant_of_generic_like(v) for v in values] except NotConstantError: if is_associative: - flattened = tuple(item for v in values for item in _decompose_operands(operator_, v)) + flattened = tuple(item for v in values for item in decompose_operands_of_generic_like(operator_, v)) return ManyIntsToIntValue(operator_, *flattened) else: return ManyIntsToIntValue(operator_, *values) @@ -228,18 +228,6 @@ def __neg__(self): IntConstant._cache[-_value] = IntConstant(-_value) -def _get_constant(value: _S | Value[_S]) -> _S: - if isinstance(value, Value): - return value.constant_value_or_raise - return value - - -def _decompose_operands(operator_: Callable, value: _S | Value[_S]) -> Sequence[_S | Value[_S]]: - if isinstance(value, Value): - return value.decompose_operands(operator_) - return (value,) - - class IntVariable(SimpleVariable[int], IntValue): pass diff --git a/src/spellbind/str_values.py b/src/spellbind/str_values.py index badbb7b..f351608 100644 --- a/src/spellbind/str_values.py +++ b/src/spellbind/str_values.py @@ -1,49 +1,107 @@ from __future__ import annotations from abc import ABC -from typing import Any, Generic, TypeVar +from typing import Any, Generic, TypeVar, Callable, Sequence, Iterable, TYPE_CHECKING -from spellbind.bool_values import BoolLike, StrLike -from spellbind.values import Value, OneToOneValue, ManyToOneValue, SimpleVariable, Constant, SelectValue +from spellbind.bool_values import BoolLike +from spellbind.values import Value, OneToOneValue, SimpleVariable, Constant, SelectValue, \ + ManyToSameValue, NotConstantError, get_constant_of_generic_like, decompose_operands_of_generic_like -StringLike = str | Value[str] + +if TYPE_CHECKING: + from spellbind.int_values import IntValue, IntConstant # pragma: no cover + + +StrLike = str | Value[str] _S = TypeVar('_S') +def _join_strs(values: Iterable[str]) -> str: + return "".join(values) + + class StrValue(Value[str], ABC): - def __add__(self, other: StringLike) -> StrValue: - return ConcatenateStrValues(self, other) + def __add__(self, other: StrLike) -> StrValue: + return StrValue.derive_many(_join_strs, self, other, is_associative=True) - def __radd__(self, other: StringLike) -> StrValue: - return ConcatenateStrValues(other, self) + def __radd__(self, other: StrLike) -> StrValue: + return StrValue.derive_many(_join_strs, other, self, is_associative=True) + + @property + def length(self) -> IntValue: + from spellbind.int_values import IntValue + str_length: Callable[[str], int] = len + return IntValue.derive_one(str_length, self) def to_str(self) -> StrValue: return self + @classmethod + def derive_many(cls, operator_: Callable[[Iterable[str]], str], *values: StrLike, + is_associative: bool = False) -> StrValue: + try: + constant_values = [get_constant_of_generic_like(v) for v in values] + except NotConstantError: + if is_associative: + flattened = tuple(item for v in values for item in decompose_operands_of_generic_like(operator_, v)) + return ManyStrsToStrValue(operator_, *flattened) + else: + return ManyStrsToStrValue(operator_, *values) + else: + return StrConstant.of(operator_(constant_values)) + class OneToStrValue(OneToOneValue[_S, str], StrValue, Generic[_S]): pass class StrConstant(Constant[str], StrValue): - pass + _cache: dict[str, StrConstant] = {} + + @classmethod + def of(cls, value: str, cache: bool = False) -> StrConstant: + try: + return cls._cache[value] + except KeyError: + constant = StrConstant(value) + if cache: + cls._cache[value] = constant + return constant + + @property + def length(self) -> IntConstant: + from spellbind.int_values import IntConstant + return IntConstant.of(len(self.value)) + + +EMPTY_STRING = StrConstant.of("") + +for _number_value in [*range(10)]: + StrConstant.of(str(_number_value), cache=True) + +for _alpha_value in "abcdefghijklmnopqrstuvwxyz": + StrConstant.of(_alpha_value, cache=True) + StrConstant.of(_alpha_value.upper(), cache=True) + +for _special_char in "!@#$%^&*()-_=+[]{}|;:'\",.<>?/": + StrConstant.of(_special_char, cache=True) class StrVariable(SimpleVariable[str], StrValue): pass +class ManyStrsToStrValue(ManyToSameValue[str], StrValue): + def __init__(self, transformer: Callable[[Sequence[str]], str], *values: StrLike): + super().__init__(transformer, *values) + + class ToStrValue(OneToOneValue[Any, str], StrValue): def __init__(self, value: Value[Any]): super().__init__(str, value) -class ConcatenateStrValues(ManyToOneValue[str, str], StrValue): - def __init__(self, *values: StringLike): - super().__init__(''.join, *values) - - class SelectStrValue(SelectValue[str], StrValue): def __init__(self, condition: BoolLike, if_true: StrLike, if_false: StrLike): super().__init__(condition, if_true, if_false) diff --git a/src/spellbind/values.py b/src/spellbind/values.py index 4271ca2..7b47997 100644 --- a/src/spellbind/values.py +++ b/src/spellbind/values.py @@ -309,3 +309,15 @@ def _calculate_value(self) -> _V: class SelectValue(ThreeToOneValue[bool, _S, _S, _S], Generic[_S]): def __init__(self, condition: BoolLike, if_true: Value[_S] | _S, if_false: Value[_S] | _S): super().__init__(lambda b, t, f: t if b else f, condition, if_true, if_false) + + +def get_constant_of_generic_like(value: _S | Value[_S]) -> _S: + if isinstance(value, Value): + return value.constant_value_or_raise + return value + + +def decompose_operands_of_generic_like(operator_: Callable, value: _S | Value[_S]) -> Sequence[_S | Value[_S]]: + if isinstance(value, Value): + return value.decompose_operands(operator_) + return (value,) diff --git a/tests/test_values/test_str_values/test_concatenate_str_values.py b/tests/test_values/test_str_values/test_concatenate_str_values.py index caacd5c..7fe3521 100644 --- a/tests/test_values/test_str_values/test_concatenate_str_values.py +++ b/tests/test_values/test_str_values/test_concatenate_str_values.py @@ -1,4 +1,6 @@ -from spellbind.str_values import StrVariable +import pytest + +from spellbind.str_values import StrVariable, ManyStrsToStrValue, StrConstant def test_concatenate_str_values(): @@ -34,3 +36,39 @@ def test_concatenate_literal_str_value(): full_name = "Ada " + last_name assert full_name.value == "Ada Lovelace" + + +def test_concatenate_many_str_values_waterfall_style_are_combined(): + v0 = StrVariable("foo") + v1 = StrVariable("bar") + v2 = StrVariable("hello") + v3 = StrVariable("world") + + v4 = v0 + v1 + v2 + v3 + assert v4.value == "foobarhelloworld" + + assert isinstance(v4, ManyStrsToStrValue) + assert v4._input_values == (v0, v1, v2, v3) + + +def test_concatenate_many_str_values_grouped_are_combined(): + v0 = StrVariable("foo") + v1 = StrVariable("bar") + v2 = StrVariable("hello") + v3 = StrVariable("world") + + v4 = (v0 + v1) + (v2 + v3) + assert v4.value == "foobarhelloworld" + + assert isinstance(v4, ManyStrsToStrValue) + assert v4._input_values == (v0, v1, v2, v3) + + +@pytest.mark.parametrize("v0, v1", [ + (StrConstant("foo"), "bar"), + ("foo", StrConstant("bar")), + (StrConstant("foo"), StrConstant("bar")), +]) +def test_concatenate_str_constants_is_constant(v0, v1): + v2 = v0 + v1 + assert v2.constant_value_or_raise == "foobar" diff --git a/tests/test_values/test_str_values/test_str_values.py b/tests/test_values/test_str_values/test_str_values.py index 17b8d15..a187023 100644 --- a/tests/test_values/test_str_values/test_str_values.py +++ b/tests/test_values/test_str_values/test_str_values.py @@ -1,6 +1,29 @@ -from spellbind.str_values import StrConstant +import pytest + +from spellbind.str_values import StrConstant, StrVariable def test_str_constant_str(): const = StrConstant("hello") assert str(const) == "hello" + + +def test_str_length_of_variable(): + v0 = StrVariable("foo") + v0_length = v0.length + assert v0_length.value == 3 + v0.value = "foobar" + assert v0_length.value == 6 + + +def test_str_length_of_constant(): + const = StrConstant("hello") + const_length = const.length + assert const_length.constant_value_or_raise == 5 + + +@pytest.mark.parametrize("value", ["a", "A", "0", "!"]) +def test_str_constant_is_same(value: str): + v0 = StrConstant.of(value) + v1 = StrConstant.of(value) + assert v0 is v1 From 586af70f7a16a30b4ffa30a38d65f29bdbe89a34 Mon Sep 17 00:00:00 2001 From: Georg Plaz Date: Mon, 30 Jun 2025 08:47:26 +0200 Subject: [PATCH 4/5] Move various @classmethods to module namespace --- README.md | 4 +- src/spellbind/float_values.py | 34 +++++++----- src/spellbind/int_values.py | 16 +++--- src/spellbind/str_values.py | 22 +++++++- .../test_average_float_values.py | 13 ++--- .../test_min_max_float_values.py | 27 +++++----- .../test_min_max_int_values.py | 16 +++--- .../test_concatenate_str_values.py | 53 +++++++++++++++++++ 8 files changed, 132 insertions(+), 53 deletions(-) diff --git a/README.md b/README.md index 22f578e..0287426 100644 --- a/README.md +++ b/README.md @@ -10,9 +10,7 @@ spellbind is a reactive programming library that lets you create Variables that ## Installation ```bash -git clone https://github.com/FancyNeuron/spellbind.git -cd spellbind -pip install -e . +pip install spellbind ``` ## Quick Start diff --git a/src/spellbind/float_values.py b/src/spellbind/float_values.py index 9fb7e96..9f77354 100644 --- a/src/spellbind/float_values.py +++ b/src/spellbind/float_values.py @@ -120,18 +120,6 @@ def clamp(self, min_value: FloatLike, max_value: FloatLike) -> FloatValue: def decompose_float_operands(self, operator_: Callable[[Sequence[float]], _S]) -> Sequence[FloatLike]: return (self,) - @classmethod - def min(cls, *values: FloatLike) -> FloatValue: - return cls.derive_many(min, *values, is_associative=True) - - @classmethod - def max(cls, *values: FloatLike) -> FloatValue: - return cls.derive_many(max, *values, is_associative=True) - - @classmethod - def average(cls, *values: FloatLike) -> FloatValue: - return cls.derive_many(_average_float, *values) - @classmethod def derive_one(cls, transformer: Callable[[float], float], of: FloatLike) -> FloatValue: try: @@ -185,6 +173,26 @@ def derive_many(cls, operator_: Callable[[Sequence[float]], float], *values: Flo return FloatConstant.of(operator_(constant_values)) +def min_float(*values: FloatLike) -> FloatValue: + return FloatValue.derive_many(min, *values, is_associative=True) + + +def max_float(*values: FloatLike) -> FloatValue: + return FloatValue.derive_many(max, *values, is_associative=True) + + +def average_floats(*values: FloatLike) -> FloatValue: + return FloatValue.derive_many(_average_float, *values) + + +def sum_floats(*values: FloatLike) -> FloatValue: + return FloatValue.derive_many(sum, *values, is_associative=True) + + +def multiply_floats(*values: FloatLike) -> FloatValue: + return FloatValue.derive_many(multiply_all_floats, *values, is_associative=True) + + class OneToFloatValue(Generic[_S], OneToOneValue[_S, float], FloatValue): pass @@ -195,7 +203,7 @@ class FloatConstant(FloatValue, Constant[float]): @classmethod def of(cls, value: float) -> FloatConstant: try: - return cls._cache[value] + return FloatConstant._cache[value] except KeyError: return FloatConstant(value) diff --git a/src/spellbind/int_values.py b/src/spellbind/int_values.py index e495974..5fe4a68 100644 --- a/src/spellbind/int_values.py +++ b/src/spellbind/int_values.py @@ -137,14 +137,6 @@ def __pos__(self) -> Self: def clamp(self, min_value: IntLike, max_value: IntLike) -> IntValue: return IntValue.derive_three(clamp_int, self, min_value, max_value) - @classmethod - def min(cls, *values: IntLike) -> IntValue: - return IntValue.derive_many(min, *values, is_associative=True) - - @classmethod - def max(cls, *values: IntLike) -> IntValue: - return IntValue.derive_many(max, *values, is_associative=True) - @classmethod def derive_one(cls, operator_: Callable[[_S], int], value: _S | Value[_S]) -> IntValue: if not isinstance(value, Value): @@ -192,6 +184,14 @@ def derive_many(cls, operator_: Callable[[Sequence[int]], int], *values: IntLike return IntConstant.of(operator_(constant_values)) +def min_int(*values: IntLike) -> IntValue: + return IntValue.derive_many(min, *values, is_associative=True) + + +def max_int(*values: IntLike) -> IntValue: + return IntValue.derive_many(max, *values, is_associative=True) + + class OneToIntValue(Generic[_S], OneToOneValue[_S, int], IntValue): pass diff --git a/src/spellbind/str_values.py b/src/spellbind/str_values.py index f351608..d6bda6c 100644 --- a/src/spellbind/str_values.py +++ b/src/spellbind/str_values.py @@ -15,10 +15,20 @@ StrLike = str | Value[str] _S = TypeVar('_S') +_JOIN_FUNCTIONS: dict[str, Callable[[Iterable[str]], str]] = {} -def _join_strs(values: Iterable[str]) -> str: - return "".join(values) +def _get_join_function(separator: str) -> Callable[[Iterable[str]], str]: + try: + return _JOIN_FUNCTIONS[separator] + except KeyError: + def join_function(values: Iterable[str]) -> str: + return separator.join(values) + _JOIN_FUNCTIONS[separator] = join_function + return join_function + + +_join_strs = _get_join_function("") class StrValue(Value[str], ABC): @@ -52,6 +62,14 @@ def derive_many(cls, operator_: Callable[[Iterable[str]], str], *values: StrLike return StrConstant.of(operator_(constant_values)) +def concatenate(*values: StrLike) -> StrValue: + return StrValue.derive_many(_join_strs, *values, is_associative=True) + + +def join(separator: str = "", *values: StrLike) -> StrValue: + return StrValue.derive_many(_get_join_function(separator), *values, is_associative=True) + + class OneToStrValue(OneToOneValue[_S, str], StrValue, Generic[_S]): pass diff --git a/tests/test_values/test_float_values/test_average_float_values.py b/tests/test_values/test_float_values/test_average_float_values.py index 41c812a..92ff276 100644 --- a/tests/test_values/test_float_values/test_average_float_values.py +++ b/tests/test_values/test_float_values/test_average_float_values.py @@ -1,4 +1,5 @@ -from spellbind.float_values import FloatValue, FloatVariable, FloatConstant +from spellbind import float_values +from spellbind.float_values import FloatVariable, FloatConstant def test_min_float_values(): @@ -6,7 +7,7 @@ def test_min_float_values(): v1 = FloatVariable(2.) v2 = FloatVariable(3.) - average_val = FloatValue.average(v0, v1, v2) + average_val = float_values.average_floats(v0, v1, v2) assert average_val.value == 2. v2.value = 6. @@ -16,7 +17,7 @@ def test_min_float_values(): def test_min_float_values_with_literals(): v0 = FloatVariable(1.) - average_val = FloatValue.average(v0, 2., 3.) + average_val = float_values.average_floats(v0, 2., 3.) assert average_val.value == 2. v0.value = 4. @@ -28,7 +29,7 @@ def test_min_int_constants_is_constant(): v1 = FloatConstant(2) v2 = FloatConstant(3) - average_val = FloatValue.average(v0, v1, v2) + average_val = float_values.average_floats(v0, v1, v2) assert isinstance(average_val, FloatConstant) @@ -37,11 +38,11 @@ def test_sum_averaged_float_values(): v1 = FloatVariable(2.) v2 = FloatVariable(3.) - average_val_0 = FloatValue.average(v0, v1, v2) + average_val_0 = float_values.average_floats(v0, v1, v2) v3 = FloatVariable(4.) v4 = FloatVariable(5.) - average_val_1 = FloatValue.average(v3, v4) + average_val_1 = float_values.average_floats(v3, v4) summed_average = average_val_0 + average_val_1 assert summed_average.value == (1. + 2. + 3.) / 3. + (4. + 5.) / 2. diff --git a/tests/test_values/test_float_values/test_min_max_float_values.py b/tests/test_values/test_float_values/test_min_max_float_values.py index a6a33ab..a4e0c1c 100644 --- a/tests/test_values/test_float_values/test_min_max_float_values.py +++ b/tests/test_values/test_float_values/test_min_max_float_values.py @@ -1,4 +1,5 @@ -from spellbind.float_values import FloatConstant, FloatValue, FloatVariable, ManyFloatsToFloatValue +from spellbind import float_values +from spellbind.float_values import FloatConstant, FloatVariable, ManyFloatsToFloatValue from spellbind.values import SimpleVariable @@ -7,7 +8,7 @@ def test_min_float_values(): v1 = SimpleVariable(20.3) v2 = SimpleVariable(5.7) - min_val = FloatValue.min(v0, v1, v2) + min_val = float_values.min_float(v0, v1, v2) assert min_val.value == 5.7 v2.value = 2.1 @@ -17,7 +18,7 @@ def test_min_float_values(): def test_min_float_values_with_literals(): v0 = SimpleVariable(10.5) - min_val = FloatValue.min(v0, 25.7, 15.2) + min_val = float_values.min_float(v0, 25.7, 15.2) assert min_val.value == 10.5 v0.value = 5.1 @@ -29,7 +30,7 @@ def test_min_int_constants_is_constant(): v1 = FloatConstant(20.3) v2 = FloatConstant(5.7) - min_val = FloatValue.min(v0, v1, v2) + min_val = float_values.min_float(v0, v1, v2) assert isinstance(min_val, FloatConstant) @@ -38,7 +39,7 @@ def test_max_float_values(): v1 = SimpleVariable(20.3) v2 = SimpleVariable(5.7) - max_val = FloatValue.max(v0, v1, v2) + max_val = float_values.max_float(v0, v1, v2) assert max_val.value == 20.3 v0.value = 30.1 @@ -48,7 +49,7 @@ def test_max_float_values(): def test_max_float_values_with_literals(): v0 = SimpleVariable(10.5) - max_val = FloatValue.max(v0, 25.7, 15.2) + max_val = float_values.max_float(v0, 25.7, 15.2) assert max_val.value == 25.7 v0.value = 30.1 @@ -60,7 +61,7 @@ def test_max_int_constants_is_constant(): v1 = FloatConstant(20.3) v2 = FloatConstant(5.7) - max_val = FloatValue.max(v0, v1, v2) + max_val = float_values.max_float(v0, v1, v2) assert isinstance(max_val, FloatConstant) @@ -69,12 +70,12 @@ def test_flattens_min_values(): v1 = FloatVariable(20.3) v2 = FloatVariable(5.7) - min_val_0 = FloatValue.min(v0, v1, v2) + min_val_0 = float_values.min_float(v0, v1, v2) v3 = FloatVariable(15.0) v4 = FloatVariable(25.0) - min_val_1 = FloatValue.min(v3, v4) - flattened_min_val = FloatValue.min(min_val_0, min_val_1) + min_val_1 = float_values.min_float(v3, v4) + flattened_min_val = float_values.min_float(min_val_0, min_val_1) assert flattened_min_val.value == 5.7 assert isinstance(flattened_min_val, ManyFloatsToFloatValue) assert flattened_min_val._input_values == (v0, v1, v2, v3, v4) @@ -85,12 +86,12 @@ def test_flattens_max_values(): v1 = FloatVariable(20.3) v2 = FloatVariable(5.7) - max_val_0 = FloatValue.max(v0, v1, v2) + max_val_0 = float_values.max_float(v0, v1, v2) v3 = FloatVariable(15.0) v4 = FloatVariable(25.0) - max_val_1 = FloatValue.max(v3, v4) - flattened_max_val = FloatValue.max(max_val_0, max_val_1) + max_val_1 = float_values.max_float(v3, v4) + flattened_max_val = float_values.max_float(max_val_0, max_val_1) assert flattened_max_val.value == 25.0 assert isinstance(flattened_max_val, ManyFloatsToFloatValue) assert flattened_max_val._input_values == (v0, v1, v2, v3, v4) diff --git a/tests/test_values/test_int_values/test_min_max_int_values.py b/tests/test_values/test_int_values/test_min_max_int_values.py index b1994d0..d7fd738 100644 --- a/tests/test_values/test_int_values/test_min_max_int_values.py +++ b/tests/test_values/test_int_values/test_min_max_int_values.py @@ -1,5 +1,5 @@ -from spellbind.float_values import FloatValue -from spellbind.int_values import IntConstant, IntValue +from spellbind import int_values +from spellbind.int_values import IntConstant from spellbind.values import SimpleVariable @@ -8,7 +8,7 @@ def test_min_int_values(): b = SimpleVariable(20) c = SimpleVariable(5) - min_val = FloatValue.min(a, b, c) + min_val = int_values.min_int(a, b, c) assert min_val.value == 5 c.value = 2 @@ -18,7 +18,7 @@ def test_min_int_values(): def test_min_int_values_with_literals(): a = SimpleVariable(10) - min_val = FloatValue.min(a, 25, 15) + min_val = int_values.min_int(a, 25, 15) assert min_val.value == 10 a.value = 5 @@ -30,7 +30,7 @@ def test_min_int_constants_is_constant(): b = IntConstant(20) c = IntConstant(5) - min_val = IntValue.min(a, b, c) + min_val = int_values.min_int(a, b, c) assert isinstance(min_val, IntConstant) @@ -39,7 +39,7 @@ def test_max_int_values(): b = SimpleVariable(20) c = SimpleVariable(5) - max_val = FloatValue.max(a, b, c) + max_val = int_values.max_int(a, b, c) assert max_val.value == 20 a.value = 30 @@ -49,7 +49,7 @@ def test_max_int_values(): def test_max_int_values_with_literals(): a = SimpleVariable(10) - max_val = FloatValue.max(a, 25, 15) + max_val = int_values.max_int(a, 25, 15) assert max_val.value == 25 a.value = 30 @@ -61,5 +61,5 @@ def test_max_int_constants_is_constant(): b = IntConstant(20) c = IntConstant(5) - max_val = IntValue.max(a, b, c) + max_val = int_values.max_int(a, b, c) assert isinstance(max_val, IntConstant) diff --git a/tests/test_values/test_str_values/test_concatenate_str_values.py b/tests/test_values/test_str_values/test_concatenate_str_values.py index 7fe3521..85a14d0 100644 --- a/tests/test_values/test_str_values/test_concatenate_str_values.py +++ b/tests/test_values/test_str_values/test_concatenate_str_values.py @@ -1,5 +1,6 @@ import pytest +from spellbind import str_values from spellbind.str_values import StrVariable, ManyStrsToStrValue, StrConstant @@ -72,3 +73,55 @@ def test_concatenate_many_str_values_grouped_are_combined(): def test_concatenate_str_constants_is_constant(v0, v1): v2 = v0 + v1 assert v2.constant_value_or_raise == "foobar" + + +def test_concatenate_many_str_values_concatenate(): + v0 = StrVariable("foo") + v1 = StrVariable("bar") + v2 = StrVariable("hello") + v3 = StrVariable("world") + + v4 = str_values.concatenate(v0, v1, v2, v3) + assert v4.value == "foobarhelloworld" + + assert isinstance(v4, ManyStrsToStrValue) + assert v4._input_values == (v0, v1, v2, v3) + + +def test_concatenate_many_str_values_group_flattens(): + v0 = StrVariable("foo") + v1 = StrVariable("bar") + v2 = StrVariable("hello") + v3 = StrVariable("world") + + v4 = str_values.concatenate(str_values.concatenate(v0, v1), str_values.concatenate(v2, v3)) + assert v4.value == "foobarhelloworld" + + assert isinstance(v4, ManyStrsToStrValue) + assert v4._input_values == (v0, v1, v2, v3) + + +def test_join_many_str_values(): + v0 = StrVariable("foo") + v1 = StrVariable("bar") + v2 = StrVariable("hello") + v3 = StrVariable("world") + + v4 = str_values.join(", ", v0, v1, v2, v3) + assert v4.value == "foo, bar, hello, world" + + assert isinstance(v4, ManyStrsToStrValue) + assert v4._input_values == (v0, v1, v2, v3) + + +def test_join_many_str_values_str_value_grouped_flattens(): + v0 = StrVariable("foo") + v1 = StrVariable("bar") + v2 = StrVariable("hello") + v3 = StrVariable("world") + + v4 = str_values.join(", ", str_values.join(", ", v0, v1), str_values.join(", ", v2, v3)) + assert v4.value == "foo, bar, hello, world" + + assert isinstance(v4, ManyStrsToStrValue) + assert v4._input_values == (v0, v1, v2, v3) From 54d1a7aac3696373a5444ea91530178bbd6f4424 Mon Sep 17 00:00:00 2001 From: Georg Plaz Date: Mon, 30 Jun 2025 11:44:45 +0200 Subject: [PATCH 5/5] Make BoolValue.select use derive functions Make BoolValue logical operators use derive functions --- src/spellbind/bool_values.py | 108 +++++++++---- src/spellbind/float_values.py | 102 ++++++------ src/spellbind/functions.py | 10 +- src/spellbind/int_values.py | 109 ++++++------- src/spellbind/str_values.py | 67 ++++---- src/spellbind/values.py | 85 ++++++++-- .../test_bool_values/test_and_bool_values.py | 42 +++++ .../test_bool_values/test_bool_constants.py | 32 ++++ .../test_bool_values/test_bool_values.py | 20 +-- .../test_logical_operators_bool_values.py | 145 ------------------ .../test_bool_values/test_or_bool_values.py | 42 +++++ .../test_bool_values/test_xor_bool_values.py | 42 +++++ .../test_add_float_values.py | 20 +++ .../test_float_values/test_float_values.py | 4 +- .../test_multiply_float_values.py | 42 +++-- .../test_int_values/test_int_values.py | 4 +- 16 files changed, 521 insertions(+), 353 deletions(-) create mode 100644 tests/test_values/test_bool_values/test_and_bool_values.py create mode 100644 tests/test_values/test_bool_values/test_bool_constants.py delete mode 100644 tests/test_values/test_bool_values/test_logical_operators_bool_values.py create mode 100644 tests/test_values/test_bool_values/test_or_bool_values.py create mode 100644 tests/test_values/test_bool_values/test_xor_bool_values.py diff --git a/src/spellbind/bool_values.py b/src/spellbind/bool_values.py index fccc416..4a28807 100644 --- a/src/spellbind/bool_values.py +++ b/src/spellbind/bool_values.py @@ -2,8 +2,10 @@ import operator from abc import ABC -from typing import TypeVar, Generic, overload, TYPE_CHECKING, TypeAlias -from spellbind.values import Value, OneToOneValue, Constant, SimpleVariable, TwoToOneValue, SelectValue +from typing import TypeVar, Generic, overload, TYPE_CHECKING, TypeAlias, Callable, Iterable + +from spellbind.values import Value, OneToOneValue, Constant, SimpleVariable, TwoToOneValue, \ + ManyToSameValue, ThreeToOneValue if TYPE_CHECKING: from spellbind.float_values import FloatValue # pragma: no cover @@ -16,6 +18,10 @@ BoolValueLike: TypeAlias = 'BoolValue | bool' _S = TypeVar('_S') +_T = TypeVar('_T') +_U = TypeVar('_U') +_V = TypeVar('_V') + BoolLike = bool | Value[bool] IntLike = int | Value[int] @@ -23,27 +29,34 @@ StrLike = str | Value[str] +def _select_function(b: bool, t: _S, f: _S) -> _S: + if b: + return t + return f + + class BoolValue(Value[bool], ABC): + @property def logical_not(self) -> BoolValue: return NotBoolValue(self) def __and__(self, other: BoolLike) -> BoolValue: - return AndBoolValues(self, other) + return BoolValue.derive_from_many(all, self, other, is_associative=True) def __rand__(self, other: bool) -> BoolValue: - return AndBoolValues(other, self) + return BoolValue.derive_from_many(all, other, self, is_associative=True) def __or__(self, other: BoolLike) -> BoolValue: - return OrBoolValues(self, other) + return BoolValue.derive_from_many(any, self, other, is_associative=True) def __ror__(self, other: bool) -> BoolValue: - return OrBoolValues(other, self) + return BoolValue.derive_from_many(any, other, self, is_associative=True) def __xor__(self, other: BoolLike) -> BoolValue: - return XorBoolValues(self, other) + return BoolValue.derive_from_two(operator.xor, self, other) def __rxor__(self, other: bool) -> BoolValue: - return XorBoolValues(other, self) + return BoolValue.derive_from_two(operator.xor, other, self) @overload def select(self, if_true: IntValueLike, if_false: IntValueLike) -> IntValue: ... @@ -61,20 +74,50 @@ def select(self, if_true: BoolValue, if_false: BoolValue) -> BoolValue: ... def select(self, if_true: Value[_S] | _S, if_false: Value[_S] | _S) -> Value[_S]: ... def select(self, if_true, if_false): - from spellbind.float_values import FloatValue, SelectFloatValue - from spellbind.int_values import IntValue, SelectIntValue - from spellbind.str_values import StrValue, SelectStrValue + from spellbind.float_values import FloatValue + from spellbind.int_values import IntValue + from spellbind.str_values import StrValue if isinstance(if_true, (FloatValue, float)) and isinstance(if_false, (FloatValue, float)): - return SelectFloatValue(self, if_true, if_false) + return FloatValue.derive_from_three(_select_function, self, if_true, if_false) elif isinstance(if_true, (StrValue, str)) and isinstance(if_false, (StrValue, str)): - return SelectStrValue(self, if_true, if_false) + return StrValue.derive_from_three(_select_function, self, if_true, if_false) elif isinstance(if_true, (BoolValue, bool)) and isinstance(if_false, (BoolValue, bool)): - return SelectBoolValue(self, if_true, if_false) + return BoolValue.derive_from_three(_select_function, self, if_true, if_false) elif isinstance(if_true, (IntValue, int)) and isinstance(if_false, (IntValue, int)): - return SelectIntValue(self, if_true, if_false) + return IntValue.derive_from_three(_select_function, self, if_true, if_false) else: - return SelectValue(self, if_true, if_false) + return Value.derive_three_value(_select_function, self, if_true, if_false) + + @classmethod + def derive_from_two(cls, transformer: Callable[[bool, bool], bool], + first: BoolLike, second: BoolLike) -> BoolValue: + return Value.derive_from_two_with_factory( + transformer, + first, second, + create_value=TwoToBoolValue.create, + create_constant=BoolConstant.of, + ) + + @classmethod + def derive_from_three(cls, transformer: Callable[[_S, _T, _U], bool], + first: _S | Value[_S], second: _T | Value[_T], third: _U | Value[_U]) -> BoolValue: + return Value.derive_from_three_with_factory( + transformer, + first, second, third, + create_value=ThreeToBoolValue.create, + create_constant=BoolConstant.of, + ) + + @classmethod + def derive_from_many(cls, transformer: Callable[[Iterable[bool]], bool], *values: BoolLike, is_associative: bool = False) -> BoolValue: + return Value.derive_from_many_with_factory( + transformer, + *values, + create_value=ManyBoolToBoolValue.create, + create_constant=BoolConstant.of, + is_associative=is_associative, + ) class OneToBoolValue(OneToOneValue[_S, bool], BoolValue, Generic[_S]): @@ -86,19 +129,10 @@ def __init__(self, value: Value[bool]): super().__init__(operator.not_, value) -class AndBoolValues(TwoToOneValue[bool, bool, bool], BoolValue): - def __init__(self, left: BoolLike, right: BoolLike): - super().__init__(operator.and_, left, right) - - -class OrBoolValues(TwoToOneValue[bool, bool, bool], BoolValue): - def __init__(self, left: BoolLike, right: BoolLike): - super().__init__(operator.or_, left, right) - - -class XorBoolValues(TwoToOneValue[bool, bool, bool], BoolValue): - def __init__(self, left: BoolLike, right: BoolLike): - super().__init__(operator.xor, left, right) +class ManyBoolToBoolValue(ManyToSameValue[bool], BoolValue): + @staticmethod + def create(transformer: Callable[[Iterable[bool]], bool], values: Iterable[BoolLike]) -> BoolValue: + return ManyBoolToBoolValue(transformer, *values) class BoolConstant(BoolValue, Constant[bool]): @@ -108,6 +142,7 @@ def of(cls, value: bool) -> BoolConstant: return TRUE return FALSE + @property def logical_not(self) -> BoolConstant: return BoolConstant.of(not self.value) @@ -120,9 +155,18 @@ class BoolVariable(SimpleVariable[bool], BoolValue): pass -class SelectBoolValue(SelectValue[bool], BoolValue): - def __init__(self, condition: BoolLike, if_true: BoolLike, if_false: BoolLike): - super().__init__(condition, if_true, if_false) +class ThreeToBoolValue(ThreeToOneValue[_S, _T, _U, bool], BoolValue): + @staticmethod + def create(transformer: Callable[[_S, _T, _U], bool], + first: _S | Value[_S], second: _T | Value[_T], third: _U | Value[_U]) -> BoolValue: + return ThreeToBoolValue(transformer, first, second, third) + + +class TwoToBoolValue(TwoToOneValue[_S, _T, bool], BoolValue): + @staticmethod + def create(transformer: Callable[[_S, _T], bool], + first: _S | Value[_S], second: _T | Value[_T]) -> BoolValue: + return TwoToBoolValue(transformer, first, second) TRUE = BoolConstant(True) diff --git a/src/spellbind/float_values.py b/src/spellbind/float_values.py index 9f77354..3ac20d3 100644 --- a/src/spellbind/float_values.py +++ b/src/spellbind/float_values.py @@ -8,10 +8,10 @@ from typing_extensions import Self from typing_extensions import TYPE_CHECKING -from spellbind.bool_values import BoolValue, BoolLike -from spellbind.functions import clamp_float, multiply_all_floats -from spellbind.values import Value, SimpleVariable, OneToOneValue, DerivedValueBase, Constant, SelectValue, \ - NotConstantError +from spellbind.bool_values import BoolValue +from spellbind.functions import _clamp_float, _multiply_all_floats +from spellbind.values import Value, SimpleVariable, OneToOneValue, DerivedValueBase, Constant, \ + NotConstantError, ThreeToOneValue if TYPE_CHECKING: from spellbind.int_values import IntValue, IntLike # pragma: no cover @@ -24,7 +24,7 @@ _COMMUTATIVE_OPERATORS = { - operator.add, sum, multiply_all_floats, max, min + operator.add, sum, _multiply_all_floats, max, min } @@ -34,40 +34,40 @@ def _average_float(values: Sequence[float]) -> float: class FloatValue(Value[float], ABC): def __add__(self, other: FloatLike) -> FloatValue: - return FloatValue.derive_many(sum, self, other, is_associative=True) + return FloatValue.derive_from_many(sum, self, other, is_associative=True) def __radd__(self, other: int | float) -> FloatValue: - return FloatValue.derive_many(sum, other, self, is_associative=True) + return FloatValue.derive_from_many(sum, other, self, is_associative=True) def __sub__(self, other: FloatLike) -> FloatValue: - return FloatValue.derive_two(operator.sub, self, other) + return FloatValue.derive_from_two(operator.sub, self, other) def __rsub__(self, other: int | float) -> FloatValue: - return FloatValue.derive_two(operator.sub, other, self) + return FloatValue.derive_from_two(operator.sub, other, self) def __mul__(self, other: FloatLike) -> FloatValue: - return FloatValue.derive_many(multiply_all_floats, self, other, is_associative=True) + return FloatValue.derive_from_many(_multiply_all_floats, self, other, is_associative=True) def __rmul__(self, other: int | float) -> FloatValue: - return FloatValue.derive_many(multiply_all_floats, other, self, is_associative=True) + return FloatValue.derive_from_many(_multiply_all_floats, other, self, is_associative=True) def __truediv__(self, other: FloatLike) -> FloatValue: - return FloatValue.derive_two(operator.truediv, self, other) + return FloatValue.derive_from_two(operator.truediv, self, other) def __rtruediv__(self, other: int | float) -> FloatValue: - return FloatValue.derive_two(operator.truediv, other, self) + return FloatValue.derive_from_two(operator.truediv, other, self) def __pow__(self, other: FloatLike) -> FloatValue: - return FloatValue.derive_two(operator.pow, self, other) + return FloatValue.derive_from_two(operator.pow, self, other) def __rpow__(self, other: FloatLike) -> FloatValue: - return FloatValue.derive_two(operator.pow, other, self) + return FloatValue.derive_from_two(operator.pow, other, self) def __mod__(self, other: FloatLike) -> FloatValue: - return FloatValue.derive_two(operator.mod, self, other) + return FloatValue.derive_from_two(operator.mod, self, other) def __rmod__(self, other: int | float) -> FloatValue: - return FloatValue.derive_two(operator.mod, other, self) + return FloatValue.derive_from_two(operator.mod, other, self) def __abs__(self) -> FloatValue: return AbsFloatValue(self) @@ -75,12 +75,12 @@ def __abs__(self) -> FloatValue: def floor(self) -> IntValue: from spellbind.int_values import IntValue floor_fun: Callable[[float], int] = math.floor - return IntValue.derive_one(floor_fun, self) + return IntValue.derive_from_one(floor_fun, self) def ceil(self) -> IntValue: from spellbind.int_values import IntValue ceil_fun: Callable[[float], int] = math.ceil - return IntValue.derive_one(ceil_fun, self) + return IntValue.derive_from_one(ceil_fun, self) @overload def round(self) -> IntValue: ... @@ -92,9 +92,9 @@ def round(self, ndigits: IntLike | None = None) -> FloatValue | IntValue: if ndigits is None: from spellbind.int_values import IntValue round_to_int_fun: Callable[[float], int] = round - return IntValue.derive_one(round_to_int_fun, self) + return IntValue.derive_from_one(round_to_int_fun, self) round_fun: Callable[[float, int], float] = round - return FloatValue.derive_two(round_fun, self, ndigits) + return FloatValue.derive_from_two(round_fun, self, ndigits) def __lt__(self, other: FloatLike) -> BoolValue: return CompareNumbersValues(self, other, operator.lt) @@ -115,13 +115,13 @@ def __pos__(self) -> Self: return self def clamp(self, min_value: FloatLike, max_value: FloatLike) -> FloatValue: - return FloatValue.derive_three(clamp_float, self, min_value, max_value) + return FloatValue.derive_from_three_floats(_clamp_float, self, min_value, max_value) def decompose_float_operands(self, operator_: Callable[[Sequence[float]], _S]) -> Sequence[FloatLike]: return (self,) @classmethod - def derive_one(cls, transformer: Callable[[float], float], of: FloatLike) -> FloatValue: + def derive_from_one(cls, transformer: Callable[[float], float], of: FloatLike) -> FloatValue: try: constant_value = _get_constant_float(of) except NotConstantError: @@ -131,14 +131,14 @@ def derive_one(cls, transformer: Callable[[float], float], of: FloatLike) -> Flo @classmethod @overload - def derive_two(cls, operator_: Callable[[float, int], float], first: FloatLike, second: IntLike) -> FloatValue: ... + def derive_from_two(cls, operator_: Callable[[float, int], float], first: FloatLike, second: IntLike) -> FloatValue: ... @classmethod @overload - def derive_two(cls, operator_: Callable[[float, float], float], first: FloatLike, second: FloatLike) -> FloatValue: ... + def derive_from_two(cls, operator_: Callable[[float, float], float], first: FloatLike, second: FloatLike) -> FloatValue: ... @classmethod - def derive_two(cls, operator_, first, second) -> FloatValue: + def derive_from_two(cls, operator_, first, second) -> FloatValue: try: constant_first = _get_constant_float(first) constant_second = _get_constant_float(second) @@ -148,49 +148,59 @@ def derive_two(cls, operator_, first, second) -> FloatValue: return FloatConstant.of(operator_(constant_first, constant_second)) @classmethod - def derive_three(cls, operator_: Callable[[float, float, float], float], - first: FloatLike, second: FloatLike, third: FloatLike) -> FloatValue: + def derive_from_three_floats(cls, transformer: Callable[[float, float, float], float], + first: FloatLike, second: FloatLike, third: FloatLike) -> FloatValue: try: constant_first = _get_constant_float(first) constant_second = _get_constant_float(second) constant_third = _get_constant_float(third) except NotConstantError: - return ThreeFloatToFloatValue(operator_, first, second, third) + return ThreeFloatToFloatValue(transformer, first, second, third) else: - return FloatConstant.of(operator_(constant_first, constant_second, constant_third)) + return FloatConstant.of(transformer(constant_first, constant_second, constant_third)) @classmethod - def derive_many(cls, operator_: Callable[[Sequence[float]], float], *values: FloatLike, is_associative: bool = False) -> FloatValue: + def derive_from_three(cls, transformer: Callable[[_S, _T, _U], float], + first: _S | Value[_S], second: _T | Value[_T], third: _U | Value[_U]) -> FloatValue: + return Value.derive_from_three_with_factory( + transformer, + first, second, third, + create_value=ThreeToFloatValue.create, + create_constant=FloatConstant.of, + ) + + @classmethod + def derive_from_many(cls, transformer: Callable[[Sequence[float]], float], *values: FloatLike, is_associative: bool = False) -> FloatValue: try: constant_values = [_get_constant_float(v) for v in values] except NotConstantError: if is_associative: - flattened = [item for v in values for item in _decompose_float_operands(operator_, v)] - return ManyFloatsToFloatValue(operator_, *flattened) + flattened = [item for v in values for item in _decompose_float_operands(transformer, v)] + return ManyFloatsToFloatValue(transformer, *flattened) else: - return ManyFloatsToFloatValue(operator_, *values) + return ManyFloatsToFloatValue(transformer, *values) else: - return FloatConstant.of(operator_(constant_values)) + return FloatConstant.of(transformer(constant_values)) def min_float(*values: FloatLike) -> FloatValue: - return FloatValue.derive_many(min, *values, is_associative=True) + return FloatValue.derive_from_many(min, *values, is_associative=True) def max_float(*values: FloatLike) -> FloatValue: - return FloatValue.derive_many(max, *values, is_associative=True) + return FloatValue.derive_from_many(max, *values, is_associative=True) def average_floats(*values: FloatLike) -> FloatValue: - return FloatValue.derive_many(_average_float, *values) + return FloatValue.derive_from_many(_average_float, *values) def sum_floats(*values: FloatLike) -> FloatValue: - return FloatValue.derive_many(sum, *values, is_associative=True) + return FloatValue.derive_from_many(sum, *values, is_associative=True) def multiply_floats(*values: FloatLike) -> FloatValue: - return FloatValue.derive_many(multiply_all_floats, *values, is_associative=True) + return FloatValue.derive_from_many(_multiply_all_floats, *values, is_associative=True) class OneToFloatValue(Generic[_S], OneToOneValue[_S, float], FloatValue): @@ -328,6 +338,13 @@ def decompose_float_operands(self, operator_: Callable) -> Sequence[FloatLike]: return (self,) +class ThreeToFloatValue(ThreeToOneValue[_S, _T, _U, float], FloatValue): + @classmethod + def create(cls, transformer: Callable[[_S, _T, _U], float], + first: _S | Value[_S], second: _T | Value[_T], third: _U | Value[_U]) -> FloatValue: + return ThreeToFloatValue(transformer, first, second, third) + + class AbsFloatValue(OneFloatToOneValue[float], FloatValue): def __init__(self, value: FloatLike): super().__init__(abs, value) @@ -350,8 +367,3 @@ def __neg__(self) -> FloatValue: class CompareNumbersValues(TwoFloatsToOneValue[bool], BoolValue): def __init__(self, left: FloatLike, right: FloatLike, op: Callable[[float, float], bool]): super().__init__(op, left, right) - - -class SelectFloatValue(SelectValue[float], FloatValue): - def __init__(self, condition: BoolLike, if_true: float | Value[float], if_false: float | Value[float]): - super().__init__(condition, if_true, if_false) diff --git a/src/spellbind/functions.py b/src/spellbind/functions.py index 5d9e0e0..4670e53 100644 --- a/src/spellbind/functions.py +++ b/src/spellbind/functions.py @@ -1,6 +1,6 @@ import inspect from inspect import Parameter -from typing import Callable, Sequence +from typing import Callable, Iterable def _is_positional_parameter(param: Parameter) -> bool: @@ -33,21 +33,21 @@ def assert_parameter_max_count(callable_: Callable, max_count: int) -> None: f"{count_non_default_parameters(callable_)} > {max_count}") -def multiply_all_ints(vals: Sequence[int]) -> int: +def _multiply_all_ints(vals: Iterable[int]) -> int: result = 1 for val in vals: result *= val return result -def multiply_all_floats(vals: Sequence[float]) -> float: +def _multiply_all_floats(vals: Iterable[float]) -> float: result = 1. for val in vals: result *= val return result -def clamp_int(value: int, min_value: int, max_value: int) -> int: +def _clamp_int(value: int, min_value: int, max_value: int) -> int: if value < min_value: return min_value elif value > max_value: @@ -55,7 +55,7 @@ def clamp_int(value: int, min_value: int, max_value: int) -> int: return value -def clamp_float(value: float, min_value: float, max_value: float) -> float: +def _clamp_float(value: float, min_value: float, max_value: float) -> float: if value < min_value: return min_value elif value > max_value: diff --git a/src/spellbind/int_values.py b/src/spellbind/int_values.py index 5fe4a68..1d5a6ef 100644 --- a/src/spellbind/int_values.py +++ b/src/spellbind/int_values.py @@ -2,16 +2,16 @@ import operator from abc import ABC -from typing import overload, Generic, Callable, Sequence +from typing import overload, Generic, Callable, Iterable from typing_extensions import Self, TypeVar -from spellbind.bool_values import BoolValue, BoolLike +from spellbind.bool_values import BoolValue from spellbind.float_values import FloatValue, \ CompareNumbersValues -from spellbind.functions import clamp_int, multiply_all_ints, multiply_all_floats +from spellbind.functions import _clamp_int, _multiply_all_ints, _multiply_all_floats from spellbind.values import Value, SimpleVariable, TwoToOneValue, OneToOneValue, Constant, \ - ThreeToOneValue, SelectValue, NotConstantError, ManyToSameValue, get_constant_of_generic_like, decompose_operands_of_generic_like + ThreeToOneValue, NotConstantError, ManyToSameValue, get_constant_of_generic_like IntLike = int | Value[int] FloatLike = IntLike | float | FloatValue @@ -31,8 +31,8 @@ def __add__(self, other: float | FloatValue) -> FloatValue: ... def __add__(self, other: FloatLike) -> IntValue | FloatValue: if isinstance(other, (float, FloatValue)): - return FloatValue.derive_many(sum, self, other, is_associative=True) - return IntValue.derive_many(sum, self, other, is_associative=True) + return FloatValue.derive_from_many(sum, self, other, is_associative=True) + return IntValue.derive_from_many(sum, self, other, is_associative=True) @overload def __radd__(self, other: int) -> IntValue: ... @@ -42,8 +42,8 @@ def __radd__(self, other: float) -> FloatValue: ... def __radd__(self, other: int | float) -> IntValue | FloatValue: if isinstance(other, float): - return FloatValue.derive_many(sum, other, self, is_associative=True) - return IntValue.derive_many(sum, other, self, is_associative=True) + return FloatValue.derive_from_many(sum, other, self, is_associative=True) + return IntValue.derive_from_many(sum, other, self, is_associative=True) @overload def __sub__(self, other: IntLike) -> IntValue: ... @@ -53,8 +53,8 @@ def __sub__(self, other: float | FloatValue) -> FloatValue: ... def __sub__(self, other: FloatLike) -> IntValue | FloatValue: if isinstance(other, (float, FloatValue)): - return FloatValue.derive_two(operator.sub, self, other) - return IntValue.derive_two(operator.sub, self, other) + return FloatValue.derive_from_two(operator.sub, self, other) + return IntValue.derive_from_two(operator.sub, self, other) @overload def __rsub__(self, other: int) -> IntValue: ... @@ -64,8 +64,8 @@ def __rsub__(self, other: float) -> FloatValue: ... def __rsub__(self, other: int | float) -> IntValue | FloatValue: if isinstance(other, float): - return FloatValue.derive_two(operator.sub, other, self) - return IntValue.derive_two(operator.sub, other, self) + return FloatValue.derive_from_two(operator.sub, other, self) + return IntValue.derive_from_two(operator.sub, other, self) @overload def __mul__(self, other: IntLike) -> IntValue: ... @@ -75,8 +75,8 @@ def __mul__(self, other: float | FloatValue) -> FloatValue: ... def __mul__(self, other: FloatLike) -> IntValue | FloatValue: if isinstance(other, (float, FloatValue)): - return FloatValue.derive_many(multiply_all_floats, self, other, is_associative=True) - return IntValue.derive_many(multiply_all_ints, self, other, is_associative=True) + return FloatValue.derive_from_many(_multiply_all_floats, self, other, is_associative=True) + return IntValue.derive_from_many(_multiply_all_ints, self, other, is_associative=True) @overload def __rmul__(self, other: int) -> IntValue: ... @@ -86,32 +86,32 @@ def __rmul__(self, other: float) -> FloatValue: ... def __rmul__(self, other: int | float) -> IntValue | FloatValue: if isinstance(other, float): - return FloatValue.derive_many(multiply_all_floats, other, self, is_associative=True) - return IntValue.derive_many(multiply_all_ints, other, self, is_associative=True) + return FloatValue.derive_from_many(_multiply_all_floats, other, self, is_associative=True) + return IntValue.derive_from_many(_multiply_all_ints, other, self, is_associative=True) def __truediv__(self, other: FloatLike) -> FloatValue: - return FloatValue.derive_two(operator.truediv, self, other) + return FloatValue.derive_from_two(operator.truediv, self, other) def __rtruediv__(self, other: int | float) -> FloatValue: - return FloatValue.derive_two(operator.truediv, other, self) + return FloatValue.derive_from_two(operator.truediv, other, self) def __floordiv__(self, other: IntLike) -> IntValue: - return IntValue.derive_two(operator.floordiv, self, other) + return IntValue.derive_from_two(operator.floordiv, self, other) def __rfloordiv__(self, other: int) -> IntValue: - return IntValue.derive_two(operator.floordiv, other, self) + return IntValue.derive_from_two(operator.floordiv, other, self) def __pow__(self, other: IntLike) -> IntValue: - return IntValue.derive_two(operator.pow, self, other) + return IntValue.derive_from_two(operator.pow, self, other) def __rpow__(self, other: int) -> IntValue: - return IntValue.derive_two(operator.pow, other, self) + return IntValue.derive_from_two(operator.pow, other, self) def __mod__(self, other: IntLike) -> IntValue: - return IntValue.derive_two(operator.mod, self, other) + return IntValue.derive_from_two(operator.mod, self, other) def __rmod__(self, other: int) -> IntValue: - return IntValue.derive_two(operator.mod, other, self) + return IntValue.derive_from_two(operator.mod, other, self) def __abs__(self) -> IntValue: return AbsIntValue(self) @@ -135,10 +135,10 @@ def __pos__(self) -> Self: return self def clamp(self, min_value: IntLike, max_value: IntLike) -> IntValue: - return IntValue.derive_three(clamp_int, self, min_value, max_value) + return IntValue.derive_from_three(_clamp_int, self, min_value, max_value) @classmethod - def derive_one(cls, operator_: Callable[[_S], int], value: _S | Value[_S]) -> IntValue: + def derive_from_one(cls, operator_: Callable[[_S], int], value: _S | Value[_S]) -> IntValue: if not isinstance(value, Value): return IntConstant.of(operator_(value)) try: @@ -149,7 +149,7 @@ def derive_one(cls, operator_: Callable[[_S], int], value: _S | Value[_S]) -> In return IntConstant.of(operator_(constant_value)) @classmethod - def derive_two(cls, operator_: Callable[[int, int], int], left: IntLike, right: IntLike) -> IntValue: + def derive_from_two(cls, operator_: Callable[[int, int], int], left: IntLike, right: IntLike) -> IntValue: try: left_value = get_constant_of_generic_like(left) right_value = get_constant_of_generic_like(right) @@ -159,37 +159,32 @@ def derive_two(cls, operator_: Callable[[int, int], int], left: IntLike, right: return IntConstant.of(operator_(left_value, right_value)) @classmethod - def derive_three(cls, operator_: Callable[[int, int, int], int], - first: IntLike, second: IntLike, third: IntLike) -> IntValue: - try: - constant_first = get_constant_of_generic_like(first) - constant_second = get_constant_of_generic_like(second) - constant_third = get_constant_of_generic_like(third) - except NotConstantError: - return ThreeToIntValue(operator_, first, second, third) - else: - return IntConstant.of(operator_(constant_first, constant_second, constant_third)) + def derive_from_three(cls, transformer: Callable[[_S, _T, _U], int], + first: _S | Value[_S], second: _T | Value[_T], third: _U | Value[_U]) -> IntValue: + return Value.derive_from_three_with_factory( + transformer, + first, second, third, + create_value=ThreeToIntValue.create, + create_constant=IntConstant.of, + ) @classmethod - def derive_many(cls, operator_: Callable[[Sequence[int]], int], *values: IntLike, is_associative: bool = False) -> IntValue: - try: - constant_values = [get_constant_of_generic_like(v) for v in values] - except NotConstantError: - if is_associative: - flattened = tuple(item for v in values for item in decompose_operands_of_generic_like(operator_, v)) - return ManyIntsToIntValue(operator_, *flattened) - else: - return ManyIntsToIntValue(operator_, *values) - else: - return IntConstant.of(operator_(constant_values)) + def derive_from_many(cls, transformer: Callable[[Iterable[int]], int], *values: IntLike, is_associative: bool = False) -> IntValue: + return Value.derive_from_many_with_factory( + transformer, + *values, + create_value=ManyIntsToIntValue.create, + create_constant=IntConstant.of, + is_associative=is_associative, + ) def min_int(*values: IntLike) -> IntValue: - return IntValue.derive_many(min, *values, is_associative=True) + return IntValue.derive_from_many(min, *values, is_associative=True) def max_int(*values: IntLike) -> IntValue: - return IntValue.derive_many(max, *values, is_associative=True) + return IntValue.derive_from_many(max, *values, is_associative=True) class OneToIntValue(Generic[_S], OneToOneValue[_S, int], IntValue): @@ -201,7 +196,9 @@ class TwoToIntValue(Generic[_S, _T], TwoToOneValue[_S, _T, int], IntValue): class ThreeToIntValue(Generic[_S, _T, _U], ThreeToOneValue[_S, _T, _U, int], IntValue): - pass + @staticmethod + def create(transformer: Callable[[_S, _T, _U], int], first: _S | Value[_S], second: _T | Value[_T], third: _U | Value[_U]) -> IntValue: + return ThreeToIntValue(transformer, first, second, third) class IntConstant(IntValue, Constant[int]): @@ -233,8 +230,9 @@ class IntVariable(SimpleVariable[int], IntValue): class ManyIntsToIntValue(ManyToSameValue[int], IntValue): - def __init__(self, operator_: Callable[[Sequence[int]], int], *values: IntLike): - super().__init__(operator_, *values) + @staticmethod + def create(transformer: Callable[[Iterable[int]], int], values: Iterable[IntLike]) -> IntValue: + return ManyIntsToIntValue(transformer, *values) class AbsIntValue(OneToOneValue[int, int], IntValue): @@ -254,8 +252,3 @@ def __neg__(self) -> IntValue: if isinstance(of, IntValue): return of return super().__neg__() - - -class SelectIntValue(SelectValue[int], IntValue): - def __init__(self, condition: BoolLike, if_true: IntLike, if_false: IntLike): - super().__init__(condition, if_true, if_false) diff --git a/src/spellbind/str_values.py b/src/spellbind/str_values.py index d6bda6c..a48ff03 100644 --- a/src/spellbind/str_values.py +++ b/src/spellbind/str_values.py @@ -1,12 +1,10 @@ from __future__ import annotations from abc import ABC -from typing import Any, Generic, TypeVar, Callable, Sequence, Iterable, TYPE_CHECKING - -from spellbind.bool_values import BoolLike -from spellbind.values import Value, OneToOneValue, SimpleVariable, Constant, SelectValue, \ - ManyToSameValue, NotConstantError, get_constant_of_generic_like, decompose_operands_of_generic_like +from typing import Any, Generic, TypeVar, Callable, Iterable, TYPE_CHECKING +from spellbind.values import Value, OneToOneValue, SimpleVariable, Constant, \ + ManyToSameValue, ThreeToOneValue if TYPE_CHECKING: from spellbind.int_values import IntValue, IntConstant # pragma: no cover @@ -15,6 +13,8 @@ StrLike = str | Value[str] _S = TypeVar('_S') +_T = TypeVar('_T') +_U = TypeVar('_U') _JOIN_FUNCTIONS: dict[str, Callable[[Iterable[str]], str]] = {} @@ -33,41 +33,47 @@ def join_function(values: Iterable[str]) -> str: class StrValue(Value[str], ABC): def __add__(self, other: StrLike) -> StrValue: - return StrValue.derive_many(_join_strs, self, other, is_associative=True) + return StrValue.derive_from_many(_join_strs, self, other, is_associative=True) def __radd__(self, other: StrLike) -> StrValue: - return StrValue.derive_many(_join_strs, other, self, is_associative=True) + return StrValue.derive_from_many(_join_strs, other, self, is_associative=True) @property def length(self) -> IntValue: from spellbind.int_values import IntValue str_length: Callable[[str], int] = len - return IntValue.derive_one(str_length, self) + return IntValue.derive_from_one(str_length, self) def to_str(self) -> StrValue: return self @classmethod - def derive_many(cls, operator_: Callable[[Iterable[str]], str], *values: StrLike, - is_associative: bool = False) -> StrValue: - try: - constant_values = [get_constant_of_generic_like(v) for v in values] - except NotConstantError: - if is_associative: - flattened = tuple(item for v in values for item in decompose_operands_of_generic_like(operator_, v)) - return ManyStrsToStrValue(operator_, *flattened) - else: - return ManyStrsToStrValue(operator_, *values) - else: - return StrConstant.of(operator_(constant_values)) + def derive_from_three(cls, transformer: Callable[[_S, _T, _U], str], + first: _S | Value[_S], second: _T | Value[_T], third: _U | Value[_U]) -> StrValue: + return Value.derive_from_three_with_factory( + transformer, + first, second, third, + create_value=ThreeToStrValue.create, + create_constant=StrConstant.of, + ) + + @classmethod + def derive_from_many(cls, transformer: Callable[[Iterable[str]], str], *values: StrLike, is_associative: bool = False) -> StrValue: + return Value.derive_from_many_with_factory( + transformer, + *values, + create_value=ManyStrsToStrValue.create, + create_constant=StrConstant.of, + is_associative=is_associative, + ) def concatenate(*values: StrLike) -> StrValue: - return StrValue.derive_many(_join_strs, *values, is_associative=True) + return StrValue.derive_from_many(_join_strs, *values, is_associative=True) def join(separator: str = "", *values: StrLike) -> StrValue: - return StrValue.derive_many(_get_join_function(separator), *values, is_associative=True) + return StrValue.derive_from_many(_get_join_function(separator), *values, is_associative=True) class OneToStrValue(OneToOneValue[_S, str], StrValue, Generic[_S]): @@ -111,15 +117,18 @@ class StrVariable(SimpleVariable[str], StrValue): class ManyStrsToStrValue(ManyToSameValue[str], StrValue): - def __init__(self, transformer: Callable[[Sequence[str]], str], *values: StrLike): - super().__init__(transformer, *values) + @staticmethod + def create(transformer: Callable[[Iterable[str]], str], values: Iterable[StrLike]) -> StrValue: + return ManyStrsToStrValue(transformer, *values) + + +class ThreeToStrValue(ThreeToOneValue[_S, _T, _U, str], StrValue, Generic[_S, _T, _U]): + @staticmethod + def create(transformer: Callable[[_S, _T, _U], str], + first: _S | Value[_S], second: _T | Value[_T], third: _U | Value[_U]) -> StrValue: + return ThreeToStrValue(transformer, first, second, third) class ToStrValue(OneToOneValue[Any, str], StrValue): def __init__(self, value: Value[Any]): super().__init__(str, value) - - -class SelectStrValue(SelectValue[str], StrValue): - def __init__(self, condition: BoolLike, if_true: StrLike, if_false: StrLike): - super().__init__(condition, if_true, if_false) diff --git a/src/spellbind/values.py b/src/spellbind/values.py index 7b47997..1a665ae 100644 --- a/src/spellbind/values.py +++ b/src/spellbind/values.py @@ -9,7 +9,8 @@ if TYPE_CHECKING: from spellbind.str_values import StrValue # pragma: no cover from spellbind.int_values import IntValue # pragma: no cover - from spellbind.bool_values import BoolValue, BoolLike # pragma: no cover + from spellbind.bool_values import BoolValue # pragma: no cover + from spellbind.float_values import FloatValue # pragma: no cover EMPTY_FROZEN_SET: frozenset = frozenset() @@ -18,6 +19,7 @@ _T = TypeVar("_T") _U = TypeVar("_U") _V = TypeVar("_V") +_W = TypeVar("_W") def _create_value_getter(value: Value[_S] | _S) -> Callable[[], _S]: @@ -75,7 +77,7 @@ def map_to_int(self, transformer: Callable[[_S], int]) -> IntValue: from spellbind.int_values import OneToIntValue return OneToIntValue(transformer, self) - def map_to_float(self, transformer: Callable[[_S], float]) -> Value[float]: + def map_to_float(self, transformer: Callable[[_S], float]) -> FloatValue: from spellbind.float_values import OneToFloatValue return OneToFloatValue(transformer, self) @@ -94,6 +96,67 @@ def constant_value_or_raise(self) -> _S: def decompose_operands(self, operator_: Callable) -> Sequence[Value[_S] | _S]: return (self,) + @classmethod + def derive_from_two_with_factory( + cls, + transformer: Callable[[_S, _T], _U], + first: _S | Value[_S], second: _T | Value[_T], + create_value: Callable[[Callable[[_S, _T], _U], _S | Value[_S], _T | Value[_T]], _V], + create_constant: Callable[[_U], _V]) -> _V: + try: + constant_first = get_constant_of_generic_like(first) + constant_second = get_constant_of_generic_like(second) + except NotConstantError: + return create_value(transformer, first, second) + else: + return create_constant(transformer(constant_first, constant_second)) + + @classmethod + def derive_from_three_with_factory( + cls, + transformer: Callable[[_S, _T, _U], _V], + first: _S | Value[_S], second: _T | Value[_T], third: _U | Value[_U], + create_value: Callable[[Callable[[_S, _T, _U], _V], _S | Value[_S], _T | Value[_T], _U | Value[_U]], _W], + create_constant: Callable[[_V], _W]) -> _W: + try: + constant_first = get_constant_of_generic_like(first) + constant_second = get_constant_of_generic_like(second) + constant_third = get_constant_of_generic_like(third) + except NotConstantError: + return create_value(transformer, first, second, third) + else: + return create_constant(transformer(constant_first, constant_second, constant_third)) + + @classmethod + def derive_three_value( + cls, + transformer: Callable[[_S, _T, _U], _V], + first: _S | Value[_S], second: _T | Value[_T], third: _U | Value[_U]) -> Value[_V]: + return Value.derive_from_three_with_factory( + transformer, + first, second, third, + create_value=ThreeToOneValue.create, + create_constant=Constant.of + ) + + @classmethod + def derive_from_many_with_factory( + cls, + transformer: Callable[[Iterable[_S]], _S], *values: _S | Value[_S], + create_value: Callable[[Callable[[Iterable[_S]], _S], Sequence[_S | Value[_S]]], _T], + create_constant: Callable[[_S], _T], + is_associative: bool = False) -> _T: + try: + constant_values = [get_constant_of_generic_like(v) for v in values] + except NotConstantError: + if is_associative: + flattened = tuple(item for v in values for item in decompose_operands_of_generic_like(transformer, v)) + return create_value(transformer, flattened) + else: + return create_value(transformer, values) + else: + return create_constant(transformer(constant_values)) + class Variable(Value[_S], Generic[_S], ABC): @property @@ -209,6 +272,10 @@ def derived_from(self) -> frozenset[Value[_S]]: def constant_value_or_raise(self) -> _S: return self._value + @classmethod + def of(cls, value: _S) -> Constant[_S]: + return Constant(value) + class DerivedValueBase(Value[_S], Generic[_S], ABC): def __init__(self, *derived_from: Value): @@ -258,7 +325,7 @@ def _calculate_value(self) -> _T: class ManyToOneValue(DerivedValueBase[_T], Generic[_S, _T]): - def __init__(self, transformer: Callable[[Sequence[_S]], _T], *values: _S | Value[_S]): + def __init__(self, transformer: Callable[[Iterable[_S]], _T], *values: _S | Value[_S]): self._input_values = tuple(values) self._value_getters = [_create_value_getter(v) for v in self._input_values] self._transformer = transformer @@ -270,8 +337,8 @@ def _calculate_value(self) -> _T: class ManyToSameValue(ManyToOneValue[_S, _S], Generic[_S]): - def decompose_operands(self, operator_: Callable) -> Sequence[Value[_S] | _S]: - if operator_ == self._transformer: + def decompose_operands(self, transformer: Callable) -> Sequence[Value[_S] | _S]: + if transformer == self._transformer: return self._input_values return (self,) @@ -305,10 +372,10 @@ def __init__(self, transformer: Callable[[_S, _T, _U], _V], def _calculate_value(self) -> _V: return self._transformer(self._first_getter(), self._second_getter(), self._third_getter()) - -class SelectValue(ThreeToOneValue[bool, _S, _S, _S], Generic[_S]): - def __init__(self, condition: BoolLike, if_true: Value[_S] | _S, if_false: Value[_S] | _S): - super().__init__(lambda b, t, f: t if b else f, condition, if_true, if_false) + @classmethod + def create(cls, transformer: Callable[[_S, _T, _U], _V], + first: _S | Value[_S], second: _T | Value[_T], third: _U | Value[_U]) -> Value[_V]: + return ThreeToOneValue(transformer, first, second, third) def get_constant_of_generic_like(value: _S | Value[_S]) -> _S: diff --git a/tests/test_values/test_bool_values/test_and_bool_values.py b/tests/test_values/test_bool_values/test_and_bool_values.py new file mode 100644 index 0000000..ec9414b --- /dev/null +++ b/tests/test_values/test_bool_values/test_and_bool_values.py @@ -0,0 +1,42 @@ +import pytest + +from spellbind.bool_values import BoolVariable, BoolConstant + + +@pytest.mark.parametrize("bool0, bool1", [(True, True), (True, False), (False, True), (False, False)]) +def test_bool_variables_and(bool0, bool1): + v0 = BoolVariable(bool0) + v1 = BoolVariable(bool1) + result = v0 & v1 + assert result.value is (bool0 and bool1) + + +@pytest.mark.parametrize("bool0, bool1", [(True, True), (True, False), (False, True), (False, False)]) +def test_bool_variable_and_constant(bool0, bool1): + v0 = BoolVariable(bool0) + v1 = BoolConstant(bool1) + result = v0 & v1 + assert result.value is (bool0 and bool1) + + +@pytest.mark.parametrize("bool0, bool1", [(True, True), (True, False), (False, True), (False, False)]) +def test_bool_variable_and_literal(bool0, bool1): + v0 = BoolVariable(bool0) + result = v0 & bool1 + assert result.value is (bool0 and bool1) + + +@pytest.mark.parametrize("bool0, bool1", [(True, True), (True, False), (False, True), (False, False)]) +def test_bool_literal_and_variable(bool0, bool1): + v1 = BoolVariable(bool1) + result = bool0 & v1 + assert result.value is (bool0 and bool1) + + +@pytest.mark.parametrize("bool0, bool1", [(True, True), (True, False), (False, True), (False, False)]) +def test_bool_constant_and_constant_is_constant(bool0, bool1): + v0 = BoolConstant(bool0) + v1 = BoolConstant(bool1) + result = v0 & v1 + assert result.value is (bool0 and bool1) + assert isinstance(result, BoolConstant) diff --git a/tests/test_values/test_bool_values/test_bool_constants.py b/tests/test_values/test_bool_values/test_bool_constants.py new file mode 100644 index 0000000..fa06451 --- /dev/null +++ b/tests/test_values/test_bool_values/test_bool_constants.py @@ -0,0 +1,32 @@ +from spellbind import bool_values +from spellbind.bool_values import BoolConstant + + +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_bool_constant_of_true(): + const_true = BoolConstant.of(True) + assert const_true is bool_values.TRUE + + +def test_bool_constant_of_false(): + const_false = BoolConstant.of(False) + assert const_false is bool_values.FALSE + + +def test_bool_constant_not_true_is_false(): + not_true = BoolConstant.of(True).logical_not + assert not_true is bool_values.FALSE + + +def test_bool_constant_not_false_is_true(): + not_false = BoolConstant.of(False).logical_not + assert not_false is bool_values.TRUE 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 d9c2f09..0b80d83 100644 --- a/tests/test_values/test_bool_values/test_bool_values.py +++ b/tests/test_values/test_bool_values/test_bool_values.py @@ -1,31 +1,21 @@ -from spellbind.bool_values import BoolConstant, BoolVariable - - -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" +from spellbind.bool_values import BoolVariable def test_logical_not_variable_true(): var = BoolVariable(True) - negated = var.logical_not() + negated = var.logical_not assert not negated.value def test_logical_not_variable_false(): var = BoolVariable(False) - negated = var.logical_not() + negated = var.logical_not assert negated.value def test_logical_not_flip_flop(): var = BoolVariable(True) - negated = var.logical_not() + negated = var.logical_not assert not negated.value var.value = False @@ -37,7 +27,7 @@ def test_logical_not_flip_flop(): def test_logical_not_double_negation(): var = BoolVariable(True) - double_negated = var.logical_not().logical_not() + double_negated = var.logical_not.logical_not assert double_negated.value var.value = False diff --git a/tests/test_values/test_bool_values/test_logical_operators_bool_values.py b/tests/test_values/test_bool_values/test_logical_operators_bool_values.py deleted file mode 100644 index 45376a5..0000000 --- a/tests/test_values/test_bool_values/test_logical_operators_bool_values.py +++ /dev/null @@ -1,145 +0,0 @@ -from spellbind.bool_values import BoolVariable - - -def test_bool_variables_and_variable_true_variable_true(): - var1 = BoolVariable(True) - var2 = BoolVariable(True) - result = var1 & var2 - assert result.value - - var1.value = False - assert not result.value - - -def test_bool_variables_and_variable_true_variable_false(): - var1 = BoolVariable(True) - var2 = BoolVariable(False) - result = var1 & var2 - assert not result.value - - var2.value = True - assert result.value - - -def test_bool_variables_and_variable_false_variable_false(): - var1 = BoolVariable(False) - var2 = BoolVariable(False) - result = var1 & var2 - assert not result.value - - var1.value = True - assert not result.value - - -def test_bool_variable_and_variable_true_literal_false(): - var = BoolVariable(True) - result = var & False - assert not result.value - - var.value = False - assert not result.value - - -def test_bool_variable_and_literal_false_variable_true(): - var = BoolVariable(True) - result = False & var - assert not result.value - - var.value = False - assert not result.value - - -def test_bool_variables_or_variable_true_variable_true(): - var1 = BoolVariable(True) - var2 = BoolVariable(True) - result = var1 | var2 - assert result.value - - var1.value = False - assert result.value - - -def test_bool_variables_or_variable_true_variable_false(): - var1 = BoolVariable(True) - var2 = BoolVariable(False) - result = var1 | var2 - assert result.value - - var1.value = False - assert not result.value - - -def test_bool_variables_or_variable_false_variable_false(): - var1 = BoolVariable(False) - var2 = BoolVariable(False) - result = var1 | var2 - assert not result.value - - var2.value = True - assert result.value - - -def test_bool_variable_or_variable_false_literal_true(): - var = BoolVariable(False) - result = var | True - assert result.value - - var.value = True - assert result.value - - -def test_bool_variable_or_literal_true_variable_false(): - var = BoolVariable(False) - result = True | var - assert result.value - - var.value = True - assert result.value - - -def test_bool_variables_xor_variable_true_variable_true(): - var1 = BoolVariable(True) - var2 = BoolVariable(True) - result = var1 ^ var2 - assert not result.value - - var1.value = False - assert result.value - - -def test_bool_variables_xor_variable_true_variable_false(): - var1 = BoolVariable(True) - var2 = BoolVariable(False) - result = var1 ^ var2 - assert result.value - - var2.value = True - assert not result.value - - -def test_bool_variables_xor_variable_false_variable_false(): - var1 = BoolVariable(False) - var2 = BoolVariable(False) - result = var1 ^ var2 - assert not result.value - - var1.value = True - assert result.value - - -def test_bool_variable_xor_variable_true_literal_true(): - var = BoolVariable(True) - result = var ^ True - assert not result.value - - var.value = False - assert result.value - - -def test_bool_variable_xor_literal_true_variable_true(): - var = BoolVariable(True) - result = True ^ var - assert not result.value - - var.value = False - assert result.value diff --git a/tests/test_values/test_bool_values/test_or_bool_values.py b/tests/test_values/test_bool_values/test_or_bool_values.py new file mode 100644 index 0000000..b342675 --- /dev/null +++ b/tests/test_values/test_bool_values/test_or_bool_values.py @@ -0,0 +1,42 @@ +import pytest + +from spellbind.bool_values import BoolVariable, BoolConstant + + +@pytest.mark.parametrize("bool0, bool1", [(True, True), (True, False), (False, True), (False, False)]) +def test_bool_variables_or(bool0, bool1): + v0 = BoolVariable(bool0) + v1 = BoolVariable(bool1) + result = v0 | v1 + assert result.value is (bool0 or bool1) + + +@pytest.mark.parametrize("bool0, bool1", [(True, True), (True, False), (False, True), (False, False)]) +def test_bool_variable_or_constant(bool0, bool1): + v0 = BoolVariable(bool0) + v1 = BoolConstant(bool1) + result = v0 | v1 + assert result.value is (bool0 or bool1) + + +@pytest.mark.parametrize("bool0, bool1", [(True, True), (True, False), (False, True), (False, False)]) +def test_bool_variable_or_literal(bool0, bool1): + v0 = BoolVariable(bool0) + result = v0 | bool1 + assert result.value is (bool0 or bool1) + + +@pytest.mark.parametrize("bool0, bool1", [(True, True), (True, False), (False, True), (False, False)]) +def test_bool_literal_or_variable(bool0, bool1): + v1 = BoolVariable(bool1) + result = bool0 | v1 + assert result.value is (bool0 or bool1) + + +@pytest.mark.parametrize("bool0, bool1", [(True, True), (True, False), (False, True), (False, False)]) +def test_bool_constant_or_constant_is_constant(bool0, bool1): + v0 = BoolConstant(bool0) + v1 = BoolConstant(bool1) + result = v0 | v1 + assert result.value is (bool0 or bool1) + assert isinstance(result, BoolConstant) diff --git a/tests/test_values/test_bool_values/test_xor_bool_values.py b/tests/test_values/test_bool_values/test_xor_bool_values.py new file mode 100644 index 0000000..22fc9dd --- /dev/null +++ b/tests/test_values/test_bool_values/test_xor_bool_values.py @@ -0,0 +1,42 @@ +import pytest + +from spellbind.bool_values import BoolVariable, BoolConstant + + +@pytest.mark.parametrize("bool0, bool1", [(True, True), (True, False), (False, True), (False, False)]) +def test_bool_variables_xor(bool0, bool1): + v0 = BoolVariable(bool0) + v1 = BoolVariable(bool1) + result = v0 ^ v1 + assert result.value is (bool0 ^ bool1) + + +@pytest.mark.parametrize("bool0, bool1", [(True, True), (True, False), (False, True), (False, False)]) +def test_bool_variable_xor_constant(bool0, bool1): + v0 = BoolVariable(bool0) + v1 = BoolConstant(bool1) + result = v0 ^ v1 + assert result.value is (bool0 ^ bool1) + + +@pytest.mark.parametrize("bool0, bool1", [(True, True), (True, False), (False, True), (False, False)]) +def test_bool_variable_xor_literal(bool0, bool1): + v0 = BoolVariable(bool0) + result = v0 ^ bool1 + assert result.value is (bool0 ^ bool1) + + +@pytest.mark.parametrize("bool0, bool1", [(True, True), (True, False), (False, True), (False, False)]) +def test_bool_literal_xor_variable(bool0, bool1): + v1 = BoolVariable(bool1) + result = bool0 ^ v1 + assert result.value is (bool0 ^ bool1) + + +@pytest.mark.parametrize("bool0, bool1", [(True, True), (True, False), (False, True), (False, False)]) +def test_bool_constant_xor_constant_is_constant(bool0, bool1): + v0 = BoolConstant(bool0) + v1 = BoolConstant(bool1) + result = v0 ^ v1 + assert result.value is (bool0 ^ bool1) + assert isinstance(result, BoolConstant) diff --git a/tests/test_values/test_float_values/test_add_float_values.py b/tests/test_values/test_float_values/test_add_float_values.py index 61e510e..6239f92 100644 --- a/tests/test_values/test_float_values/test_add_float_values.py +++ b/tests/test_values/test_float_values/test_add_float_values.py @@ -1,3 +1,4 @@ +from spellbind import float_values from spellbind.float_values import FloatVariable, ManyFloatsToFloatValue, FloatConstant from spellbind.int_values import IntVariable @@ -106,3 +107,22 @@ def test_add_literal_to_constant_is_constant(): v2 = v0 + v1 assert v2.value == 4.0 assert isinstance(v2, FloatConstant) + + +def test_sum_float_values(): + v0 = FloatVariable(1.5) + v1 = FloatVariable(2.5) + v2 = FloatVariable(3.5) + summed = float_values.sum_floats(v0, v1, v2) + assert summed.value == 7.5 + + v0.value = 2.5 + assert summed.value == 8.5 + + +def test_sum_float_constants(): + v0 = FloatConstant(1.5) + v1 = FloatConstant(2.5) + v2 = FloatConstant(3.5) + summed = float_values.sum_floats(v0, v1, v2) + assert summed.constant_value_or_raise == 7.5 diff --git a/tests/test_values/test_float_values/test_float_values.py b/tests/test_values/test_float_values/test_float_values.py index 4362d37..a61adda 100644 --- a/tests/test_values/test_float_values/test_float_values.py +++ b/tests/test_values/test_float_values/test_float_values.py @@ -35,10 +35,10 @@ def test_add_int_values_garbage_collected(): def test_derive_float_constant_returns_constant(): v0 = FloatConstant(4.5) - derived = FloatValue.derive_one(lambda x: x + 1.0, v0) + derived = FloatValue.derive_from_one(lambda x: x + 1.0, v0) assert derived.constant_value_or_raise == 5.5 def test_derive_float_literal_returns_constant(): - derived = FloatValue.derive_one(lambda x: x + 1.0, 4.5) + derived = FloatValue.derive_from_one(lambda x: x + 1.0, 4.5) assert derived.constant_value_or_raise == 5.5 diff --git a/tests/test_values/test_float_values/test_multiply_float_values.py b/tests/test_values/test_float_values/test_multiply_float_values.py index 8aeb8a9..2aa5920 100644 --- a/tests/test_values/test_float_values/test_multiply_float_values.py +++ b/tests/test_values/test_float_values/test_multiply_float_values.py @@ -1,8 +1,9 @@ +from spellbind import float_values from spellbind.float_values import FloatVariable, ManyFloatsToFloatValue, FloatConstant from spellbind.int_values import IntVariable -def test_multiply_float_values(): +def test_mul_float_values(): v0 = FloatVariable(2.5) v1 = FloatVariable(3.0) v2 = v0 * v1 @@ -12,7 +13,7 @@ def test_multiply_float_values(): assert v2.value == 12.0 -def test_multiply_float_value_times_float(): +def test_mul_float_value_times_float(): v0 = FloatVariable(2.5) v2 = v0 * 3.0 assert v2.value == 7.5 @@ -21,7 +22,7 @@ def test_multiply_float_value_times_float(): assert v2.value == 12.0 -def test_multiply_float_value_times_int(): +def test_mul_float_value_times_int(): v0 = FloatVariable(2.5) v2 = v0 * 3 assert v2.value == 7.5 @@ -30,7 +31,7 @@ def test_multiply_float_value_times_int(): assert v2.value == 12.0 -def test_multiply_float_value_times_int_value(): +def test_mul_float_value_times_int_value(): v0 = FloatVariable(2.5) v1 = IntVariable(3) v2 = v0 * v1 @@ -40,7 +41,7 @@ def test_multiply_float_value_times_int_value(): assert v2.value == 12.0 -def test_multiply_float_times_float_value(): +def test_mul_float_times_float_value(): v1 = FloatVariable(3.0) v2 = 2.5 * v1 assert v2.value == 7.5 @@ -49,7 +50,7 @@ def test_multiply_float_times_float_value(): assert v2.value == 10.0 -def test_multiply_int_times_float_value(): +def test_mul_int_times_float_value(): v1 = FloatVariable(3.0) v2 = 2 * v1 assert v2.value == 6.0 @@ -58,7 +59,7 @@ def test_multiply_int_times_float_value(): assert v2.value == 8.0 -def test_multiply_many_values_waterfall_style_are_combined(): +def test_mul_many_values_waterfall_style_are_combined(): v0 = FloatVariable(1.5) v1 = FloatVariable(2.5) v2 = FloatVariable(3.5) @@ -71,7 +72,7 @@ def test_multiply_many_values_waterfall_style_are_combined(): assert v4._input_values == (v0, v1, v2, v3) -def test_multiply_many_values_grouped_are_combined(): +def test_mul_many_values_grouped_are_combined(): v0 = FloatVariable(1.5) v1 = FloatVariable(2.5) v2 = FloatVariable(3.5) @@ -84,7 +85,7 @@ def test_multiply_many_values_grouped_are_combined(): assert v4._input_values == (v0, v1, v2, v3) -def test_multiply_constant_by_literal_is_constant(): +def test_mul_constant_by_literal_is_constant(): v0 = FloatConstant(1.5) v1 = 2.5 v2 = v0 * v1 @@ -92,7 +93,7 @@ def test_multiply_constant_by_literal_is_constant(): assert isinstance(v2, FloatConstant) -def test_multiply_constant_by_constant_is_constant(): +def test_mul_constant_by_constant_is_constant(): v0 = FloatConstant(1.5) v1 = FloatConstant(2.5) v2 = v0 * v1 @@ -100,9 +101,28 @@ def test_multiply_constant_by_constant_is_constant(): assert isinstance(v2, FloatConstant) -def test_multiply_literal_by_constant_is_constant(): +def test_mul_literal_by_constant_is_constant(): v0 = 1.5 v1 = FloatConstant(2.5) v2 = v0 * v1 assert v2.value == 3.75 assert isinstance(v2, FloatConstant) + + +def test_multiply_float_values(): + v0 = FloatVariable(1.5) + v1 = FloatVariable(2.5) + v2 = FloatVariable(3.5) + v2 = float_values.multiply_floats(v0, v1, v2) + assert v2.value == 13.125 + + v0.value = 2.0 + assert v2.value == 17.5 + + +def test_multiply_float_constants(): + v0 = FloatConstant(1.5) + v1 = FloatConstant(2.5) + v2 = FloatConstant(3.5) + v2 = float_values.multiply_floats(v0, v1, v2) + assert v2.constant_value_or_raise == 13.125 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 8d9a9a2..a8a406c 100644 --- a/tests/test_values/test_int_values/test_int_values.py +++ b/tests/test_values/test_int_values/test_int_values.py @@ -22,10 +22,10 @@ def test_int_const_repr(): def test_derive_int_constant_returns_constant(): v0 = IntConstant(4) - derived = IntValue.derive_one(lambda x: x + 1, v0) + derived = IntValue.derive_from_one(lambda x: x + 1, v0) assert derived.constant_value_or_raise == 5 def test_derive_int_literal_returns_constant(): - derived = IntValue.derive_one(lambda x: x + 1, 4) + derived = IntValue.derive_from_one(lambda x: x + 1, 4) assert derived.constant_value_or_raise == 5