Skip to content

Commit ccf05aa

Browse files
Wrap up low level placeholder handling into PlaceholderConfig helper class. (#96)
1 parent 562b91c commit ccf05aa

File tree

3 files changed

+68
-59
lines changed

3 files changed

+68
-59
lines changed

tdom/placeholders.py

Lines changed: 44 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,63 +1,72 @@
1+
from dataclasses import dataclass, field
12
import random
23
import re
34
import string
45

56
from .template_utils import TemplateRef
67

78

8-
_PLACEHOLDER_PREFIX = f"t🐍{''.join(random.choices(string.ascii_lowercase, k=2))}-"
9-
_PLACEHOLDER_SUFFIX = f"-{''.join(random.choices(string.ascii_lowercase, k=2))}🐍t"
10-
_PLACEHOLDER_PATTERN = re.compile(
11-
re.escape(_PLACEHOLDER_PREFIX) + r"(\d+)" + re.escape(_PLACEHOLDER_SUFFIX)
12-
)
9+
def make_placeholder_config() -> PlaceholderConfig:
10+
prefix = f"t🐍{''.join(random.choices(string.ascii_lowercase, k=2))}-"
11+
suffix = f"-{''.join(random.choices(string.ascii_lowercase, k=2))}🐍t"
12+
return PlaceholderConfig(
13+
prefix=prefix,
14+
suffix=suffix,
15+
pattern=re.compile(re.escape(prefix) + r"(\d+)" + re.escape(suffix)),
16+
)
1317

1418

15-
def make_placeholder(i: int) -> str:
16-
"""Generate a placeholder for the i-th interpolation."""
17-
return f"{_PLACEHOLDER_PREFIX}{i}{_PLACEHOLDER_SUFFIX}"
19+
@dataclass(frozen=True)
20+
class PlaceholderConfig:
21+
"""String operations for working with a placeholder pattern."""
1822

23+
prefix: str
24+
suffix: str
25+
pattern: re.Pattern
1926

20-
def match_placeholders(s: str) -> list[re.Match[str]]:
21-
"""Find all placeholders in a string."""
22-
return list(_PLACEHOLDER_PATTERN.finditer(s))
27+
def make_placeholder(self, i: int) -> str:
28+
"""Generate a placeholder for the i-th interpolation."""
29+
return f"{self.prefix}{i}{self.suffix}"
2330

31+
def match_placeholders(self, s: str) -> list[re.Match[str]]:
32+
"""Find all placeholders in a string."""
33+
return list(self.pattern.finditer(s))
2434

25-
def find_placeholders(s: str) -> TemplateRef:
26-
"""
27-
Find all placeholders in a string and return a TemplateRef.
35+
def find_placeholders(self, s: str) -> TemplateRef:
36+
"""
37+
Find all placeholders in a string and return a TemplateRef.
2838
29-
If no placeholders are found, returns a static TemplateRef.
30-
"""
31-
matches = match_placeholders(s)
32-
if not matches:
33-
return TemplateRef.literal(s)
39+
If no placeholders are found, returns a static TemplateRef.
40+
"""
41+
matches = self.match_placeholders(s)
42+
if not matches:
43+
return TemplateRef.literal(s)
3444

35-
strings: list[str] = []
36-
i_indexes: list[int] = []
37-
last_index = 0
38-
for match in matches:
39-
start, end = match.span()
40-
strings.append(s[last_index:start])
41-
i_indexes.append(int(match[1]))
42-
last_index = end
43-
strings.append(s[last_index:])
45+
strings: list[str] = []
46+
i_indexes: list[int] = []
47+
last_index = 0
48+
for match in matches:
49+
start, end = match.span()
50+
strings.append(s[last_index:start])
51+
i_indexes.append(int(match[1]))
52+
last_index = end
53+
strings.append(s[last_index:])
4454

45-
return TemplateRef(tuple(strings), tuple(i_indexes))
55+
return TemplateRef(tuple(strings), tuple(i_indexes))
4656

4757

58+
@dataclass
4859
class PlaceholderState:
49-
known: set[int]
60+
known: set[int] = field(default_factory=set)
61+
config: PlaceholderConfig = field(default_factory=make_placeholder_config)
5062
"""Collection of currently 'known and active' placeholder indexes."""
5163

52-
def __init__(self):
53-
self.known = set()
54-
5564
@property
5665
def is_empty(self) -> bool:
5766
return len(self.known) == 0
5867

5968
def add_placeholder(self, index: int) -> str:
60-
placeholder = make_placeholder(index)
69+
placeholder = self.config.make_placeholder(index)
6170
self.known.add(index)
6271
return placeholder
6372

@@ -69,7 +78,7 @@ def remove_placeholders(self, text: str) -> TemplateRef:
6978
7079
If no placeholders are found, returns a static PlaceholderRef.
7180
"""
72-
pt = find_placeholders(text)
81+
pt = self.config.find_placeholders(text)
7382
for index in pt.i_indexes:
7483
if index not in self.known:
7584
raise ValueError(f"Unknown placeholder index {index} found in text.")

tdom/placeholders_test.py

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,52 @@
11
import pytest
22

33
from .placeholders import (
4-
_PLACEHOLDER_PREFIX,
5-
_PLACEHOLDER_SUFFIX,
4+
make_placeholder_config,
65
PlaceholderState,
7-
find_placeholders,
8-
make_placeholder,
9-
match_placeholders,
106
)
117

128

139
def test_make_placeholder() -> None:
14-
assert make_placeholder(0) == f"{_PLACEHOLDER_PREFIX}0{_PLACEHOLDER_SUFFIX}"
15-
assert make_placeholder(42) == f"{_PLACEHOLDER_PREFIX}42{_PLACEHOLDER_SUFFIX}"
10+
config = make_placeholder_config()
11+
assert config.make_placeholder(0) == f"{config.prefix}0{config.suffix}"
12+
assert config.make_placeholder(42) == f"{config.prefix}42{config.suffix}"
1613

1714

1815
def test_match_placeholders() -> None:
19-
s = f"Start {_PLACEHOLDER_PREFIX}0{_PLACEHOLDER_SUFFIX} middle {_PLACEHOLDER_PREFIX}1{_PLACEHOLDER_SUFFIX} end"
20-
matches = match_placeholders(s)
16+
config = make_placeholder_config()
17+
s = f"Start {config.prefix}0{config.suffix} middle {config.prefix}1{config.suffix} end"
18+
matches = config.match_placeholders(s)
2119
assert len(matches) == 2
22-
assert matches[0].group(0) == f"{_PLACEHOLDER_PREFIX}0{_PLACEHOLDER_SUFFIX}"
20+
assert matches[0].group(0) == f"{config.prefix}0{config.suffix}"
2321
assert matches[0][1] == "0"
24-
assert matches[1].group(0) == f"{_PLACEHOLDER_PREFIX}1{_PLACEHOLDER_SUFFIX}"
22+
assert matches[1].group(0) == f"{config.prefix}1{config.suffix}"
2523
assert matches[1][1] == "1"
2624

2725

2826
def test_find_placeholders() -> None:
29-
s = f"Hello {_PLACEHOLDER_PREFIX}0{_PLACEHOLDER_SUFFIX}, today is {_PLACEHOLDER_PREFIX}1{_PLACEHOLDER_SUFFIX}."
30-
pt = find_placeholders(s)
27+
config = make_placeholder_config()
28+
s = f"Hello {config.prefix}0{config.suffix}, today is {config.prefix}1{config.suffix}."
29+
pt = config.find_placeholders(s)
3130
assert pt.strings == ("Hello ", ", today is ", ".")
3231
assert pt.i_indexes == (0, 1)
3332

3433
literal_s = "No placeholders here."
35-
literal_pt = find_placeholders(literal_s)
34+
literal_pt = config.find_placeholders(literal_s)
3635
assert literal_pt.strings == (literal_s,)
3736
assert literal_pt.i_indexes == ()
3837

3938

4039
def test_placeholder_state() -> None:
41-
state = PlaceholderState()
40+
config = make_placeholder_config()
41+
state = PlaceholderState(config=config)
4242
assert state.is_empty
4343

4444
p0 = state.add_placeholder(0)
45-
assert p0 == make_placeholder(0)
45+
assert p0 == config.make_placeholder(0)
4646
assert not state.is_empty
4747

4848
p1 = state.add_placeholder(1)
49-
assert p1 == make_placeholder(1)
49+
assert p1 == config.make_placeholder(1)
5050

5151
text = f"Values: {p0}, {p1}"
5252
pt = state.remove_placeholders(text)
@@ -55,4 +55,4 @@ def test_placeholder_state() -> None:
5555
assert state.is_empty
5656

5757
with pytest.raises(ValueError):
58-
state.remove_placeholders(f"Unknown placeholder: {make_placeholder(2)}")
58+
state.remove_placeholders(f"Unknown placeholder: {config.make_placeholder(2)}")

tdom/processor_test.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from markupsafe import Markup
88

99
from .nodes import Comment, DocumentType, Element, Fragment, Node, Text
10-
from .placeholders import _PLACEHOLDER_PREFIX, _PLACEHOLDER_SUFFIX
10+
from .placeholders import make_placeholder_config
1111
from .processor import html
1212

1313
# --------------------------------------------------------------------------
@@ -597,25 +597,25 @@ def test_interpolated_attribute_value_tricky_multiple_placeholders():
597597

598598

599599
def test_placeholder_collision_avoidance():
600+
config = make_placeholder_config()
600601
# This test is to ensure that our placeholder detection avoids collisions
601602
# even with content that might look like a placeholder.
602603
tricky = "123"
603604
template = Template(
604605
'<div data-tricky="',
605-
_PLACEHOLDER_PREFIX,
606+
config.prefix,
606607
Interpolation(tricky, "tricky"),
607-
_PLACEHOLDER_SUFFIX,
608+
config.suffix,
608609
'"></div>',
609610
)
610611
node = html(template)
611612
assert node == Element(
612613
"div",
613-
attrs={"data-tricky": _PLACEHOLDER_PREFIX + tricky + _PLACEHOLDER_SUFFIX},
614+
attrs={"data-tricky": config.prefix + tricky + config.suffix},
614615
children=[],
615616
)
616617
assert (
617-
str(node)
618-
== f'<div data-tricky="{_PLACEHOLDER_PREFIX}{tricky}{_PLACEHOLDER_SUFFIX}"></div>'
618+
str(node) == f'<div data-tricky="{config.prefix}{tricky}{config.suffix}"></div>'
619619
)
620620

621621

0 commit comments

Comments
 (0)