Coverage for tdom/template_utils_test.py: 100%
115 statements
« prev ^ index » next coverage.py v7.16.0, created at 2026-09-19 21:37 +0000
« prev ^ index » next coverage.py v7.16.0, created at 2026-09-19 21:37 +0000
1from string.templatelib import Interpolation, Template
3import pytest
5from .template_utils import (
6 PartPosition,
7 TemplateRef,
8 TemplateSpan,
9 template_from_parts,
10)
13def test_template_from_parts() -> None:
14 strings = ("Hello, ", "! Today is ", ".")
15 interpolations = (Interpolation("Alice"), Interpolation("Monday"))
16 template = template_from_parts(strings, interpolations)
17 assert template.strings == strings
18 assert template.interpolations == interpolations
21def test_template_ref_is_literal() -> None:
22 literal = TemplateRef.literal("Hello")
23 assert literal.is_literal
25 non_literal = TemplateRef(("", ""))
26 assert not non_literal.is_literal
29def test_template_ref_is_empty() -> None:
30 empty = TemplateRef.empty()
31 assert empty.is_empty
33 non_empty = TemplateRef.literal("Hello")
34 assert not non_empty.is_empty
37def test_template_ref_is_singleton() -> None:
38 singleton = TemplateRef.singleton(0)
39 assert singleton.is_singleton
41 non_singleton = TemplateRef.literal("Hello")
42 assert not non_singleton.is_singleton
45def test_template_ref_interpolation_range() -> None:
46 ref = TemplateRef(("A", "B", "C"), i_start=3)
48 assert ref.i_start == 3
49 assert ref.i_count == 2
50 assert ref.i_stop == 5
53def test_template_ref_post_init_validation() -> None:
54 with pytest.raises(ValueError, match="at least one string"):
55 _ = TemplateRef(())
57 with pytest.raises(ValueError, match="Literal TemplateRef"):
58 _ = TemplateRef(("Hello",), i_start=1)
61def test_template_ref_concat():
62 refs = (
63 TemplateRef.literal("ab"),
64 TemplateRef(("c", "d")),
65 TemplateRef(("ef", ""), i_start=1),
66 TemplateRef(("", "ghi"), i_start=2),
67 )
68 combined = refs[0]
69 for ref in refs[1:]:
70 combined = combined.concat(ref)
72 assert combined == TemplateRef(("abc", "def", "", "ghi"))
75def test_template_ref_concat_literals():
76 assert TemplateRef.literal("abc").concat(
77 TemplateRef.literal("def")
78 ) == TemplateRef.literal("abcdef")
81def test_template_ref_concat_with_nonzero_start():
82 combined = (
83 TemplateRef.literal("a")
84 .concat(TemplateRef(("b", "c"), i_start=3))
85 .concat(TemplateRef.literal("d"))
86 .concat(TemplateRef(("e", "f"), i_start=4))
87 )
89 assert combined == TemplateRef(("ab", "cde", "f"), i_start=3)
92def test_template_ref_concat_rejects_discontiguous_ranges():
93 with pytest.raises(ValueError, match="ranges must be contiguous"):
94 _ = TemplateRef.singleton(1).concat(TemplateRef.singleton(3))
97def test_template_ref_iter_singleton():
98 assert list(TemplateRef.singleton(1)) == [1]
101def test_template_ref_iter_empty():
102 assert list(TemplateRef.empty()) == []
105def test_template_ref_iter_empty_prefix():
106 assert list(TemplateRef(("", "def"), i_start=1)) == [1, "def"]
109def test_template_ref_iter_empty_suffix():
110 assert list(TemplateRef(("abc", ""), i_start=1)) == ["abc", 1]
113def test_template_ref_iter_literal():
114 assert list(TemplateRef.literal("abc")) == ["abc"]
117def test_template_ref_iter_only_interpolations():
118 assert list(TemplateRef(("", "", "", ""), i_start=1)) == [1, 2, 3]
121def test_template_ref_iter_complete():
122 assert list(TemplateRef(("abc", "def", "ghi", "jkl"), i_start=1)) == [
123 "abc",
124 1,
125 "def",
126 2,
127 "ghi",
128 3,
129 "jkl",
130 ]
133def test_template_ref_bind():
134 src_t = t"{'a'}b{'c'}d{'e'}f"
135 ref = TemplateRef(strings=("before ", " after"), i_start=1)
136 bound = ref.bind(src_t.interpolations)
137 assert bound.values == ("c",)
138 assert bound.strings == ref.strings
141class TestPartPosition:
142 def test_is_relative_to_static_string(self) -> None:
143 position = PartPosition(s_index=1, offset=2)
145 assert position.s_index == 1
146 assert position.offset == 2
148 @pytest.mark.parametrize(
149 ("s_index", "offset", "message"),
150 (
151 (-1, 0, "String index must always be positive or zero"),
152 (0, -1, "Offset must always be positive or zero"),
153 ),
154 )
155 def test_invalid(self, s_index: int, offset: int, message: str) -> None:
156 with pytest.raises(ValueError, match=message):
157 _ = PartPosition(s_index, offset)
159 @pytest.mark.parametrize(
160 ("earlier", "later"),
161 (
162 (PartPosition(0), PartPosition(0, 1)),
163 (PartPosition(0, 1), PartPosition(1)),
164 ),
165 )
166 def test_ordering(self, earlier: PartPosition, later: PartPosition) -> None:
167 assert earlier < later
170class TestTemplateSpan:
171 @pytest.mark.parametrize(
172 ("start", "stop"),
173 (
174 (PartPosition(1), PartPosition(0)),
175 (PartPosition(0, 2), PartPosition(0, 1)),
176 ),
177 )
178 def test_reversed_range(self, start: PartPosition, stop: PartPosition) -> None:
179 with pytest.raises(ValueError, match="start must not be after stop"):
180 _ = TemplateSpan(start=start, stop=stop)
182 @pytest.mark.parametrize(
183 "span",
184 (
185 TemplateSpan(PartPosition(1), PartPosition(1)),
186 TemplateSpan(PartPosition(0), PartPosition(1)),
187 ),
188 )
189 def test_position_outside_template(self, span: TemplateSpan) -> None:
190 with pytest.raises(ValueError, match="PartPosition string index"):
191 _ = span.extract(t"ABC")
193 def test_offset_outside_template_string(self) -> None:
194 span = TemplateSpan(PartPosition(0, 4), PartPosition(0, 4))
196 with pytest.raises(ValueError, match="PartPosition offset"):
197 _ = span.extract(t"ABC")
199 def test_retains_original_interpolation_objects(self) -> None:
200 source = t"before {object()} after"
201 span = TemplateSpan(PartPosition(0, 7), PartPosition(1, 0))
203 extracted = span.extract(source)
205 assert extracted.strings == ("", "")
206 assert extracted.interpolations[0] is source.interpolations[0]
209class TestTemplateSpanExtract:
210 @pytest.mark.parametrize(
211 ("t", "start", "stop", "result"),
212 (
213 (t"<div></div>", None, PartPosition(0, offset=5), ("<div>",)),
214 (t"<div></div>", PartPosition(0, offset=5), None, ("</div>",)),
215 (
216 t"<div></div>",
217 PartPosition(0, offset=4),
218 PartPosition(0, offset=6),
219 ("><",),
220 ),
221 (t"<div></div>", PartPosition(0, offset=5), PartPosition(0, offset=5), ()),
222 (t"<div>{0}</div>", None, PartPosition(0, offset=5), ("<div>",)),
223 (
224 t"<div>{0}</div>",
225 PartPosition(0, offset=5),
226 None,
227 (0, "</div>"),
228 ),
229 (t"<div>{0}</div>", PartPosition(1, offset=0), None, ("</div>",)),
230 (t"<div>{0}</div>", None, PartPosition(1, offset=0), ("<div>", 0)),
231 (
232 t"<div>{0}</div>",
233 PartPosition(0, offset=5),
234 PartPosition(1, offset=0),
235 (0,),
236 ),
237 (
238 t"<div>{0}</div>",
239 PartPosition(0, offset=5),
240 PartPosition(0, offset=5),
241 (),
242 ),
243 (t"", None, PartPosition(0, offset=0), ()),
244 (t"", PartPosition(0, offset=0), None, ()),
245 (t"", None, None, ()),
246 (t"{0}", None, PartPosition(1, offset=0), (0,)),
247 (t"{0}", PartPosition(0, offset=0), None, (0,)),
248 (t"{0}", None, None, (0,)),
249 ),
250 )
251 def test_interval(
252 self,
253 t: Template,
254 start: PartPosition | None,
255 stop: PartPosition | None,
256 result: tuple[str | int, ...],
257 ) -> None:
258 span = TemplateSpan(
259 start=start or PartPosition(0),
260 stop=stop or PartPosition(len(t.strings) - 1, len(t.strings[-1])),
261 )
262 extracted = span.extract(t)
263 parts: list[str | object] = []
264 for index, string in enumerate(extracted.strings):
265 if string:
266 parts.append(string)
267 if index < len(extracted.values):
268 parts.append(extracted.values[index])
269 assert tuple(parts) == result
271 def test_interpolation_interval(self) -> None:
272 extracted = TemplateSpan(
273 start=PartPosition(0, len("<div>")),
274 stop=PartPosition(1),
275 ).extract(t"<div>{0}</div>")
277 assert extracted.strings == ("", "")
278 assert extracted.values == (0,)