Skip to content

Commit a3d56fe

Browse files
feat: change colorization logic(minimal set of colors, align with godbolt) & adjust test cases
1 parent c3378fb commit a3d56fe

3 files changed

Lines changed: 129 additions & 75 deletions

File tree

Lib/_colorize.py

Lines changed: 7 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -219,45 +219,18 @@ class Difflib(ThemeSection):
219219

220220
@dataclass(frozen=True, kw_only=True)
221221
class Dis(ThemeSection):
222-
label_bg: str = ANSIColors.BACKGROUND_CYAN
223-
label_fg: str = ANSIColors.BLACK
222+
disassembly_header: str = ANSIColors.GREEN
224223

225-
exception_label: str = ANSIColors.CYAN
226-
argument_detail: str = "\x1B[3m"
224+
jump_target: str = ANSIColors.GREEN
225+
exception_label: str = ANSIColors.GREEN
227226

228-
op_load: str = ANSIColors.BLUE
229-
op_pop: str = ANSIColors.MAGENTA
230-
op_call_return: str = ANSIColors.YELLOW
231-
op_control_flow: str = ANSIColors.GREEN
227+
opname: str = ANSIColors.BLUE
228+
opname_with_label: str = ANSIColors.GREEN
229+
230+
arg: str = ANSIColors.YELLOW
232231

233232
reset: str = ANSIColors.RESET
234233

235-
def color_by_opname(self, opname: str) -> str:
236-
if opname.startswith("LOAD_"):
237-
return self.op_load
238-
239-
if opname.startswith("POP_"):
240-
return self.op_pop
241-
242-
if opname.startswith(("CALL", "RETURN")) or opname in (
243-
"YIELD_VALUE",
244-
"MAKE_FUNCTION",
245-
"SET_FUNCTION_ATTRIBUTE",
246-
"RESUME",
247-
):
248-
return self.op_call_return
249-
250-
if opname.startswith(("JUMP_", "POP_JUMP_", "FOR_ITER")) or opname in (
251-
"SEND",
252-
"GET_AWAITABLE",
253-
"GET_AITER",
254-
"GET_ANEXT",
255-
"END_ASYNC_FOR",
256-
"CLEANUP_THROW",
257-
):
258-
return self.op_control_flow
259-
260-
return self.reset
261234

262235
@dataclass(frozen=True, kw_only=True)
263236
class FancyCompleter(ThemeSection):

Lib/dis.py

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -525,7 +525,8 @@ def print_instruction_line(self, instr, mark_as_current):
525525
# Column: Label
526526
if instr.label is not None:
527527
lbl = f"L{instr.label}:"
528-
fields.append(f"{lbl:>{label_width}}")
528+
padded = f"{lbl:>{label_width}}"
529+
fields.append(f"{theme.jump_target}{padded}{theme.reset}")
529530
else:
530531
fields.append(' ' * label_width)
531532
# Column: Instruction offset from start of code sequence
@@ -537,17 +538,18 @@ def print_instruction_line(self, instr, mark_as_current):
537538
else:
538539
fields.append(' ')
539540
# Column: Opcode name
540-
fields.append(f"{theme.color_by_opname(instr.opname)}{instr.opname.ljust(_OPNAME_WIDTH)}{theme.reset}")
541+
opname_color = theme.opname_with_label if instr.label is not None else theme.opname
542+
fields.append(f"{opname_color}{instr.opname.ljust(_OPNAME_WIDTH)}{theme.reset}")
541543
# Column: Opcode argument
542544
if instr.arg is not None:
543545
# If opname is longer than _OPNAME_WIDTH, we allow it to overflow into
544546
# the space reserved for oparg. This results in fewer misaligned opargs
545547
# in the disassembly output.
546548
opname_excess = max(0, len(instr.opname) - _OPNAME_WIDTH)
547-
fields.append(repr(instr.arg).rjust(_OPARG_WIDTH - opname_excess))
549+
fields.append(f"{theme.arg}{repr(instr.arg)}{theme.reset}".rjust(_OPARG_WIDTH - opname_excess))
548550
# Column: Opcode argument details
549551
if instr.argrepr:
550-
fields.append(f'{theme.argument_detail}(' + instr.argrepr + f'){theme.reset}')
552+
fields.append('(' + instr.argrepr + ')')
551553
print(' '.join(fields).rstrip(), file=self.file)
552554

553555
def print_exception_table(self, exception_entries):
@@ -591,6 +593,7 @@ def get_label_for_offset(self, offset):
591593
return self.labels_map.get(offset, None)
592594

