aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/Lib/test
diff options
context:
space:
mode:
Diffstat (limited to 'Lib/test')
-rw-r--r--Lib/test/.ruff.toml3
-rw-r--r--Lib/test/support/ast_helper.py3
-rw-r--r--Lib/test/test_ast/data/ast_repr.txt7
-rw-r--r--Lib/test/test_ast/snippets.py11
-rw-r--r--Lib/test/test_ast/test_ast.py19
-rw-r--r--Lib/test/test_fstring.py1
-rw-r--r--Lib/test/test_grammar.py6
-rw-r--r--Lib/test/test_string/__init__.py5
-rw-r--r--Lib/test/test_string/_support.py55
-rw-r--r--Lib/test/test_string/test_string.py (renamed from Lib/test/test_string.py)0
-rw-r--r--Lib/test/test_string/test_templatelib.py122
-rw-r--r--Lib/test/test_syntax.py8
-rw-r--r--Lib/test/test_tstring.py313
-rw-r--r--Lib/test/test_unparse.py11
14 files changed, 561 insertions, 3 deletions
diff --git a/Lib/test/.ruff.toml b/Lib/test/.ruff.toml
index fa8b2b42579..54126bf3261 100644
--- a/Lib/test/.ruff.toml
+++ b/Lib/test/.ruff.toml
@@ -7,6 +7,9 @@ extend-exclude = [
# Non UTF-8 files
"encoded_modules/module_iso_8859_1.py",
"encoded_modules/module_koi8_r.py",
+ # SyntaxError because of t-strings
+ "test_tstring.py",
+ "test_string/test_templatelib.py",
# New grammar constructions may not yet be recognized by Ruff,
# and tests re-use the same names as only the grammar is being checked.
"test_grammar.py",
diff --git a/Lib/test/support/ast_helper.py b/Lib/test/support/ast_helper.py
index 8a0415b6aae..173d299afee 100644
--- a/Lib/test/support/ast_helper.py
+++ b/Lib/test/support/ast_helper.py
@@ -16,6 +16,9 @@ class ASTTestMixin:
self.fail(f"{type(a)!r} is not {type(b)!r}")
if isinstance(a, ast.AST):
for field in a._fields:
+ if isinstance(a, ast.Constant) and field == "kind":
+ # Skip the 'kind' field for ast.Constant
+ continue
value1 = getattr(a, field, missing)
value2 = getattr(b, field, missing)
# Singletons are equal by definition, so further
diff --git a/Lib/test/test_ast/data/ast_repr.txt b/Lib/test/test_ast/data/ast_repr.txt
index 3778b9e70a4..1c1985519cd 100644
--- a/Lib/test/test_ast/data/ast_repr.txt
+++ b/Lib/test/test_ast/data/ast_repr.txt
@@ -206,4 +206,9 @@ Module(body=[Expr(value=IfExp(test=Name(...), body=Call(...), orelse=Call(...)))
Module(body=[Expr(value=JoinedStr(values=[FormattedValue(...)]))], type_ignores=[])
Module(body=[Expr(value=JoinedStr(values=[FormattedValue(...)]))], type_ignores=[])
Module(body=[Expr(value=JoinedStr(values=[FormattedValue(...)]))], type_ignores=[])
-Module(body=[Expr(value=JoinedStr(values=[Constant(...), ..., Constant(...)]))], type_ignores=[]) \ No newline at end of file
+Module(body=[Expr(value=JoinedStr(values=[Constant(...), ..., Constant(...)]))], type_ignores=[])
+Module(body=[Expr(value=TemplateStr(values=[Interpolation(...)]))], type_ignores=[])
+Module(body=[Expr(value=TemplateStr(values=[Interpolation(...)]))], type_ignores=[])
+Module(body=[Expr(value=TemplateStr(values=[Interpolation(...)]))], type_ignores=[])
+Module(body=[Expr(value=TemplateStr(values=[Interpolation(...)]))], type_ignores=[])
+Module(body=[Expr(value=TemplateStr(values=[Constant(...), ..., Constant(...)]))], type_ignores=[]) \ No newline at end of file
diff --git a/Lib/test/test_ast/snippets.py b/Lib/test/test_ast/snippets.py
index 28d32b2941f..b76f98901d2 100644
--- a/Lib/test/test_ast/snippets.py
+++ b/Lib/test/test_ast/snippets.py
@@ -364,6 +364,12 @@ eval_tests = [
"f'{a:.2f}'",
"f'{a!r}'",
"f'foo({a})'",
+ # TemplateStr and Interpolation
+ "t'{a}'",
+ "t'{a:.2f}'",
+ "t'{a!r}'",
+ "t'{a!r:.2f}'",
+ "t'foo({a})'",
]
@@ -597,5 +603,10 @@ eval_results = [
('Expression', ('JoinedStr', (1, 0, 1, 10), [('FormattedValue', (1, 2, 1, 9), ('Name', (1, 3, 1, 4), 'a', ('Load',)), -1, ('JoinedStr', (1, 4, 1, 8), [('Constant', (1, 5, 1, 8), '.2f', None)]))])),
('Expression', ('JoinedStr', (1, 0, 1, 8), [('FormattedValue', (1, 2, 1, 7), ('Name', (1, 3, 1, 4), 'a', ('Load',)), 114, None)])),
('Expression', ('JoinedStr', (1, 0, 1, 11), [('Constant', (1, 2, 1, 6), 'foo(', None), ('FormattedValue', (1, 6, 1, 9), ('Name', (1, 7, 1, 8), 'a', ('Load',)), -1, None), ('Constant', (1, 9, 1, 10), ')', None)])),
+('Expression', ('TemplateStr', (1, 0, 1, 6), [('Interpolation', (1, 2, 1, 5), ('Name', (1, 3, 1, 4), 'a', ('Load',)), 'a', -1, None)])),
+('Expression', ('TemplateStr', (1, 0, 1, 10), [('Interpolation', (1, 2, 1, 9), ('Name', (1, 3, 1, 4), 'a', ('Load',)), 'a', -1, ('JoinedStr', (1, 4, 1, 8), [('Constant', (1, 5, 1, 8), '.2f', None)]))])),
+('Expression', ('TemplateStr', (1, 0, 1, 8), [('Interpolation', (1, 2, 1, 7), ('Name', (1, 3, 1, 4), 'a', ('Load',)), 'a', 114, None)])),
+('Expression', ('TemplateStr', (1, 0, 1, 12), [('Interpolation', (1, 2, 1, 11), ('Name', (1, 3, 1, 4), 'a', ('Load',)), 'a', 114, ('JoinedStr', (1, 6, 1, 10), [('Constant', (1, 7, 1, 10), '.2f', None)]))])),
+('Expression', ('TemplateStr', (1, 0, 1, 11), [('Constant', (1, 2, 1, 6), 'foo(', None), ('Interpolation', (1, 6, 1, 9), ('Name', (1, 7, 1, 8), 'a', ('Load',)), 'a', -1, None), ('Constant', (1, 9, 1, 10), ')', None)])),
]
main()
diff --git a/Lib/test/test_ast/test_ast.py b/Lib/test/test_ast/test_ast.py
index dd459487afe..eeac7c21eda 100644
--- a/Lib/test/test_ast/test_ast.py
+++ b/Lib/test/test_ast/test_ast.py
@@ -880,6 +880,25 @@ class AST_Tests(unittest.TestCase):
for src in srcs:
ast.parse(src)
+ def test_tstring(self):
+ # Test AST structure for simple t-string
+ tree = ast.parse('t"Hello"')
+ self.assertIsInstance(tree.body[0].value, ast.TemplateStr)
+ self.assertIsInstance(tree.body[0].value.values[0], ast.Constant)
+
+ # Test AST for t-string with interpolation
+ tree = ast.parse('t"Hello {name}"')
+ self.assertIsInstance(tree.body[0].value, ast.TemplateStr)
+ self.assertIsInstance(tree.body[0].value.values[0], ast.Constant)
+ self.assertIsInstance(tree.body[0].value.values[1], ast.Interpolation)
+
+ # Test AST for implicit concat of t-string with f-string
+ tree = ast.parse('t"Hello {name}" f"{name}"')
+ self.assertIsInstance(tree.body[0].value, ast.TemplateStr)
+ self.assertIsInstance(tree.body[0].value.values[0], ast.Constant)
+ self.assertIsInstance(tree.body[0].value.values[1], ast.Interpolation)
+ self.assertIsInstance(tree.body[0].value.values[2], ast.FormattedValue)
+
class CopyTests(unittest.TestCase):
"""Test copying and pickling AST nodes."""
diff --git a/Lib/test/test_fstring.py b/Lib/test/test_fstring.py
index e75e7db378c..a10d1fd5fd2 100644
--- a/Lib/test/test_fstring.py
+++ b/Lib/test/test_fstring.py
@@ -1358,7 +1358,6 @@ x = (
self.assertAllRaise(SyntaxError, "f-string: expecting '}'",
["f'{3!'",
"f'{3!s'",
- "f'{3!g'",
])
self.assertAllRaise(SyntaxError, 'f-string: missing conversion character',
diff --git a/Lib/test/test_grammar.py b/Lib/test/test_grammar.py
index 35cd6984267..c0681bccd9e 100644
--- a/Lib/test/test_grammar.py
+++ b/Lib/test/test_grammar.py
@@ -1507,6 +1507,8 @@ class GrammarTests(unittest.TestCase):
check('[None (3, 4)]')
check('[True (3, 4)]')
check('[... (3, 4)]')
+ check('[t"{x}" (3, 4)]')
+ check('[t"x={x}" (3, 4)]')
msg=r'is not subscriptable; perhaps you missed a comma\?'
check('[{1, 2} [i, j]]')
@@ -1529,6 +1531,8 @@ class GrammarTests(unittest.TestCase):
check('[f"x={x}" [i, j]]')
check('["abc" [i, j]]')
check('[b"abc" [i, j]]')
+ check('[t"{x}" [i, j]]')
+ check('[t"x={x}" [i, j]]')
msg=r'indices must be integers or slices, not tuple;'
check('[[1, 2] [3, 4]]')
@@ -1549,6 +1553,8 @@ class GrammarTests(unittest.TestCase):
check('[[1, 2] [f"{x}"]]')
check('[[1, 2] [f"x={x}"]]')
check('[[1, 2] ["abc"]]')
+ check('[[1, 2] [t"{x}"]]')
+ check('[[1, 2] [t"x={x}"]]')
msg=r'indices must be integers or slices, not'
check('[[1, 2] [b"abc"]]')
check('[[1, 2] [12.3]]')
diff --git a/Lib/test/test_string/__init__.py b/Lib/test/test_string/__init__.py
new file mode 100644
index 00000000000..4b16ecc3115
--- /dev/null
+++ b/Lib/test/test_string/__init__.py
@@ -0,0 +1,5 @@
+import os
+from test.support import load_package_tests
+
+def load_tests(*args):
+ return load_package_tests(os.path.dirname(__file__), *args)
diff --git a/Lib/test/test_string/_support.py b/Lib/test/test_string/_support.py
new file mode 100644
index 00000000000..eaa3354a559
--- /dev/null
+++ b/Lib/test/test_string/_support.py
@@ -0,0 +1,55 @@
+import unittest
+from string.templatelib import Interpolation
+
+
+class TStringBaseCase:
+ def assertTStringEqual(self, t, strings, interpolations):
+ """Test template string literal equality.
+
+ The *strings* argument must be a tuple of strings equal to *t.strings*.
+
+ The *interpolations* argument must be a sequence of tuples which are
+ compared against *t.interpolations*. Each tuple consists of
+ (value, expression, conversion, format_spec), though the final two
+ items may be omitted, and are assumed to be None and '' respectively.
+ """
+ self.assertEqual(t.strings, strings)
+ self.assertEqual(len(t.interpolations), len(interpolations))
+
+ for i, exp in zip(t.interpolations, interpolations, strict=True):
+ if len(exp) == 4:
+ actual = (i.value, i.expression, i.conversion, i.format_spec)
+ self.assertEqual(actual, exp)
+ continue
+
+ if len(exp) == 3:
+ self.assertEqual((i.value, i.expression, i.conversion), exp)
+ self.assertEqual(i.format_spec, '')
+ continue
+
+ self.assertEqual((i.value, i.expression), exp)
+ self.assertEqual(i.format_spec, '')
+ self.assertIsNone(i.conversion)
+
+
+def convert(value, conversion):
+ if conversion == "a":
+ return ascii(value)
+ elif conversion == "r":
+ return repr(value)
+ elif conversion == "s":
+ return str(value)
+ return value
+
+
+def fstring(template):
+ parts = []
+ for item in template:
+ match item:
+ case str() as s:
+ parts.append(s)
+ case Interpolation(value, _, conversion, format_spec):
+ value = convert(value, conversion)
+ value = format(value, format_spec)
+ parts.append(value)
+ return "".join(parts)
diff --git a/Lib/test/test_string.py b/Lib/test/test_string/test_string.py
index f6d112d8a93..f6d112d8a93 100644
--- a/Lib/test/test_string.py
+++ b/Lib/test/test_string/test_string.py
diff --git a/Lib/test/test_string/test_templatelib.py b/Lib/test/test_string/test_templatelib.py
new file mode 100644
index 00000000000..5cf18782851
--- /dev/null
+++ b/Lib/test/test_string/test_templatelib.py
@@ -0,0 +1,122 @@
+import pickle
+import unittest
+from string.templatelib import Template, Interpolation
+
+from test.test_string._support import TStringBaseCase, fstring
+
+
+class TestTemplate(unittest.TestCase, TStringBaseCase):
+
+ def test_common(self):
+ self.assertEqual(type(t'').__name__, 'Template')
+ self.assertEqual(type(t'').__qualname__, 'Template')
+ self.assertEqual(type(t'').__module__, 'string.templatelib')
+
+ a = 'a'
+ i = t'{a}'.interpolations[0]
+ self.assertEqual(type(i).__name__, 'Interpolation')
+ self.assertEqual(type(i).__qualname__, 'Interpolation')
+ self.assertEqual(type(i).__module__, 'string.templatelib')
+
+ def test_basic_creation(self):
+ # Simple t-string creation
+ t = t'Hello, world'
+ self.assertIsInstance(t, Template)
+ self.assertTStringEqual(t, ('Hello, world',), ())
+ self.assertEqual(fstring(t), 'Hello, world')
+
+ # Empty t-string
+ t = t''
+ self.assertTStringEqual(t, ('',), ())
+ self.assertEqual(fstring(t), '')
+
+ # Multi-line t-string
+ t = t"""Hello,
+world"""
+ self.assertEqual(t.strings, ('Hello,\nworld',))
+ self.assertEqual(len(t.interpolations), 0)
+ self.assertEqual(fstring(t), 'Hello,\nworld')
+
+ def test_creation_interleaving(self):
+ # Should add strings on either side
+ t = Template(Interpolation('Maria', 'name', None, ''))
+ self.assertTStringEqual(t, ('', ''), [('Maria', 'name')])
+ self.assertEqual(fstring(t), 'Maria')
+
+ # Should prepend empty string
+ t = Template(Interpolation('Maria', 'name', None, ''), ' is my name')
+ self.assertTStringEqual(t, ('', ' is my name'), [('Maria', 'name')])
+ self.assertEqual(fstring(t), 'Maria is my name')
+
+ # Should append empty string
+ t = Template('Hello, ', Interpolation('Maria', 'name', None, ''))
+ self.assertTStringEqual(t, ('Hello, ', ''), [('Maria', 'name')])
+ self.assertEqual(fstring(t), 'Hello, Maria')
+
+ # Should concatenate strings
+ t = Template('Hello', ', ', Interpolation('Maria', 'name', None, ''),
+ '!')
+ self.assertTStringEqual(t, ('Hello, ', '!'), [('Maria', 'name')])
+ self.assertEqual(fstring(t), 'Hello, Maria!')
+
+ # Should add strings on either side and in between
+ t = Template(Interpolation('Maria', 'name', None, ''),
+ Interpolation('Python', 'language', None, ''))
+ self.assertTStringEqual(
+ t, ('', '', ''), [('Maria', 'name'), ('Python', 'language')]
+ )
+ self.assertEqual(fstring(t), 'MariaPython')
+
+ def test_template_values(self):
+ t = t'Hello, world'
+ self.assertEqual(t.values, ())
+
+ name = "Lys"
+ t = t'Hello, {name}'
+ self.assertEqual(t.values, ("Lys",))
+
+ country = "GR"
+ age = 0
+ t = t'Hello, {name}, {age} from {country}'
+ self.assertEqual(t.values, ("Lys", 0, "GR"))
+
+ def test_pickle_template(self):
+ user = 'test'
+ for template in (
+ t'',
+ t"No values",
+ t'With inter {user}',
+ t'With ! {user!r}',
+ t'With format {1 / 0.3:.2f}',
+ Template(),
+ Template('a'),
+ Template(Interpolation('Nikita', 'name', None, '')),
+ Template('a', Interpolation('Nikita', 'name', 'r', '')),
+ ):
+ for proto in range(pickle.HIGHEST_PROTOCOL + 1):
+ with self.subTest(proto=proto, template=template):
+ pickled = pickle.dumps(template, protocol=proto)
+ unpickled = pickle.loads(pickled)
+
+ self.assertEqual(unpickled.values, template.values)
+ self.assertEqual(fstring(unpickled), fstring(template))
+
+ def test_pickle_interpolation(self):
+ for interpolation in (
+ Interpolation('Nikita', 'name', None, ''),
+ Interpolation('Nikita', 'name', 'r', ''),
+ Interpolation(1/3, 'x', None, '.2f'),
+ ):
+ for proto in range(pickle.HIGHEST_PROTOCOL + 1):
+ with self.subTest(proto=proto, interpolation=interpolation):
+ pickled = pickle.dumps(interpolation, protocol=proto)
+ unpickled = pickle.loads(pickled)
+
+ self.assertEqual(unpickled.value, interpolation.value)
+ self.assertEqual(unpickled.expression, interpolation.expression)
+ self.assertEqual(unpickled.conversion, interpolation.conversion)
+ self.assertEqual(unpickled.format_spec, interpolation.format_spec)
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/Lib/test/test_syntax.py b/Lib/test/test_syntax.py
index 4c001f9c9b0..55492350d00 100644
--- a/Lib/test/test_syntax.py
+++ b/Lib/test/test_syntax.py
@@ -1877,6 +1877,14 @@ SyntaxError: cannot assign to f-string expression here. Maybe you meant '==' ins
Traceback (most recent call last):
SyntaxError: cannot assign to f-string expression here. Maybe you meant '==' instead of '='?
+>>> t'{x}' = 42
+Traceback (most recent call last):
+SyntaxError: cannot assign to t-string expression here. Maybe you meant '==' instead of '='?
+
+>>> t'{x}-{y}' = 42
+Traceback (most recent call last):
+SyntaxError: cannot assign to t-string expression here. Maybe you meant '==' instead of '='?
+
>>> (x, y, z=3, d, e)
Traceback (most recent call last):
SyntaxError: invalid syntax. Maybe you meant '==' or ':=' instead of '='?
diff --git a/Lib/test/test_tstring.py b/Lib/test/test_tstring.py
new file mode 100644
index 00000000000..e72a1ea5417
--- /dev/null
+++ b/Lib/test/test_tstring.py
@@ -0,0 +1,313 @@
+import unittest
+
+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_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")
+
+ 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")
+
+ 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")])
+
+
+ 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"
+ combined = t1 + ", world"
+ self.assertTStringEqual(combined, ("Hello, world",), ())
+ self.assertEqual(fstring(combined), "Hello, 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
+ t = "Hello, " + t"{name}"
+ self.assertTStringEqual(t, ("Hello, ", ""), [(name, "name")])
+ self.assertEqual(fstring(t), "Hello, Python")
+
+ 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'", "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'"),
+ ("t'{lambda:1}'", "t-string: lambda expressions are not allowed "
+ "without parentheses"),
+ ("t'{x:{;}}'", "t-string: expecting a valid expression after '{'"),
+ ):
+ 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 concatenation with string literal
+ name = "Python"
+ t = t"Hello, {name}" "and welcome!"
+ self.assertTStringEqual(
+ t, ("Hello, ", "and welcome!"), [(name, "name")]
+ )
+ self.assertEqual(fstring(t), "Hello, Pythonand welcome!")
+
+ # Test concatenation with Unicode literal
+ name = "Python"
+ t = t"Hello, {name}" u"and welcome!"
+ self.assertTStringEqual(
+ t, ("Hello, ", "and welcome!"), [(name, "name")]
+ )
+ self.assertEqual(fstring(t), "Hello, Pythonand welcome!")
+
+ # Test concatenation with f-string literal
+ tab = '\t'
+ t = t"Tab: {tab}. " f"f-tab: {tab}."
+ self.assertTStringEqual(t, ("Tab: ", ". f-tab: \t."), [(tab, "tab")])
+ self.assertEqual(fstring(t), "Tab: \t. f-tab: \t.")
+
+ # Test concatenation with raw string literal
+ tab = '\t'
+ t = t"Tab: {tab}. " r"Raw tab: \t."
+ self.assertTStringEqual(
+ t, ("Tab: ", r". Raw tab: \t."), [(tab, "tab")]
+ )
+ self.assertEqual(fstring(t), "Tab: \t. Raw tab: \\t.")
+
+ # Test concatenation with raw f-string literal
+ tab = '\t'
+ t = t"Tab: {tab}. " rf"f-tab: {tab}. Raw tab: \t."
+ self.assertTStringEqual(
+ t, ("Tab: ", ". f-tab: \t. Raw tab: \\t."), [(tab, "tab")]
+ )
+ self.assertEqual(fstring(t), "Tab: \t. f-tab: \t. Raw tab: \\t.")
+
+ what = 't'
+ expected_msg = 'cannot mix bytes and nonbytes literals'
+ for case in (
+ "t'{what}-string literal' b'bytes literal'",
+ "t'{what}-string literal' br'raw bytes literal'",
+ ):
+ 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 ")
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/Lib/test/test_unparse.py b/Lib/test/test_unparse.py
index 839326f6436..d3af7a8489e 100644
--- a/Lib/test/test_unparse.py
+++ b/Lib/test/test_unparse.py
@@ -202,6 +202,15 @@ class UnparseTestCase(ASTTestCase):
self.check_ast_roundtrip('f" something { my_dict["key"] } something else "')
self.check_ast_roundtrip('f"{f"{f"{f"{f"{f"{1+1}"}"}"}"}"}"')
+ def test_tstrings(self):
+ self.check_ast_roundtrip("t'foo'")
+ self.check_ast_roundtrip("t'foo {bar}'")
+ self.check_ast_roundtrip("t'foo {bar!s:.2f}'")
+ self.check_ast_roundtrip("t'foo {bar}' f'{bar}'")
+ self.check_ast_roundtrip("f'{bar}' t'foo {bar}'")
+ self.check_ast_roundtrip("t'foo {bar}' fr'\\hello {bar}'")
+ self.check_ast_roundtrip("t'foo {bar}' u'bar'")
+
def test_strings(self):
self.check_ast_roundtrip("u'foo'")
self.check_ast_roundtrip("r'foo'")
@@ -918,7 +927,7 @@ class DirectoryTestCase(ASTTestCase):
run_always_files = {"test_grammar.py", "test_syntax.py", "test_compile.py",
"test_ast.py", "test_asdl_parser.py", "test_fstring.py",
"test_patma.py", "test_type_alias.py", "test_type_params.py",
- "test_tokenize.py"}
+ "test_tokenize.py", "test_tstring.py"}
_files_to_test = None