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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 0 additions & 15 deletions experimentation.py

This file was deleted.

38 changes: 37 additions & 1 deletion src/spellbind/str_values.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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:
Expand Down
110 changes: 110 additions & 0 deletions tests/test_values/test_str_values/test_format_str_values.py
Original file line number Diff line number Diff line change
@@ -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)"]
Loading