Skip to content

Commit 2c4c26c

Browse files
gh-108469: Update ast.unparse for unescaped quote support from PEP701 [3.12] (#108553)
Co-authored-by: sunmy2019 <59365878+sunmy2019@users.noreply.github.com>
1 parent 9bf350b commit 2c4c26c

File tree

4 files changed

+31
-28
lines changed

4 files changed

+31
-28
lines changed

Lib/ast.py

Lines changed: 10 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1225,17 +1225,7 @@ def _write_str_avoiding_backslashes(self, string, *, quote_types=_ALL_QUOTES):
12251225

12261226
def visit_JoinedStr(self, node):
12271227
self.write("f")
1228-
if self._avoid_backslashes:
1229-
with self.buffered() as buffer:
1230-
self._write_fstring_inner(node)
1231-
return self._write_str_avoiding_backslashes("".join(buffer))
1232-
1233-
# If we don't need to avoid backslashes globally (i.e., we only need
1234-
# to avoid them inside FormattedValues), it's cosmetically preferred
1235-
# to use escaped whitespace. That is, it's preferred to use backslashes
1236-
# for cases like: f"{x}\n". To accomplish this, we keep track of what
1237-
# in our buffer corresponds to FormattedValues and what corresponds to
1238-
# Constant parts of the f-string, and allow escapes accordingly.
1228+
12391229
fstring_parts = []
12401230
for value in node.values:
12411231
with self.buffered() as buffer:
@@ -1247,11 +1237,14 @@ def visit_JoinedStr(self, node):
12471237
new_fstring_parts = []
12481238
quote_types = list(_ALL_QUOTES)
12491239
for value, is_constant in fstring_parts:
1250-
value, quote_types = self._str_literal_helper(
1251-
value,
1252-
quote_types=quote_types,
1253-
escape_special_whitespace=is_constant,
1254-
)
1240+
if is_constant:
1241+
value, quote_types = self._str_literal_helper(
1242+
value,
1243+
quote_types=quote_types,
1244+
escape_special_whitespace=True,
1245+
)
1246+
elif "\n" in value:
1247+
quote_types = [q for q in quote_types if q in _MULTI_QUOTES]
12551248
new_fstring_parts.append(value)
12561249

12571250
value = "".join(new_fstring_parts)
@@ -1273,16 +1266,12 @@ def _write_fstring_inner(self, node):
12731266

12741267
def visit_FormattedValue(self, node):
12751268
def unparse_inner(inner):
1276-
unparser = type(self)(_avoid_backslashes=True)
1269+
unparser = type(self)()
12771270
unparser.set_precedence(_Precedence.TEST.next(), inner)
12781271
return unparser.visit(inner)
12791272

12801273
with self.delimit("{", "}"):
12811274
expr = unparse_inner(node.value)
1282-
if "\\" in expr:
1283-
raise ValueError(
1284-
"Unable to avoid backslash in f-string expression part"
1285-
)
12861275
if expr.startswith("{"):
12871276
# Separate pair of opening brackets as "{ {"
12881277
self.write(" ")

Lib/test/test_tokenize.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1860,7 +1860,7 @@ def test_random_files(self):
18601860

18611861
testfiles.remove(os.path.join(tempdir, "test_unicode_identifiers.py"))
18621862

1863-
# TODO: Remove this once we can unparse PEP 701 syntax
1863+
# TODO: Remove this once we can untokenize PEP 701 syntax
18641864
testfiles.remove(os.path.join(tempdir, "test_fstring.py"))
18651865

18661866
for f in ('buffer', 'builtin', 'fileio', 'inspect', 'os', 'platform', 'sys'):

Lib/test/test_unparse.py

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,10 @@ def test_fstrings_complicated(self):
197197
self.check_ast_roundtrip('''f"a\\r\\nb"''')
198198
self.check_ast_roundtrip('''f"\\u2028{'x'}"''')
199199

200+
def test_fstrings_pep701(self):
201+
self.check_ast_roundtrip('f" something { my_dict["key"] } something else "')
202+
self.check_ast_roundtrip('f"{f"{f"{f"{f"{f"{1+1}"}"}"}"}"}"')
203+
200204
def test_strings(self):
201205
self.check_ast_roundtrip("u'foo'")
202206
self.check_ast_roundtrip("r'foo'")
@@ -378,8 +382,15 @@ def test_invalid_fstring_value(self):
378382
)
379383
)
380384

381-
def test_invalid_fstring_backslash(self):
382-
self.check_invalid(ast.FormattedValue(value=ast.Constant(value="\\\\")))
385+
def test_fstring_backslash(self):
386+
# valid since Python 3.12
387+
self.assertEqual(ast.unparse(
388+
ast.FormattedValue(
389+
value=ast.Constant(value="\\\\"),
390+
conversion=-1,
391+
format_spec=None,
392+
)
393+
), "{'\\\\\\\\'}")
383394

384395
def test_invalid_yield_from(self):
385396
self.check_invalid(ast.YieldFrom(value=None))
@@ -502,11 +513,11 @@ def test_class_bases_and_keywords(self):
502513
self.check_src_roundtrip("class X(*args, **kwargs):\n pass")
503514

504515
def test_fstrings(self):
505-
self.check_src_roundtrip('''f\'\'\'-{f"""*{f"+{f'.{x}.'}+"}*"""}-\'\'\'''')
506-
self.check_src_roundtrip('''f"\\u2028{'x'}"''')
516+
self.check_src_roundtrip("f'-{f'*{f'+{f'.{x}.'}+'}*'}-'")
517+
self.check_src_roundtrip("f'\\u2028{'x'}'")
507518
self.check_src_roundtrip(r"f'{x}\n'")
508-
self.check_src_roundtrip('''f''\'{"""\n"""}\\n''\'''')
509-
self.check_src_roundtrip('''f''\'{f"""{x}\n"""}\\n''\'''')
519+
self.check_src_roundtrip("f'{'\\n'}\\n'")
520+
self.check_src_roundtrip("f'{f'{x}\\n'}\\n'")
510521

511522
def test_docstrings(self):
512523
docstrings = (
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
:func:`ast.unparse` now supports new :term:`f-string` syntax introduced in
2+
Python 3.12. Note that the :term:`f-string` quotes are reselected for simplicity
3+
under the new syntax. (Patch by Steven Sun)

0 commit comments

Comments
 (0)