Skip to content

Commit 6d4ca16

Browse files
gh-148981: Add color parameter to ast.dump (#148982)
And turn on color for the `ast` module CLI.
1 parent 0a39730 commit 6d4ca16

6 files changed

Lines changed: 91 additions & 17 deletions

File tree

Doc/library/ast.rst

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2480,7 +2480,7 @@ and classes for traversing abstract syntax trees:
24802480
node = YourTransformer().visit(node)
24812481

24822482

2483-
.. function:: dump(node, annotate_fields=True, include_attributes=False, *, indent=None, show_empty=False)
2483+
.. function:: dump(node, annotate_fields=True, include_attributes=False, *, color=False, indent=None, show_empty=False)
24842484

24852485
Return a formatted dump of the tree in *node*. This is mainly useful for
24862486
debugging purposes. If *annotate_fields* is true (by default),
@@ -2490,6 +2490,10 @@ and classes for traversing abstract syntax trees:
24902490
numbers and column offsets are not dumped by default. If this is wanted,
24912491
*include_attributes* can be set to true.
24922492

2493+
If *color* is ``True``, the returned string is syntax highlighted using
2494+
ANSI escape sequences.
2495+
If ``False`` (the default), colored output is always disabled.
2496+
24932497
If *indent* is a non-negative integer or string, then the tree will be
24942498
pretty-printed with that indent level. An indent level
24952499
of 0, negative, or ``""`` will only insert newlines. ``None`` (the default)
@@ -2527,6 +2531,9 @@ and classes for traversing abstract syntax trees:
25272531
.. versionchanged:: 3.15
25282532
Omit optional ``Load()`` values by default.
25292533

2534+
.. versionchanged:: next
2535+
Added the *color* parameter.
2536+
25302537

25312538
.. _ast-compiler-flags:
25322539

@@ -2584,6 +2591,10 @@ Command-line usage
25842591

25852592
.. versionadded:: 3.9
25862593

2594+
.. versionchanged:: next
2595+
The output is now syntax highlighted by default. This can be
2596+
:ref:`controlled using environment variables <using-on-controlling-color>`.
2597+
25872598
The :mod:`!ast` module can be executed as a script from the command line.
25882599
It is as simple as:
25892600

Doc/whatsnew/3.15.rst

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -704,6 +704,20 @@ array
704704
(Contributed by Sergey B Kirpichev in :gh:`146238`.)
705705

706706

707+
ast
708+
---
709+
710+
* Add *color* parameter to :func:`~ast.dump`.
711+
If ``True``, the returned string is syntax highlighted using ANSI escape
712+
sequences.
713+
If ``False`` (the default), colored output is always disabled.
714+
(Contributed by Stan Ulbrych in :gh:`148981`.)
715+
716+
* The :ref:`command-line <ast-cli>` output is now syntax highlighted by default.
717+
This can be :ref:`controlled using environment variables <using-on-controlling-color>`.
718+
(Contributed by Stan Ulbrych in :gh:`148981`.)
719+
720+
707721
base64
708722
------
709723

Lib/_colorize.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,17 @@ class Argparse(ThemeSection):
189189
message: str = ANSIColors.MAGENTA
190190

191191

192+
@dataclass(frozen=True, kw_only=True)
193+
class Ast(ThemeSection):
194+
node: str = ANSIColors.CYAN
195+
field: str = ANSIColors.BLUE
196+
attribute: str = ANSIColors.GREY
197+
string: str = ANSIColors.GREEN
198+
number: str = ANSIColors.YELLOW
199+
keyword: str = ANSIColors.BOLD_BLUE
200+
reset: str = ANSIColors.RESET
201+
202+
192203
@dataclass(frozen=True, kw_only=True)
193204
class Difflib(ThemeSection):
194205
"""A 'git diff'-like theme for `difflib.unified_diff`."""
@@ -405,6 +416,7 @@ class Theme:
405416
below.
406417
"""
407418
argparse: Argparse = field(default_factory=Argparse)
419+
ast: Ast = field(default_factory=Ast)
408420
difflib: Difflib = field(default_factory=Difflib)
409421
fancycompleter: FancyCompleter = field(default_factory=FancyCompleter)
410422
http_server: HttpServer = field(default_factory=HttpServer)
@@ -418,6 +430,7 @@ def copy_with(
418430
self,
419431
*,
420432
argparse: Argparse | None = None,
433+
ast: Ast | None = None,
421434
difflib: Difflib | None = None,
422435
fancycompleter: FancyCompleter | None = None,
423436
http_server: HttpServer | None = None,
@@ -434,6 +447,7 @@ def copy_with(
434447
"""
435448
return type(self)(
436449
argparse=argparse or self.argparse,
450+
ast=ast or self.ast,
437451
difflib=difflib or self.difflib,
438452
fancycompleter=fancycompleter or self.fancycompleter,
439453
http_server=http_server or self.http_server,
@@ -454,6 +468,7 @@ def no_colors(cls) -> Self:
454468
"""
455469
return cls(
456470
argparse=Argparse.no_colors(),
471+
ast=Ast.no_colors(),
457472
difflib=Difflib.no_colors(),
458473
fancycompleter=FancyCompleter.no_colors(),
459474
http_server=HttpServer.no_colors(),

Lib/ast.py

Lines changed: 38 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
:license: Python License.
2222
"""
2323
from _ast import *
24+
lazy from _colorize import can_colorize, get_theme
2425

2526

2627
def parse(source, filename='<unknown>', mode='exec', *,
@@ -117,21 +118,32 @@ def _convert_literal(node):
117118
def dump(
118119
node, annotate_fields=True, include_attributes=False,
119120
*,
120-
indent=None, show_empty=False,
121+
color=False, indent=None, show_empty=False,
121122
):
122123
"""
123124
Return a formatted dump of the tree in node. This is mainly useful for
124-
debugging purposes. If annotate_fields is true (by default),
125-
the returned string will show the names and the values for fields.
126-
If annotate_fields is false, the result string will be more compact by
127-
omitting unambiguous field names. Attributes such as line
128-
numbers and column offsets are not dumped by default. If this is wanted,
129-
include_attributes can be set to true. If indent is a non-negative
130-
integer or string, then the tree will be pretty-printed with that indent
131-
level. None (the default) selects the single line representation.
125+
debugging purposes.
126+
127+
If annotate_fields is true (by default), the returned string will show the
128+
names and the values for fields. If annotate_fields is false, the result
129+
string will be more compact by omitting unambiguous field names.
130+
131+
Attributes such as line numbers and column offsets are not dumped by default.
132+
If this is wanted, include_attributes can be set to true.
133+
134+
If color is true, the returned string is syntax highlighted using ANSI
135+
escape sequences. If color is false (the default), colored output is always
136+
disabled.
137+
138+
If indent is a non-negative integer or string, then the tree will be
139+
pretty-printed with that indent level. If indent is None (the default),
140+
the tree is dumped on a single line.
141+
132142
If show_empty is False, then empty lists and fields that are None
133143
will be omitted from the output for better readability.
134144
"""
145+
t = get_theme(force_color=color, force_no_color=not color).ast
146+
135147
def _format(node, level=0):
136148
if indent is not None:
137149
level += 1
@@ -166,15 +178,17 @@ def _format(node, level=0):
166178
field_type = cls._field_types.get(name, object)
167179
if field_type is expr_context:
168180
if not keywords:
169-
args_buffer.append(repr(value))
181+
args_buffer.append(
182+
f'{t.node}{type(value).__name__}'
183+
f'{t.reset}()')
170184
continue
171185
if not keywords:
172186
args.extend(args_buffer)
173187
args_buffer = []
174188
value, simple = _format(value, level)
175189
allsimple = allsimple and simple
176190
if keywords:
177-
args.append('%s=%s' % (name, value))
191+
args.append(f'{t.field}{name}{t.reset}={value}')
178192
else:
179193
args.append(value)
180194
if include_attributes and node._attributes:
@@ -187,14 +201,21 @@ def _format(node, level=0):
187201
continue
188202
value, simple = _format(value, level)
189203
allsimple = allsimple and simple
190-
args.append('%s=%s' % (name, value))
204+
args.append(f'{t.attribute}{name}{t.reset}={value}')
205+
cls_name = f'{t.node}{cls.__name__}{t.reset}'
191206
if allsimple and len(args) <= 3:
192-
return '%s(%s)' % (node.__class__.__name__, ', '.join(args)), not args
193-
return '%s(%s%s)' % (node.__class__.__name__, prefix, sep.join(args)), False
207+
return f'{cls_name}({", ".join(args)})', not args
208+
return f'{cls_name}({prefix}{sep.join(args)})', False
194209
elif isinstance(node, list):
195210
if not node:
196211
return '[]', True
197212
return '[%s%s]' % (prefix, sep.join(_format(x, level)[0] for x in node)), False
213+
if isinstance(node, bool) or node is None or node is Ellipsis:
214+
return f'{t.keyword}{node!r}{t.reset}', True
215+
if isinstance(node, (int, float, complex)):
216+
return f'{t.number}{node!r}{t.reset}', True
217+
if isinstance(node, (str, bytes)):
218+
return f'{t.string}{node!r}{t.reset}', True
198219
return repr(node), True
199220

200221
if not isinstance(node, AST):
@@ -642,7 +663,7 @@ def main(args=None):
642663
import argparse
643664
import sys
644665

645-
parser = argparse.ArgumentParser(color=True)
666+
parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter)
646667
parser.add_argument('infile', nargs='?', default='-',
647668
help='the file to parse; defaults to stdin')
648669
parser.add_argument('-m', '--mode', default='exec',
@@ -661,7 +682,7 @@ def main(args=None):
661682
'(for example, 3.10)')
662683
parser.add_argument('-O', '--optimize',
663684
type=int, default=-1, metavar='LEVEL',
664-
help='optimization level for parser (default -1)')
685+
help='optimization level for parser')
665686
parser.add_argument('--show-empty', default=False, action='store_true',
666687
help='show empty lists and fields in dump output')
667688
args = parser.parse_args(args)
@@ -688,6 +709,7 @@ def main(args=None):
688709
tree = parse(source, name, args.mode, type_comments=args.no_type_comments,
689710
feature_version=feature_version, optimize=args.optimize)
690711
print(dump(tree, include_attributes=args.include_attributes,
712+
color=can_colorize(file=sys.stdout),
691713
indent=args.indent, show_empty=args.show_empty))
692714

693715
if __name__ == '__main__':

Lib/test/test_ast/test_ast.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1705,6 +1705,16 @@ def check_text(code, empty, full, **kwargs):
17051705
full="Module(body=[Import(names=[alias(name='_ast', asname='ast')], is_lazy=0), ImportFrom(module='module', names=[alias(name='sub')], level=0, is_lazy=0)], type_ignores=[])",
17061706
)
17071707

1708+
def test_dump_with_color(self):
1709+
node = ast.parse("x = 1")
1710+
self.assertNotIn("\x1b[", ast.dump(node))
1711+
self.assertNotIn("\x1b[", ast.dump(node, color=False))
1712+
self.assertIn("\x1b[", ast.dump(node, color=True))
1713+
1714+
node = ast.Constant(value="\x1b[31m")
1715+
self.assertEqual(ast.dump(node), "Constant(value='\\x1b[31m')")
1716+
self.assertIn("'\\x1b[31m'", ast.dump(node, color=True))
1717+
17081718
def test_copy_location(self):
17091719
src = ast.parse('1 + 1', mode='eval')
17101720
src.body.right = ast.copy_location(ast.Constant(2), src.body.right)
@@ -3415,6 +3425,7 @@ def test_subinterpreter(self):
34153425
self.assertEqual(res, 0)
34163426

34173427

3428+
@support.force_not_colorized_test_class
34183429
class CommandLineTests(unittest.TestCase):
34193430
def setUp(self):
34203431
self.filename = tempfile.mktemp()
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add *color* parameter to :func:`ast.dump`.

0 commit comments

Comments
 (0)