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

diff --git a/Lib/ast.py b/Lib/ast.py
index 5ab023f..498484f 100644
--- a/Lib/ast.py
+++ b/Lib/ast.py
@@ -96,7 +96,7 @@
     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 @@
     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 @@
                 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):
diff --git a/Lib/test/test_ast.py b/Lib/test/test_ast.py
index 07bbb4c..47e259e 100644
--- a/Lib/test/test_ast.py
+++ b/Lib/test/test_ast.py
@@ -645,6 +645,68 @@
             "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),