From ba69b494f8fb51ccff834d1312a1f61b296773a6 Mon Sep 17 00:00:00 2001 From: Georg Plaz Date: Mon, 1 Dec 2025 13:29:25 +0100 Subject: [PATCH] Add StrValue.format --- experimentation.py | 15 --- src/spellbind/str_values.py | 38 +++++- .../test_str_values/test_format_str_values.py | 110 ++++++++++++++++++ 3 files changed, 147 insertions(+), 16 deletions(-) delete mode 100644 experimentation.py create mode 100644 tests/test_values/test_str_values/test_format_str_values.py diff --git a/experimentation.py b/experimentation.py deleted file mode 100644 index 68459f3..0000000 --- a/experimentation.py +++ /dev/null @@ -1,15 +0,0 @@ -from typing import override - - -class Base: - def some_method(self) -> str: - return "This is a method from the Base class." - - -class Sub(Base): - def some_method(self) -> str: - return "This is a method from the Sub class." - - @override - def some_methods(self) -> str: - return "This is a method from the Sub class." diff --git a/src/spellbind/str_values.py b/src/spellbind/str_values.py index a1d1633..6f86ec1 100644 --- a/src/spellbind/str_values.py +++ b/src/spellbind/str_values.py @@ -1,7 +1,7 @@ from __future__ import annotations from abc import ABC -from typing import Any, Generic, TypeVar, Callable, Iterable, TYPE_CHECKING +from typing import Any, Generic, TypeVar, Callable, Iterable, TYPE_CHECKING, Mapping from typing_extensions import override @@ -50,6 +50,42 @@ def length(self) -> IntValue: def to_str(self) -> StrValue: return self + def format(self, **kwargs) -> StrValue: + """Format this StrValue using the provided keyword arguments. + + Updates to self or any of the keyword arguments will cause the resulting StrValue to update accordingly. + + Args: + **kwargs: Keyword arguments to be used for formatting the string, may be StrValue or str. + + Raises: + KeyError: If a required keyword argument is missing during initialisation. + If the key "gets lost" during updates to self, the unformatted string will be returned instead. + """ + + is_initialisation = True + + def formatter(args: Iterable[str]) -> str: + args_tuple = tuple(args) + to_format = args_tuple[0] + current_kwargs = to_format_kwargs(*args_tuple[1:]) + try: + return to_format.format(**current_kwargs) + except KeyError: + if is_initialisation: + raise + else: + return to_format + + copied_kwargs: dict[str, StrLike] = {key: value for key, value in kwargs.items()} + + def to_format_kwargs(*args: str) -> Mapping[str, str]: + return {k: v for k, v in zip(copied_kwargs.keys(), args)} + + result = StrValue.derive_from_many(formatter, self, *copied_kwargs.values()) + is_initialisation = False + return result + @classmethod def derive_from_three(cls, transformer: Callable[[_S, _T, _U], str], first: _S | Value[_S], second: _T | Value[_T], third: _U | Value[_U]) -> StrValue: diff --git a/tests/test_values/test_str_values/test_format_str_values.py b/tests/test_values/test_str_values/test_format_str_values.py new file mode 100644 index 0000000..fbbfc5b --- /dev/null +++ b/tests/test_values/test_str_values/test_format_str_values.py @@ -0,0 +1,110 @@ +import pytest + +from conftest import OneParameterObserver +from spellbind.str_values import StrVariable, StrConstant + + +def test_format_variable_with_str(): + variable = StrVariable("Hello {name}") + formatted = variable.format(name="World") + assert formatted.value == "Hello World" + + +def test_format_variable_with_variable(): + variable = StrVariable("Hello {name}") + name_variable = StrVariable("World") + formatted = variable.format(name=name_variable) + assert formatted.value == "Hello World" + + +def test_format_variable_with_constant(): + variable = StrVariable("Hello {name}") + name_constant = StrConstant.of("World") + formatted = variable.format(name=name_constant) + assert formatted.value == "Hello World" + + +def test_format_constant_with_str(): + constant = StrConstant.of("Hello {name}") + formatted = constant.format(name="World") + assert formatted.value == "Hello World" + + +def test_format_constant_with_variable(): + constant = StrConstant.of("Hello {name}") + name_variable = StrVariable("World") + formatted = constant.format(name=name_variable) + assert formatted.value == "Hello World" + + +def test_format_constant_with_constant(): + constant = StrConstant.of("Hello {name}") + name_constant = StrConstant.of("World") + formatted = constant.format(name=name_constant) + assert formatted.value == "Hello World" + + +def test_format_variable_with_str_change_base(): + variable = StrVariable("Hello {name}") + formatted = variable.format(name="World") + observer = OneParameterObserver() + formatted.observe(observer) + assert formatted.value == "Hello World" + + variable.value = "Hi {name}" + assert formatted.value == "Hi World" + observer.assert_called_once_with("Hi World") + + +def test_format_const_with_variable_change_option(): + constant = StrConstant.of("Hello {name}") + name_variable = StrVariable("World") + formatted = constant.format(name=name_variable) + observer = OneParameterObserver() + formatted.observe(observer) + assert formatted.value == "Hello World" + + name_variable.value = "Universe" + assert formatted.value == "Hello Universe" + observer.assert_called_once_with("Hello Universe") + + +def test_format_constant_with_non_existing_key_raises(): + constant = StrConstant.of("Hello {name}") + with pytest.raises(KeyError): + constant.format(age=30) + + +def test_format_variable_with_str_base_changes_to_invalid_key_does_not_raise(): + variable = StrVariable("Hello {name}") + formatted = variable.format(name="World") + observer = OneParameterObserver() + formatted.observe(observer) + assert formatted.value == "Hello World" + + variable.value = "Hello {namee}" + assert formatted.value == "Hello {namee}" + observer.assert_called_once_with("Hello {namee}") + + +def test_format_constant_with_two_strs(): + constant = StrConstant.of("Coordinates: ({x}, {y})") + formatted = constant.format(x="10", y="20") + assert formatted.value == "Coordinates: (10, 20)" + + +def test_format_constant_with_two_variables_change_both(): + constant = StrConstant.of("Coordinates: ({x}, {y})") + x_var = StrVariable("10") + y_var = StrVariable("20") + formatted = constant.format(x=x_var, y=y_var) + observer = OneParameterObserver() + formatted.observe(observer) + assert formatted.value == "Coordinates: (10, 20)" + + x_var.value = "15" + assert formatted.value == "Coordinates: (15, 20)" + + y_var.value = "25" + assert formatted.value == "Coordinates: (15, 25)" + assert observer.calls == ["Coordinates: (15, 20)", "Coordinates: (15, 25)"]