diff --git a/src/spellbind/bool_values.py b/src/spellbind/bool_values.py index 468fb04..90590c7 100644 --- a/src/spellbind/bool_values.py +++ b/src/spellbind/bool_values.py @@ -1,9 +1,10 @@ from __future__ import annotations +import operator from abc import ABC -from typing import TypeVar, Generic, Callable +from typing import TypeVar, Generic -from spellbind.values import Value, DerivedValue, Constant, SimpleVariable +from spellbind.values import Value, OneToOneValue, Constant, SimpleVariable _S = TypeVar('_S') @@ -13,21 +14,13 @@ def logical_not(self) -> BoolValue: return NotBoolValue(self) -class MappedBoolValue(Generic[_S], DerivedValue[_S, bool], BoolValue): - def __init__(self, value: Value[_S], transform: Callable[[_S], bool]) -> None: - self._transform = transform - super().__init__(value) - - def transform(self, value: _S) -> bool: - return self._transform(value) +class OneToBoolValue(OneToOneValue[_S, bool], BoolValue, Generic[_S]): + pass -class NotBoolValue(DerivedValue[bool, bool], BoolValue): +class NotBoolValue(OneToOneValue[bool, bool], BoolValue): def __init__(self, value: Value[bool]): - super().__init__(value) - - def transform(self, value: bool) -> bool: - return not value + super().__init__(operator.not_, value) class BoolConstant(BoolValue, Constant[bool]): diff --git a/src/spellbind/float_values.py b/src/spellbind/float_values.py index 3f09d3e..5f19375 100644 --- a/src/spellbind/float_values.py +++ b/src/spellbind/float_values.py @@ -1,14 +1,15 @@ from __future__ import annotations import operator -from abc import ABC, abstractmethod +from abc import ABC from typing import Generic, Callable, Sequence, TypeVar, overload from typing_extensions import Self from typing_extensions import TYPE_CHECKING from spellbind.bool_values import BoolValue -from spellbind.values import Value, SimpleVariable, DerivedValue, DerivedValueBase, Constant, CombinedTwoValues +from spellbind.functions import clamp_float, multiply_all_floats +from spellbind.values import Value, SimpleVariable, OneToOneValue, DerivedValueBase, Constant, TwoToOneValue if TYPE_CHECKING: from spellbind.int_values import IntValue, IntLike # pragma: no cover @@ -102,13 +103,8 @@ def clamp(self, min_value: FloatLike, max_value: FloatLike) -> FloatValue: return ClampFloatValue(self, min_value, max_value) -class MappedFloatValue(Generic[_S], DerivedValue[_S, float], FloatValue): - def __init__(self, value: Value[_S], transform: Callable[[_S], float]) -> None: - self._transform = transform - super().__init__(value) - - def transform(self, value: _S) -> float: - return self._transform(value) +class OneToFloatValue(Generic[_S], OneToOneValue[_S, float], FloatValue): + pass class FloatConstant(FloatValue, Constant[float]): @@ -119,141 +115,123 @@ class FloatVariable(SimpleVariable[float], FloatValue): pass -def _get_float(value: float | Value[int] | Value[float]) -> float: +def _create_float_getter(value: float | Value[int] | Value[float]) -> Callable[[], float]: if isinstance(value, Value): - return value.value + return lambda: value.value else: - return value + return lambda: value -class CombinedFloatValues(DerivedValueBase[_U], Generic[_U], ABC): - def __init__(self, *values: float | Value[int] | Value[float]): - super().__init__(*[v for v in values if isinstance(v, Value)]) - self._gotten_values = [_get_float(v) for v in values] - self._callbacks: list[Callable] = [] - for i, v in enumerate(values): - if isinstance(v, Value): - v.weak_observe(self._create_on_n_changed(i)) - self._value = self._calculate_value() - - def _create_on_n_changed(self, index: int) -> Callable[[float], None]: - def on_change(new_value: float) -> None: - self._gotten_values[index] = new_value - self._on_result_change(self._calculate_value()) - self._callbacks.append(on_change) # keep strong reference to callback so it won't be garbage collected - return on_change - - def _calculate_value(self) -> _U: - return self.transform(self._gotten_values) - - def _on_result_change(self, new_value: _U) -> None: - if new_value != self._value: - self._value = new_value - self._on_change(self._value) - - @abstractmethod - def transform(self, values: Sequence[float]) -> _U: ... +class OneFloatToOneValue(DerivedValueBase[_S], Generic[_S]): + def __init__(self, transformer: Callable[[float], _S], of: FloatLike): + self._getter = _create_float_getter(of) + self._transformer = transformer + super().__init__(*[v for v in (of,) if isinstance(v, Value)]) @property - def value(self) -> _U: + def value(self) -> _S: return self._value + def _calculate_value(self) -> _S: + return self._transformer(self._getter()) -class MaxFloatValues(CombinedFloatValues[float], FloatValue): - def transform(self, values: Sequence[float]) -> float: - return max(values) +class ManyFloatToOneValue(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._transformer = transformer + super().__init__(*[v for v in values if isinstance(v, Value)]) -class MinFloatValues(CombinedFloatValues[float], FloatValue): - def transform(self, values: Sequence[float]) -> float: - return min(values) + def _calculate_value(self) -> _S: + gotten_values = [getter() for getter in self._value_getters] + return self._transformer(gotten_values) -class CombinedTwoFloatValues(CombinedFloatValues[_U], Generic[_U], ABC): - def __init__(self, left: FloatLike, right: FloatLike): - super().__init__(left, right) +class TwoFloatToOneValue(DerivedValueBase[_S], Generic[_S]): + def __init__(self, transformer: Callable[[float, float], _S], + first: FloatLike, second: FloatLike): + self._transformer = transformer + 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)]) - def transform(self, values: Sequence[float]) -> _U: - return self.transform_two(values[0], values[1]) + def _calculate_value(self) -> _S: + return self._transformer(self._first_getter(), self._second_getter()) - @abstractmethod - def transform_two(self, left: float, right: float) -> _U: ... +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._first_getter = _create_float_getter(first) + self._second_getter = _create_float_getter(second) + self._third_getter = _create_float_getter(third) + super().__init__(*[v for v in (first, second, third) if isinstance(v, Value)]) -class CombinedThreeFloatValues(CombinedFloatValues[_U], Generic[_U], ABC): - def __init__(self, left: FloatLike, middle: FloatLike, right: FloatLike): - super().__init__(left, middle, right) + def _calculate_value(self) -> _S: + return self._transformer(self._first_getter(), self._second_getter(), self._third_getter()) - def transform(self, values: Sequence[float]) -> _U: - return self.transform_three(values[0], values[1], values[2]) - @abstractmethod - def transform_three(self, left: float, middle: float, right: float) -> _U: ... +class MaxFloatValues(ManyFloatToOneValue[float], FloatValue): + def __init__(self, *values: FloatLike): + super().__init__(max, *values) -class AddFloatValues(CombinedFloatValues[float], FloatValue): - def transform(self, values: Sequence[float]) -> float: - return sum(values) +class MinFloatValues(ManyFloatToOneValue[float], FloatValue): + def __init__(self, *values: FloatLike): + super().__init__(min, *values) -class SubtractFloatValues(CombinedTwoFloatValues[float], FloatValue): - def transform_two(self, left: float, right: float) -> float: - return left - right +class AddFloatValues(ManyFloatToOneValue[float], FloatValue): + def __init__(self, *values: FloatLike): + super().__init__(sum, *values) -class MultiplyFloatValues(CombinedFloatValues[float], FloatValue): - def transform(self, values: Sequence[float]) -> float: - result = 1.0 - for value in values: - result *= value - return result +class SubtractFloatValues(TwoFloatToOneValue[float], FloatValue): + def __init__(self, left: FloatLike, right: FloatLike): + super().__init__(operator.sub, left, right) -class DivideValues(CombinedTwoFloatValues[float], FloatValue): - def transform_two(self, left: float, right: float) -> float: - return left / right +class MultiplyFloatValues(ManyFloatToOneValue[float], FloatValue): + def __init__(self, *values: FloatLike): + super().__init__(multiply_all_floats, *values) -class RoundFloatValue(CombinedTwoValues[float, int, float], FloatValue): +class RoundFloatValue(TwoToOneValue[float, int, float], FloatValue): def __init__(self, value: FloatValue, ndigits: IntLike): - super().__init__(value, ndigits) + super().__init__(round, value, ndigits) - def transform(self, value: float, ndigits: int) -> float: - return round(value, ndigits) - -class ModuloFloatValues(CombinedTwoFloatValues[float], FloatValue): - def transform_two(self, left: float, right: float) -> float: - return left % right +class DivideValues(TwoFloatToOneValue[float], FloatValue): + def __init__(self, left: FloatLike, right: FloatLike): + super().__init__(operator.truediv, left, right) -class AbsFloatValue(DerivedValue[float, float], FloatValue): - def transform(self, value: float) -> float: - return abs(value) +class ModuloFloatValues(TwoFloatToOneValue[float], FloatValue): + def __init__(self, left: FloatLike, right: FloatLike): + super().__init__(operator.mod, left, right) -class PowerFloatValues(CombinedTwoFloatValues[float], FloatValue): - def transform_two(self, left: float, right: float) -> float: - return left ** right +class AbsFloatValue(OneFloatToOneValue[float], FloatValue): + def __init__(self, value: FloatLike): + super().__init__(abs, value) -class NegateFloatValue(DerivedValue[float, float], FloatValue): - def transform(self, value: float) -> float: - return -value +class PowerFloatValues(TwoFloatToOneValue[float], FloatValue): + def __init__(self, left: FloatLike, right: FloatLike): + super().__init__(operator.pow, left, right) -class CompareNumbersValues(CombinedTwoFloatValues[bool], BoolValue): - def __init__(self, left: FloatLike, right: FloatLike, op: Callable[[float, float], bool]): - self._op = op - super().__init__(left, right) +class NegateFloatValue(OneFloatToOneValue[float], FloatValue): + def __init__(self, value: FloatLike): + super().__init__(operator.neg, value) - def transform_two(self, left: float, right: float) -> bool: - return self._op(left, right) +class CompareNumbersValues(TwoFloatToOneValue[bool], BoolValue): + def __init__(self, left: FloatLike, right: FloatLike, op: Callable[[float, float], bool]): + super().__init__(op, left, right) -class ClampFloatValue(CombinedThreeFloatValues[float], FloatValue): - def __init__(self, value: FloatLike, min_value: FloatLike, max_value: FloatLike) -> None: - super().__init__(value, min_value, max_value) - def transform_three(self, value: float, min_value: float, max_value: float) -> float: - return max(min_value, min(max_value, value)) +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) diff --git a/src/spellbind/functions.py b/src/spellbind/functions.py index 53b48a5..5d9e0e0 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 +from typing import Callable, Sequence def _is_positional_parameter(param: Parameter) -> bool: @@ -31,3 +31,33 @@ def assert_parameter_max_count(callable_: Callable, max_count: int) -> None: callable_name = str(callable_) # pragma: no cover raise ValueError(f"Callable {callable_name} has too many non-default parameters: " f"{count_non_default_parameters(callable_)} > {max_count}") + + +def multiply_all_ints(vals: Sequence[int]) -> int: + result = 1 + for val in vals: + result *= val + return result + + +def multiply_all_floats(vals: Sequence[float]) -> float: + result = 1. + for val in vals: + result *= val + return result + + +def clamp_int(value: int, min_value: int, max_value: int) -> int: + if value < min_value: + return min_value + elif value > max_value: + return max_value + return value + + +def clamp_float(value: float, min_value: float, max_value: float) -> float: + if value < min_value: + return min_value + elif value > max_value: + return max_value + return value diff --git a/src/spellbind/int_values.py b/src/spellbind/int_values.py index e30f0e4..34cbc50 100644 --- a/src/spellbind/int_values.py +++ b/src/spellbind/int_values.py @@ -3,15 +3,16 @@ import math import operator from abc import ABC -from typing import overload, Generic, Callable +from typing import overload, Generic from typing_extensions import Self, TypeVar from spellbind.bool_values import BoolValue from spellbind.float_values import FloatValue, MultiplyFloatValues, DivideValues, SubtractFloatValues, \ AddFloatValues, CompareNumbersValues -from spellbind.values import Value, CombinedMixedValues, SimpleVariable, CombinedTwoValues, DerivedValue, Constant, \ - CombinedThreeValues +from spellbind.functions import clamp_int, multiply_all_ints +from spellbind.values import Value, ManyToOneValue, SimpleVariable, TwoToOneValue, OneToOneValue, Constant, \ + ThreeToOneValue IntLike = int | Value[int] FloatLike = IntLike | float | FloatValue @@ -136,13 +137,8 @@ def clamp(self, min_value: IntLike, max_value: IntLike) -> IntValue: return ClampIntValue(self, min_value, max_value) -class MappedIntValue(Generic[_S], DerivedValue[_S, int], IntValue): - def __init__(self, value: Value[_S], transform: Callable[[_S], int]) -> None: - self._transform = transform - super().__init__(value) - - def transform(self, value: _S) -> int: - return self._transform(value) +class OneToIntValue(Generic[_S], OneToOneValue[_S, int], IntValue): + pass class IntConstant(IntValue, Constant[int]): @@ -153,77 +149,71 @@ class IntVariable(SimpleVariable[int], IntValue): pass -class MaxIntValues(CombinedMixedValues[int, int], IntValue): - def transform(self, *values: int) -> int: - return max(values) +class MaxIntValues(ManyToOneValue[int, int], IntValue): + def __init__(self, *values: IntLike): + super().__init__(max, *values) -class MinIntValues(CombinedMixedValues[int, int], IntValue): - def transform(self, *values: int) -> int: - return min(values) +class MinIntValues(ManyToOneValue[int, int], IntValue): + def __init__(self, *values: IntLike): + super().__init__(min, *values) -class AddIntValues(CombinedMixedValues[int, int], IntValue): - def transform(self, *values: int) -> int: - return sum(values) +class AddIntValues(ManyToOneValue[int, int], IntValue): + def __init__(self, *values: IntLike): + super().__init__(sum, *values) -class SubtractIntValues(CombinedTwoValues[int, int, int], IntValue): - def transform(self, left: int, right: int) -> int: - return left - right +class SubtractIntValues(TwoToOneValue[int, int, int], IntValue): + def __init__(self, left: IntLike, right: IntLike): + super().__init__(operator.sub, left, right) -class MultiplyIntValues(CombinedMixedValues[int, int], IntValue): - def transform(self, *values: int) -> int: - result = 1 - for value in values: - result *= value - return result +class MultiplyIntValues(ManyToOneValue[int, int], IntValue): + def __init__(self, *values: IntLike): + super().__init__(multiply_all_ints, *values) -class FloorDivideIntValues(CombinedTwoValues[int, int, int], IntValue): - def transform(self, left: int, right: int) -> int: - return left // right +class FloorDivideIntValues(TwoToOneValue[int, int, int], IntValue): + def __init__(self, left: IntLike, right: IntLike): + super().__init__(operator.floordiv, left, right) -class PowerIntValues(CombinedTwoValues[int, int, int], IntValue): - def transform(self, left: int, right: int) -> int: - return left ** right +class PowerIntValues(TwoToOneValue[int, int, int], IntValue): + def __init__(self, left: IntLike, right: IntLike): + super().__init__(operator.pow, left, right) -class ModuloIntValues(CombinedTwoValues[int, int, int], IntValue): - def transform(self, left: int, right: int) -> int: - return left % right +class ModuloIntValues(TwoToOneValue[int, int, int], IntValue): + def __init__(self, left: IntLike, right: IntLike): + super().__init__(operator.mod, left, right) -class AbsIntValue(DerivedValue[int, int], IntValue): - def transform(self, value: int) -> int: - return abs(value) +class AbsIntValue(OneToOneValue[int, int], IntValue): + def __init__(self, value: Value[int]): + super().__init__(abs, value) -class NegateIntValue(DerivedValue[int, int], IntValue): - def transform(self, value: int) -> int: - return -value +class NegateIntValue(OneToOneValue[int, int], IntValue): + def __init__(self, value: Value[int]): + super().__init__(operator.neg, value) -class FloorFloatValue(DerivedValue[float, int], IntValue): - def transform(self, value: float) -> int: - return math.floor(value) +class FloorFloatValue(OneToOneValue[float, int], IntValue): + def __init__(self, value: Value[float]): + super().__init__(math.floor, value) -class CeilFloatValue(DerivedValue[float, int], IntValue): - def transform(self, value: float) -> int: - return math.ceil(value) +class CeilFloatValue(OneToOneValue[float, int], IntValue): + def __init__(self, value: Value[float]): + super().__init__(math.ceil, value) -class RoundFloatToIntValue(DerivedValue[float, int], IntValue): - def transform(self, value: float) -> int: - return round(value) +class RoundFloatToIntValue(OneToOneValue[float, int], IntValue): + def __init__(self, value: Value[float]): + super().__init__(round, value) -class ClampIntValue(CombinedThreeValues[int, int], IntValue): +class ClampIntValue(ThreeToOneValue[int, int, int, int], IntValue): def __init__(self, value: IntLike, min_value: IntLike, max_value: IntLike) -> None: - super().__init__(value, min_value, max_value) - - def transform_three(self, value: int, min_value: int, max_value: int) -> int: - return max(min_value, min(max_value, value)) + super().__init__(clamp_int, value, min_value, max_value) diff --git a/src/spellbind/str_values.py b/src/spellbind/str_values.py index a0575fa..8c8f5fe 100644 --- a/src/spellbind/str_values.py +++ b/src/spellbind/str_values.py @@ -1,9 +1,9 @@ from __future__ import annotations from abc import ABC -from typing import Any, Generic, Callable, TypeVar +from typing import Any, Generic, TypeVar -from spellbind.values import Value, DerivedValue, CombinedMixedValues, SimpleVariable, Constant +from spellbind.values import Value, OneToOneValue, ManyToOneValue, SimpleVariable, Constant StringLike = str | Value[str] @@ -21,16 +21,11 @@ def to_str(self) -> StrValue: return self -class MappedStrValue(Generic[_S], DerivedValue[_S, str], StrValue): - def __init__(self, value: Value[_S], transform: Callable[[_S], str]) -> None: - self._transform = transform - super().__init__(value) - - def transform(self, value: _S) -> str: - return self._transform(value) +class OneToStrValue(OneToOneValue[_S, str], StrValue, Generic[_S]): + pass -class StrConstant(StrValue, Constant[str]): +class StrConstant(Constant[str], StrValue): pass @@ -38,11 +33,11 @@ class StrVariable(SimpleVariable[str], StrValue): pass -class ToStrValue(DerivedValue[Any, str], StrValue): - def transform(self, value: Any) -> str: - return str(value) +class ToStrValue(OneToOneValue[Any, str], StrValue): + def __init__(self, value: Value[Any]): + super().__init__(str, value) -class ConcatenateStrValues(CombinedMixedValues[str, str], StrValue): - def transform(self, *values: str) -> str: - return ''.join(value for value in values) +class ConcatenateStrValues(ManyToOneValue[str, str], StrValue): + def __init__(self, *values: StringLike): + super().__init__(''.join, *values) diff --git a/src/spellbind/values.py b/src/spellbind/values.py index 6397d9c..4ab1ef6 100644 --- a/src/spellbind/values.py +++ b/src/spellbind/values.py @@ -1,7 +1,7 @@ from __future__ import annotations from abc import ABC, abstractmethod -from typing import TypeVar, Generic, Optional, Any, Iterable, TYPE_CHECKING, Callable +from typing import TypeVar, Generic, Optional, Any, Iterable, TYPE_CHECKING, Callable, Sequence from spellbind.event import ValueEvent from spellbind.observables import ValueObservable, Observer, ValueObserver @@ -17,6 +17,14 @@ _S = TypeVar("_S") _T = TypeVar("_T") _U = TypeVar("_U") +_V = TypeVar("_V") + + +def _create_value_getter(value: Value[_S] | _S) -> Callable[[], _S]: + if isinstance(value, Value): + return lambda: value.value + else: + return lambda: value class Value(ValueObservable[_S], Generic[_S], ABC): @@ -57,23 +65,23 @@ def __repr__(self) -> str: return f"{self.__class__.__name__}({self.value!r})" def map(self, transformer: Callable[[_S], _T]) -> Value[_T]: - return MappedValue(self, transformer) + return OneToOneValue(transformer, self) def map_to_int(self, transformer: Callable[[_S], int]) -> IntValue: - from spellbind.int_values import MappedIntValue - return MappedIntValue(self, transformer) + from spellbind.int_values import OneToIntValue + return OneToIntValue(transformer, self) def map_to_float(self, transformer: Callable[[_S], float]) -> Value[float]: - from spellbind.float_values import MappedFloatValue - return MappedFloatValue(self, transformer) + from spellbind.float_values import OneToFloatValue + return OneToFloatValue(transformer, self) def map_to_str(self, transformer: Callable[[_S], str]) -> StrValue: - from spellbind.str_values import MappedStrValue - return MappedStrValue(self, transformer) + from spellbind.str_values import OneToStrValue + return OneToStrValue(transformer, self) def map_to_bool(self, transformer: Callable[[_S], bool]) -> BoolValue: - from spellbind.bool_values import MappedBoolValue - return MappedBoolValue(self, transformer) + from spellbind.bool_values import OneToBoolValue + return OneToBoolValue(transformer, self) class Variable(Value[_S], Generic[_S], ABC): @@ -187,141 +195,83 @@ def derived_from(self) -> frozenset[Value[_S]]: return EMPTY_FROZEN_SET -class DerivedValueBase(Value[_T], Generic[_T], ABC): - def __init__(self, *values: Value): - self._values = frozenset(values) - self._on_change: ValueEvent[_T] = ValueEvent() - - def derived_from(self) -> frozenset[Value]: - return self._values +class DerivedValueBase(Value[_S], Generic[_S], ABC): + def __init__(self, *derived_from: Value): + self._derived_from = frozenset(derived_from) + self._on_change: ValueEvent[_S] = ValueEvent() + for value in derived_from: + value.weak_observe(self._on_dependency_changed) + self._value = self._calculate_value() - def observe(self, observer: Observer | ValueObserver[_T], times: int | None = None) -> None: + def observe(self, observer: Observer | ValueObserver[_S], times: int | None = None) -> None: self._on_change.observe(observer, times) - def weak_observe(self, observer: Observer | ValueObserver[_T], times: int | None = None) -> None: + def weak_observe(self, observer: Observer | ValueObserver[_S], times: int | None = None) -> None: self._on_change.weak_observe(observer, times) - def unobserve(self, observer: Observer | ValueObserver[_T]) -> None: + def unobserve(self, observer: Observer | ValueObserver[_S]) -> None: self._on_change.unobserve(observer) + def derived_from(self) -> frozenset[Value]: + return self._derived_from -class DerivedValue(DerivedValueBase[_T], Generic[_S, _T], ABC): - def __init__(self, of: Value[_S]): - super().__init__(of) - self._value = self.transform(of.value) - of.weak_observe(self._on_source_change) - - @abstractmethod - def transform(self, value: _S) -> _T: ... - - def _on_source_change(self, new_source_value: _S) -> None: - new_transformed_value = self.transform(new_source_value) - if new_transformed_value != self._value: - self._value = new_transformed_value - self._on_change(self._value) - - @property - def value(self) -> _T: - return self._value - - -class MappedValue(DerivedValue[_S, _T], Generic[_S, _T]): - def __init__(self, of: Value[_S], transformer: Callable[[_S], _T]): - self._transformer = transformer - self._of = of - super().__init__(of) - - def transform(self, value: _S) -> _T: - return self._transformer(value) - - -def _create_value_getter(value: Value[_S] | _S) -> Callable[[], _S]: - if isinstance(value, Value): - return lambda: value.value - else: - return lambda: value - - -class CombinedTwoValues(DerivedValueBase[_U], Generic[_S, _T, _U], ABC): - _on_change: ValueEvent[_U] - - def __init__(self, left: Value[_S] | _S, right: Value[_T] | _T): - super().__init__(*[v for v in (left, right) if isinstance(v, Value)]) - self._left_getter = _create_value_getter(left) - self._right_getter = _create_value_getter(right) - if isinstance(left, Value): - left.weak_observe(self._on_left_change) - if isinstance(right, Value): - right.weak_observe(self._on_right_change) - self._value = self.transform(self._left_getter(), self._right_getter()) - - def _on_left_change(self, new_left_value: _S) -> None: - new_value = self.transform(new_left_value, self._right_getter()) - self._on_result_change(new_value) - - def _on_right_change(self, new_right_value: _T) -> None: - new_value = self.transform(self._left_getter(), new_right_value) - self._on_result_change(new_value) - - def _on_result_change(self, new_value: _U) -> None: + def _on_dependency_changed(self) -> None: + new_value = self._calculate_value() if new_value != self._value: self._value = new_value self._on_change(self._value) @abstractmethod - def transform(self, left: _S, right: _T) -> _U: ... + def _calculate_value(self) -> _S: ... @property - def value(self) -> _U: + def value(self) -> _S: return self._value -def _get_value(value: Value[_S] | _S) -> _S: - if isinstance(value, Value): - return value.value - else: - return value +class OneToOneValue(DerivedValueBase[_T], Generic[_S, _T]): + _getter: Callable[[], _S] + + def __init__(self, transformer: Callable[[_S], _T], of: Value[_S]): + self._getter = _create_value_getter(of) + self._transformer = transformer + super().__init__(*[v for v in (of,) if isinstance(v, Value)]) + def _calculate_value(self) -> _T: + return self._transformer(self._getter()) -class CombinedMixedValues(DerivedValueBase[_T], Generic[_S, _T], ABC): - def __init__(self, *sources: Value[_S] | _S): - super().__init__(*[v for v in sources if isinstance(v, Value)]) - self.gotten_values = [_get_value(v) for v in sources] - self._callbacks: list[Callable] = [] - for i, v in enumerate(sources): - if isinstance(v, Value): - v.weak_observe(self._create_on_n_changed(i)) - self._value = self._calculate_value() - def _create_on_n_changed(self, index: int) -> Callable[[_S], None]: - def on_change(new_value: _S) -> None: - self.gotten_values[index] = new_value - self._on_result_change(self._calculate_value()) - self._callbacks.append(on_change) # keep strong reference to callback so it won't be garbage collected - return on_change +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._transformer = transformer + super().__init__(*[v for v in values if isinstance(v, Value)]) def _calculate_value(self) -> _T: - return self.transform(*self.gotten_values) - - def _on_result_change(self, new_value: _T) -> None: - if new_value != self._value: - self._value = new_value - self._on_change(self._value) + gotten_values = [getter() for getter in self._value_getters] + return self._transformer(gotten_values) - @abstractmethod - def transform(self, *args: _S) -> _T: ... - @property - def value(self) -> _T: - return self._value +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._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)]) + def _calculate_value(self) -> _U: + return self._transformer(self._first_getter(), self._second_getter()) -class CombinedThreeValues(CombinedMixedValues[_S, _T], Generic[_S, _T], ABC): - def __init__(self, left: Value[_S] | _S, middle: Value[_S] | _S, right: Value[_S] | _S): - super().__init__(left, middle, right) - def transform(self, *values: _S) -> _T: - return self.transform_three(values[0], values[1], values[2]) +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._first_getter = _create_value_getter(first) + self._second_getter = _create_value_getter(second) + self._third_getter = _create_value_getter(third) + super().__init__(*[v for v in (first, second, third) if isinstance(v, Value)]) - @abstractmethod - def transform_three(self, left: _S, middle: _S, right: _S) -> _T: ... + def _calculate_value(self) -> _V: + return self._transformer(self._first_getter(), self._second_getter(), self._third_getter())