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

1from string.templatelib import Interpolation, Template 

2 

3import pytest 

4 

5from .template_utils import ( 

6 PartPosition, 

7 TemplateRef, 

8 TemplateSpan, 

9 template_from_parts, 

10) 

11 

12 

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 

19 

20 

21def test_template_ref_is_literal() -> None: 

22 literal = TemplateRef.literal("Hello") 

23 assert literal.is_literal 

24 

25 non_literal = TemplateRef(("", "")) 

26 assert not non_literal.is_literal 

27 

28 

29def test_template_ref_is_empty() -> None: 

30 empty = TemplateRef.empty() 

31 assert empty.is_empty 

32 

33 non_empty = TemplateRef.literal("Hello") 

34 assert not non_empty.is_empty 

35 

36 

37def test_template_ref_is_singleton() -> None: 

38 singleton = TemplateRef.singleton(0) 

39 assert singleton.is_singleton 

40 

41 non_singleton = TemplateRef.literal("Hello") 

42 assert not non_singleton.is_singleton 

43 

44 

45def test_template_ref_interpolation_range() -> None: 

46 ref = TemplateRef(("A", "B", "C"), i_start=3) 

47 

48 assert ref.i_start == 3 

49 assert ref.i_count == 2 

50 assert ref.i_stop == 5 

51 

52 

53def test_template_ref_post_init_validation() -> None: 

54 with pytest.raises(ValueError, match="at least one string"): 

55 _ = TemplateRef(()) 

56 

57 with pytest.raises(ValueError, match="Literal TemplateRef"): 

58 _ = TemplateRef(("Hello",), i_start=1) 

59 

60 

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) 

71 

72 assert combined == TemplateRef(("abc", "def", "", "ghi")) 

73 

74 

75def test_template_ref_concat_literals(): 

76 assert TemplateRef.literal("abc").concat( 

77 TemplateRef.literal("def") 

78 ) == TemplateRef.literal("abcdef") 

79 

80 

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 ) 

88 

89 assert combined == TemplateRef(("ab", "cde", "f"), i_start=3) 

90 

91 

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)) 

95 

96 

97def test_template_ref_iter_singleton(): 

98 assert list(TemplateRef.singleton(1)) == [1] 

99 

100 

101def test_template_ref_iter_empty(): 

102 assert list(TemplateRef.empty()) == [] 

103 

104 

105def test_template_ref_iter_empty_prefix(): 

106 assert list(TemplateRef(("", "def"), i_start=1)) == [1, "def"] 

107 

108 

109def test_template_ref_iter_empty_suffix(): 

110 assert list(TemplateRef(("abc", ""), i_start=1)) == ["abc", 1] 

111 

112 

113def test_template_ref_iter_literal(): 

114 assert list(TemplateRef.literal("abc")) == ["abc"] 

115 

116 

117def test_template_ref_iter_only_interpolations(): 

118 assert list(TemplateRef(("", "", "", ""), i_start=1)) == [1, 2, 3] 

119 

120 

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 ] 

131 

132 

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 

139 

140 

141class TestPartPosition: 

142 def test_is_relative_to_static_string(self) -> None: 

143 position = PartPosition(s_index=1, offset=2) 

144 

145 assert position.s_index == 1 

146 assert position.offset == 2 

147 

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) 

158 

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 

168 

169 

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) 

181 

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") 

192 

193 def test_offset_outside_template_string(self) -> None: 

194 span = TemplateSpan(PartPosition(0, 4), PartPosition(0, 4)) 

195 

196 with pytest.raises(ValueError, match="PartPosition offset"): 

197 _ = span.extract(t"ABC") 

198 

199 def test_retains_original_interpolation_objects(self) -> None: 

200 source = t"before {object()} after" 

201 span = TemplateSpan(PartPosition(0, 7), PartPosition(1, 0)) 

202 

203 extracted = span.extract(source) 

204 

205 assert extracted.strings == ("", "") 

206 assert extracted.interpolations[0] is source.interpolations[0] 

207 

208 

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 

270 

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>") 

276 

277 assert extracted.strings == ("", "") 

278 assert extracted.values == (0,)