Skip to content

Commit 89dbd42

Browse files
authored
Merge pull request RustPython#6946 from fanninpm/3.14-test_tstring
Update `test_tstring` from v3.14.2
2 parents d46a3b4 + 491d230 commit 89dbd42

2 files changed

Lines changed: 297 additions & 0 deletions

File tree

.github/workflows/ci.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ env:
101101
test_subclassinit
102102
test_super
103103
test_syntax
104+
test_tstring
104105
test_tuple
105106
test_types
106107
test_unary

Lib/test/test_tstring.py

Lines changed: 296 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,296 @@
1+
import unittest
2+
3+
from test.test_string._support import TStringBaseCase, fstring
4+
5+
6+
class TestTString(unittest.TestCase, TStringBaseCase):
7+
@unittest.expectedFailure # TODO: RUSTPYTHON; + Template(strings=('Hello',), interpolations=())
8+
def test_string_representation(self):
9+
# Test __repr__
10+
t = t"Hello"
11+
self.assertEqual(repr(t), "Template(strings=('Hello',), interpolations=())")
12+
13+
name = "Python"
14+
t = t"Hello, {name}"
15+
self.assertEqual(repr(t),
16+
"Template(strings=('Hello, ', ''), "
17+
"interpolations=(Interpolation('Python', 'name', None, ''),))"
18+
)
19+
20+
def test_interpolation_basics(self):
21+
# Test basic interpolation
22+
name = "Python"
23+
t = t"Hello, {name}"
24+
self.assertTStringEqual(t, ("Hello, ", ""), [(name, "name")])
25+
self.assertEqual(fstring(t), "Hello, Python")
26+
27+
# Multiple interpolations
28+
first = "Python"
29+
last = "Developer"
30+
t = t"{first} {last}"
31+
self.assertTStringEqual(
32+
t, ("", " ", ""), [(first, 'first'), (last, 'last')]
33+
)
34+
self.assertEqual(fstring(t), "Python Developer")
35+
36+
# Interpolation with expressions
37+
a = 10
38+
b = 20
39+
t = t"Sum: {a + b}"
40+
self.assertTStringEqual(t, ("Sum: ", ""), [(a + b, "a + b")])
41+
self.assertEqual(fstring(t), "Sum: 30")
42+
43+
# Interpolation with function
44+
def square(x):
45+
return x * x
46+
t = t"Square: {square(5)}"
47+
self.assertTStringEqual(
48+
t, ("Square: ", ""), [(square(5), "square(5)")]
49+
)
50+
self.assertEqual(fstring(t), "Square: 25")
51+
52+
# Test attribute access in expressions
53+
class Person:
54+
def __init__(self, name):
55+
self.name = name
56+
57+
def upper(self):
58+
return self.name.upper()
59+
60+
person = Person("Alice")
61+
t = t"Name: {person.name}"
62+
self.assertTStringEqual(
63+
t, ("Name: ", ""), [(person.name, "person.name")]
64+
)
65+
self.assertEqual(fstring(t), "Name: Alice")
66+
67+
# Test method calls
68+
t = t"Name: {person.upper()}"
69+
self.assertTStringEqual(
70+
t, ("Name: ", ""), [(person.upper(), "person.upper()")]
71+
)
72+
self.assertEqual(fstring(t), "Name: ALICE")
73+
74+
# Test dictionary access
75+
data = {"name": "Bob", "age": 30}
76+
t = t"Name: {data['name']}, Age: {data['age']}"
77+
self.assertTStringEqual(
78+
t, ("Name: ", ", Age: ", ""),
79+
[(data["name"], "data['name']"), (data["age"], "data['age']")],
80+
)
81+
self.assertEqual(fstring(t), "Name: Bob, Age: 30")
82+
83+
def test_format_specifiers(self):
84+
# Test basic format specifiers
85+
value = 3.14159
86+
t = t"Pi: {value:.2f}"
87+
self.assertTStringEqual(
88+
t, ("Pi: ", ""), [(value, "value", None, ".2f")]
89+
)
90+
self.assertEqual(fstring(t), "Pi: 3.14")
91+
92+
def test_conversions(self):
93+
# Test !s conversion (str)
94+
obj = object()
95+
t = t"Object: {obj!s}"
96+
self.assertTStringEqual(t, ("Object: ", ""), [(obj, "obj", "s")])
97+
self.assertEqual(fstring(t), f"Object: {str(obj)}")
98+
99+
# Test !r conversion (repr)
100+
t = t"Data: {obj!r}"
101+
self.assertTStringEqual(t, ("Data: ", ""), [(obj, "obj", "r")])
102+
self.assertEqual(fstring(t), f"Data: {repr(obj)}")
103+
104+
# Test !a conversion (ascii)
105+
text = "Café"
106+
t = t"ASCII: {text!a}"
107+
self.assertTStringEqual(t, ("ASCII: ", ""), [(text, "text", "a")])
108+
self.assertEqual(fstring(t), f"ASCII: {ascii(text)}")
109+
110+
# Test !z conversion (error)
111+
num = 1
112+
with self.assertRaises(SyntaxError):
113+
eval("t'{num!z}'")
114+
115+
@unittest.expectedFailure # TODO: RUSTPYTHON; ? ++++++
116+
def test_debug_specifier(self):
117+
# Test debug specifier
118+
value = 42
119+
t = t"Value: {value=}"
120+
self.assertTStringEqual(
121+
t, ("Value: value=", ""), [(value, "value", "r")]
122+
)
123+
self.assertEqual(fstring(t), "Value: value=42")
124+
125+
# Test debug specifier with format (conversion default to !r)
126+
t = t"Value: {value=:.2f}"
127+
self.assertTStringEqual(
128+
t, ("Value: value=", ""), [(value, "value", None, ".2f")]
129+
)
130+
self.assertEqual(fstring(t), "Value: value=42.00")
131+
132+
# Test debug specifier with conversion
133+
t = t"Value: {value=!s}"
134+
self.assertTStringEqual(
135+
t, ("Value: value=", ""), [(value, "value", "s")]
136+
)
137+
138+
# Test white space in debug specifier
139+
t = t"Value: {value = }"
140+
self.assertTStringEqual(
141+
t, ("Value: value = ", ""), [(value, "value", "r")]
142+
)
143+
self.assertEqual(fstring(t), "Value: value = 42")
144+
145+
def test_raw_tstrings(self):
146+
path = r"C:\Users"
147+
t = rt"{path}\Documents"
148+
self.assertTStringEqual(t, ("", r"\Documents"), [(path, "path")])
149+
self.assertEqual(fstring(t), r"C:\Users\Documents")
150+
151+
# Test alternative prefix
152+
t = tr"{path}\Documents"
153+
self.assertTStringEqual(t, ("", r"\Documents"), [(path, "path")])
154+
155+
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: "can only concatenate string.templatelib.Template \(not "str"\) to string.templatelib.Template" does not match "can only concatenate Template (not 'str') to Template"
156+
def test_template_concatenation(self):
157+
# Test template + template
158+
t1 = t"Hello, "
159+
t2 = t"world"
160+
combined = t1 + t2
161+
self.assertTStringEqual(combined, ("Hello, world",), ())
162+
self.assertEqual(fstring(combined), "Hello, world")
163+
164+
# Test template + string
165+
t1 = t"Hello"
166+
expected_msg = 'can only concatenate string.templatelib.Template ' \
167+
'\\(not "str"\\) to string.templatelib.Template'
168+
with self.assertRaisesRegex(TypeError, expected_msg):
169+
t1 + ", world"
170+
171+
# Test template + template with interpolation
172+
name = "Python"
173+
t1 = t"Hello, "
174+
t2 = t"{name}"
175+
combined = t1 + t2
176+
self.assertTStringEqual(combined, ("Hello, ", ""), [(name, "name")])
177+
self.assertEqual(fstring(combined), "Hello, Python")
178+
179+
# Test string + template
180+
expected_msg = 'can only concatenate str ' \
181+
'\\(not "string.templatelib.Template"\\) to str'
182+
with self.assertRaisesRegex(TypeError, expected_msg):
183+
"Hello, " + t"{name}"
184+
185+
def test_nested_templates(self):
186+
# Test a template inside another template expression
187+
name = "Python"
188+
inner = t"{name}"
189+
t = t"Language: {inner}"
190+
191+
t_interp = t.interpolations[0]
192+
self.assertEqual(t.strings, ("Language: ", ""))
193+
self.assertEqual(t_interp.value.strings, ("", ""))
194+
self.assertEqual(t_interp.value.interpolations[0].value, name)
195+
self.assertEqual(t_interp.value.interpolations[0].expression, "name")
196+
self.assertEqual(t_interp.value.interpolations[0].conversion, None)
197+
self.assertEqual(t_interp.value.interpolations[0].format_spec, "")
198+
self.assertEqual(t_interp.expression, "inner")
199+
self.assertEqual(t_interp.conversion, None)
200+
self.assertEqual(t_interp.format_spec, "")
201+
202+
@unittest.expectedFailure # TODO: RUSTPYTHON multiple instances of AssertionError
203+
def test_syntax_errors(self):
204+
for case, err in (
205+
("t'", "unterminated t-string literal"),
206+
("t'''", "unterminated triple-quoted t-string literal"),
207+
("t''''", "unterminated triple-quoted t-string literal"),
208+
("t'{", "'{' was never closed"),
209+
("t'{'", "t-string: expecting '}'"),
210+
("t'{a'", "t-string: expecting '}'"),
211+
("t'}'", "t-string: single '}' is not allowed"),
212+
("t'{}'", "t-string: valid expression required before '}'"),
213+
("t'{=x}'", "t-string: valid expression required before '='"),
214+
("t'{!x}'", "t-string: valid expression required before '!'"),
215+
("t'{:x}'", "t-string: valid expression required before ':'"),
216+
("t'{x;y}'", "t-string: expecting '=', or '!', or ':', or '}'"),
217+
("t'{x=y}'", "t-string: expecting '!', or ':', or '}'"),
218+
("t'{x!s!}'", "t-string: expecting ':' or '}'"),
219+
("t'{x!s:'", "t-string: expecting '}', or format specs"),
220+
("t'{x!}'", "t-string: missing conversion character"),
221+
("t'{x=!}'", "t-string: missing conversion character"),
222+
("t'{x!z}'", "t-string: invalid conversion character 'z': "
223+
"expected 's', 'r', or 'a'"),
224+
("t'{lambda:1}'", "t-string: lambda expressions are not allowed "
225+
"without parentheses"),
226+
("t'{x:{;}}'", "t-string: expecting a valid expression after '{'"),
227+
("t'{1:d\n}'", "t-string: newlines are not allowed in format specifiers")
228+
):
229+
with self.subTest(case), self.assertRaisesRegex(SyntaxError, err):
230+
eval(case)
231+
232+
def test_runtime_errors(self):
233+
# Test missing variables
234+
with self.assertRaises(NameError):
235+
eval("t'Hello, {name}'")
236+
237+
@unittest.expectedFailure # TODO: RUSTPYTHON
238+
def test_literal_concatenation(self):
239+
# Test concatenation of t-string literals
240+
t = t"Hello, " t"world"
241+
self.assertTStringEqual(t, ("Hello, world",), ())
242+
self.assertEqual(fstring(t), "Hello, world")
243+
244+
# Test concatenation with interpolation
245+
name = "Python"
246+
t = t"Hello, " t"{name}"
247+
self.assertTStringEqual(t, ("Hello, ", ""), [(name, "name")])
248+
self.assertEqual(fstring(t), "Hello, Python")
249+
250+
# Test disallowed mix of t-string and string/f-string (incl. bytes)
251+
what = 't'
252+
expected_msg = 'cannot mix t-string literals with string or bytes literals'
253+
for case in (
254+
"t'{what}-string literal' 'str literal'",
255+
"t'{what}-string literal' u'unicode literal'",
256+
"t'{what}-string literal' f'f-string literal'",
257+
"t'{what}-string literal' r'raw string literal'",
258+
"t'{what}-string literal' rf'raw f-string literal'",
259+
"t'{what}-string literal' b'bytes literal'",
260+
"t'{what}-string literal' br'raw bytes literal'",
261+
"'str literal' t'{what}-string literal'",
262+
"u'unicode literal' t'{what}-string literal'",
263+
"f'f-string literal' t'{what}-string literal'",
264+
"r'raw string literal' t'{what}-string literal'",
265+
"rf'raw f-string literal' t'{what}-string literal'",
266+
"b'bytes literal' t'{what}-string literal'",
267+
"br'raw bytes literal' t'{what}-string literal'",
268+
):
269+
with self.subTest(case):
270+
with self.assertRaisesRegex(SyntaxError, expected_msg):
271+
eval(case)
272+
273+
def test_triple_quoted(self):
274+
# Test triple-quoted t-strings
275+
t = t"""
276+
Hello,
277+
world
278+
"""
279+
self.assertTStringEqual(
280+
t, ("\n Hello,\n world\n ",), ()
281+
)
282+
self.assertEqual(fstring(t), "\n Hello,\n world\n ")
283+
284+
# Test triple-quoted with interpolation
285+
name = "Python"
286+
t = t"""
287+
Hello,
288+
{name}
289+
"""
290+
self.assertTStringEqual(
291+
t, ("\n Hello,\n ", "\n "), [(name, "name")]
292+
)
293+
self.assertEqual(fstring(t), "\n Hello,\n Python\n ")
294+
295+
if __name__ == '__main__':
296+
unittest.main()

0 commit comments

Comments
 (0)