https://github.com/python/cpython/commit/bfac7d2edc6e461e21b4762f8db03fca85f3a0be
commit: bfac7d2edc6e461e21b4762f8db03fca85f3a0be
branch: main
author: Jelle Zijlstra <jelle.zijls...@gmail.com>
committer: JelleZijlstra <jelle.zijls...@gmail.com>
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 -- python-checkins@python.org
To unsubscribe send an email to python-checkins-le...@python.org
https://mail.python.org/mailman3/lists/python-checkins.python.org/
Member address: arch...@mail-archive.com

Reply via email to