]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-138774: use `value` to `ast.unparse` code when `str` is `None` in `ast.Interpolati...
authorGeorge Ogden <38294960+George-Ogden@users.noreply.github.com>
Thu, 23 Oct 2025 13:56:05 +0000 (14:56 +0100)
committerGitHub <noreply@github.com>
Thu, 23 Oct 2025 13:56:05 +0000 (13:56 +0000)
Doc/library/ast.rst
Lib/_ast_unparse.py
Lib/test/test_unparse.py
Misc/NEWS.d/next/Library/2025-10-23-12-12-22.gh-issue-138774.mnh2gU.rst [new file with mode: 0644]

index ea3ec7d95dc45d8b8178c295dfa33e1cec3b4fb9..494621672171f2bbc8e3aa3b056e31a8ce49f021 100644 (file)
@@ -363,6 +363,11 @@ Literals
      function call).
      This has the same meaning as ``FormattedValue.value``.
    * ``str`` is a constant containing the text of the interpolation expression.
+
+     If ``str`` is set to ``None``, then ``value`` is used to generate code
+     when calling :func:`ast.unparse`. This no longer guarantees that the
+     generated code is identical to the original and is intended for code
+     generation.
    * ``conversion`` is an integer:
 
      * -1: no conversion
index 16cf56f62cc1e58fc7923618256a2bba22e7c2ae..1c8741b5a5548334cd95041a07968accde03df72 100644 (file)
@@ -658,9 +658,9 @@ class Unparser(NodeVisitor):
         unparser.set_precedence(_Precedence.TEST.next(), inner)
         return unparser.visit(inner)
 
-    def _write_interpolation(self, node, is_interpolation=False):
+    def _write_interpolation(self, node, use_str_attr=False):
         with self.delimit("{", "}"):
-            if is_interpolation:
+            if use_str_attr:
                 expr = node.str
             else:
                 expr = self._unparse_interpolation_value(node.value)
@@ -678,7 +678,8 @@ class Unparser(NodeVisitor):
         self._write_interpolation(node)
 
     def visit_Interpolation(self, node):
-        self._write_interpolation(node, is_interpolation=True)
+        # If `str` is set to `None`, use the `value` to generate the source code.
+        self._write_interpolation(node, use_str_attr=node.str is not None)
 
     def visit_Name(self, node):
         self.write(node.id)
index 0d6b05bc660b76ae4c5ff9f790dc918580e37341..35e4652a87b423cf28166b2dd9ddd2f1c8d5318d 100644 (file)
@@ -206,6 +206,97 @@ class UnparseTestCase(ASTTestCase):
         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'{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''")
+        self.check_ast_roundtrip('t""')
+        self.check_ast_roundtrip("t'{(lambda x: x)}'")
+        self.check_ast_roundtrip("t'{t'{x}'}'")
+
+    def test_tstring_with_nonsensical_str_field(self):
+        # `value` suggests that the original code is `t'{test1}`, but `str` suggests otherwise
+        self.assertEqual(
+            ast.unparse(
+                ast.TemplateStr(
+                    values=[
+                        ast.Interpolation(
+                            value=ast.Name(id="test1", ctx=ast.Load()), str="test2", conversion=-1
+                        )
+                    ]
+                )
+            ),
+            "t'{test2}'",
+        )
+
+    def test_tstring_with_none_str_field(self):
+        self.assertEqual(
+            ast.unparse(
+                ast.TemplateStr(
+                    [ast.Interpolation(value=ast.Name(id="test1"), str=None, conversion=-1)]
+                )
+            ),
+            "t'{test1}'",
+        )
+        self.assertEqual(
+            ast.unparse(
+                ast.TemplateStr(
+                    [
+                        ast.Interpolation(
+                            value=ast.Lambda(
+                                args=ast.arguments(args=[ast.arg(arg="x")]),
+                                body=ast.Name(id="x"),
+                            ),
+                            str=None,
+                            conversion=-1,
+                        )
+                    ]
+                )
+            ),
+            "t'{(lambda x: x)}'",
+        )
+        self.assertEqual(
+            ast.unparse(
+                ast.TemplateStr(
+                    values=[
+                        ast.Interpolation(
+                            value=ast.TemplateStr(
+                                # `str` field kept here
+                                [ast.Interpolation(value=ast.Name(id="x"), str="y", conversion=-1)]
+                            ),
+                            str=None,
+                            conversion=-1,
+                        )
+                    ]
+                )
+            ),
+            '''t"{t'{y}'}"''',
+        )
+        self.assertEqual(
+            ast.unparse(
+                ast.TemplateStr(
+                    values=[
+                        ast.Interpolation(
+                            value=ast.TemplateStr(
+                                [ast.Interpolation(value=ast.Name(id="x"), str=None, conversion=-1)]
+                            ),
+                            str=None,
+                            conversion=-1,
+                        )
+                    ]
+                )
+            ),
+            '''t"{t'{x}'}"''',
+        )
+        self.assertEqual(
+            ast.unparse(ast.TemplateStr(
+                [ast.Interpolation(value=ast.Constant(value="foo"), str=None, conversion=114)]
+            )),
+            '''t"{'foo'!r}"''',
+        )
 
     def test_strings(self):
         self.check_ast_roundtrip("u'foo'")
@@ -813,15 +904,6 @@ class CosmeticTestCase(ASTTestCase):
         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."""
diff --git a/Misc/NEWS.d/next/Library/2025-10-23-12-12-22.gh-issue-138774.mnh2gU.rst b/Misc/NEWS.d/next/Library/2025-10-23-12-12-22.gh-issue-138774.mnh2gU.rst
new file mode 100644 (file)
index 0000000..e12f789
--- /dev/null
@@ -0,0 +1,2 @@
+:func:`ast.unparse` now generates full source code when handling
+:class:`ast.Interpolation` nodes that do not have a specified source.