https://github.com/python/cpython/commit/bfac7d2edc6e461e21b4762f8db03fca85f3a0be
commit: bfac7d2edc6e461e21b4762f8db03fca85f3a0be
branch: main
author: Jelle Zijlstra <[email protected]>
committer: JelleZijlstra <[email protected]>
date: 2025-05-08T06:13:57-07:00
summary:
gh-133581: Improve AST unparsing of t-strings (#133635)
files:
A Misc/NEWS.d/next/Library/2025-05-07-19-16-41.gh-issue-133581.kERUCJ.rst
M Lib/_ast_unparse.py
M Lib/test/test_future_stmt/test_future.py
M Lib/test/test_unparse.py
M Python/ast_unparse.c
diff --git a/Lib/_ast_unparse.py b/Lib/_ast_unparse.py
index 0b669edb2ffec6..c25066eb107de1 100644
--- a/Lib/_ast_unparse.py
+++ b/Lib/_ast_unparse.py
@@ -627,6 +627,9 @@ def _write_ftstring(self, values, prefix):
self._ftstring_helper(fstring_parts)
def _tstring_helper(self, node):
+ if not node.values:
+ self._write_ftstring([], "t")
+ return
last_idx = 0
for i, value in enumerate(node.values):
# This can happen if we have an implicit concat of a t-string
@@ -679,9 +682,12 @@ def _unparse_interpolation_value(self, inner):
unparser.set_precedence(_Precedence.TEST.next(), inner)
return unparser.visit(inner)
- def _write_interpolation(self, node):
+ def _write_interpolation(self, node, is_interpolation=False):
with self.delimit("{", "}"):
- expr = self._unparse_interpolation_value(node.value)
+ if is_interpolation:
+ expr = node.str
+ else:
+ expr = self._unparse_interpolation_value(node.value)
if expr.startswith("{"):
# Separate pair of opening brackets as "{ {"
self.write(" ")
@@ -696,7 +702,7 @@ def visit_FormattedValue(self, node):
self._write_interpolation(node)
def visit_Interpolation(self, node):
- self._write_interpolation(node)
+ self._write_interpolation(node, is_interpolation=True)
def visit_Name(self, node):
self.write(node.id)
diff --git a/Lib/test/test_future_stmt/test_future.py
b/Lib/test/test_future_stmt/test_future.py
index 42c6cb3fefac33..71f1e616116d81 100644
--- a/Lib/test/test_future_stmt/test_future.py
+++ b/Lib/test/test_future_stmt/test_future.py
@@ -422,6 +422,11 @@ def test_annotations(self):
eq('(((a)))', 'a')
eq('(((a, b)))', '(a, b)')
eq("1 + 2 + 3")
+ eq("t''")
+ eq("t'{a + b}'")
+ eq("t'{a!s}'")
+ eq("t'{a:b}'")
+ eq("t'{a:b=}'")
def test_fstring_debug_annotations(self):
# f-strings with '=' don't round trip very well, so set the expected
diff --git a/Lib/test/test_unparse.py b/Lib/test/test_unparse.py
index 5616129eb63c2f..d4db5e60af7978 100644
--- a/Lib/test/test_unparse.py
+++ b/Lib/test/test_unparse.py
@@ -817,6 +817,15 @@ def test_type_params(self):
self.check_ast_roundtrip("def f[T: int = int, **P = int, *Ts =
*int]():\n pass")
self.check_ast_roundtrip("class C[T: int = int, **P = int, *Ts =
*int]():\n pass")
+ def test_tstr(self):
+ self.check_ast_roundtrip("t'{a + b}'")
+ self.check_ast_roundtrip("t'{a + b:x}'")
+ self.check_ast_roundtrip("t'{a + b!s}'")
+ self.check_ast_roundtrip("t'{ {a}}'")
+ self.check_ast_roundtrip("t'{ {a}=}'")
+ self.check_ast_roundtrip("t'{{a}}'")
+ self.check_ast_roundtrip("t''")
+
class ManualASTCreationTestCase(unittest.TestCase):
"""Test that AST nodes created without a type_params field unparse
correctly."""
@@ -942,7 +951,6 @@ def files_to_test(cls):
for directory in cls.test_directories
for item in directory.glob("*.py")
if not item.name.startswith("bad")
- and item.name != "annotationlib.py" # gh-133581: t"" does not
roundtrip
]
# Test limited subset of files unless the 'cpu' resource is specified.
diff --git
a/Misc/NEWS.d/next/Library/2025-05-07-19-16-41.gh-issue-133581.kERUCJ.rst
b/Misc/NEWS.d/next/Library/2025-05-07-19-16-41.gh-issue-133581.kERUCJ.rst
new file mode 100644
index 00000000000000..3749904cd9b8f1
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2025-05-07-19-16-41.gh-issue-133581.kERUCJ.rst
@@ -0,0 +1,4 @@
+Improve unparsing of t-strings in :func:`ast.unparse` and ``from __future__
+import annotations``. Empty t-strings now round-trip correctly and
+formatting in interpolations is preserved.
+Patch by Jelle Zijlstra.
diff --git a/Python/ast_unparse.c b/Python/ast_unparse.c
index c121ec096aebf4..ae623e0b4171f8 100644
--- a/Python/ast_unparse.c
+++ b/Python/ast_unparse.c
@@ -702,6 +702,13 @@ append_templatestr(PyUnicodeWriter *writer, expr_ty e)
Py_ssize_t last_idx = 0;
Py_ssize_t len = asdl_seq_LEN(e->v.TemplateStr.values);
+ if (len == 0) {
+ int result = _write_values_subarray(writer, e->v.TemplateStr.values,
+ 0, len - 1, 't', arena);
+ _PyArena_Free(arena);
+ return result;
+ }
+
for (Py_ssize_t i = 0; i < len; i++) {
expr_ty value = asdl_seq_GET(e->v.TemplateStr.values, i);
@@ -774,32 +781,37 @@ append_joinedstr(PyUnicodeWriter *writer, expr_ty e, bool
is_format_spec)
}
static int
-append_interpolation_value(PyUnicodeWriter *writer, expr_ty e)
+append_interpolation_str(PyUnicodeWriter *writer, PyObject *str)
{
const char *outer_brace = "{";
- /* Grammar allows PR_TUPLE, but use >PR_TEST for adding parenthesis
- around a lambda with ':' */
- PyObject *temp_fv_str = expr_as_unicode(e, PR_TEST + 1);
- if (!temp_fv_str) {
- return -1;
- }
- if (PyUnicode_Find(temp_fv_str, _Py_LATIN1_CHR('{'), 0, 1, 1) == 0) {
+ if (PyUnicode_Find(str, _Py_LATIN1_CHR('{'), 0, 1, 1) == 0) {
/* Expression starts with a brace, split it with a space from the outer
one. */
outer_brace = "{ ";
}
if (-1 == append_charp(writer, outer_brace)) {
- Py_DECREF(temp_fv_str);
return -1;
}
- if (-1 == PyUnicodeWriter_WriteStr(writer, temp_fv_str)) {
- Py_DECREF(temp_fv_str);
+ if (-1 == PyUnicodeWriter_WriteStr(writer, str)) {
return -1;
}
- Py_DECREF(temp_fv_str);
return 0;
}
+static int
+append_interpolation_value(PyUnicodeWriter *writer, expr_ty e)
+{
+ /* Grammar allows PR_TUPLE, but use >PR_TEST for adding parenthesis
+ around a lambda with ':' */
+ PyObject *temp_fv_str = expr_as_unicode(e, PR_TEST + 1);
+ if (!temp_fv_str) {
+ return -1;
+ }
+ int result = append_interpolation_str(writer, temp_fv_str);
+ Py_DECREF(temp_fv_str);
+ return result;
+}
+
static int
append_interpolation_conversion(PyUnicodeWriter *writer, int conversion)
{
@@ -843,7 +855,7 @@ append_interpolation_format_spec(PyUnicodeWriter *writer,
expr_ty e)
static int
append_interpolation(PyUnicodeWriter *writer, expr_ty e)
{
- if (-1 == append_interpolation_value(writer, e->v.Interpolation.value)) {
+ if (-1 == append_interpolation_str(writer, e->v.Interpolation.str)) {
return -1;
}
_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3/lists/python-checkins.python.org/
Member address: [email protected]