Skip to content

Commit 799601e

Browse files
committed
Add colour to pickletools CLI output
1 parent 1e7dfbc commit 799601e

6 files changed

Lines changed: 60 additions & 15 deletions

File tree

Doc/library/pickletools.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,9 @@ Command-line options
7979

8080
A pickle file to read, or ``-`` to indicate reading from standard input.
8181

82+
.. versionadded:: next
83+
Output is in color by default and can be
84+
:ref:`controlled using environment variables <using-on-controlling-color>`.
8285

8386

8487
Programmatic interface

Doc/whatsnew/3.15.rst

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1010,6 +1010,15 @@ pickle
10101010
(Contributed by Zackery Spytz and Serhiy Storchaka in :gh:`77188`.)
10111011

10121012

1013+
pickletools
1014+
-----------
1015+
1016+
* The output of the :mod:`pickletools` command-line interface is colored by
1017+
default. This can be controlled with
1018+
:ref:`environment variables <using-on-controlling-color>`.
1019+
(Contributed by Hugo van Kemenade in :gh:`149026`.)
1020+
1021+
10131022
pprint
10141023
------
10151024

Lib/_colorize.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -359,6 +359,19 @@ class LiveProfiler(ThemeSection):
359359
)
360360

361361

362+
@dataclass(frozen=True, kw_only=True)
363+
class Pickletools(ThemeSection):
364+
annotation: str = ANSIColors.GREY
365+
arg_number: str = ANSIColors.YELLOW
366+
arg_string: str = ANSIColors.GREEN
367+
mark: str = ANSIColors.GREY
368+
opcode_code: str = ANSIColors.CYAN
369+
opcode_name: str = ANSIColors.BOLD_BLUE
370+
position: str = ANSIColors.GREY
371+
proto: str = ANSIColors.YELLOW
372+
reset: str = ANSIColors.RESET
373+
374+
362375
@dataclass(frozen=True, kw_only=True)
363376
class Syntax(ThemeSection):
364377
prompt: str = ANSIColors.BOLD_MAGENTA
@@ -429,6 +442,7 @@ class Theme:
429442
fancycompleter: FancyCompleter = field(default_factory=FancyCompleter)
430443
http_server: HttpServer = field(default_factory=HttpServer)
431444
live_profiler: LiveProfiler = field(default_factory=LiveProfiler)
445+
pickletools: Pickletools = field(default_factory=Pickletools)
432446
syntax: Syntax = field(default_factory=Syntax)
433447
timeit: Timeit = field(default_factory=Timeit)
434448
tokenize: Tokenize = field(default_factory=Tokenize)
@@ -444,6 +458,7 @@ def copy_with(
444458
fancycompleter: FancyCompleter | None = None,
445459
http_server: HttpServer | None = None,
446460
live_profiler: LiveProfiler | None = None,
461+
pickletools: Pickletools | None = None,
447462
syntax: Syntax | None = None,
448463
timeit: Timeit | None = None,
449464
tokenize: Tokenize | None = None,
@@ -462,6 +477,7 @@ def copy_with(
462477
fancycompleter=fancycompleter or self.fancycompleter,
463478
http_server=http_server or self.http_server,
464479
live_profiler=live_profiler or self.live_profiler,
480+
pickletools=pickletools or self.pickletools,
465481
syntax=syntax or self.syntax,
466482
timeit=timeit or self.timeit,
467483
tokenize=tokenize or self.tokenize,
@@ -484,6 +500,7 @@ def no_colors(cls) -> Self:
484500
fancycompleter=FancyCompleter.no_colors(),
485501
http_server=HttpServer.no_colors(),
486502
live_profiler=LiveProfiler.no_colors(),
503+
pickletools=Pickletools.no_colors(),
487504
syntax=Syntax.no_colors(),
488505
timeit=Timeit.no_colors(),
489506
tokenize=Tokenize.no_colors(),

Lib/pickletools.py

Lines changed: 28 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
import re
1717
import sys
1818

19+
lazy from _colorize import decolor, get_theme
20+
1921
__all__ = ['dis', 'genops', 'optimize']
2022

2123
bytes_types = pickle.bytes_types
@@ -2443,13 +2445,16 @@ def dis(pickle, out=None, memo=None, indentlevel=4, annotate=0):
24432445
indentchunk = ' ' * indentlevel
24442446
errormsg = None
24452447
annocol = annotate # column hint for annotations
2448+
t = get_theme(tty_file=out if out is not None else sys.stdout).pickletools
24462449
for opcode, arg, pos in genops(pickle):
24472450
if pos is not None:
2448-
print("%5d:" % pos, end=' ', file=out)
2451+
print(f"{t.position}{pos:5d}:{t.reset}", end=' ', file=out)
24492452

2450-
line = "%-4s %s%s" % (repr(opcode.code)[1:-1],
2451-
indentchunk * len(markstack),
2452-
opcode.name)
2453+
line = (
2454+
f"{t.opcode_code}{repr(opcode.code)[1:-1]:<4}{t.reset} "
2455+
f"{indentchunk * len(markstack)}"
2456+
f"{t.opcode_name}{opcode.name}{t.reset}"
2457+
)
24532458

24542459
maxproto = max(maxproto, opcode.proto)
24552460
before = opcode.stack_before # don't mutate
@@ -2510,18 +2515,26 @@ def dis(pickle, out=None, memo=None, indentlevel=4, annotate=0):
25102515
line += ' ' * (10 - len(opcode.name))
25112516
if arg is not None:
25122517
if opcode.name in ("STRING", "BINSTRING", "SHORT_BINSTRING"):
2513-
line += ' ' + ascii(arg)
2518+
arg_text = ascii(arg)
25142519
else:
2515-
line += ' ' + repr(arg)
2520+
arg_text = repr(arg)
2521+
arg_color = (
2522+
t.arg_number
2523+
if isinstance(arg, (int, float))
2524+
else t.arg_string
2525+
)
2526+
line += f" {arg_color}{arg_text}{t.reset}"
25162527
if markmsg:
2517-
line += ' ' + markmsg
2528+
line += f" {t.mark}{markmsg}{t.reset}"
25182529
if annotate:
2519-
line += ' ' * (annocol - len(line))
2530+
visible_len = len(decolor(line))
2531+
line += ' ' * (annocol - visible_len)
25202532
# make a mild effort to align annotations
2521-
annocol = len(line)
2533+
annocol = max(visible_len, annocol)
25222534
if annocol > 50:
25232535
annocol = annotate
2524-
line += ' ' + opcode.doc.split('\n', 1)[0]
2536+
doc = opcode.doc.split('\n', 1)[0]
2537+
line += f" {t.annotation}{doc}{t.reset}"
25252538
print(line, file=out)
25262539

25272540
if errormsg:
@@ -2541,7 +2554,10 @@ def dis(pickle, out=None, memo=None, indentlevel=4, annotate=0):
25412554

25422555
stack.extend(after)
25432556

2544-
print("highest protocol among opcodes =", maxproto, file=out)
2557+
print(
2558+
f"highest protocol among opcodes = {t.proto}{maxproto}{t.reset}",
2559+
file=out,
2560+
)
25452561
if stack:
25462562
raise ValueError("stack not empty after STOP: %r" % stack)
25472563

@@ -2841,10 +2857,7 @@ def __init__(self, value):
28412857

28422858
def _main(args=None):
28432859
import argparse
2844-
parser = argparse.ArgumentParser(
2845-
description='disassemble one or more pickle files',
2846-
color=True,
2847-
)
2860+
parser = argparse.ArgumentParser(description='disassemble one or more pickle files')
28482861
parser.add_argument(
28492862
'pickle_file',
28502863
nargs='+', help='the pickle file')

Lib/test/test_pickletools.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,7 @@ def test_unknown_opcode_without_pos(self):
160160
next(it)
161161

162162

163+
@support.force_not_colorized_test_class
163164
class DisTests(unittest.TestCase):
164165
maxDiff = None
165166

@@ -518,6 +519,7 @@ def test__all__(self):
518519
support.check__all__(self, pickletools, not_exported=not_exported)
519520

520521

522+
@support.force_not_colorized_test_class
521523
class CommandLineTest(unittest.TestCase):
522524
def setUp(self):
523525
self.filename = tempfile.mktemp()
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add colour to :mod:`pickletools` CLI output. Patch by Hugo van Kemenade.

0 commit comments

Comments
 (0)