mirror of
https://github.com/RustPython/RustPython.git
synced 2026-10-29 03:04:38 +08:00
* Preserve interpolation source text and canonical metadata Source-only checkpoint; candidate validation and the existing annotation expectation reconciliation remain pending. Assisted-by: OpenAI Codex:model version unavailable * Align existing annotation expectations with CPython metadata Update only the nine existing expected strings confirmed against pinned CPython 3.15, with explicit user approval. Assisted-by: OpenAI Codex:model version unavailable * Preserve template metadata through compilation and interactive input Escape literal debug prefixes, retain None AST expression metadata internally, and continue incomplete triple-quoted t-strings in the existing shell. Assisted-by: OpenAI Codex:model version unavailable * Classify normalized unclosed brackets as REPL line continuations Assisted-by: OpenAI Codex:model version unavailable * Nest equivalent REPL parse-error alternatives for Clippy Assisted-by: OpenAI Codex:model version unavailable * Remove fulfilled annotation migration lint workaround The canonical expected strings no longer trigger literal_string_with_formatting_args, so the previous lint expectation is obsolete. Assisted-by: OpenAI Codex:model version unavailable * Complete lazy-import exception and traceback library closure Copy canonical traceback and legacy exception mappings with preserved RustPython guards. Copy explicit non-Python library assets by exact path so exception hierarchy data does not expand into the entire test package. Assisted-by: OpenAI Codex:model version unavailable * Provide Python 3.15 traceback metadata for missing modules and attributes Preserve AttributeError context for missing class attribute deletion, matching type_update_dict. Expose canonical generated missing-stdlib messages through the native sysconfig modules without importing CPython build-specific disabled-module assumptions. Assisted-by: OpenAI Codex:model version unavailable
414 lines
15 KiB
Python
Vendored
414 lines
15 KiB
Python
Vendored
import subprocess
|
|
import unittest
|
|
|
|
from test import support
|
|
from test.support.os_helper import temp_cwd
|
|
from test.support.script_helper import assert_python_ok, spawn_python
|
|
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")
|
|
|
|
@support.requires_subprocess()
|
|
def test_expression_in_interactive_after_buffer_resize(self):
|
|
expression = "(\n" + (" " * 64 + "\n") * 256 + "1\n)"
|
|
source = (
|
|
f"result = t'''{{{expression}}}'''\n"
|
|
"print(repr(result.interpolations[0].expression))\n"
|
|
)
|
|
with spawn_python('-i', '-q', stderr=subprocess.PIPE) as process:
|
|
stdout, stderr = process.communicate(
|
|
source.encode(), timeout=support.SHORT_TIMEOUT)
|
|
self.assertEqual(process.returncode, 0, stderr)
|
|
self.assertEqual(stdout.decode().strip(), repr(expression))
|
|
|
|
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)
|
|
|
|
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")])
|
|
|
|
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'", "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 '{'"),
|
|
("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 ")
|
|
|
|
if __name__ == '__main__':
|
|
unittest.main()
|