Skip to content

Commit 495ba70

Browse files
miss-islingtontonybaloneysunmy2019
authored
[3.12] gh-108469: Update ast.unparse for unescaped quote support from PEP701 [3.12] (GH-108553) (#108960)
Co-authored-by: Anthony Shaw <anthony.p.shaw@gmail.com> Co-authored-by: sunmy2019 <59365878+sunmy2019@users.noreply.github.com>
1 parent 460043b commit 495ba70

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
@@ -1223,17 +1223,7 @@ def _write_str_avoiding_backslashes(self, string, *, quote_types=_ALL_QUOTES):
12231223

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

12551248
value = "".join(new_fstring_parts)
@@ -1271,16 +1264,12 @@ def _write_fstring_inner(self, node):
12711264

12721265
def visit_FormattedValue(self, node):
12731266
def unparse_inner(inner):
1274-
unparser = type(self)(_avoid_backslashes=True)
1267+
unparser = type(self)()
12751268
unparser.set_precedence(_Precedence.TEST.next(), inner)
12761269
return unparser.visit(inner)
12771270

12781271
with self.delimit("{", "}"):
12791272
expr = unparse_inner(node.value)
1280-
if "\\" in expr:
1281-
raise ValueError(
1282-
"Unable to avoid backslash in f-string expression part"
1283-
)
12841273
if expr.startswith("{"):
12851274
# Separate pair of opening brackets as "{ {"
12861275
self.write(" ")

Lib/test/test_tokenize.py

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

18581858
testfiles.remove(os.path.join(tempdir, "test_unicode_identifiers.py"))
18591859

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

18631863
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)