Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion Doc/library/ast.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand All @@ -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
Expand Down
8 changes: 8 additions & 0 deletions Doc/whatsnew/3.9.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
---------

Expand Down
31 changes: 23 additions & 8 deletions Lib/ast.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,17 +96,26 @@ def _convert(node):
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),
the returned string will show the names and the values for fields.
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 = []
keywords = annotate_fields
Expand All @@ -117,21 +126,27 @@ def _format(node):
keywords = True
else:
if keywords:
args.append('%s=%s' % (field, _format(value)))
args.append('%s=%s' % (field, _format(value, level)))
else:
args.append(_format(value))
args.append(_format(value, level))
if include_attributes and node._attributes:
for a in node._attributes:
try:
args.append('%s=%s' % (a, _format(getattr(node, a))))
args.append('%s=%s' % (a, _format(getattr(node, a), level)))
except AttributeError:
pass
return '%s(%s)' % (node.__class__.__name__, ', '.join(args))
if not args:
return '%s()' % (node.__class__.__name__,)
return '%s(%s%s)' % (node.__class__.__name__, prefix, sep.join(args))
elif isinstance(node, list):
return '[%s]' % ', '.join(_format(x) for x in node)
if not node:
return '[]'
return '[%s%s]' % (prefix, sep.join(_format(x, level) for x in node))
return repr(node)
if not isinstance(node, AST):
raise TypeError('expected AST, got %r' % node.__class__.__name__)
if indent is not None and not isinstance(indent, str):
indent = ' ' * indent
return _format(node)


Expand Down
74 changes: 74 additions & 0 deletions Lib/test/test_ast.py
Original file line number Diff line number Diff line change
Expand Up @@ -645,6 +645,80 @@ def test_dump(self):
"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(
\t\t\t\t\t'spam',
\t\t\t\t\tLoad()),
\t\t\t\t[
\t\t\t\t\tName(
\t\t\t\t\t\t'eggs',
\t\t\t\t\t\tLoad()),
\t\t\t\t\tConstant(
\t\t\t\t\t\t'and cheese',
\t\t\t\t\t\tNone)],
\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),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Added the *indent* option to :func:`ast.dump` which allows it to produce a
multiline indented output.