Skip to content

Commit 4f93d5e

Browse files
authored
Add StrValue.format (#90)
1 parent 4b9d0ef commit 4f93d5e

File tree

3 files changed

+147
-16
lines changed

3 files changed

+147
-16
lines changed

experimentation.py

Lines changed: 0 additions & 15 deletions
This file was deleted.

src/spellbind/str_values.py

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from __future__ import annotations
22

33
from abc import ABC
4-
from typing import Any, Generic, TypeVar, Callable, Iterable, TYPE_CHECKING
4+
from typing import Any, Generic, TypeVar, Callable, Iterable, TYPE_CHECKING, Mapping
55

66
from typing_extensions import override
77

@@ -50,6 +50,42 @@ def length(self) -> IntValue:
5050
def to_str(self) -> StrValue:
5151
return self
5252

53+
def format(self, **kwargs) -> StrValue:
54+
"""Format this StrValue using the provided keyword arguments.
55+
56+
Updates to self or any of the keyword arguments will cause the resulting StrValue to update accordingly.
57+
58+
Args:
59+
**kwargs: Keyword arguments to be used for formatting the string, may be StrValue or str.
60+
61+
Raises:
62+
KeyError: If a required keyword argument is missing during initialisation.
63+
If the key "gets lost" during updates to self, the unformatted string will be returned instead.
64+
"""
65+
66+
is_initialisation = True
67+
68+
def formatter(args: Iterable[str]) -> str:
69+
args_tuple = tuple(args)
70+
to_format = args_tuple[0]
71+
current_kwargs = to_format_kwargs(*args_tuple[1:])
72+
try:
73+
return to_format.format(**current_kwargs)
74+
except KeyError:
75+
if is_initialisation:
76+
raise
77+
else:
78+
return to_format
79+
80+
copied_kwargs: dict[str, StrLike] = {key: value for key, value in kwargs.items()}
81+
82+
def to_format_kwargs(*args: str) -> Mapping[str, str]:
83+
return {k: v for k, v in zip(copied_kwargs.keys(), args)}
84+
85+
result = StrValue.derive_from_many(formatter, self, *copied_kwargs.values())
86+
is_initialisation = False
87+
return result
88+
5389
@classmethod
5490
def derive_from_three(cls, transformer: Callable[[_S, _T, _U], str],
5591
first: _S | Value[_S], second: _T | Value[_T], third: _U | Value[_U]) -> StrValue:
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import pytest
2+
3+
from conftest import OneParameterObserver
4+
from spellbind.str_values import StrVariable, StrConstant
5+
6+
7+
def test_format_variable_with_str():
8+
variable = StrVariable("Hello {name}")
9+
formatted = variable.format(name="World")
10+
assert formatted.value == "Hello World"
11+
12+
13+
def test_format_variable_with_variable():
14+
variable = StrVariable("Hello {name}")
15+
name_variable = StrVariable("World")
16+
formatted = variable.format(name=name_variable)
17+
assert formatted.value == "Hello World"
18+
19+
20+
def test_format_variable_with_constant():
21+
variable = StrVariable("Hello {name}")
22+
name_constant = StrConstant.of("World")
23+
formatted = variable.format(name=name_constant)
24+
assert formatted.value == "Hello World"
25+
26+
27+
def test_format_constant_with_str():
28+
constant = StrConstant.of("Hello {name}")
29+
formatted = constant.format(name="World")
30+
assert formatted.value == "Hello World"
31+
32+
33+
def test_format_constant_with_variable():
34+
constant = StrConstant.of("Hello {name}")
35+
name_variable = StrVariable("World")
36+
formatted = constant.format(name=name_variable)
37+
assert formatted.value == "Hello World"
38+
39+
40+
def test_format_constant_with_constant():
41+
constant = StrConstant.of("Hello {name}")
42+
name_constant = StrConstant.of("World")
43+
formatted = constant.format(name=name_constant)
44+
assert formatted.value == "Hello World"
45+
46+
47+
def test_format_variable_with_str_change_base():
48+
variable = StrVariable("Hello {name}")
49+
formatted = variable.format(name="World")
50+
observer = OneParameterObserver()
51+
formatted.observe(observer)
52+
assert formatted.value == "Hello World"
53+
54+
variable.value = "Hi {name}"
55+
assert formatted.value == "Hi World"
56+
observer.assert_called_once_with("Hi World")
57+
58+
59+
def test_format_const_with_variable_change_option():
60+
constant = StrConstant.of("Hello {name}")
61+
name_variable = StrVariable("World")
62+
formatted = constant.format(name=name_variable)
63+
observer = OneParameterObserver()
64+
formatted.observe(observer)
65+
assert formatted.value == "Hello World"
66+
67+
name_variable.value = "Universe"
68+
assert formatted.value == "Hello Universe"
69+
observer.assert_called_once_with("Hello Universe")
70+
71+
72+
def test_format_constant_with_non_existing_key_raises():
73+
constant = StrConstant.of("Hello {name}")
74+
with pytest.raises(KeyError):
75+
constant.format(age=30)
76+
77+
78+
def test_format_variable_with_str_base_changes_to_invalid_key_does_not_raise():
79+
variable = StrVariable("Hello {name}")
80+
formatted = variable.format(name="World")
81+
observer = OneParameterObserver()
82+
formatted.observe(observer)
83+
assert formatted.value == "Hello World"
84+
85+
variable.value = "Hello {namee}"
86+
assert formatted.value == "Hello {namee}"
87+
observer.assert_called_once_with("Hello {namee}")
88+
89+
90+
def test_format_constant_with_two_strs():
91+
constant = StrConstant.of("Coordinates: ({x}, {y})")
92+
formatted = constant.format(x="10", y="20")
93+
assert formatted.value == "Coordinates: (10, 20)"
94+
95+
96+
def test_format_constant_with_two_variables_change_both():
97+
constant = StrConstant.of("Coordinates: ({x}, {y})")
98+
x_var = StrVariable("10")
99+
y_var = StrVariable("20")
100+
formatted = constant.format(x=x_var, y=y_var)
101+
observer = OneParameterObserver()
102+
formatted.observe(observer)
103+
assert formatted.value == "Coordinates: (10, 20)"
104+
105+
x_var.value = "15"
106+
assert formatted.value == "Coordinates: (15, 20)"
107+
108+
y_var.value = "25"
109+
assert formatted.value == "Coordinates: (15, 25)"
110+
assert observer.calls == ["Coordinates: (15, 20)", "Coordinates: (15, 25)"]

0 commit comments

Comments
 (0)