mirror of
https://github.com/python/cpython.git
synced 2025-09-26 10:19:53 +00:00
bpo-43417: Better buffer handling for ast.unparse (GH-24772)
This commit is contained in:
parent
a0bd9e9c11
commit
3d98ececda
2 changed files with 87 additions and 66 deletions
118
Lib/ast.py
118
Lib/ast.py
|
@ -678,7 +678,6 @@ class _Unparser(NodeVisitor):
|
||||||
|
|
||||||
def __init__(self, *, _avoid_backslashes=False):
|
def __init__(self, *, _avoid_backslashes=False):
|
||||||
self._source = []
|
self._source = []
|
||||||
self._buffer = []
|
|
||||||
self._precedences = {}
|
self._precedences = {}
|
||||||
self._type_ignores = {}
|
self._type_ignores = {}
|
||||||
self._indent = 0
|
self._indent = 0
|
||||||
|
@ -721,14 +720,15 @@ class _Unparser(NodeVisitor):
|
||||||
"""Append a piece of text"""
|
"""Append a piece of text"""
|
||||||
self._source.append(text)
|
self._source.append(text)
|
||||||
|
|
||||||
def buffer_writer(self, text):
|
@contextmanager
|
||||||
self._buffer.append(text)
|
def buffered(self, buffer = None):
|
||||||
|
if buffer is None:
|
||||||
|
buffer = []
|
||||||
|
|
||||||
@property
|
original_source = self._source
|
||||||
def buffer(self):
|
self._source = buffer
|
||||||
value = "".join(self._buffer)
|
yield buffer
|
||||||
self._buffer.clear()
|
self._source = original_source
|
||||||
return value
|
|
||||||
|
|
||||||
@contextmanager
|
@contextmanager
|
||||||
def block(self, *, extra = None):
|
def block(self, *, extra = None):
|
||||||
|
@ -1127,9 +1127,9 @@ class _Unparser(NodeVisitor):
|
||||||
def visit_JoinedStr(self, node):
|
def visit_JoinedStr(self, node):
|
||||||
self.write("f")
|
self.write("f")
|
||||||
if self._avoid_backslashes:
|
if self._avoid_backslashes:
|
||||||
self._fstring_JoinedStr(node, self.buffer_writer)
|
with self.buffered() as buffer:
|
||||||
self._write_str_avoiding_backslashes(self.buffer)
|
self._write_fstring_inner(node)
|
||||||
return
|
return self._write_str_avoiding_backslashes("".join(buffer))
|
||||||
|
|
||||||
# If we don't need to avoid backslashes globally (i.e., we only need
|
# If we don't need to avoid backslashes globally (i.e., we only need
|
||||||
# to avoid them inside FormattedValues), it's cosmetically preferred
|
# to avoid them inside FormattedValues), it's cosmetically preferred
|
||||||
|
@ -1137,60 +1137,62 @@ class _Unparser(NodeVisitor):
|
||||||
# for cases like: f"{x}\n". To accomplish this, we keep track of what
|
# for cases like: f"{x}\n". To accomplish this, we keep track of what
|
||||||
# in our buffer corresponds to FormattedValues and what corresponds to
|
# in our buffer corresponds to FormattedValues and what corresponds to
|
||||||
# Constant parts of the f-string, and allow escapes accordingly.
|
# Constant parts of the f-string, and allow escapes accordingly.
|
||||||
buffer = []
|
fstring_parts = []
|
||||||
for value in node.values:
|
for value in node.values:
|
||||||
meth = getattr(self, "_fstring_" + type(value).__name__)
|
with self.buffered() as buffer:
|
||||||
meth(value, self.buffer_writer)
|
self._write_fstring_inner(value)
|
||||||
buffer.append((self.buffer, isinstance(value, Constant)))
|
fstring_parts.append(
|
||||||
new_buffer = []
|
("".join(buffer), isinstance(value, Constant))
|
||||||
quote_types = _ALL_QUOTES
|
|
||||||
for value, is_constant in buffer:
|
|
||||||
# Repeatedly narrow down the list of possible quote_types
|
|
||||||
value, quote_types = self._str_literal_helper(
|
|
||||||
value, quote_types=quote_types,
|
|
||||||
escape_special_whitespace=is_constant
|
|
||||||
)
|
)
|
||||||
new_buffer.append(value)
|
|
||||||
value = "".join(new_buffer)
|
new_fstring_parts = []
|
||||||
|
quote_types = list(_ALL_QUOTES)
|
||||||
|
for value, is_constant in fstring_parts:
|
||||||
|
value, quote_types = self._str_literal_helper(
|
||||||
|
value,
|
||||||
|
quote_types=quote_types,
|
||||||
|
escape_special_whitespace=is_constant,
|
||||||
|
)
|
||||||
|
new_fstring_parts.append(value)
|
||||||
|
|
||||||
|
value = "".join(new_fstring_parts)
|
||||||
quote_type = quote_types[0]
|
quote_type = quote_types[0]
|
||||||
self.write(f"{quote_type}{value}{quote_type}")
|
self.write(f"{quote_type}{value}{quote_type}")
|
||||||
|
|
||||||
|
def _write_fstring_inner(self, node):
|
||||||
|
if isinstance(node, JoinedStr):
|
||||||
|
# for both the f-string itself, and format_spec
|
||||||
|
for value in node.values:
|
||||||
|
self._write_fstring_inner(value)
|
||||||
|
elif isinstance(node, Constant) and isinstance(node.value, str):
|
||||||
|
value = node.value.replace("{", "{{").replace("}", "}}")
|
||||||
|
self.write(value)
|
||||||
|
elif isinstance(node, FormattedValue):
|
||||||
|
self.visit_FormattedValue(node)
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Unexpected node inside JoinedStr, {node!r}")
|
||||||
|
|
||||||
def visit_FormattedValue(self, node):
|
def visit_FormattedValue(self, node):
|
||||||
self.write("f")
|
def unparse_inner(inner):
|
||||||
self._fstring_FormattedValue(node, self.buffer_writer)
|
unparser = type(self)(_avoid_backslashes=True)
|
||||||
self._write_str_avoiding_backslashes(self.buffer)
|
unparser.set_precedence(_Precedence.TEST.next(), inner)
|
||||||
|
return unparser.visit(inner)
|
||||||
|
|
||||||
def _fstring_JoinedStr(self, node, write):
|
with self.delimit("{", "}"):
|
||||||
for value in node.values:
|
expr = unparse_inner(node.value)
|
||||||
meth = getattr(self, "_fstring_" + type(value).__name__)
|
if "\\" in expr:
|
||||||
meth(value, write)
|
raise ValueError(
|
||||||
|
"Unable to avoid backslash in f-string expression part"
|
||||||
def _fstring_Constant(self, node, write):
|
)
|
||||||
if not isinstance(node.value, str):
|
if expr.startswith("{"):
|
||||||
raise ValueError("Constants inside JoinedStr should be a string.")
|
# Separate pair of opening brackets as "{ {"
|
||||||
value = node.value.replace("{", "{{").replace("}", "}}")
|
self.write(" ")
|
||||||
write(value)
|
self.write(expr)
|
||||||
|
if node.conversion != -1:
|
||||||
def _fstring_FormattedValue(self, node, write):
|
self.write(f"!{chr(node.conversion)}")
|
||||||
write("{")
|
if node.format_spec:
|
||||||
unparser = type(self)(_avoid_backslashes=True)
|
self.write(":")
|
||||||
unparser.set_precedence(_Precedence.TEST.next(), node.value)
|
self._write_fstring_inner(node.format_spec)
|
||||||
expr = unparser.visit(node.value)
|
|
||||||
if expr.startswith("{"):
|
|
||||||
write(" ") # Separate pair of opening brackets as "{ {"
|
|
||||||
if "\\" in expr:
|
|
||||||
raise ValueError("Unable to avoid backslash in f-string expression part")
|
|
||||||
write(expr)
|
|
||||||
if node.conversion != -1:
|
|
||||||
conversion = chr(node.conversion)
|
|
||||||
if conversion not in "sra":
|
|
||||||
raise ValueError("Unknown f-string conversion.")
|
|
||||||
write(f"!{conversion}")
|
|
||||||
if node.format_spec:
|
|
||||||
write(":")
|
|
||||||
meth = getattr(self, "_fstring_" + type(node.format_spec).__name__)
|
|
||||||
meth(node.format_spec, write)
|
|
||||||
write("}")
|
|
||||||
|
|
||||||
def visit_Name(self, node):
|
def visit_Name(self, node):
|
||||||
self.write(node.id)
|
self.write(node.id)
|
||||||
|
|
|
@ -149,6 +149,27 @@ class UnparseTestCase(ASTTestCase):
|
||||||
# Tests for specific bugs found in earlier versions of unparse
|
# Tests for specific bugs found in earlier versions of unparse
|
||||||
|
|
||||||
def test_fstrings(self):
|
def test_fstrings(self):
|
||||||
|
self.check_ast_roundtrip("f'a'")
|
||||||
|
self.check_ast_roundtrip("f'{{}}'")
|
||||||
|
self.check_ast_roundtrip("f'{{5}}'")
|
||||||
|
self.check_ast_roundtrip("f'{{5}}5'")
|
||||||
|
self.check_ast_roundtrip("f'X{{}}X'")
|
||||||
|
self.check_ast_roundtrip("f'{a}'")
|
||||||
|
self.check_ast_roundtrip("f'{ {1:2}}'")
|
||||||
|
self.check_ast_roundtrip("f'a{a}a'")
|
||||||
|
self.check_ast_roundtrip("f'a{a}{a}a'")
|
||||||
|
self.check_ast_roundtrip("f'a{a}a{a}a'")
|
||||||
|
self.check_ast_roundtrip("f'{a!r}x{a!s}12{{}}{a!a}'")
|
||||||
|
self.check_ast_roundtrip("f'{a:10}'")
|
||||||
|
self.check_ast_roundtrip("f'{a:100_000{10}}'")
|
||||||
|
self.check_ast_roundtrip("f'{a!r:10}'")
|
||||||
|
self.check_ast_roundtrip("f'{a:a{b}10}'")
|
||||||
|
self.check_ast_roundtrip(
|
||||||
|
"f'a{b}{c!s}{d!r}{e!a}{f:a}{g:a{b}}{h!s:a}"
|
||||||
|
"{j!s:{a}b}{k!s:a{b}c}{l!a:{b}c{d}}{x+y=}'"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_fstrings_special_chars(self):
|
||||||
# See issue 25180
|
# See issue 25180
|
||||||
self.check_ast_roundtrip(r"""f'{f"{0}"*3}'""")
|
self.check_ast_roundtrip(r"""f'{f"{0}"*3}'""")
|
||||||
self.check_ast_roundtrip(r"""f'{f"{y}"*3}'""")
|
self.check_ast_roundtrip(r"""f'{f"{y}"*3}'""")
|
||||||
|
@ -323,15 +344,13 @@ class UnparseTestCase(ASTTestCase):
|
||||||
def test_invalid_raise(self):
|
def test_invalid_raise(self):
|
||||||
self.check_invalid(ast.Raise(exc=None, cause=ast.Name(id="X")))
|
self.check_invalid(ast.Raise(exc=None, cause=ast.Name(id="X")))
|
||||||
|
|
||||||
def test_invalid_fstring_constant(self):
|
def test_invalid_fstring_value(self):
|
||||||
self.check_invalid(ast.JoinedStr(values=[ast.Constant(value=100)]))
|
|
||||||
|
|
||||||
def test_invalid_fstring_conversion(self):
|
|
||||||
self.check_invalid(
|
self.check_invalid(
|
||||||
ast.FormattedValue(
|
ast.JoinedStr(
|
||||||
value=ast.Constant(value="a", kind=None),
|
values=[
|
||||||
conversion=ord("Y"), # random character
|
ast.Name(id="test"),
|
||||||
format_spec=None,
|
ast.Constant(value="test")
|
||||||
|
]
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue