bpo-37995: Add an option to ast.dump() to produce a multiline output. (GH-15631)

This commit is contained in:
Serhiy Storchaka 2019-09-09 19:33:13 +03:00 committed by GitHub
parent 92709a263e
commit 850573b836
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 118 additions and 12 deletions

View File

@ -319,7 +319,7 @@ and classes for traversing abstract syntax trees:
node = YourTransformer().visit(node)
.. function:: dump(node, annotate_fields=True, include_attributes=False)
.. function:: dump(node, annotate_fields=True, include_attributes=False, *, indent=None)
Return a formatted dump of the tree in *node*. This is mainly useful for
debugging purposes. If *annotate_fields* is true (by default),
@ -329,6 +329,17 @@ and classes for traversing abstract syntax trees:
numbers and column offsets are not dumped by default. If this is wanted,
*include_attributes* can be set to true.
If *indent* is a non-negative integer or string, then the tree will be
pretty-printed with that indent level. An indent level
of 0, negative, or ``""`` will only insert newlines. ``None`` (the default)
selects the single line representation. Using a positive integer indent
indents that many spaces per level. If *indent* is a string (such as ``"\t"``),
that string is used to indent each level.
.. versionchanged:: 3.9
Added the *indent* option.
.. seealso::
`Green Tree Snakes <https://greentreesnakes.readthedocs.io/>`_, an external documentation resource, has good

View File

@ -109,6 +109,14 @@ New Modules
Improved Modules
================
ast
---
Added the *indent* option to :func:`~ast.dump` which allows it to produce a
multiline indented output.
(Contributed by Serhiy Storchaka in :issue:`37995`.)
threading
---------

View File

@ -96,7 +96,7 @@ def literal_eval(node_or_string):
return _convert(node_or_string)
def dump(node, annotate_fields=True, include_attributes=False):
def dump(node, annotate_fields=True, include_attributes=False, *, indent=None):
"""
Return a formatted dump of the tree in node. This is mainly useful for
debugging purposes. If annotate_fields is true (by default),
@ -104,11 +104,21 @@ def dump(node, annotate_fields=True, include_attributes=False):
If annotate_fields is false, the result string will be more compact by
omitting unambiguous field names. Attributes such as line
numbers and column offsets are not dumped by default. If this is wanted,
include_attributes can be set to true.
include_attributes can be set to true. If indent is a non-negative
integer or string, then the tree will be pretty-printed with that indent
level. None (the default) selects the single line representation.
"""
def _format(node):
def _format(node, level=0):
if indent is not None:
level += 1
prefix = '\n' + indent * level
sep = ',\n' + indent * level
else:
prefix = ''
sep = ', '
if isinstance(node, AST):
args = []
allsimple = True
keywords = annotate_fields
for field in node._fields:
try:
@ -116,23 +126,36 @@ def dump(node, annotate_fields=True, include_attributes=False):
except AttributeError:
keywords = True
else:
value, simple = _format(value, level)
allsimple = allsimple and simple
if keywords:
args.append('%s=%s' % (field, _format(value)))
args.append('%s=%s' % (field, value))
else:
args.append(_format(value))
args.append(value)
if include_attributes and node._attributes:
for a in node._attributes:
for attr in node._attributes:
try:
args.append('%s=%s' % (a, _format(getattr(node, a))))
value = getattr(node, attr)
except AttributeError:
pass
return '%s(%s)' % (node.__class__.__name__, ', '.join(args))
else:
value, simple = _format(value, level)
allsimple = allsimple and simple
args.append('%s=%s' % (attr, value))
if allsimple and len(args) <= 3:
return '%s(%s)' % (node.__class__.__name__, ', '.join(args)), not args
return '%s(%s%s)' % (node.__class__.__name__, prefix, sep.join(args)), False
elif isinstance(node, list):
return '[%s]' % ', '.join(_format(x) for x in node)
return repr(node)
if not node:
return '[]', True
return '[%s%s]' % (prefix, sep.join(_format(x, level)[0] for x in node)), False
return repr(node), True
if not isinstance(node, AST):
raise TypeError('expected AST, got %r' % node.__class__.__name__)
return _format(node)
if indent is not None and not isinstance(indent, str):
indent = ' ' * indent
return _format(node)[0]
def copy_location(new_node, old_node):

View File

@ -645,6 +645,68 @@ class ASTHelpers_Test(unittest.TestCase):
"lineno=1, col_offset=0, end_lineno=1, end_col_offset=24)], type_ignores=[])"
)
def test_dump_indent(self):
node = ast.parse('spam(eggs, "and cheese")')
self.assertEqual(ast.dump(node, indent=3), """\
Module(
body=[
Expr(
value=Call(
func=Name(id='spam', ctx=Load()),
args=[
Name(id='eggs', ctx=Load()),
Constant(value='and cheese', kind=None)],
keywords=[]))],
type_ignores=[])""")
self.assertEqual(ast.dump(node, annotate_fields=False, indent='\t'), """\
Module(
\t[
\t\tExpr(
\t\t\tCall(
\t\t\t\tName('spam', Load()),
\t\t\t\t[
\t\t\t\t\tName('eggs', Load()),
\t\t\t\t\tConstant('and cheese', None)],
\t\t\t\t[]))],
\t[])""")
self.assertEqual(ast.dump(node, include_attributes=True, indent=3), """\
Module(
body=[
Expr(
value=Call(
func=Name(
id='spam',
ctx=Load(),
lineno=1,
col_offset=0,
end_lineno=1,
end_col_offset=4),
args=[
Name(
id='eggs',
ctx=Load(),
lineno=1,
col_offset=5,
end_lineno=1,
end_col_offset=9),
Constant(
value='and cheese',
kind=None,
lineno=1,
col_offset=11,
end_lineno=1,
end_col_offset=23)],
keywords=[],
lineno=1,
col_offset=0,
end_lineno=1,
end_col_offset=24),
lineno=1,
col_offset=0,
end_lineno=1,
end_col_offset=24)],
type_ignores=[])""")
def test_dump_incomplete(self):
node = ast.Raise(lineno=3, col_offset=4)
self.assertEqual(ast.dump(node),

View File

@ -0,0 +1,2 @@
Added the *indent* option to :func:`ast.dump` which allows it to produce a
multiline indented output.