Skip to content

Commit bdef10a

Browse files
koxudaxidpgeorge
authored andcommitted
tests: Add full feature and coverage tests for PEP 750 template strings.
Includes corresponding .exp files because this feature is only available in Python 3.14+. Tests for `!a` conversion specifier and space after `!` are not included because they are not supported by MicroPython. Signed-off-by: Koudai Aono <koxudaxi@gmail.com> Signed-off-by: Damien George <damien@micropython.org>
1 parent a989585 commit bdef10a

30 files changed

Lines changed: 2517 additions & 0 deletions

pyproject.toml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,15 @@ target-version = "py38"
3636

3737
[tool.ruff.lint]
3838
exclude = [ # Ruff finds Python SyntaxError in these files
39+
"tests/basics/string_module_tstring.py",
40+
"tests/basics/string_tstring_basic.py",
41+
"tests/basics/string_tstring_basic1.py",
42+
"tests/basics/string_tstring_constructor.py",
43+
"tests/basics/string_tstring_errors1.py",
44+
"tests/basics/string_tstring_format1.py",
45+
"tests/basics/string_tstring_interpolation1.py",
46+
"tests/basics/string_tstring_operations.py",
47+
"tests/basics/string_tstring_parser1.py",
3948
"tests/cmdline/cmd_compile_only_error.py",
4049
"tests/cmdline/repl_autocomplete.py",
4150
"tests/cmdline/repl_autocomplete_underscore.py",
@@ -47,6 +56,8 @@ exclude = [ # Ruff finds Python SyntaxError in these files
4756
"tests/cmdline/repl_words_move.py",
4857
"tests/feature_check/repl_emacs_check.py",
4958
"tests/feature_check/repl_words_move_check.py",
59+
"tests/feature_check/tstring.py",
60+
"tests/micropython/heapalloc_fail_tstring.py",
5061
"tests/micropython/viper_args.py",
5162
]
5263
extend-select = ["C9", "PLC"]
@@ -78,11 +89,14 @@ mccabe.max-complexity = 40
7889
# Exclude third-party code, and exclude the following tests:
7990
# basics: needs careful attention before applying automatic formatting
8091
# repl_: not real python files
92+
# tstring: ruff does not support template strings
8193
# viper_args: uses f(*)
8294
exclude = [
8395
"tests/basics/*.py",
8496
"tests/*/repl_*.py",
8597
"tests/cmdline/cmd_compile_only_error.py",
98+
"tests/feature_check/tstring.py",
99+
"tests/micropython/heapalloc_fail_tstring.py",
86100
"tests/micropython/test_normalize_newlines.py",
87101
"tests/micropython/viper_args.py",
88102
]
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# Test basic templatelib functionality.
2+
# This test requires t-strings support.
3+
4+
from string.templatelib import Template
5+
6+
print("templatelib", isinstance(t"hi", Template))
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
templatelib True
Lines changed: 338 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,338 @@
1+
from string.templatelib import Template, Interpolation
2+
3+
print("=== Basic functionality ===")
4+
t = t"Hello World"
5+
print(type(t).__name__)
6+
7+
name = "World"
8+
t2 = t"Hello {name}"
9+
print(f"Strings: {t2.strings}")
10+
print(f"Value: {t2.interpolations[0].value}")
11+
print(f"str(): {str(t2)}")
12+
13+
t_raw = rt"Path: C:\test\{name}"
14+
print(f"Raw: '{t_raw.strings[0]}'")
15+
16+
t_tr = tr"Path: C:\test\{name}"
17+
print(f"tr: '{t_tr.strings[0]}'")
18+
19+
print("\n=== Parser tests ===")
20+
data = {"a": {"b": [1, 2, 3]}}
21+
t_complex = t"{data['a']['b'][0]}"
22+
print(f"Complex: {str(t_complex)}")
23+
24+
print(f"None: {str(t'{None}')}")
25+
print(f"True: {str(t'{True}')}")
26+
print(f"False: {str(t'{False}')}")
27+
print(f"Ellipsis: {str(t'{...}')}")
28+
29+
obj = type('Obj', (), {
30+
'__getattr__': lambda s, n: s,
31+
'__getitem__': lambda s, k: s,
32+
'__call__': lambda s, *a: 42,
33+
'__str__': lambda s: "42"
34+
})()
35+
print(f"Deep nest: {str(t'{obj.a.b[0].c()}')}")
36+
37+
print("\n=== Conversions and formatting ===")
38+
val = {"key": "value"}
39+
print(f"repr: {str(t'{val!r}')}")
40+
print(f"str: {str(t'{val!s}')}")
41+
# print(f"ascii: {str(t'{val!a}')}")
42+
43+
print(f"Width: '{str(t'{42:10d}')}'")
44+
print(f"Precision: {str(t'{3.14159:.2f}')}")
45+
46+
x = 42
47+
t_debug = t"{x=}"
48+
assert t_debug.strings == ("x=", "")
49+
assert t_debug.interpolations[0].expression == "x"
50+
assert t_debug.interpolations[0].conversion == "r"
51+
assert t_debug.interpolations[0].value == 42
52+
print(f"Debug: {str(t_debug)}")
53+
54+
y = 10
55+
t_debug2 = t"{x + y=}"
56+
assert t_debug2.strings == ("x + y=", "")
57+
assert t_debug2.interpolations[0].expression == "x + y"
58+
assert t_debug2.interpolations[0].conversion == "r"
59+
assert t_debug2.interpolations[0].value == 52
60+
61+
pi = 3.14159
62+
t_debug3 = t"{pi=:.2f}"
63+
assert t_debug3.strings == ("pi=", "")
64+
assert t_debug3.interpolations[0].expression == "pi"
65+
assert t_debug3.interpolations[0].conversion is None
66+
assert t_debug3.interpolations[0].format_spec == ".2f"
67+
assert t_debug3.interpolations[0].value == 3.14159
68+
69+
print("\n=== Constructor tests ===")
70+
t_empty = Template()
71+
print(f"Empty: {t_empty.strings}")
72+
73+
t_single = Template("single")
74+
print(f"Single: {t_single.strings}")
75+
76+
t_multi = Template("a", "b", "c")
77+
print(f"Multi: {t_multi.strings}")
78+
79+
i1 = Interpolation(1, "x")
80+
i2 = Interpolation(2, "y")
81+
t_mixed = Template("start", i1, "middle", i2, "end")
82+
print(f"Mixed: strings={t_mixed.strings}, values={t_mixed.values}")
83+
84+
print("\n=== Operations ===")
85+
t1 = t"Hello"
86+
t2 = t" World"
87+
t_concat = t1 + t2
88+
print(f"Concat: '{str(t_concat)}'")
89+
90+
items = list(t"a{1}b{2}c")
91+
print(f"Iterator: {[type(x).__name__ for x in items]}")
92+
93+
t_attr = t"test{42}"
94+
print(f"strings attr: {t_attr.strings}")
95+
print(f"interpolations attr: {len(t_attr.interpolations)}")
96+
print(f"values attr: {t_attr.values}")
97+
98+
print(f"repr: {repr(t2)[:50]}...")
99+
100+
print("\n=== Escaped braces evaluation ===")
101+
t_escaped = Template("{", Interpolation(42, "x"), "}", Interpolation(42, "x"), "{{", Interpolation(42, "x"), "}}")
102+
print(f"Escaped eval: '{str(t_escaped)}'")
103+
104+
t_braces = Template("{{hello}}", Interpolation(1, "a"), " {", Interpolation(2, "b"), "} ", Interpolation(3, "c"), "{{world}}")
105+
print(f"Braces in strings: '{str(t_braces)}'")
106+
107+
print("\n=== Memory stress test ===")
108+
for n in [10, 20, 30]:
109+
args = []
110+
for i in range(n):
111+
args.append("s")
112+
args.append(Interpolation(i, f"var{i}"))
113+
args.append("s")
114+
t_mem = Template(*args)
115+
result = str(t_mem)
116+
print(f"Memory test [{n}]: {len(result)} chars")
117+
118+
large_args = []
119+
for i in range(20):
120+
large_args.append("")
121+
large_args.append(Interpolation(i, f"v{i}"))
122+
large_args.append("")
123+
t_large = Template(*large_args)
124+
print(f"Large values: {len(t_large.values)} values")
125+
126+
print("\n=== Nested quotes and braces tests ===")
127+
t_nested1 = t"{"{}"}"
128+
print(f"Nested quotes 1: {str(t_nested1)}")
129+
print(f" Value: {t_nested1.interpolations[0].value}")
130+
print(f" Expression: {t_nested1.interpolations[0].expression}")
131+
132+
t_nested2 = t"{'hello'}"
133+
print(f"Nested quotes 2: {str(t_nested2)}")
134+
print(f" Value: {t_nested2.interpolations[0].value}")
135+
136+
t_nested3 = t'{"world"}'
137+
print(f"Nested quotes 3: {str(t_nested3)}")
138+
print(f" Value: {t_nested3.interpolations[0].value}")
139+
140+
t_nested4 = t"{['a', 'b', 'c'][1]}"
141+
print(f"Nested quotes 4: {str(t_nested4)}")
142+
print(f" Value: {t_nested4.interpolations[0].value}")
143+
144+
d = {"key": "value", "x": 123}
145+
t_nested5 = t"{d['key']}"
146+
print(f"Nested quotes 5: {str(t_nested5)}")
147+
print(f" Value: {t_nested5.interpolations[0].value}")
148+
149+
t_escaped1 = t'{{""}}'
150+
print(f"Escaped braces 1: {str(t_escaped1)}")
151+
print(f" Strings: {t_escaped1.strings}")
152+
153+
t_escaped2 = t"{{}}}}"
154+
print(f"Escaped braces 2: {str(t_escaped2)}")
155+
print(f" Strings: {t_escaped2.strings}")
156+
157+
x = "test"
158+
t_mixed = t"{{before}} {x} {{after}}"
159+
print(f"Mixed escaped: {str(t_mixed)}")
160+
print(f" Strings: {t_mixed.strings}")
161+
print(f" Value: {t_mixed.interpolations[0].value}")
162+
163+
t_escape = t"{'\n\t'}"
164+
print(f"Escape sequences: {str(t_escape)}")
165+
print(f" Value repr: {repr(t_escape.interpolations[0].value)}")
166+
167+
inner = "{}"
168+
t_nested_expr = t"{inner}"
169+
print(f"Nested expr: {str(t_nested_expr)}")
170+
print(f" Value: {t_nested_expr.interpolations[0].value}")
171+
172+
print("\n=== Interpolation attribute tests ===")
173+
i_basic = Interpolation(42, "x")
174+
print(f"Basic conversion: {i_basic.conversion}")
175+
print(f"Basic format_spec: {i_basic.format_spec}")
176+
177+
i_with_conv = Interpolation(42, "x", "s")
178+
print(f"With conversion: {i_with_conv.conversion}")
179+
180+
i_with_fmt = Interpolation(42, "x", None, ":>10")
181+
print(f"With format_spec: {i_with_fmt.format_spec}")
182+
183+
i_full = Interpolation(42, "x", "r", ":>10")
184+
print(f"Full conversion: {i_full.conversion}")
185+
print(f"Full format_spec: {i_full.format_spec}")
186+
187+
t_conv = t"{42!s}"
188+
print(f"Template conversion: {t_conv.interpolations[0].conversion}")
189+
190+
t_fmt = t"{42:>10}"
191+
print(f"Template format_spec: {t_fmt.interpolations[0].format_spec}")
192+
193+
t_both = t"{42!r:>10}"
194+
print(f"Both conversion: {t_both.interpolations[0].conversion}")
195+
print(f"Both format_spec: {t_both.interpolations[0].format_spec}")
196+
197+
print("\n=== Escape sequence tests ===")
198+
print(repr(t"Line1\nLine2"))
199+
print(repr(t"Path\\to\\file"))
200+
print(repr(t"It\'s working"))
201+
print(repr(t"She said \"Hello\""))
202+
print(repr(t"Bell\a"))
203+
print(repr(t"Back\bspace"))
204+
print(repr(t"Tab\there"))
205+
print(repr(t"Vertical\vtab"))
206+
print(repr(t"Form\ffeed"))
207+
print(repr(t"Carriage\rreturn"))
208+
209+
# Valid hex escapes
210+
print(repr(t"\x41"))
211+
print(repr(t"\x7F"))
212+
213+
# Valid octal escapes
214+
print(repr(t"\0"))
215+
print(repr(t"\7"))
216+
print(repr(t"\77"))
217+
print(repr(t"\101"))
218+
219+
print(repr(t"\1a"))
220+
print(repr(t"\123c"))
221+
222+
# Line continuation
223+
print(repr(t"First \
224+
second"))
225+
print(repr(t"One\
226+
Two\
227+
Three"))
228+
229+
# Multiple escapes
230+
print(repr(t"\n\t\r"))
231+
232+
# Raw strings
233+
print(repr(rt"\n\t\x41"))
234+
print(repr(rt"C:\new\test"))
235+
236+
# Triple quoted strings
237+
print(repr(t"""\n\t\x41"""))
238+
print(repr(rt"""\n\t\x41"""))
239+
240+
print("\n=== Triple quote edge cases ===")
241+
doc = '''This is a
242+
multi-line
243+
docstring'''
244+
result1 = t"""Documentation:
245+
{doc}
246+
End of doc"""
247+
print(f"Triple in interpolation: {str(result1)}")
248+
print(f" Value: {repr(result1.interpolations[0].value)}")
249+
250+
data = {
251+
"users": [
252+
{"name": "Alice", "msg": 'Say "Hello"'},
253+
{"name": "Bob", "msg": "It's great!"}
254+
]
255+
}
256+
complex_nested = t"""Users:
257+
{data['users'][0]['name']}: {data['users'][0]['msg']}
258+
{data['users'][1]['name']}: {data['users'][1]['msg']}"""
259+
print(f"Complex nested: {str(complex_nested)}")
260+
print(f" Values: {complex_nested.values}")
261+
262+
import os
263+
path_sep = os.sep
264+
raw_with_interp = rt"""Path: C:\Users\{path_sep}Documents
265+
Raw newline: \n
266+
Raw tab: \t"""
267+
print(f"Raw with interpolation: {str(raw_with_interp)}")
268+
print(f" String 0 repr: {repr(raw_with_interp.strings[0])}")
269+
270+
long_line = "x" * 50
271+
long_triple = t"""Start
272+
{long_line}
273+
End"""
274+
print(f"Long line: strings={long_triple.strings}")
275+
print(f" Value length: {len(long_triple.values[0])}")
276+
277+
inner = t"inner {42}"
278+
outer = t"""Outer template:
279+
{inner}
280+
End"""
281+
print(f"Nested templates: {str(outer)}")
282+
print(f" Inner value: {outer.values[0]}")
283+
284+
formatted = t"""Math constants:
285+
Pi: {314:.2f}
286+
E: {271:.1e}
287+
Sqrt(2): {141:.0f}"""
288+
print(f"Formatted values: {repr(formatted)}")
289+
print(f" Interpolation count: {len(formatted.interpolations)}")
290+
print(f" Format specs: {[i.format_spec for i in formatted.interpolations]}")
291+
292+
x, y, z = 1, 2, 3
293+
debug_complex = t"""Debug info:
294+
{x + y=} {x * y * z=} {x < y < z=}"""
295+
print(f"Debug complex: {repr(debug_complex)}")
296+
print(f" Expressions: {[i.expression for i in debug_complex.interpolations]}")
297+
298+
part1 = t"""Part 1
299+
with newline"""
300+
part2 = t'''Part 2
301+
also multiline'''
302+
concatenated = part1 + part2
303+
print(f"Concatenated triple: {repr(concatenated)}")
304+
print(f" Strings: {concatenated.strings}")
305+
306+
print("\n=== PEP 701 brace compliance ===")
307+
t_brace1 = t"{{"
308+
print(f"Escaped {{{{: {t_brace1.strings}")
309+
assert t_brace1.strings == ('{',)
310+
311+
t_brace2 = t"}}"
312+
print(f"Escaped }}}}: {t_brace2.strings}")
313+
assert t_brace2.strings == ('}',)
314+
315+
t_brace3 = t"test{{escape}}here"
316+
print(f"Mixed: {t_brace3.strings}")
317+
assert t_brace3.strings == ('test{escape}here',)
318+
319+
print("\n=== Format spec scope resolution ===")
320+
def test_format_scope():
321+
width = 10
322+
x = 42
323+
t_scope = t"{x:{width}}"
324+
print(f"Local vars: width={width}, x={x}")
325+
print(f"Format spec: {t_scope.interpolations[0].format_spec}")
326+
return t_scope.interpolations[0].format_spec == '10'
327+
328+
result = test_format_scope()
329+
print(f"Scope test: {'PASS' if result else 'FAIL'}")
330+
assert result, "Format spec scope resolution failed"
331+
332+
precision = 2
333+
value = 3.14159
334+
t_prec = t"{value:.{precision}f}"
335+
print(f"Precision: format_spec={t_prec.interpolations[0].format_spec}")
336+
assert t_prec.interpolations[0].format_spec == '.2f'
337+
338+
print("\nBasic tests completed!")

0 commit comments

Comments
 (0)