From 15cbbd815ac91615390ff3ad22e48aa15e34cc32 Mon Sep 17 00:00:00 2001 From: Georg Plaz Date: Tue, 10 Jun 2025 14:56:01 +0200 Subject: [PATCH] Add map and map_to_X methods to Value --- src/spellbind/bool_values.py | 12 ++++ src/spellbind/float_values.py | 9 +++ src/spellbind/int_values.py | 16 ++++- src/spellbind/str_values.py | 13 +++- src/spellbind/values.py | 31 ++++++++++ ...test_boo_values.py => test_bool_values.py} | 0 tests/test_simple_variable.py | 60 +++++++++++++++++++ 7 files changed, 138 insertions(+), 3 deletions(-) rename tests/{test_boo_values.py => test_bool_values.py} (100%) diff --git a/src/spellbind/bool_values.py b/src/spellbind/bool_values.py index 1c7b2e0..6cefb9f 100644 --- a/src/spellbind/bool_values.py +++ b/src/spellbind/bool_values.py @@ -1,15 +1,27 @@ from __future__ import annotations from abc import ABC +from typing import TypeVar, Generic, Callable from spellbind.values import Value, DerivedValue, Constant +_S = TypeVar('_S') + class BoolValue(Value[bool], ABC): 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 NotBoolValue(DerivedValue[bool, bool], BoolValue): def __init__(self, value: Value[bool]): super().__init__(value) diff --git a/src/spellbind/float_values.py b/src/spellbind/float_values.py index caa8a4a..606782b 100644 --- a/src/spellbind/float_values.py +++ b/src/spellbind/float_values.py @@ -99,6 +99,15 @@ def __pos__(self) -> Self: return self +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 FloatConstant(FloatValue, Constant[float]): pass diff --git a/src/spellbind/int_values.py b/src/spellbind/int_values.py index 5f74717..38d9e52 100644 --- a/src/spellbind/int_values.py +++ b/src/spellbind/int_values.py @@ -1,10 +1,10 @@ from __future__ import annotations -from typing_extensions import Self +from typing_extensions import Self, TypeVar import math import operator from abc import ABC -from typing import overload +from typing import overload, Generic, Callable from spellbind.float_values import FloatValue, MultiplyFloatValues, DivideValues, SubtractFloatValues, \ AddFloatValues, CompareNumbersValues @@ -15,6 +15,9 @@ FloatLike = IntLike | float | FloatValue +_S = TypeVar('_S') + + class IntValue(Value[int], ABC): @overload def __add__(self, other: IntLike) -> IntValue: ... @@ -128,6 +131,15 @@ def __pos__(self) -> Self: return self +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 IntConstant(IntValue, Constant[int]): pass diff --git a/src/spellbind/str_values.py b/src/spellbind/str_values.py index 24e9575..f1e023c 100644 --- a/src/spellbind/str_values.py +++ b/src/spellbind/str_values.py @@ -1,12 +1,14 @@ from __future__ import annotations from abc import ABC -from typing import Any +from typing import Any, Generic, Callable, TypeVar from spellbind.values import Value, DerivedValue, CombinedMixedValues, SimpleVariable, Constant StringLike = str | Value[str] +_S = TypeVar('_S') + class StrValue(Value[str], ABC): def __add__(self, other: StringLike) -> StrValue: @@ -16,6 +18,15 @@ def __radd__(self, other: StringLike) -> StrValue: return ConcatenateStrValues(other, 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 StrConstant(StrValue, Constant[str]): pass diff --git a/src/spellbind/values.py b/src/spellbind/values.py index 90b1001..584e792 100644 --- a/src/spellbind/values.py +++ b/src/spellbind/values.py @@ -8,6 +8,8 @@ if TYPE_CHECKING: from spellbind.str_values import StrValue + from spellbind.int_values import IntValue + from spellbind.bool_values import BoolValue EMPTY_FROZEN_SET: frozenset = frozenset() @@ -64,6 +66,25 @@ def __str__(self) -> str: 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) + + def map_to_int(self, transformer: Callable[[_S], int]) -> IntValue: + from spellbind.int_values import MappedIntValue + return MappedIntValue(self, transformer) + + def map_to_float(self, transformer: Callable[[_S], float]) -> Value[float]: + from spellbind.float_values import MappedFloatValue + return MappedFloatValue(self, transformer) + + def map_to_str(self, transformer: Callable[[_S], str]) -> StrValue: + from spellbind.str_values import MappedStrValue + return MappedStrValue(self, transformer) + + def map_to_bool(self, transformer: Callable[[_S], bool]) -> BoolValue: + from spellbind.bool_values import MappedBoolValue + return MappedBoolValue(self, transformer) + class Variable(Value[_S], Generic[_S], ABC): @property @@ -219,6 +240,16 @@ 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]): + super().__init__(of) + self._transformer = transformer + self._of = 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 diff --git a/tests/test_boo_values.py b/tests/test_bool_values.py similarity index 100% rename from tests/test_boo_values.py rename to tests/test_bool_values.py diff --git a/tests/test_simple_variable.py b/tests/test_simple_variable.py index 7fc580d..f612c69 100644 --- a/tests/test_simple_variable.py +++ b/tests/test_simple_variable.py @@ -1,6 +1,11 @@ import gc import pytest + +from spellbind.bool_values import BoolValue +from spellbind.float_values import FloatValue +from spellbind.int_values import IntValue +from spellbind.str_values import StrValue from spellbind.values import SimpleVariable, Constant from conftest import NoParametersObserver, OneParameterObserver @@ -408,3 +413,58 @@ def test_simple_bool_variable_str(): def test_simple_none_variable_str(): none_bool = SimpleVariable(None) assert str(none_bool) == "None" + + +def test_map_float_to_int(): + value = SimpleVariable(42.5) + mapped = value.map_to_int(int) + + assert isinstance(mapped, IntValue) + assert mapped.value == 42 + + value.value = 100.9 + assert mapped.value == 100 + + +def test_map_int_to_float(): + value = SimpleVariable(42) + mapped = value.map_to_float(float) + + assert isinstance(mapped, FloatValue) + assert mapped.value == 42.0 + + value.value = 100 + assert mapped.value == 100.0 + + +def test_map_int_to_str(): + value = SimpleVariable(42) + mapped = value.map_to_str(str) + + assert isinstance(mapped, StrValue) + assert mapped.value == "42" + + value.value = 100 + assert mapped.value == "100" + + +def test_map_int_to_bool(): + value = SimpleVariable(0) + mapped = value.map_to_bool(bool) + + assert isinstance(mapped, BoolValue) + assert mapped.value is False + + value.value = 1 + assert mapped.value is True + + +def test_map_str_to_int(): + value = SimpleVariable("hello") + mapped = value.map_to_int(len) + + assert isinstance(mapped, IntValue) + assert mapped.value == 5 + + value.value = "world!" + assert mapped.value == 6