From: Miss Islington (bot) <31488909+miss-islington@users.noreply.github.com> Date: Thu, 23 Oct 2025 14:39:02 +0000 (+0200) Subject: [3.14] gh-138774: use `value` to `ast.unparse` code when `str` is `None` in `ast... X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=564bb00b2763d57d0379be5f522dd9a2b81b61f7;p=thirdparty%2FPython%2Fcpython.git [3.14] gh-138774: use `value` to `ast.unparse` code when `str` is `None` in `ast.Interpolation` (GH-139415) (#140503) gh-138774: use `value` to `ast.unparse` code when `str` is `None` in `ast.Interpolation` (GH-139415) (cherry picked from commit 4d0849426f4c6862e50658c4e35341ffb5ab288b) Co-authored-by: George Ogden <38294960+George-Ogden@users.noreply.github.com> --- diff --git a/Doc/library/ast.rst b/Doc/library/ast.rst index d6c30aad5f13..83cb6e9f2158 100644 --- a/Doc/library/ast.rst +++ b/Doc/library/ast.rst @@ -364,6 +364,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 diff --git a/Lib/_ast_unparse.py b/Lib/_ast_unparse.py index 16cf56f62cc1..1c8741b5a554 100644 --- a/Lib/_ast_unparse.py +++ b/Lib/_ast_unparse.py @@ -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) diff --git a/Lib/test/test_unparse.py b/Lib/test/test_unparse.py index 0d6b05bc660b..35e4652a87b4 100644 --- a/Lib/test/test_unparse.py +++ b/Lib/test/test_unparse.py @@ -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 index 000000000000..e12f789e6744 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-10-23-12-12-22.gh-issue-138774.mnh2gU.rst @@ -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.