593595
def get_argval_argrepr(self, op, arg, offset):
596+
theme = _get_dis_theme()
594597
get_name = None if self.names is None else self.names.__getitem__
595598
argval = None
596599
argrepr = ''
@@ -629,7 +632,7 @@ def get_argval_argrepr(self, op, arg, offset):
629632
lbl = self.get_label_for_offset(argval)
630633
assert lbl is not None
631634
preposition = "from" if deop == END_ASYNC_FOR else "to"
632-
argrepr = f"{preposition} L{lbl}"
635+
argrepr = f"{preposition} {theme.jump_target}L{lbl}{theme.reset}"
633636
elif deop in (LOAD_FAST_LOAD_FAST, LOAD_FAST_BORROW_LOAD_FAST_BORROW, STORE_FAST_LOAD_FAST, STORE_FAST_STORE_FAST):
634637
arg1 = arg >> 4
635638
arg2 = arg & 15
@@ -870,7 +873,7 @@ def _disassemble_recursive(co, *, file=None, depth=None, show_caches=False, adap
870873
for x in co.co_consts:
871874
if hasattr(x, 'co_code'):
872875
print(file=file)
873-
print(f"{theme.label_bg}{theme.label_fg}Disassembly of {x!r}:{theme.reset}", file=file)
876+
print(f"Disassembly of {theme.disassembly_header}{x!r}{theme.reset}:", file=file)
874877
_disassemble_recursive(
875878
x, file=file, depth=depth, show_caches=show_caches,
876879
adaptive=adaptive, show_offsets=show_offsets,

Lib/test/test_dis.py

Lines changed: 113 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -2680,6 +2680,7 @@ def test_specialized_code(self):
26802680
for flag in ['-S', '--specialized']:
26812681
self.check_output(source, expect, flag)
26822682

2683+
26832684
@force_colorized_test_class
26842685
class DisColoredTests(unittest.TestCase):
26852686
def get_colored_output(self, func):
@@ -2690,55 +2691,132 @@ def get_colored_output(self, func):
26902691

26912692
return output.getvalue()
26922693

2693-
def assertOpColored(self, output, opname, color):
2694-
self.assertIn(
2695-
f"{color}{opname}", output,
2696-
f"{opname} should be colored with {color!r}"
2697-
)
2694+
def _check_colored(self, output, opname, color, as_not_colored):
2695+
# allow spaces, ANSI colors etc.
2696+
inter_word_pattern = r"(?:\s|\x1b\[[0-9;]*m)*"
26982697

2699-
def test_load_ops_colored(self):
2700-
def f(a):
2701-
return a
2702-
out = self.get_colored_output(f)
2703-
self.assertOpColored(out, "LOAD_FAST", theme.op_load)
2698+
tokens = opname.split()
2699+
escaped_tokens = [re.escape(token) for token in tokens]
2700+
joined_opname = inter_word_pattern.join(escaped_tokens)
27042701

2705-
def test_call_return_ops_colored(self):
2706-
def f():
2707-
return 1
2708-
out = self.get_colored_output(f)
2709-
self.assertOpColored(out, "RETURN_VALUE", theme.op_call_return)
2710-
self.assertOpColored(out, "RESUME", theme.op_call_return)
2702+
pattern = re.escape(color) + inter_word_pattern + joined_opname
2703+
2704+
if as_not_colored:
2705+
self.assertNotRegex(
2706+
output,
2707+
pattern,
2708+
f"{opname} should NOT be colored with {color!r}",
2709+
)
2710+
else:
2711+
self.assertRegex(
2712+
output, pattern, f"{opname} should be colored with {color!r}"
2713+
)
27112714

2712-
def test_pop_ops_colored(self):
2715+
def assertOpColoredAs(self, output, opname, color):
2716+
self._check_colored(output, opname, color, as_not_colored=False)
2717+
2718+
def assertOpNotColoredAs(self, output, opname, wrong_color):
2719+
self._check_colored(output, opname, wrong_color, as_not_colored=True)
2720+
2721+
def test_opname_and_arg_colored(self):
27132722
def f(a):
2714-
print(a)
2723+
return a
2724+
27152725
out = self.get_colored_output(f)
2716-
self.assertOpColored(out, "POP_TOP", theme.op_pop)
2726+
self.assertOpColoredAs(out, "LOAD_FAST_BORROW", theme.opname)
2727+
self.assertOpColoredAs(out, "RETURN_VALUE", theme.opname)
2728+
self.assertOpColoredAs(out, "0", theme.arg)
27172729

27182730
def test_control_flow_ops_colored(self):
27192731
def f(a):
27202732
for _ in a:
27212733
pass
2734+
27222735
out = self.get_colored_output(f)
2723-
self.assertOpColored(out, "FOR_ITER", theme.op_control_flow)
2724-
self.assertOpColored(out, "JUMP_BACKWARD", theme.op_control_flow)
27252736

2726-
def test_argrepr_colored(self):
2737+
self.assertOpNotColoredAs(out, "FOR_ITER", theme.opname)
2738+
self.assertOpNotColoredAs(out, "END_FOR", theme.opname)
2739+
2740+
self.assertOpColoredAs(out, "FOR_ITER", theme.opname_with_label)
2741+
self.assertOpColoredAs(out, "END_FOR", theme.opname_with_label)
2742+
2743+
opnames = (
2744+
"RESUME",
2745+
"LOAD_FAST",
2746+
"GET_ITER",
2747+
"STORE_FAST",
2748+
"JUMP_BACKWARD",
2749+
"POP_ITER",
2750+
"LOAD_COMMON_CONSTANT",
2751+
"RETURN_VALUE",
2752+
)
2753+
2754+
for opname in opnames:
2755+
self.assertOpColoredAs(out, opname, theme.opname)
2756+
2757+
def test_jump_targets_colored(self):
2758+
# sample code from:
2759+
# https://github.com/python/cpython/pull/144208#issuecomment-5375286176
2760+
def f(a, c):
2761+
_t2.d if (
2762+
_t2 := (
2763+
_t1
2764+
if (_t1 := a.b if a is not None else None) is not None
2765+
else c
2766+
)
2767+
) is not None else None
2768+
2769+
out = self.get_colored_output(f)
2770+
2771+
for n in range(1, 6):
2772+
self.assertOpColoredAs(out, f"L{n}:", theme.jump_target)
2773+
self.assertIn(f"(to {theme.jump_target}L{n}{theme.reset})", out)
2774+
2775+
cases = (
2776+
"L1: LOAD_COMMON_CONSTANT",
2777+
"L2: COPY",
2778+
"L3: LOAD_FAST",
2779+
"L4: COPY",
2780+
"L5: LOAD_COMMON_CONSTANT",
2781+
)
2782+
2783+
for part in cases:
2784+
self.assertOpColoredAs(out, part, theme.jump_target)
2785+
2786+
def test_exception_table_colored(self):
27272787
def f(a):
2728-
print(a)
2788+
try:
2789+
a
2790+
except Exception:
2791+
pass
2792+
else:
2793+
return a
2794+
27292795
out = self.get_colored_output(f)
2730-
self.assertIn(f"{theme.argument_detail}(", out)
2731-
2732-
def test_color_by_opname_coverage(self):
2733-
self.assertEqual(theme.color_by_opname("LOAD_FAST"), theme.op_load)
2734-
self.assertEqual(theme.color_by_opname("LOAD_GLOBAL"), theme.op_load)
2735-
self.assertEqual(theme.color_by_opname("POP_TOP"), theme.op_pop)
2736-
self.assertEqual(theme.color_by_opname("CALL"), theme.op_call_return)
2737-
self.assertEqual(theme.color_by_opname("RETURN_VALUE"), theme.op_call_return)
2738-
self.assertEqual(theme.color_by_opname("RESUME"), theme.op_call_return)
2739-
self.assertEqual(theme.color_by_opname("FOR_ITER"), theme.op_control_flow)
2740-
self.assertEqual(theme.color_by_opname("JUMP_BACKWARD"), theme.op_control_flow)
2741-
self.assertEqual(theme.color_by_opname("BINARY_OP"), theme.reset) # uncolored
2796+
2797+
cases = (
2798+
("L1", "L2", "L3"),
2799+
("L3", "L4", "L8"),
2800+
("L5", "L6", "L8"),
2801+
("L7", "L8", "L8"),
2802+
)
2803+
2804+
def assertExceptionTableRow(pairs, out):
2805+
p1, p2, p3 = pairs
2806+
part = f"{theme.jump_target}{p1}{theme.reset} to {theme.jump_target}{p2}{theme.reset} -> {theme.jump_target}{p3}{theme.reset}"
2807+
self.assertIn(part, out)
2808+
2809+
for pairs in cases:
2810+
assertExceptionTableRow(pairs, out)
2811+
2812+
cases = (
2813+
"L3: PUSH_EXC_INFO",
2814+
"L6: POP_EXCEPT",
2815+
"L7: RERAISE",
2816+
)
2817+
2818+
for part in cases:
2819+
self.assertOpColoredAs(out, part, theme.jump_target)
27422820

27432821
if __name__ == "__main__":
27442822
unittest.main()

0 commit comments

Comments
 (0)