| import unittest | |
| from test import support | |
| from test.support.os_helper import temp_cwd | |
| from test.support.script_helper import assert_python_ok | |
| from test.test_string._support import TStringBaseCase, fstring | |
| class TestTString(unittest.TestCase, TStringBaseCase): | |
| def test_string_representation(self): | |
| # Test __repr__ | |
| t = t"Hello" | |
| self.assertEqual(repr(t), "Template(strings=('Hello',), interpolations=())") | |
| name = "Python" | |
| t = t"Hello, {name}" | |
| self.assertEqual(repr(t), | |
| "Template(strings=('Hello, ', ''), " | |
| "interpolations=(Interpolation('Python', 'name', None, ''),))" | |
| ) | |
| def test_interpolation_basics(self): | |
| # Test basic interpolation | |
| name = "Python" | |
| t = t"Hello, {name}" | |
| self.assertTStringEqual(t, ("Hello, ", ""), [(name, "name")]) | |
| self.assertEqual(fstring(t), "Hello, Python") | |
| # Multiple interpolations | |
| first = "Python" | |
| last = "Developer" | |
| t = t"{first} {last}" | |
| self.assertTStringEqual( | |
| t, ("", " ", ""), [(first, 'first'), (last, 'last')] | |
| ) | |
| self.assertEqual(fstring(t), "Python Developer") | |
| # Interpolation with expressions | |
| a = 10 | |
| b = 20 | |
| t = t"Sum: {a + b}" | |
| self.assertTStringEqual(t, ("Sum: ", ""), [(a + b, "a + b")]) | |
| self.assertEqual(fstring(t), "Sum: 30") | |
| # Interpolation with function | |
| def square(x): | |
| return x * x | |
| t = t"Square: {square(5)}" | |
| self.assertTStringEqual( | |
| t, ("Square: ", ""), [(square(5), "square(5)")] | |
| ) | |
| self.assertEqual(fstring(t), "Square: 25") | |
| # Test attribute access in expressions | |
| class Person: | |
| def __init__(self, name): | |
| self.name = name | |
| def upper(self): | |
| return self.name.upper() | |
| person = Person("Alice") | |
| t = t"Name: {person.name}" | |
| self.assertTStringEqual( | |
| t, ("Name: ", ""), [(person.name, "person.name")] | |
| ) | |
| self.assertEqual(fstring(t), "Name: Alice") | |
| # Test method calls | |
| t = t"Name: {person.upper()}" | |
| self.assertTStringEqual( | |
| t, ("Name: ", ""), [(person.upper(), "person.upper()")] | |
| ) | |
| self.assertEqual(fstring(t), "Name: ALICE") | |
| # Test dictionary access | |
| data = {"name": "Bob", "age": 30} | |
| t = t"Name: {data['name']}, Age: {data['age']}" | |
| self.assertTStringEqual( | |
| t, ("Name: ", ", Age: ", ""), | |
| [(data["name"], "data['name']"), (data["age"], "data['age']")], | |
| ) | |
| self.assertEqual(fstring(t), "Name: Bob, Age: 30") | |
| def test_interpolation_expression_in_file_after_buffer_resize(self): | |
| expression = "(\n" + (" " * 64 + "\n") * 256 + "1\n)" | |
| with temp_cwd(): | |
| script = 'script.py' | |
| source = ( | |
| f"template = t'''{{{expression}}}'''\n" | |
| "interpolation = template.interpolations[0]\n" | |
| f"assert interpolation.expression == {expression!r}\n" | |
| ) | |
| with open(script, 'w') as f: | |
| f.write(source) | |
| assert_python_ok(script) | |
| @support.requires_resource('cpu') | |
| def test_many_tstrings_in_module(self): | |
| fields = ''.join(f'{{x{i}}}' for i in range(100)) | |
| source = ''.join( | |
| f"value_{i} = t'{fields}'\n" for i in range(1_000) | |
| ) | |
| namespace = {f'x{i}': str(i) for i in range(100)} | |
| expected = ''.join(str(i) for i in range(100)) | |
| exec(source, namespace) | |
| self.assertEqual(fstring(namespace['value_0']), expected) | |
| self.assertEqual(fstring(namespace['value_999']), expected) | |
| def test_format_specifiers(self): | |
| # Test basic format specifiers | |
| value = 3.14159 | |
| t = t"Pi: {value:.2f}" | |
| self.assertTStringEqual( | |
| t, ("Pi: ", ""), [(value, "value", None, ".2f")] | |
| ) | |
| self.assertEqual(fstring(t), "Pi: 3.14") | |
| a = 3 | |
| b = 4 | |
| t = t"{a!=b:>10}" | |
| self.assertTStringEqual( | |
| t, ("", ""), [(a != b, "a!=b", None, ">10")] | |
| ) | |
| self.assertEqual(fstring(t), " 1") | |
| def test_conversions(self): | |
| # Test !s conversion (str) | |
| obj = object() | |
| t = t"Object: {obj!s}" | |
| self.assertTStringEqual(t, ("Object: ", ""), [(obj, "obj", "s")]) | |
| self.assertEqual(fstring(t), f"Object: {str(obj)}") | |
| # Test !r conversion (repr) | |
| t = t"Data: {obj!r}" | |
| self.assertTStringEqual(t, ("Data: ", ""), [(obj, "obj", "r")]) | |
| self.assertEqual(fstring(t), f"Data: {repr(obj)}") | |
| # Test !a conversion (ascii) | |
| text = "Café" | |
| t = t"ASCII: {text!a}" | |
| self.assertTStringEqual(t, ("ASCII: ", ""), [(text, "text", "a")]) | |
| self.assertEqual(fstring(t), f"ASCII: {ascii(text)}") | |
| # Test !z conversion (error) | |
| num = 1 | |
| with self.assertRaises(SyntaxError): | |
| eval("t'{num!z}'") | |
| def test_debug_specifier(self): | |
| # Test debug specifier | |
| value = 42 | |
| t = t"Value: {value=}" | |
| self.assertTStringEqual( | |
| t, ("Value: value=", ""), [(value, "value", "r")] | |
| ) | |
| self.assertEqual(fstring(t), "Value: value=42") | |
| # Test debug specifier with format (conversion default to !r) | |
| t = t"Value: {value=:.2f}" | |
| self.assertTStringEqual( | |
| t, ("Value: value=", ""), [(value, "value", None, ".2f")] | |
| ) | |
| self.assertEqual(fstring(t), "Value: value=42.00") | |
| # Test debug specifier with conversion | |
| t = t"Value: {value=!s}" | |
| self.assertTStringEqual( | |
| t, ("Value: value=", ""), [(value, "value", "s")] | |
| ) | |
| # Test white space in debug specifier | |
| t = t"Value: {value = }" | |
| self.assertTStringEqual( | |
| t, ("Value: value = ", ""), [(value, "value ", "r")] | |
| ) | |
| self.assertEqual(fstring(t), "Value: value = 42") | |
| # Explicit line continuations after the debug marker are part of | |
| # the debug text, not the interpolation expression. | |
| for template, strings, interpolation, rendered in ( | |
| ( | |
| t"""Value: {value =\ | |
| }""", | |
| ("Value: value =\\\n", ""), | |
| (value, "value ", "r"), | |
| "Value: value =\\\n42", | |
| ), | |
| ( | |
| t"""Value: {value =\ | |
| !r}""", | |
| ("Value: value =\\\n", ""), | |
| (value, "value ", "r"), | |
| "Value: value =\\\n42", | |
| ), | |
| ( | |
| t"""Value: {value =\ | |
| :04}""", | |
| ("Value: value =\\\n", ""), | |
| (value, "value ", None, "04"), | |
| "Value: value =\\\n0042", | |
| ), | |
| ( | |
| t"""Value: {value =\ | |
| \ | |
| }""", | |
| ("Value: value =\\\n\\\n", ""), | |
| (value, "value ", "r"), | |
| "Value: value =\\\n\\\n42", | |
| ), | |
| ): | |
| with self.subTest(template=template): | |
| self.assertTStringEqual(template, strings, [interpolation]) | |
| self.assertEqual(fstring(template), rendered) | |
| class C: | |
| def __format__(self, spec): | |
| return f"FORMAT-{spec}" | |
| x = y = C() | |
| t = t"{x:{y:{value=}}}" | |
| self.assertEqual(t.interpolations[0].format_spec, | |
| "FORMAT-value=42") | |
| def test_interpolation_expression_whitespace(self): | |
| x = 42 | |
| for template, expected in ( | |
| (t"{x}", "x"), | |
| (t"{x }", "x "), | |
| (t"{ x}", " x"), | |
| (t"{ x }", " x "), | |
| (t"{ x }", " x "), | |
| (t"""{ | |
| x | |
| }""", "\n x\n"), | |
| (t"{ x !r}", " x "), | |
| (t"{ x :.2f}", " x "), | |
| (t"{ x = }", " x "), | |
| (t"{ x = !r}", " x "), | |
| (t"{ x = :.2f}", " x "), | |
| (t"{x == 42 = }", "x == 42 "), | |
| ): | |
| with self.subTest(template=template): | |
| self.assertEqual( | |
| template.interpolations[0].expression, | |
| expected, | |
| ) | |
| def test_interpolation_expression_with_reconstructed_metadata(self): | |
| regular = t'''{( | |
| 1, # Force lexer metadata reconstruction. | |
| "\"#")}''' | |
| debug = t'''{( | |
| 1, # Force lexer metadata reconstruction. | |
| "\"#")=}''' | |
| expected = '(\n 1, \n "\\"#")' | |
| self.assertEqual(regular.interpolations[0].expression, expected) | |
| self.assertEqual(debug.interpolations[0].expression, expected) | |
| def test_raw_tstrings(self): | |
| path = r"C:\Users" | |
| t = rt"{path}\Documents" | |
| self.assertTStringEqual(t, ("", r"\Documents"), [(path, "path")]) | |
| self.assertEqual(fstring(t), r"C:\Users\Documents") | |
| # Test alternative prefix | |
| t = tr"{path}\Documents" | |
| self.assertTStringEqual(t, ("", r"\Documents"), [(path, "path")]) | |
| value = 42 | |
| t = rt"{value:{f'\xFF'}}\n" | |
| self.assertTStringEqual( | |
| t, ("", "\\n"), [(value, "value", None, 'ÿ')]) | |
| t = t"{value:{rf'\xFF'}}\n" | |
| self.assertTStringEqual( | |
| t, ("", "\n"), [(value, "value", None, '\\xFF')]) | |
| def test_template_concatenation(self): | |
| # Test template + template | |
| t1 = t"Hello, " | |
| t2 = t"world" | |
| combined = t1 + t2 | |
| self.assertTStringEqual(combined, ("Hello, world",), ()) | |
| self.assertEqual(fstring(combined), "Hello, world") | |
| # Test template + string | |
| t1 = t"Hello" | |
| expected_msg = 'can only concatenate string.templatelib.Template ' \ | |
| '\\(not "str"\\) to string.templatelib.Template' | |
| with self.assertRaisesRegex(TypeError, expected_msg): | |
| t1 + ", world" | |
| # Test template + template with interpolation | |
| name = "Python" | |
| t1 = t"Hello, " | |
| t2 = t"{name}" | |
| combined = t1 + t2 | |
| self.assertTStringEqual(combined, ("Hello, ", ""), [(name, "name")]) | |
| self.assertEqual(fstring(combined), "Hello, Python") | |
| # Test string + template | |
| expected_msg = 'can only concatenate str ' \ | |
| '\\(not "string.templatelib.Template"\\) to str' | |
| with self.assertRaisesRegex(TypeError, expected_msg): | |
| "Hello, " + t"{name}" | |
| def test_nested_templates(self): | |
| # Test a template inside another template expression | |
| name = "Python" | |
| inner = t"{name}" | |
| t = t"Language: {inner}" | |
| t_interp = t.interpolations[0] | |
| self.assertEqual(t.strings, ("Language: ", "")) | |
| self.assertEqual(t_interp.value.strings, ("", "")) | |
| self.assertEqual(t_interp.value.interpolations[0].value, name) | |
| self.assertEqual(t_interp.value.interpolations[0].expression, "name") | |
| self.assertEqual(t_interp.value.interpolations[0].conversion, None) | |
| self.assertEqual(t_interp.value.interpolations[0].format_spec, "") | |
| self.assertEqual(t_interp.expression, "inner") | |
| self.assertEqual(t_interp.conversion, None) | |
| self.assertEqual(t_interp.format_spec, "") | |
| def test_syntax_errors(self): | |
| for case, err in ( | |
| ('t"""{(\n1\n)}\ntail', "unterminated triple-quoted t-string literal"), | |
| ('f"""{(\n1\n)}\ntail', "unterminated triple-quoted f-string literal"), | |
| ("t'", "unterminated t-string literal"), | |
| ("t'''", "unterminated triple-quoted t-string literal"), | |
| ("t''''", "unterminated triple-quoted t-string literal"), | |
| ("t'{", "'{' was never closed"), | |
| ("t'{'", "t-string: expecting '}'"), | |
| ("t'{a'", "t-string: expecting '}'"), | |
| ("t'}'", "t-string: single '}' is not allowed"), | |
| ("t'{}'", "t-string: valid expression required before '}'"), | |
| ("t'{=x}'", "t-string: valid expression required before '='"), | |
| ("t'{!x}'", "t-string: valid expression required before '!'"), | |
| ("t'{:x}'", "t-string: valid expression required before ':'"), | |
| ("t'{x;y}'", "t-string: expecting '=', or '!', or ':', or '}'"), | |
| ("t'{x=y}'", "t-string: expecting '!', or ':', or '}'"), | |
| ("t'{x!s!}'", "t-string: expecting ':' or '}'"), | |
| ("t'{x!s:'", "t-string: expecting '}', or format specs"), | |
| ("t'{x!}'", "t-string: missing conversion character"), | |
| ("t'{x=!}'", "t-string: missing conversion character"), | |
| ("t'{x!z}'", "t-string: invalid conversion character 'z': " | |
| "expected 's', 'r', or 'a'"), | |
| ("f\"{t'{x!z}'}\"", "t-string: invalid conversion character 'z': " | |
| "expected 's', 'r', or 'a'"), | |
| ("t'{f\"{x!z}\"}'", "f-string: invalid conversion character 'z': " | |
| "expected 's', 'r', or 'a'"), | |
| ("t'{lambda:1}'", "t-string: lambda expressions are not allowed " | |
| "without parentheses"), | |
| ("t'{x:{;}}'", "t-string: expecting a valid expression after '{'"), | |
| ("t'{1:d\n}'", "t-string: newlines are not allowed in format specifiers") | |
| ): | |
| with self.subTest(case), self.assertRaisesRegex(SyntaxError, err): | |
| eval(case) | |
| def test_runtime_errors(self): | |
| # Test missing variables | |
| with self.assertRaises(NameError): | |
| eval("t'Hello, {name}'") | |
| def test_literal_concatenation(self): | |
| # Test concatenation of t-string literals | |
| t = t"Hello, " t"world" | |
| self.assertTStringEqual(t, ("Hello, world",), ()) | |
| self.assertEqual(fstring(t), "Hello, world") | |
| # Test concatenation with interpolation | |
| name = "Python" | |
| t = t"Hello, " t"{name}" | |
| self.assertTStringEqual(t, ("Hello, ", ""), [(name, "name")]) | |
| self.assertEqual(fstring(t), "Hello, Python") | |
| # Test disallowed mix of t-string and string/f-string (incl. bytes) | |
| what = 't' | |
| expected_msg = 'cannot mix t-string literals with string or bytes literals' | |
| for case in ( | |
| "t'{what}-string literal' 'str literal'", | |
| "t'{what}-string literal' u'unicode literal'", | |
| "t'{what}-string literal' f'f-string literal'", | |
| "t'{what}-string literal' r'raw string literal'", | |
| "t'{what}-string literal' rf'raw f-string literal'", | |
| "t'{what}-string literal' b'bytes literal'", | |
| "t'{what}-string literal' br'raw bytes literal'", | |
| "'str literal' t'{what}-string literal'", | |
| "u'unicode literal' t'{what}-string literal'", | |
| "f'f-string literal' t'{what}-string literal'", | |
| "r'raw string literal' t'{what}-string literal'", | |
| "rf'raw f-string literal' t'{what}-string literal'", | |
| "b'bytes literal' t'{what}-string literal'", | |
| "br'raw bytes literal' t'{what}-string literal'", | |
| ): | |
| with self.subTest(case): | |
| with self.assertRaisesRegex(SyntaxError, expected_msg): | |
| eval(case) | |
| def test_triple_quoted(self): | |
| # Test triple-quoted t-strings | |
| t = t""" | |
| Hello, | |
| world | |
| """ | |
| self.assertTStringEqual( | |
| t, ("\n Hello,\n world\n ",), () | |
| ) | |
| self.assertEqual(fstring(t), "\n Hello,\n world\n ") | |
| # Test triple-quoted with interpolation | |
| name = "Python" | |
| t = t""" | |
| Hello, | |
| {name} | |
| """ | |
| self.assertTStringEqual( | |
| t, ("\n Hello,\n ", "\n "), [(name, "name")] | |
| ) | |
| self.assertEqual(fstring(t), "\n Hello,\n Python\n ") | |
| t = t'{"""a" # inside"""}' | |
| self.assertEqual(t.interpolations[0].expression, | |
| '"""a" # inside"""') | |
| t = t'{"""a""""#" # outside | |
| }' | |
| self.assertEqual(t.interpolations[0].expression, '"""a""""#" \n') | |
| x, y = 1, 2 | |
| t = t'{x != y # outside | |
| }' | |
| self.assertEqual(t.interpolations[0].expression, 'x != y \n') | |
| d = {'a#b': 42} | |
| t = t'''{f"{d["a#b"]}"}''' | |
| self.assertEqual(t.interpolations[0].expression, 'f"{d["a#b"]}"') | |
| if __name__ == '__main__': | |
| unittest.main() |