From 0574dfe43761835cdbd1f1bbd954d9c2ece5d1be Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Sat, 31 Aug 2019 12:23:23 +0300 Subject: [PATCH 1/4] bpo-37995: Add an option to ast.dump() to produce a multiline output. --- Doc/library/ast.rst | 9 ++- Doc/whatsnew/3.9.rst | 8 ++ Lib/ast.py | 31 ++++++-- Lib/test/test_ast.py | 74 +++++++++++++++++++ .../2019-08-31-13-36-09.bpo-37995.rS8HzT.rst | 2 + 5 files changed, 115 insertions(+), 9 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2019-08-31-13-36-09.bpo-37995.rS8HzT.rst diff --git a/Doc/library/ast.rst b/Doc/library/ast.rst index 92bf8912eb53dd..81392539452a60 100644 --- a/Doc/library/ast.rst +++ b/Doc/library/ast.rst @@ -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,13 @@ 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. + + .. versionchange:: 3.9 + Added the *indent* option. + + .. seealso:: `Green Tree Snakes `_, an external documentation resource, has good diff --git a/Doc/whatsnew/3.9.rst b/Doc/whatsnew/3.9.rst index e20ae47462eb42..fb1d27c5346d4a 100644 --- a/Doc/whatsnew/3.9.rst +++ b/Doc/whatsnew/3.9.rst @@ -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 --------- diff --git a/Lib/ast.py b/Lib/ast.py index 5ab023f6c3c069..2cc8491112cc72 100644 --- a/Lib/ast.py +++ b/Lib/ast.py @@ -96,7 +96,7 @@ 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), @@ -104,9 +104,18 @@ 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. """ - 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 @@ -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) diff --git a/Lib/test/test_ast.py b/Lib/test/test_ast.py index 07bbb4cc772305..fa08420ed1a635 100644 --- a/Lib/test/test_ast.py +++ b/Lib/test/test_ast.py @@ -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), diff --git a/Misc/NEWS.d/next/Library/2019-08-31-13-36-09.bpo-37995.rS8HzT.rst b/Misc/NEWS.d/next/Library/2019-08-31-13-36-09.bpo-37995.rS8HzT.rst new file mode 100644 index 00000000000000..19482b644e14df --- /dev/null +++ b/Misc/NEWS.d/next/Library/2019-08-31-13-36-09.bpo-37995.rS8HzT.rst @@ -0,0 +1,2 @@ +Added the *indent* option to :func:`ast.dump` which allows it to produce a +multiline indented output. From 40e535f601fdf3f73e031a1e6256ad35cfdf9bae Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Sun, 1 Sep 2019 11:04:43 +0300 Subject: [PATCH 2/4] Extend the documentation. --- Doc/library/ast.rst | 8 ++++++-- Lib/ast.py | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/Doc/library/ast.rst b/Doc/library/ast.rst index 81392539452a60..cb8e7ec829bed7 100644 --- a/Doc/library/ast.rst +++ b/Doc/library/ast.rst @@ -330,9 +330,13 @@ and classes for traversing abstract syntax trees: *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. + 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. - .. versionchange:: 3.9 + .. versionchanged:: 3.9 Added the *indent* option. diff --git a/Lib/ast.py b/Lib/ast.py index 2cc8491112cc72..8e88bed40c9246 100644 --- a/Lib/ast.py +++ b/Lib/ast.py @@ -106,7 +106,7 @@ def dump(node, annotate_fields=True, include_attributes=False, indent=None): 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. + level. None (the default) selects the single line representation. """ def _format(node, level=0): if indent is not None: From b62129fe161f96556d1c23957382c91572114080 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Sat, 7 Sep 2019 10:47:22 +0300 Subject: [PATCH 3/4] Make indent a keyword-only parameter. --- Lib/ast.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/ast.py b/Lib/ast.py index 8e88bed40c9246..4ccbf6fde25f60 100644 --- a/Lib/ast.py +++ b/Lib/ast.py @@ -96,7 +96,7 @@ def _convert(node): return _convert(node_or_string) -def dump(node, annotate_fields=True, include_attributes=False, indent=None): +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), From 050ead8022e796744a6aea184c1be687a915ee6c Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Sat, 7 Sep 2019 11:29:22 +0300 Subject: [PATCH 4/4] More compact representation of simple nodes. --- Lib/ast.py | 30 +++++++++++++++++++----------- Lib/test/test_ast.py | 24 ++++++------------------ 2 files changed, 25 insertions(+), 29 deletions(-) diff --git a/Lib/ast.py b/Lib/ast.py index 4ccbf6fde25f60..498484f1985599 100644 --- a/Lib/ast.py +++ b/Lib/ast.py @@ -118,6 +118,7 @@ def _format(node, level=0): sep = ', ' if isinstance(node, AST): args = [] + allsimple = True keywords = annotate_fields for field in node._fields: try: @@ -125,29 +126,36 @@ def _format(node, level=0): except AttributeError: keywords = True else: + value, simple = _format(value, level) + allsimple = allsimple and simple if keywords: - args.append('%s=%s' % (field, _format(value, level))) + args.append('%s=%s' % (field, value)) else: - args.append(_format(value, level)) + 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), level))) + value = getattr(node, attr) except AttributeError: pass - if not args: - return '%s()' % (node.__class__.__name__,) - return '%s(%s%s)' % (node.__class__.__name__, prefix, sep.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): if not node: - return '[]' - return '[%s%s]' % (prefix, sep.join(_format(x, level) for x in node)) - return repr(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__) if indent is not None and not isinstance(indent, str): indent = ' ' * indent - return _format(node) + return _format(node)[0] def copy_location(new_node, old_node): diff --git a/Lib/test/test_ast.py b/Lib/test/test_ast.py index fa08420ed1a635..47e259eb2656c7 100644 --- a/Lib/test/test_ast.py +++ b/Lib/test/test_ast.py @@ -652,16 +652,10 @@ def test_dump_indent(self): body=[ Expr( value=Call( - func=Name( - id='spam', - ctx=Load()), + func=Name(id='spam', ctx=Load()), args=[ - Name( - id='eggs', - ctx=Load()), - Constant( - value='and cheese', - kind=None)], + Name(id='eggs', ctx=Load()), + Constant(value='and cheese', kind=None)], keywords=[]))], type_ignores=[])""") self.assertEqual(ast.dump(node, annotate_fields=False, indent='\t'), """\ @@ -669,16 +663,10 @@ def test_dump_indent(self): \t[ \t\tExpr( \t\t\tCall( -\t\t\t\tName( -\t\t\t\t\t'spam', -\t\t\t\t\tLoad()), +\t\t\t\tName('spam', Load()), \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\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), """\