From 1e0b1abc3a4dd40d074f9b90e0eec9315308510d Mon Sep 17 00:00:00 2001 From: Abduaziz Ziyodov Date: Sat, 24 Jan 2026 00:58:01 +0500 Subject: [PATCH 01/24] Add support for syntax highlighting(via themes). Highlights opcode's name, arguments, exception table labels. --- Lib/_colorize.py | 84 ++++++++++++++++++++++++++++++++++++++++++++++++ Lib/dis.py | 16 ++++++--- 2 files changed, 95 insertions(+), 5 deletions(-) diff --git a/Lib/_colorize.py b/Lib/_colorize.py index 5c4903f14aa86b7..d8e7eaa744a4510 100644 --- a/Lib/_colorize.py +++ b/Lib/_colorize.py @@ -344,6 +344,88 @@ class Unittest(ThemeSection): reset: str = ANSIColors.RESET +@dataclass(frozen=True, kw_only=True) +class Dis(ThemeSection): + label_bg: str = ANSIColors.BACKGROUND_BLUE + label_fg: str = ANSIColors.BLACK + exception_label: str = ANSIColors.CYAN + argument_detail: str = ANSIColors.GREY + + op_stack: str = ANSIColors.BOLD_YELLOW + op_load_store: str = ANSIColors.BOLD_CYAN + op_call_return: str = ANSIColors.BOLD_MAGENTA + op_binary_unary: str = ANSIColors.BOLD_BLUE + op_control_flow: str = ANSIColors.BOLD_GREEN + op_build: str = ANSIColors.BOLD_WHITE + op_exceptions: str = ANSIColors.BOLD_RED + op_other: str = ANSIColors.GREY + + reset: str = ANSIColors.RESET + + def color_by_opname(self, opname: str) -> str: + if opname in ( + "POP_TOP", + "POP_ITER", + "END_FOR", + "END_SEND", + "COPY", + "SWAP", + "PUSH_NULL", + "PUSH_EXC_INFO", + "NOP", + "CACHE", + ): + return self.op_stack + + if opname.startswith(("LOAD_", "STORE_", "DELETE_", "IMPORT_")): + return self.op_load_store + + if opname.startswith(("CALL", "RETURN")) or opname in ( + "YIELD_VALUE", + "MAKE_FUNCTION", + "SET_FUNCTION_ATTRIBUTE", + "RESUME", + ): + return self.op_call_return + + if opname.startswith(("BINARY_", "UNARY_")) or opname in ( + "COMPARE_OP", + "IS_OP", + "CONTAINS_OP", + "GET_ITER", + "GET_YIELD_FROM_ITER", + "TO_BOOL", + "DELETE_SUBSCR", + ): + return self.op_binary_unary + + if opname.startswith(("JUMP_", "POP_JUMP_", "FOR_ITER")) or opname in ( + "SEND", + "GET_AWAITABLE", + "GET_AITER", + "GET_ANEXT", + "END_ASYNC_FOR", + "CLEANUP_THROW", + ): + return self.op_control_flow + + if opname.startswith( + ("BUILD_", "LIST_", "DICT_", "UNPACK_") + ) or opname in ("SET_ADD", "MAP_ADD", "SET_UPDATE"): + return self.op_build + + if opname.startswith(("SETUP_", "CHECK_")) or opname in ( + "POP_EXCEPT", + "RERAISE", + "WITH_EXCEPT_START", + "RAISE_VARARGS", + "POP_BLOCK", + ): + return self.op_exceptions + + return self.op_other + + @dataclass(frozen=True, kw_only=True) class Theme: """A suite of themes for all sections of Python. @@ -357,6 +439,7 @@ class Theme: syntax: Syntax = field(default_factory=Syntax) traceback: Traceback = field(default_factory=Traceback) unittest: Unittest = field(default_factory=Unittest) + dis: Dis = field(default_factory=Dis) def copy_with( self, @@ -397,6 +480,7 @@ def no_colors(cls) -> Self: syntax=Syntax.no_colors(), traceback=Traceback.no_colors(), unittest=Unittest.no_colors(), + dis=Dis.no_colors(), ) diff --git a/Lib/dis.py b/Lib/dis.py index 8c257d118fb23be..fb7a32520aaaf62 100644 --- a/Lib/dis.py +++ b/Lib/dis.py @@ -436,6 +436,9 @@ def __str__(self): formatter.print_instruction(self, False) return output.getvalue() +def get_dis_theme(): + from _colorize import get_theme + return get_theme().dis class Formatter: @@ -478,8 +481,9 @@ def print_instruction(self, instr, mark_as_current=False): False, None, None, instr.positions), False) - def print_instruction_line(self, instr, mark_as_current): + def print_instruction_line(self, instr: Instruction, mark_as_current: bool) -> None: """Format instruction details for inclusion in disassembly output.""" + theme = get_dis_theme() lineno_width = self.lineno_width offset_width = self.offset_width label_width = self.label_width @@ -527,7 +531,7 @@ def print_instruction_line(self, instr, mark_as_current): else: fields.append(' ') # Column: Opcode name - fields.append(instr.opname.ljust(_OPNAME_WIDTH)) + fields.append(f"{theme.color_by_opname(instr.opname)}{instr.opname.ljust(_OPNAME_WIDTH)}{theme.reset}") # Column: Opcode argument if instr.arg is not None: # If opname is longer than _OPNAME_WIDTH, we allow it to overflow into @@ -537,11 +541,12 @@ def print_instruction_line(self, instr, mark_as_current): fields.append(repr(instr.arg).rjust(_OPARG_WIDTH - opname_excess)) # Column: Opcode argument details if instr.argrepr: - fields.append('(' + instr.argrepr + ')') + fields.append(f'{theme.argument_detail}(' + instr.argrepr + f'){theme.reset}') print(' '.join(fields).rstrip(), file=self.file) def print_exception_table(self, exception_entries): file = self.file + theme = get_dis_theme() if exception_entries: print("ExceptionTable:", file=file) for entry in exception_entries: @@ -549,7 +554,7 @@ def print_exception_table(self, exception_entries): start = entry.start_label end = entry.end_label target = entry.target_label - print(f" L{start} to L{end} -> L{target} [{entry.depth}]{lasti}", file=file) + print(f" {theme.exception_label}L{start}{theme.reset} to {theme.exception_label}L{end}{theme.reset} -> {theme.exception_label}L{target}{theme.reset} [{entry.depth}]{lasti}", file=file) class ArgResolver: @@ -833,13 +838,14 @@ def disassemble(co, lasti=-1, *, file=None, show_caches=False, adaptive=False, def _disassemble_recursive(co, *, file=None, depth=None, show_caches=False, adaptive=False, show_offsets=False, show_positions=False): disassemble(co, file=file, show_caches=show_caches, adaptive=adaptive, show_offsets=show_offsets, show_positions=show_positions) + theme = get_dis_theme() if depth is None or depth > 0: if depth is not None: depth = depth - 1 for x in co.co_consts: if hasattr(x, 'co_code'): print(file=file) - print("Disassembly of %r:" % (x,), file=file) + print(f"{theme.label_bg}{theme.label_fg}Disassembly of {x!r}:{theme.reset}", file=file) _disassemble_recursive( x, file=file, depth=depth, show_caches=show_caches, adaptive=adaptive, show_offsets=show_offsets, show_positions=show_positions From 468e561c2e21b7abb5c0cf5c8a35090f6abbe989 Mon Sep 17 00:00:00 2001 From: Abduaziz Ziyodov Date: Sat, 24 Jan 2026 01:42:23 +0500 Subject: [PATCH 02/24] Set NO_COLOR to 1 for test_compiler_assemble & test_dis test cases, because dis is not defaulting to syntax highligthing. Of course it needs better handling which I'm not sure now. --- Lib/test/test_compiler_assemble.py | 3 +++ Lib/test/test_dis.py | 3 +++ 2 files changed, 6 insertions(+) diff --git a/Lib/test/test_compiler_assemble.py b/Lib/test/test_compiler_assemble.py index 99a11e99d564852..f321d7f7e9c8383 100644 --- a/Lib/test/test_compiler_assemble.py +++ b/Lib/test/test_compiler_assemble.py @@ -1,11 +1,14 @@ import dis import io +import os import textwrap import types from test.support.bytecode_helper import AssemblerTestCase +os.environ.setdefault("NO_COLOR", "1") + # Tests for the code-object creation stage of the compiler. class IsolatedAssembleTests(AssemblerTestCase): diff --git a/Lib/test/test_dis.py b/Lib/test/test_dis.py index 8529afaa3f53706..930905213797945 100644 --- a/Lib/test/test_dis.py +++ b/Lib/test/test_dis.py @@ -7,6 +7,7 @@ import io import itertools import opcode +import os import re import sys import tempfile @@ -19,6 +20,8 @@ from test.support.bytecode_helper import BytecodeTestCase +os.environ.setdefault("NO_COLOR", "1") + CACHE = dis.opmap["CACHE"] def get_tb(): From 4067c9bbcc068c2b7d9a5477fc6118ca3dddaf0c Mon Sep 17 00:00:00 2001 From: Abduaziz Ziyodov Date: Sat, 24 Jan 2026 14:55:06 +0500 Subject: [PATCH 03/24] Replace NO_COLOR=1 trick with `@force_not_colorized*` helpers --- Lib/test/test_compiler_assemble.py | 6 ++---- Lib/test/test_dis.py | 16 ++++++++++------ 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/Lib/test/test_compiler_assemble.py b/Lib/test/test_compiler_assemble.py index f321d7f7e9c8383..135dc2df9b1864d 100644 --- a/Lib/test/test_compiler_assemble.py +++ b/Lib/test/test_compiler_assemble.py @@ -1,13 +1,10 @@ import dis import io -import os import textwrap import types from test.support.bytecode_helper import AssemblerTestCase - - -os.environ.setdefault("NO_COLOR", "1") +from test.support import force_not_colorized # Tests for the code-object creation stage of the compiler. @@ -118,6 +115,7 @@ def inner(): self.assemble_test(instructions, metadata, expected) + @force_not_colorized def test_exception_table(self): metadata = { 'filename' : 'exc.py', diff --git a/Lib/test/test_dis.py b/Lib/test/test_dis.py index 930905213797945..3f4f09742392b99 100644 --- a/Lib/test/test_dis.py +++ b/Lib/test/test_dis.py @@ -7,21 +7,18 @@ import io import itertools import opcode -import os import re import sys import tempfile import textwrap import types import unittest -from test.support import (captured_stdout, requires_debug_ranges, - requires_specialization, cpython_only, - os_helper, import_helper, reset_code) +from test.support import (captured_stdout, force_not_colorized_test_class, + requires_debug_ranges, requires_specialization, + cpython_only, os_helper, import_helper, reset_code) from test.support.bytecode_helper import BytecodeTestCase -os.environ.setdefault("NO_COLOR", "1") - CACHE = dis.opmap["CACHE"] def get_tb(): @@ -995,6 +992,7 @@ def do_disassembly_compare(self, got, expected): self.assertEqual(got, expected) +@force_not_colorized_test_class class DisTests(DisTestBase): maxDiff = None @@ -1471,6 +1469,7 @@ def f(): self.assertEqual(assem_op, assem_cache) +@force_not_colorized_test_class class DisWithFileTests(DisTests): # Run the tests again, using the file arg instead of print @@ -1993,6 +1992,7 @@ def assertInstructionsEqual(self, instrs_1, instrs_2, /): instrs_2 = [instr_2._replace(positions=None, cache_info=None) for instr_2 in instrs_2] self.assertEqual(instrs_1, instrs_2) +@force_not_colorized_test_class class InstructionTests(InstructionTestCase): def __init__(self, *args): @@ -2314,6 +2314,7 @@ def test_cache_offset_and_end_offset(self): # get_instructions has its own tests above, so can rely on it to validate # the object oriented API +@force_not_colorized_test_class class BytecodeTests(InstructionTestCase, DisTestBase): def test_instantiation(self): @@ -2445,6 +2446,7 @@ def func(): self.assertEqual(offsets, [0, 2]) +@force_not_colorized_test_class class TestDisTraceback(DisTestBase): def setUp(self) -> None: try: # We need to clean up existing tracebacks @@ -2482,6 +2484,7 @@ def test_distb_explicit_arg(self): self.do_disassembly_compare(self.get_disassembly(tb), dis_traceback) +@force_not_colorized_test_class class TestDisTracebackWithFile(TestDisTraceback): # Run the `distb` tests again, using the file arg instead of print def get_disassembly(self, tb): @@ -2516,6 +2519,7 @@ def _unroll_caches_as_Instructions(instrs, show_caches=False): False, None, None, instr.positions) +@force_not_colorized_test_class class TestDisCLI(unittest.TestCase): def setUp(self): From f3a0d2e4be09c40727523e11613dfb0be6e795c3 Mon Sep 17 00:00:00 2001 From: Abduaziz Ziyodov Date: Sat, 24 Jan 2026 14:58:52 +0500 Subject: [PATCH 04/24] Move `Dis` to top, keep alphabetical order --- Lib/_colorize.py | 165 +++++++++++++++++++++++------------------------ 1 file changed, 82 insertions(+), 83 deletions(-) diff --git a/Lib/_colorize.py b/Lib/_colorize.py index d8e7eaa744a4510..99657a9d3580ec3 100644 --- a/Lib/_colorize.py +++ b/Lib/_colorize.py @@ -200,6 +200,88 @@ class Difflib(ThemeSection): reset: str = ANSIColors.RESET +@dataclass(frozen=True, kw_only=True) +class Dis(ThemeSection): + label_bg: str = ANSIColors.BACKGROUND_BLUE + label_fg: str = ANSIColors.BLACK + exception_label: str = ANSIColors.CYAN + argument_detail: str = ANSIColors.GREY + + op_stack: str = ANSIColors.BOLD_YELLOW + op_load_store: str = ANSIColors.BOLD_CYAN + op_call_return: str = ANSIColors.BOLD_MAGENTA + op_binary_unary: str = ANSIColors.BOLD_BLUE + op_control_flow: str = ANSIColors.BOLD_GREEN + op_build: str = ANSIColors.BOLD_WHITE + op_exceptions: str = ANSIColors.BOLD_RED + op_other: str = ANSIColors.GREY + + reset: str = ANSIColors.RESET + + def color_by_opname(self, opname: str) -> str: + if opname in ( + "POP_TOP", + "POP_ITER", + "END_FOR", + "END_SEND", + "COPY", + "SWAP", + "PUSH_NULL", + "PUSH_EXC_INFO", + "NOP", + "CACHE", + ): + return self.op_stack + + if opname.startswith(("LOAD_", "STORE_", "DELETE_", "IMPORT_")): + return self.op_load_store + + if opname.startswith(("CALL", "RETURN")) or opname in ( + "YIELD_VALUE", + "MAKE_FUNCTION", + "SET_FUNCTION_ATTRIBUTE", + "RESUME", + ): + return self.op_call_return + + if opname.startswith(("BINARY_", "UNARY_")) or opname in ( + "COMPARE_OP", + "IS_OP", + "CONTAINS_OP", + "GET_ITER", + "GET_YIELD_FROM_ITER", + "TO_BOOL", + "DELETE_SUBSCR", + ): + return self.op_binary_unary + + if opname.startswith(("JUMP_", "POP_JUMP_", "FOR_ITER")) or opname in ( + "SEND", + "GET_AWAITABLE", + "GET_AITER", + "GET_ANEXT", + "END_ASYNC_FOR", + "CLEANUP_THROW", + ): + return self.op_control_flow + + if opname.startswith( + ("BUILD_", "LIST_", "DICT_", "UNPACK_") + ) or opname in ("SET_ADD", "MAP_ADD", "SET_UPDATE"): + return self.op_build + + if opname.startswith(("SETUP_", "CHECK_")) or opname in ( + "POP_EXCEPT", + "RERAISE", + "WITH_EXCEPT_START", + "RAISE_VARARGS", + "POP_BLOCK", + ): + return self.op_exceptions + + return self.op_other + + @dataclass(frozen=True, kw_only=True) class LiveProfiler(ThemeSection): """Theme section for the live profiling TUI (Tachyon profiler). @@ -343,89 +425,6 @@ class Unittest(ThemeSection): fail_info: str = ANSIColors.BOLD_RED reset: str = ANSIColors.RESET - -@dataclass(frozen=True, kw_only=True) -class Dis(ThemeSection): - label_bg: str = ANSIColors.BACKGROUND_BLUE - label_fg: str = ANSIColors.BLACK - exception_label: str = ANSIColors.CYAN - argument_detail: str = ANSIColors.GREY - - op_stack: str = ANSIColors.BOLD_YELLOW - op_load_store: str = ANSIColors.BOLD_CYAN - op_call_return: str = ANSIColors.BOLD_MAGENTA - op_binary_unary: str = ANSIColors.BOLD_BLUE - op_control_flow: str = ANSIColors.BOLD_GREEN - op_build: str = ANSIColors.BOLD_WHITE - op_exceptions: str = ANSIColors.BOLD_RED - op_other: str = ANSIColors.GREY - - reset: str = ANSIColors.RESET - - def color_by_opname(self, opname: str) -> str: - if opname in ( - "POP_TOP", - "POP_ITER", - "END_FOR", - "END_SEND", - "COPY", - "SWAP", - "PUSH_NULL", - "PUSH_EXC_INFO", - "NOP", - "CACHE", - ): - return self.op_stack - - if opname.startswith(("LOAD_", "STORE_", "DELETE_", "IMPORT_")): - return self.op_load_store - - if opname.startswith(("CALL", "RETURN")) or opname in ( - "YIELD_VALUE", - "MAKE_FUNCTION", - "SET_FUNCTION_ATTRIBUTE", - "RESUME", - ): - return self.op_call_return - - if opname.startswith(("BINARY_", "UNARY_")) or opname in ( - "COMPARE_OP", - "IS_OP", - "CONTAINS_OP", - "GET_ITER", - "GET_YIELD_FROM_ITER", - "TO_BOOL", - "DELETE_SUBSCR", - ): - return self.op_binary_unary - - if opname.startswith(("JUMP_", "POP_JUMP_", "FOR_ITER")) or opname in ( - "SEND", - "GET_AWAITABLE", - "GET_AITER", - "GET_ANEXT", - "END_ASYNC_FOR", - "CLEANUP_THROW", - ): - return self.op_control_flow - - if opname.startswith( - ("BUILD_", "LIST_", "DICT_", "UNPACK_") - ) or opname in ("SET_ADD", "MAP_ADD", "SET_UPDATE"): - return self.op_build - - if opname.startswith(("SETUP_", "CHECK_")) or opname in ( - "POP_EXCEPT", - "RERAISE", - "WITH_EXCEPT_START", - "RAISE_VARARGS", - "POP_BLOCK", - ): - return self.op_exceptions - - return self.op_other - - @dataclass(frozen=True, kw_only=True) class Theme: """A suite of themes for all sections of Python. From a87f3cf1d824d2e059cf1259201a315ec18f6310 Mon Sep 17 00:00:00 2001 From: Abduaziz Ziyodov Date: Sat, 24 Jan 2026 15:00:32 +0500 Subject: [PATCH 05/24] Revert unrelated(type annotation) change(s) --- Lib/dis.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/dis.py b/Lib/dis.py index fb7a32520aaaf62..dc803a6862db2ea 100644 --- a/Lib/dis.py +++ b/Lib/dis.py @@ -481,7 +481,7 @@ def print_instruction(self, instr, mark_as_current=False): False, None, None, instr.positions), False) - def print_instruction_line(self, instr: Instruction, mark_as_current: bool) -> None: + def print_instruction_line(self, instr, mark_as_current) -> None: """Format instruction details for inclusion in disassembly output.""" theme = get_dis_theme() lineno_width = self.lineno_width From 71c37a121815e040653dc035526acaa420b4fdd4 Mon Sep 17 00:00:00 2001 From: Abduaziz Ziyodov Date: Sun, 25 Jan 2026 12:40:56 +0500 Subject: [PATCH 06/24] re-add removed line, remove(revert) return type annotation --- Lib/_colorize.py | 1 + Lib/dis.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Lib/_colorize.py b/Lib/_colorize.py index 99657a9d3580ec3..767575b6f816c4f 100644 --- a/Lib/_colorize.py +++ b/Lib/_colorize.py @@ -425,6 +425,7 @@ class Unittest(ThemeSection): fail_info: str = ANSIColors.BOLD_RED reset: str = ANSIColors.RESET + @dataclass(frozen=True, kw_only=True) class Theme: """A suite of themes for all sections of Python. diff --git a/Lib/dis.py b/Lib/dis.py index dc803a6862db2ea..a912916387f1632 100644 --- a/Lib/dis.py +++ b/Lib/dis.py @@ -481,7 +481,7 @@ def print_instruction(self, instr, mark_as_current=False): False, None, None, instr.positions), False) - def print_instruction_line(self, instr, mark_as_current) -> None: + def print_instruction_line(self, instr, mark_as_current): """Format instruction details for inclusion in disassembly output.""" theme = get_dis_theme() lineno_width = self.lineno_width From 3e9a547e24feba48f4fa562efbdfadb200ffd95f Mon Sep 17 00:00:00 2001 From: Abduaziz Ziyodov Date: Sun, 25 Jan 2026 14:49:23 +0500 Subject: [PATCH 07/24] Wrap long lines --- Lib/dis.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Lib/dis.py b/Lib/dis.py index a912916387f1632..d49cb71d0a28b68 100644 --- a/Lib/dis.py +++ b/Lib/dis.py @@ -554,7 +554,12 @@ def print_exception_table(self, exception_entries): start = entry.start_label end = entry.end_label target = entry.target_label - print(f" {theme.exception_label}L{start}{theme.reset} to {theme.exception_label}L{end}{theme.reset} -> {theme.exception_label}L{target}{theme.reset} [{entry.depth}]{lasti}", file=file) + print( + f" {theme.exception_label}L{start}{theme.reset} to " + f"{theme.exception_label}L{end}{theme.reset} " + f"-> {theme.exception_label}L{target}{theme.reset} [{entry.depth}]{lasti}", + file=file, + ) class ArgResolver: From 9310e305d8e0572ab25b6ed1efad04b374298557 Mon Sep 17 00:00:00 2001 From: Abduaziz Ziyodov Date: Sun, 25 Jan 2026 15:35:46 +0500 Subject: [PATCH 08/24] Update What's new section, add news entry --- Doc/whatsnew/3.15.rst | 9 +++++++++ .../2026-01-25-15-26-23.gh-issue-144207.G2c_qd.rst | 3 +++ 2 files changed, 12 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2026-01-25-15-26-23.gh-issue-144207.G2c_qd.rst diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index 8c92ac8e0319dad..eade9a1394557ed 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -534,6 +534,15 @@ difflib (Contributed by Jiahao Li in :gh:`134580`.) +dis +--------- + + .. _whatsnew315-color-dis: + +* :func:`dis.dis` supports colored output by default which can also be controlled through ``NO_COLOR=1`` environment variable. + (Contributed by Abduaziz Ziyodov in :gh:`144207`) + + functools --------- diff --git a/Misc/NEWS.d/next/Library/2026-01-25-15-26-23.gh-issue-144207.G2c_qd.rst b/Misc/NEWS.d/next/Library/2026-01-25-15-26-23.gh-issue-144207.G2c_qd.rst new file mode 100644 index 000000000000000..6d3aa196c03583c --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-01-25-15-26-23.gh-issue-144207.G2c_qd.rst @@ -0,0 +1,3 @@ +:func:`dis.dis` supports colored output by default which can also be +controlled through ``NO_COLOR=1`` environment variable. Contributed by +Abduaziz Ziyodov. From 8d389bea1a0cef09bc8540803ebd5ebd155c93ee Mon Sep 17 00:00:00 2001 From: Abduaziz Ziyodov Date: Sun, 25 Jan 2026 15:39:16 +0500 Subject: [PATCH 09/24] Make get_dis_theme protected function --- Lib/dis.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Lib/dis.py b/Lib/dis.py index d49cb71d0a28b68..2c778125f190a69 100644 --- a/Lib/dis.py +++ b/Lib/dis.py @@ -436,7 +436,7 @@ def __str__(self): formatter.print_instruction(self, False) return output.getvalue() -def get_dis_theme(): +def _get_dis_theme(): from _colorize import get_theme return get_theme().dis @@ -483,7 +483,7 @@ def print_instruction(self, instr, mark_as_current=False): def print_instruction_line(self, instr, mark_as_current): """Format instruction details for inclusion in disassembly output.""" - theme = get_dis_theme() + theme = _get_dis_theme() lineno_width = self.lineno_width offset_width = self.offset_width label_width = self.label_width @@ -546,7 +546,7 @@ def print_instruction_line(self, instr, mark_as_current): def print_exception_table(self, exception_entries): file = self.file - theme = get_dis_theme() + theme = _get_dis_theme() if exception_entries: print("ExceptionTable:", file=file) for entry in exception_entries: @@ -843,7 +843,7 @@ def disassemble(co, lasti=-1, *, file=None, show_caches=False, adaptive=False, def _disassemble_recursive(co, *, file=None, depth=None, show_caches=False, adaptive=False, show_offsets=False, show_positions=False): disassemble(co, file=file, show_caches=show_caches, adaptive=adaptive, show_offsets=show_offsets, show_positions=show_positions) - theme = get_dis_theme() + theme = _get_dis_theme() if depth is None or depth > 0: if depth is not None: depth = depth - 1 From f9ede488084c9c50ad971c84a2441e8e3f07c475 Mon Sep 17 00:00:00 2001 From: Abduaziz Date: Sun, 25 Jan 2026 21:41:08 +0500 Subject: [PATCH 10/24] Update Doc/whatsnew/3.15.rst Co-authored-by: Stan Ulbrych <89152624+StanFromIreland@users.noreply.github.com> --- Doc/whatsnew/3.15.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index eade9a1394557ed..ea83d27187e7994 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -535,7 +535,7 @@ difflib dis ---------- +--- .. _whatsnew315-color-dis: From 103124db784662f9f5bf880812f8df17ba811f3a Mon Sep 17 00:00:00 2001 From: Abduaziz Ziyodov Date: Sun, 25 Jan 2026 22:39:18 +0500 Subject: [PATCH 11/24] Wrap lines, refer to proper documentation section for controlling color --- Doc/whatsnew/3.15.rst | 6 ++++-- .../Library/2026-01-25-15-26-23.gh-issue-144207.G2c_qd.rst | 4 ++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index ea83d27187e7994..c2bf689ab230b8f 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -539,8 +539,10 @@ dis .. _whatsnew315-color-dis: -* :func:`dis.dis` supports colored output by default which can also be controlled through ``NO_COLOR=1`` environment variable. - (Contributed by Abduaziz Ziyodov in :gh:`144207`) +* :func:`dis.dis` supports colored output by default, which can also be + :ref:`controlled ` through ``NO_COLOR=1`` + environment variable. + (Contributed by Abduaziz Ziyodov in :gh:`144207`.) functools diff --git a/Misc/NEWS.d/next/Library/2026-01-25-15-26-23.gh-issue-144207.G2c_qd.rst b/Misc/NEWS.d/next/Library/2026-01-25-15-26-23.gh-issue-144207.G2c_qd.rst index 6d3aa196c03583c..7640069e8734241 100644 --- a/Misc/NEWS.d/next/Library/2026-01-25-15-26-23.gh-issue-144207.G2c_qd.rst +++ b/Misc/NEWS.d/next/Library/2026-01-25-15-26-23.gh-issue-144207.G2c_qd.rst @@ -1,3 +1,3 @@ :func:`dis.dis` supports colored output by default which can also be -controlled through ``NO_COLOR=1`` environment variable. Contributed by -Abduaziz Ziyodov. +:ref:`controlled ` through ``NO_COLOR=1`` +environment variable. Contributed by Abduaziz Ziyodov. From 7ed9c9ec649971e480323a7685b9451ba1e21938 Mon Sep 17 00:00:00 2001 From: Abduaziz Ziyodov Date: Sun, 25 Jan 2026 22:40:57 +0500 Subject: [PATCH 12/24] Remove unused link --- Doc/whatsnew/3.15.rst | 2 -- 1 file changed, 2 deletions(-) diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index f3831a83c6a177b..b6d4bd556fb97e0 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -537,8 +537,6 @@ difflib dis --- - .. _whatsnew315-color-dis: - * :func:`dis.dis` supports colored output by default, which can also be :ref:`controlled ` through ``NO_COLOR=1`` environment variable. From 19436ee5dca452cc270a4171d90b49b25c99c150 Mon Sep 17 00:00:00 2001 From: Abduaziz Ziyodov Date: Wed, 25 Feb 2026 08:06:57 +0500 Subject: [PATCH 13/24] feat: minimize use of colors, emphasis on load/pop, update theme according to feedbacks --- Lib/_colorize.py | 61 ++++++++++-------------------------------------- 1 file changed, 12 insertions(+), 49 deletions(-) diff --git a/Lib/_colorize.py b/Lib/_colorize.py index 767575b6f816c4f..ba3b7d6a26d68cc 100644 --- a/Lib/_colorize.py +++ b/Lib/_colorize.py @@ -202,39 +202,26 @@ class Difflib(ThemeSection): @dataclass(frozen=True, kw_only=True) class Dis(ThemeSection): - label_bg: str = ANSIColors.BACKGROUND_BLUE + label_bg: str = ANSIColors.BACKGROUND_CYAN label_fg: str = ANSIColors.BLACK + + L:str = ANSIColors.BOLD_RED exception_label: str = ANSIColors.CYAN - argument_detail: str = ANSIColors.GREY + argument_detail: str = ANSIColors.CYAN - op_stack: str = ANSIColors.BOLD_YELLOW - op_load_store: str = ANSIColors.BOLD_CYAN - op_call_return: str = ANSIColors.BOLD_MAGENTA - op_binary_unary: str = ANSIColors.BOLD_BLUE + op_load: str = ANSIColors.BOLD_BLUE + op_pop: str = ANSIColors.BOLD_MAGENTA + op_call_return: str = ANSIColors.BOLD_YELLOW op_control_flow: str = ANSIColors.BOLD_GREEN - op_build: str = ANSIColors.BOLD_WHITE - op_exceptions: str = ANSIColors.BOLD_RED - op_other: str = ANSIColors.GREY reset: str = ANSIColors.RESET def color_by_opname(self, opname: str) -> str: - if opname in ( - "POP_TOP", - "POP_ITER", - "END_FOR", - "END_SEND", - "COPY", - "SWAP", - "PUSH_NULL", - "PUSH_EXC_INFO", - "NOP", - "CACHE", - ): - return self.op_stack + if opname.startswith("LOAD_"): + return self.op_load - if opname.startswith(("LOAD_", "STORE_", "DELETE_", "IMPORT_")): - return self.op_load_store + if opname.startswith("POP_"): + return self.op_pop if opname.startswith(("CALL", "RETURN")) or opname in ( "YIELD_VALUE", @@ -244,17 +231,6 @@ def color_by_opname(self, opname: str) -> str: ): return self.op_call_return - if opname.startswith(("BINARY_", "UNARY_")) or opname in ( - "COMPARE_OP", - "IS_OP", - "CONTAINS_OP", - "GET_ITER", - "GET_YIELD_FROM_ITER", - "TO_BOOL", - "DELETE_SUBSCR", - ): - return self.op_binary_unary - if opname.startswith(("JUMP_", "POP_JUMP_", "FOR_ITER")) or opname in ( "SEND", "GET_AWAITABLE", @@ -265,21 +241,8 @@ def color_by_opname(self, opname: str) -> str: ): return self.op_control_flow - if opname.startswith( - ("BUILD_", "LIST_", "DICT_", "UNPACK_") - ) or opname in ("SET_ADD", "MAP_ADD", "SET_UPDATE"): - return self.op_build - - if opname.startswith(("SETUP_", "CHECK_")) or opname in ( - "POP_EXCEPT", - "RERAISE", - "WITH_EXCEPT_START", - "RAISE_VARARGS", - "POP_BLOCK", - ): - return self.op_exceptions - return self.op_other + return self.reset @dataclass(frozen=True, kw_only=True) From 60f090481e2b919fbb6a3c19bd2218757ff8ec17 Mon Sep 17 00:00:00 2001 From: Abduaziz Ziyodov Date: Sat, 28 Feb 2026 20:46:34 +0500 Subject: [PATCH 14/24] test: dis colorization, `DisColored` test case --- Lib/_colorize.py | 1 - Lib/test/test_dis.py | 70 ++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 68 insertions(+), 3 deletions(-) diff --git a/Lib/_colorize.py b/Lib/_colorize.py index ba3b7d6a26d68cc..8c768b5a3005e29 100644 --- a/Lib/_colorize.py +++ b/Lib/_colorize.py @@ -205,7 +205,6 @@ class Dis(ThemeSection): label_bg: str = ANSIColors.BACKGROUND_CYAN label_fg: str = ANSIColors.BLACK - L:str = ANSIColors.BOLD_RED exception_label: str = ANSIColors.CYAN argument_detail: str = ANSIColors.CYAN diff --git a/Lib/test/test_dis.py b/Lib/test/test_dis.py index fa0d1f6958aeb58..76e503262d70bbd 100644 --- a/Lib/test/test_dis.py +++ b/Lib/test/test_dis.py @@ -14,8 +14,9 @@ import types import unittest from test.support import (captured_stdout, force_not_colorized_test_class, - requires_debug_ranges, requires_specialization, - cpython_only, os_helper, import_helper, reset_code) + force_colorized_test_class, requires_debug_ranges, + requires_specialization, cpython_only, os_helper, + import_helper, reset_code) from test.support.bytecode_helper import BytecodeTestCase @@ -36,6 +37,12 @@ def _error(): TRACEBACK_CODE = get_tb().tb_frame.f_code +def _get_dis_theme(): + from _colorize import get_theme + return get_theme().dis + +theme = _get_dis_theme() + class _C: def __init__(self, x): self.x = x == 1 @@ -2633,6 +2640,65 @@ def test_specialized_code(self): for flag in ['-S', '--specialized']: self.check_output(source, expect, flag) +@force_colorized_test_class +class DisColored(unittest.TestCase): + def get_colored_output(self, func): + output = io.StringIO() + + with contextlib.redirect_stdout(output): + dis.dis(func) + + return output.getvalue() + + def assertOpColored(self, output, opname, color): + self.assertIn( + f"{color}{opname}", output, + f"{opname} should be colored with {color!r}" + ) + + def test_load_ops_colored(self): + def f(a): + return a + out = self.get_colored_output(f) + self.assertOpColored(out, "LOAD_FAST", theme.op_load) + + def test_call_return_ops_colored(self): + def f(): + return 1 + out = self.get_colored_output(f) + self.assertOpColored(out, "RETURN_VALUE", theme.op_call_return) + self.assertOpColored(out, "RESUME", theme.op_call_return) + + def test_pop_ops_colored(self): + def f(a): + print(a) + out = self.get_colored_output(f) + self.assertOpColored(out, "POP_TOP", theme.op_pop) + + def test_control_flow_ops_colored(self): + def f(a): + for _ in a: + pass + out = self.get_colored_output(f) + self.assertOpColored(out, "FOR_ITER", theme.op_control_flow) + self.assertOpColored(out, "JUMP_BACKWARD", theme.op_control_flow) + + def test_argrepr_colored(self): + def f(a): + print(a) + out = self.get_colored_output(f) + self.assertIn(f"{theme.argument_detail}(", out) + + def test_color_by_opname_coverage(self): + self.assertEqual(theme.color_by_opname("LOAD_FAST"), theme.op_load) + self.assertEqual(theme.color_by_opname("LOAD_GLOBAL"), theme.op_load) + self.assertEqual(theme.color_by_opname("POP_TOP"), theme.op_pop) + self.assertEqual(theme.color_by_opname("CALL"), theme.op_call_return) + self.assertEqual(theme.color_by_opname("RETURN_VALUE"), theme.op_call_return) + self.assertEqual(theme.color_by_opname("RESUME"), theme.op_call_return) + self.assertEqual(theme.color_by_opname("FOR_ITER"), theme.op_control_flow) + self.assertEqual(theme.color_by_opname("JUMP_BACKWARD"), theme.op_control_flow) + self.assertEqual(theme.color_by_opname("BINARY_OP"), theme.reset) # uncolored if __name__ == "__main__": unittest.main() From 488c1a7e665e34504a609daddd4179626e625cae Mon Sep 17 00:00:00 2001 From: Abduaziz Ziyodov Date: Sat, 28 Feb 2026 22:16:28 +0500 Subject: [PATCH 15/24] feat: support for customization, add `dis` to `copy_with` arguments --- Lib/_colorize.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Lib/_colorize.py b/Lib/_colorize.py index 8c768b5a3005e29..273f4b04a754c01 100644 --- a/Lib/_colorize.py +++ b/Lib/_colorize.py @@ -412,6 +412,7 @@ def copy_with( syntax: Syntax | None = None, traceback: Traceback | None = None, unittest: Unittest | None = None, + dis: Dis | None = None ) -> Self: """Return a new Theme based on this instance with some sections replaced. @@ -425,6 +426,7 @@ def copy_with( syntax=syntax or self.syntax, traceback=traceback or self.traceback, unittest=unittest or self.unittest, + dis=dis or self.dis ) @classmethod From 32e696c202c68c65ee40a1a7f91290bec0b8023d Mon Sep 17 00:00:00 2001 From: Abduaziz Ziyodov Date: Sat, 28 Feb 2026 22:19:45 +0500 Subject: [PATCH 16/24] refactor: rename test case (related to highlighting) --- Lib/test/test_dis.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_dis.py b/Lib/test/test_dis.py index 76e503262d70bbd..55c3a071b5518cc 100644 --- a/Lib/test/test_dis.py +++ b/Lib/test/test_dis.py @@ -2641,7 +2641,7 @@ def test_specialized_code(self): self.check_output(source, expect, flag) @force_colorized_test_class -class DisColored(unittest.TestCase): +class DisColoredTests(unittest.TestCase): def get_colored_output(self, func): output = io.StringIO() From 8edd8345de5435a3dfdbb5d8a4e1d593786d08d0 Mon Sep 17 00:00:00 2001 From: Abduaziz Ziyodov Date: Sun, 1 Mar 2026 13:02:01 +0500 Subject: [PATCH 17/24] refactor: use existing import for `_get_dis_theme` --- Lib/test/test_dis.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/Lib/test/test_dis.py b/Lib/test/test_dis.py index 55c3a071b5518cc..2e5dd6a453dc0b1 100644 --- a/Lib/test/test_dis.py +++ b/Lib/test/test_dis.py @@ -37,11 +37,7 @@ def _error(): TRACEBACK_CODE = get_tb().tb_frame.f_code -def _get_dis_theme(): - from _colorize import get_theme - return get_theme().dis - -theme = _get_dis_theme() +theme = dis._get_dis_theme() class _C: def __init__(self, x): From ab3a36880270f4e25b552ca15a9a9bfbdfb50fdd Mon Sep 17 00:00:00 2001 From: Abduaziz Date: Tue, 18 Aug 2026 04:56:07 +0500 Subject: [PATCH 18/24] fix: missing dataclass decorator (after merge conflict resolution) --- Lib/_colorize.py | 1 + 1 file changed, 1 insertion(+) diff --git a/Lib/_colorize.py b/Lib/_colorize.py index 3013142be957b8c..dfbda177315dd7d 100644 --- a/Lib/_colorize.py +++ b/Lib/_colorize.py @@ -260,6 +260,7 @@ def color_by_opname(self, opname: str) -> str: return self.reset +@dataclass(frozen=True, kw_only=True) class FancyCompleter(ThemeSection): # functions and methods function: builtins.str = ANSIColors.BOLD_BLUE From 0db081c0e98152c429fb211a68c88f3030b0013e Mon Sep 17 00:00:00 2001 From: Abduaziz Ziyodov Date: Tue, 18 Aug 2026 05:10:02 +0500 Subject: [PATCH 19/24] chore: move whatsnew from 3.15 into 3.16 file --- Doc/whatsnew/3.15.rst | 9 --------- Doc/whatsnew/3.16.rst | 9 +++++++++ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index 3b58c2073939cb7..5a8ab88a30fcf58 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -1144,15 +1144,6 @@ difflib (Contributed by Jiahao Li in :gh:`134580`.) -dis ---- - -* :func:`dis.dis` supports colored output by default, which can also be - :ref:`controlled ` through ``NO_COLOR=1`` - environment variable. - (Contributed by Abduaziz Ziyodov in :gh:`144207`.) - - email ----- diff --git a/Doc/whatsnew/3.16.rst b/Doc/whatsnew/3.16.rst index 063755e1eadcb53..25b8781d77f551a 100644 --- a/Doc/whatsnew/3.16.rst +++ b/Doc/whatsnew/3.16.rst @@ -281,6 +281,15 @@ ctypes (Contributed by Peter Bierma in :gh:`153903`.) +dis +--- + +* :func:`dis.dis` supports colored output by default, which can also be + :ref:`controlled ` through ``NO_COLOR=1`` + environment variable. + (Contributed by Abduaziz Ziyodov in :gh:`144207`.) + + concurrent.futures ------------------ From 0c42960cb49bac3082193302c310050fdbce02ca Mon Sep 17 00:00:00 2001 From: Abduaziz Ziyodov Date: Tue, 18 Aug 2026 05:31:16 +0500 Subject: [PATCH 20/24] fix: tests, change bold colors into normal --- Lib/_colorize.py | 8 ++++---- Lib/dis.py | 1 + 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/Lib/_colorize.py b/Lib/_colorize.py index dfbda177315dd7d..ec5fe31b33079ec 100644 --- a/Lib/_colorize.py +++ b/Lib/_colorize.py @@ -225,10 +225,10 @@ class Dis(ThemeSection): exception_label: str = ANSIColors.CYAN argument_detail: str = ANSIColors.CYAN - op_load: str = ANSIColors.BOLD_BLUE - op_pop: str = ANSIColors.BOLD_MAGENTA - op_call_return: str = ANSIColors.BOLD_YELLOW - op_control_flow: str = ANSIColors.BOLD_GREEN + op_load: str = ANSIColors.BLUE + op_pop: str = ANSIColors.MAGENTA + op_call_return: str = ANSIColors.YELLOW + op_control_flow: str = ANSIColors.GREEN reset: str = ANSIColors.RESET diff --git a/Lib/dis.py b/Lib/dis.py index 3b6678b790b8aab..164e99db3f95c2a 100644 --- a/Lib/dis.py +++ b/Lib/dis.py @@ -866,6 +866,7 @@ def _disassemble_recursive(co, *, file=None, depth=None, show_caches=False, adap if depth is None or depth > 0: if depth is not None: depth = depth - 1 + theme = _get_dis_theme() for x in co.co_consts: if hasattr(x, 'co_code'): print(file=file) From da2329bb72395a48516acc68ac3100dafa795f97 Mon Sep 17 00:00:00 2001 From: Abduaziz Ziyodov Date: Tue, 18 Aug 2026 06:15:58 +0500 Subject: [PATCH 21/24] feat: add optional(secondary) goldbold-like styling --- Lib/_colorize.py | 4 ++++ Lib/dis.py | 57 ++++++++++++++++++++++++++++++++++-------------- 2 files changed, 45 insertions(+), 16 deletions(-) diff --git a/Lib/_colorize.py b/Lib/_colorize.py index ec5fe31b33079ec..eef4dfadb59bbe9 100644 --- a/Lib/_colorize.py +++ b/Lib/_colorize.py @@ -48,6 +48,7 @@ class ANSIColors: BACKGROUND_BLUE = "\x1b[44m" BACKGROUND_CYAN = "\x1b[46m" BACKGROUND_GREEN = "\x1b[42m" + BACKGROUND_GREY = "\x1b[48;5;236m" BACKGROUND_MAGENTA = "\x1b[45m" BACKGROUND_RED = "\x1b[41m" BACKGROUND_WHITE = "\x1b[47m" @@ -219,6 +220,9 @@ class Difflib(ThemeSection): @dataclass(frozen=True, kw_only=True) class Dis(ThemeSection): + alt_block_first_bg:str = ANSIColors.BACKGROUND_GREY + alt_block_second_bg:str = ANSIColors.RESET # mb black bg ? but what about light mode ? + label_bg: str = ANSIColors.BACKGROUND_CYAN label_fg: str = ANSIColors.BLACK diff --git a/Lib/dis.py b/Lib/dis.py index 164e99db3f95c2a..6a6040f75c9e54f 100644 --- a/Lib/dis.py +++ b/Lib/dis.py @@ -84,7 +84,7 @@ def _try_compile(source, name): return compile(source, name, 'exec') def dis(x=None, *, file=None, depth=None, show_caches=False, adaptive=False, - show_offsets=False, show_positions=False, show_jit=False): + show_offsets=False, show_positions=False, show_jit=False,show_block_bg=False): """Disassemble classes, methods, functions, and other compiled objects. With no argument, disassemble the last traceback. @@ -96,7 +96,7 @@ def dis(x=None, *, file=None, depth=None, show_caches=False, adaptive=False, if x is None: distb(file=file, show_caches=show_caches, adaptive=adaptive, show_offsets=show_offsets, show_positions=show_positions, - show_jit=show_jit) + show_jit=show_jit, show_block_bg=show_block_bg) return # Extract functions from methods. if hasattr(x, '__func__'): @@ -118,30 +118,35 @@ def dis(x=None, *, file=None, depth=None, show_caches=False, adaptive=False, print("Disassembly of %s:" % name, file=file) try: dis(x1, file=file, depth=depth, show_caches=show_caches, adaptive=adaptive, - show_offsets=show_offsets, show_positions=show_positions, show_jit=show_jit) + show_offsets=show_offsets, show_positions=show_positions, show_jit=show_jit, + show_block_bg=show_block_bg) except TypeError as msg: print("Sorry:", msg, file=file) print(file=file) elif hasattr(x, 'co_code'): # Code object _disassemble_recursive(x, file=file, depth=depth, show_caches=show_caches, adaptive=adaptive, - show_offsets=show_offsets, show_positions=show_positions, show_jit=show_jit) + show_offsets=show_offsets, show_positions=show_positions, show_jit=show_jit, + show_block_bg=show_block_bg) elif isinstance(x, (bytes, bytearray)): # Raw bytecode labels_map = _make_labels_map(x) label_width = 4 + len(str(len(labels_map))) formatter = Formatter(file=file, offset_width=len(str(max(len(x) - 2, 9999))) if show_offsets else 0, label_width=label_width, - show_caches=show_caches) + show_caches=show_caches, + show_block_bg=show_block_bg) arg_resolver = ArgResolver(labels_map=labels_map) _disassemble_bytes(x, arg_resolver=arg_resolver, formatter=formatter) elif isinstance(x, str): # Source code _disassemble_str(x, file=file, depth=depth, show_caches=show_caches, adaptive=adaptive, - show_offsets=show_offsets, show_positions=show_positions, show_jit=show_jit) + show_offsets=show_offsets, show_positions=show_positions, show_jit=show_jit, + show_block_bg=show_block_bg) else: raise TypeError("don't know how to disassemble %s objects" % type(x).__name__) -def distb(tb=None, *, file=None, show_caches=False, adaptive=False, show_offsets=False, show_positions=False, show_jit=False): +def distb(tb=None, *, file=None, show_caches=False, adaptive=False, show_offsets=False, show_positions=False, show_jit=False, + show_block_bg=False): """Disassemble a traceback (default: last traceback).""" if tb is None: try: @@ -153,7 +158,8 @@ def distb(tb=None, *, file=None, show_caches=False, adaptive=False, show_offsets raise RuntimeError("no last traceback to disassemble") from None while tb.tb_next: tb = tb.tb_next disassemble(tb.tb_frame.f_code, tb.tb_lasti, file=file, show_caches=show_caches, adaptive=adaptive, - show_offsets=show_offsets, show_positions=show_positions, show_jit=show_jit) + show_offsets=show_offsets, show_positions=show_positions, show_jit=show_jit, + show_block_bg=show_block_bg) # The inspect module interrogates this dictionary to build its # list of CO_* constants. It is also used by pretty_flags to @@ -449,7 +455,8 @@ def _get_dis_theme(): class Formatter: def __init__(self, file=None, lineno_width=0, offset_width=0, label_width=0, - line_offset=0, show_caches=False, *, show_positions=False): + line_offset=0, show_caches=False, *, show_positions=False, + show_block_bg=False): """Create a Formatter *file* where to write the output @@ -468,6 +475,8 @@ def __init__(self, file=None, lineno_width=0, offset_width=0, label_width=0, self.label_width = label_width self.show_caches = show_caches self.show_positions = show_positions + self.show_block_bg = show_block_bg + self._alt_block = False # toggle between first/second alt block color def print_instruction(self, instr, mark_as_current=False): self.print_instruction_line(instr, mark_as_current) @@ -499,6 +508,8 @@ def print_instruction_line(self, instr, mark_as_current): instr.offset > 0) if new_source_line: print(file=self.file) + if self.show_block_bg: + self._alt_block = not self._alt_block fields = [] # Column: Source code locations information @@ -548,7 +559,14 @@ def print_instruction_line(self, instr, mark_as_current): # Column: Opcode argument details if instr.argrepr: fields.append(f'{theme.argument_detail}(' + instr.argrepr + f'){theme.reset}') - print(' '.join(fields).rstrip(), file=self.file) + + line = ' '.join(fields).rstrip() + + if self.show_block_bg: + bg = theme.alt_block_first_bg if self._alt_block else theme.alt_block_second_bg + line = bg + line.replace(theme.reset, theme.reset + bg) + "\x1b[K" + theme.reset + + print(line, file=self.file) def print_exception_table(self, exception_entries): file = self.file @@ -837,7 +855,8 @@ def _get_instructions_bytes(code, linestarts=None, line_offset=0, co_positions=N def disassemble(co, lasti=-1, *, file=None, show_caches=False, adaptive=False, - show_offsets=False, show_positions=False, show_jit=False): + show_offsets=False, show_positions=False, show_jit=False, + show_block_bg=False): """Disassemble a code object.""" linestarts = dict(findlinestarts(co)) exception_entries = _parse_exception_table(co) @@ -852,7 +871,8 @@ def disassemble(co, lasti=-1, *, file=None, show_caches=False, adaptive=False, offset_width=len(str(max(len(co.co_code) - 2, 9999))) if show_offsets else 0, label_width=label_width, show_caches=show_caches, - show_positions=show_positions) + show_positions=show_positions, + show_block_bg=show_block_bg) arg_resolver = ArgResolver(co_consts=co.co_consts, names=co.co_names, varname_from_oparg=co._varname_from_oparg, @@ -861,8 +881,9 @@ def disassemble(co, lasti=-1, *, file=None, show_caches=False, adaptive=False, exception_entries=exception_entries, co_positions=co.co_positions(), original_code=co.co_code, arg_resolver=arg_resolver, formatter=formatter) -def _disassemble_recursive(co, *, file=None, depth=None, show_caches=False, adaptive=False, show_offsets=False, show_positions=False, show_jit=False): - disassemble(co, file=file, show_caches=show_caches, adaptive=adaptive, show_offsets=show_offsets, show_positions=show_positions, show_jit=show_jit) +def _disassemble_recursive(co, *, file=None, depth=None, show_caches=False, adaptive=False, show_offsets=False, show_positions=False, show_jit=False, show_block_bg=False): + disassemble(co, file=file, show_caches=show_caches, adaptive=adaptive, show_offsets=show_offsets, show_positions=show_positions, show_jit=show_jit, + show_block_bg=show_block_bg) if depth is None or depth > 0: if depth is not None: depth = depth - 1 @@ -874,7 +895,8 @@ def _disassemble_recursive(co, *, file=None, depth=None, show_caches=False, adap _disassemble_recursive( x, file=file, depth=depth, show_caches=show_caches, adaptive=adaptive, show_offsets=show_offsets, - show_positions=show_positions, show_jit=show_jit + show_positions=show_positions, show_jit=show_jit, + show_block_bg=show_block_bg ) @@ -1175,6 +1197,8 @@ def main(args=None): help='show instruction positions') parser.add_argument('-S', '--specialized', action='store_true', help='show specialized bytecode') + parser.add_argument('-B', '--block-bg', action='store_true', + help='alternate background per source-line block') parser.add_argument('infile', nargs='?', default='-') args = parser.parse_args(args=args) if args.infile == '-': @@ -1186,7 +1210,8 @@ def main(args=None): source = infile.read() code = compile(source, name, "exec") dis(code, show_caches=args.show_caches, adaptive=args.specialized, - show_offsets=args.show_offsets, show_positions=args.show_positions) + show_offsets=args.show_offsets, show_positions=args.show_positions, + show_block_bg=args.block_bg) if __name__ == "__main__": main() From de06a89a85441f1a723308d35d427cc849bf3af7 Mon Sep 17 00:00:00 2001 From: Abduaziz Ziyodov Date: Wed, 19 Aug 2026 00:23:48 +0500 Subject: [PATCH 22/24] revert: block bg --- Lib/_colorize.py | 4 ---- Lib/dis.py | 57 ++++++++++++++---------------------------------- 2 files changed, 16 insertions(+), 45 deletions(-) diff --git a/Lib/_colorize.py b/Lib/_colorize.py index eef4dfadb59bbe9..ec5fe31b33079ec 100644 --- a/Lib/_colorize.py +++ b/Lib/_colorize.py @@ -48,7 +48,6 @@ class ANSIColors: BACKGROUND_BLUE = "\x1b[44m" BACKGROUND_CYAN = "\x1b[46m" BACKGROUND_GREEN = "\x1b[42m" - BACKGROUND_GREY = "\x1b[48;5;236m" BACKGROUND_MAGENTA = "\x1b[45m" BACKGROUND_RED = "\x1b[41m" BACKGROUND_WHITE = "\x1b[47m" @@ -220,9 +219,6 @@ class Difflib(ThemeSection): @dataclass(frozen=True, kw_only=True) class Dis(ThemeSection): - alt_block_first_bg:str = ANSIColors.BACKGROUND_GREY - alt_block_second_bg:str = ANSIColors.RESET # mb black bg ? but what about light mode ? - label_bg: str = ANSIColors.BACKGROUND_CYAN label_fg: str = ANSIColors.BLACK diff --git a/Lib/dis.py b/Lib/dis.py index 6a6040f75c9e54f..164e99db3f95c2a 100644 --- a/Lib/dis.py +++ b/Lib/dis.py @@ -84,7 +84,7 @@ def _try_compile(source, name): return compile(source, name, 'exec') def dis(x=None, *, file=None, depth=None, show_caches=False, adaptive=False, - show_offsets=False, show_positions=False, show_jit=False,show_block_bg=False): + show_offsets=False, show_positions=False, show_jit=False): """Disassemble classes, methods, functions, and other compiled objects. With no argument, disassemble the last traceback. @@ -96,7 +96,7 @@ def dis(x=None, *, file=None, depth=None, show_caches=False, adaptive=False, if x is None: distb(file=file, show_caches=show_caches, adaptive=adaptive, show_offsets=show_offsets, show_positions=show_positions, - show_jit=show_jit, show_block_bg=show_block_bg) + show_jit=show_jit) return # Extract functions from methods. if hasattr(x, '__func__'): @@ -118,35 +118,30 @@ def dis(x=None, *, file=None, depth=None, show_caches=False, adaptive=False, print("Disassembly of %s:" % name, file=file) try: dis(x1, file=file, depth=depth, show_caches=show_caches, adaptive=adaptive, - show_offsets=show_offsets, show_positions=show_positions, show_jit=show_jit, - show_block_bg=show_block_bg) + show_offsets=show_offsets, show_positions=show_positions, show_jit=show_jit) except TypeError as msg: print("Sorry:", msg, file=file) print(file=file) elif hasattr(x, 'co_code'): # Code object _disassemble_recursive(x, file=file, depth=depth, show_caches=show_caches, adaptive=adaptive, - show_offsets=show_offsets, show_positions=show_positions, show_jit=show_jit, - show_block_bg=show_block_bg) + show_offsets=show_offsets, show_positions=show_positions, show_jit=show_jit) elif isinstance(x, (bytes, bytearray)): # Raw bytecode labels_map = _make_labels_map(x) label_width = 4 + len(str(len(labels_map))) formatter = Formatter(file=file, offset_width=len(str(max(len(x) - 2, 9999))) if show_offsets else 0, label_width=label_width, - show_caches=show_caches, - show_block_bg=show_block_bg) + show_caches=show_caches) arg_resolver = ArgResolver(labels_map=labels_map) _disassemble_bytes(x, arg_resolver=arg_resolver, formatter=formatter) elif isinstance(x, str): # Source code _disassemble_str(x, file=file, depth=depth, show_caches=show_caches, adaptive=adaptive, - show_offsets=show_offsets, show_positions=show_positions, show_jit=show_jit, - show_block_bg=show_block_bg) + show_offsets=show_offsets, show_positions=show_positions, show_jit=show_jit) else: raise TypeError("don't know how to disassemble %s objects" % type(x).__name__) -def distb(tb=None, *, file=None, show_caches=False, adaptive=False, show_offsets=False, show_positions=False, show_jit=False, - show_block_bg=False): +def distb(tb=None, *, file=None, show_caches=False, adaptive=False, show_offsets=False, show_positions=False, show_jit=False): """Disassemble a traceback (default: last traceback).""" if tb is None: try: @@ -158,8 +153,7 @@ def distb(tb=None, *, file=None, show_caches=False, adaptive=False, show_offsets raise RuntimeError("no last traceback to disassemble") from None while tb.tb_next: tb = tb.tb_next disassemble(tb.tb_frame.f_code, tb.tb_lasti, file=file, show_caches=show_caches, adaptive=adaptive, - show_offsets=show_offsets, show_positions=show_positions, show_jit=show_jit, - show_block_bg=show_block_bg) + show_offsets=show_offsets, show_positions=show_positions, show_jit=show_jit) # The inspect module interrogates this dictionary to build its # list of CO_* constants. It is also used by pretty_flags to @@ -455,8 +449,7 @@ def _get_dis_theme(): class Formatter: def __init__(self, file=None, lineno_width=0, offset_width=0, label_width=0, - line_offset=0, show_caches=False, *, show_positions=False, - show_block_bg=False): + line_offset=0, show_caches=False, *, show_positions=False): """Create a Formatter *file* where to write the output @@ -475,8 +468,6 @@ def __init__(self, file=None, lineno_width=0, offset_width=0, label_width=0, self.label_width = label_width self.show_caches = show_caches self.show_positions = show_positions - self.show_block_bg = show_block_bg - self._alt_block = False # toggle between first/second alt block color def print_instruction(self, instr, mark_as_current=False): self.print_instruction_line(instr, mark_as_current) @@ -508,8 +499,6 @@ def print_instruction_line(self, instr, mark_as_current): instr.offset > 0) if new_source_line: print(file=self.file) - if self.show_block_bg: - self._alt_block = not self._alt_block fields = [] # Column: Source code locations information @@ -559,14 +548,7 @@ def print_instruction_line(self, instr, mark_as_current): # Column: Opcode argument details if instr.argrepr: fields.append(f'{theme.argument_detail}(' + instr.argrepr + f'){theme.reset}') - - line = ' '.join(fields).rstrip() - - if self.show_block_bg: - bg = theme.alt_block_first_bg if self._alt_block else theme.alt_block_second_bg - line = bg + line.replace(theme.reset, theme.reset + bg) + "\x1b[K" + theme.reset - - print(line, file=self.file) + print(' '.join(fields).rstrip(), file=self.file) def print_exception_table(self, exception_entries): file = self.file @@ -855,8 +837,7 @@ def _get_instructions_bytes(code, linestarts=None, line_offset=0, co_positions=N def disassemble(co, lasti=-1, *, file=None, show_caches=False, adaptive=False, - show_offsets=False, show_positions=False, show_jit=False, - show_block_bg=False): + show_offsets=False, show_positions=False, show_jit=False): """Disassemble a code object.""" linestarts = dict(findlinestarts(co)) exception_entries = _parse_exception_table(co) @@ -871,8 +852,7 @@ def disassemble(co, lasti=-1, *, file=None, show_caches=False, adaptive=False, offset_width=len(str(max(len(co.co_code) - 2, 9999))) if show_offsets else 0, label_width=label_width, show_caches=show_caches, - show_positions=show_positions, - show_block_bg=show_block_bg) + show_positions=show_positions) arg_resolver = ArgResolver(co_consts=co.co_consts, names=co.co_names, varname_from_oparg=co._varname_from_oparg, @@ -881,9 +861,8 @@ def disassemble(co, lasti=-1, *, file=None, show_caches=False, adaptive=False, exception_entries=exception_entries, co_positions=co.co_positions(), original_code=co.co_code, arg_resolver=arg_resolver, formatter=formatter) -def _disassemble_recursive(co, *, file=None, depth=None, show_caches=False, adaptive=False, show_offsets=False, show_positions=False, show_jit=False, show_block_bg=False): - disassemble(co, file=file, show_caches=show_caches, adaptive=adaptive, show_offsets=show_offsets, show_positions=show_positions, show_jit=show_jit, - show_block_bg=show_block_bg) +def _disassemble_recursive(co, *, file=None, depth=None, show_caches=False, adaptive=False, show_offsets=False, show_positions=False, show_jit=False): + disassemble(co, file=file, show_caches=show_caches, adaptive=adaptive, show_offsets=show_offsets, show_positions=show_positions, show_jit=show_jit) if depth is None or depth > 0: if depth is not None: depth = depth - 1 @@ -895,8 +874,7 @@ def _disassemble_recursive(co, *, file=None, depth=None, show_caches=False, adap _disassemble_recursive( x, file=file, depth=depth, show_caches=show_caches, adaptive=adaptive, show_offsets=show_offsets, - show_positions=show_positions, show_jit=show_jit, - show_block_bg=show_block_bg + show_positions=show_positions, show_jit=show_jit ) @@ -1197,8 +1175,6 @@ def main(args=None): help='show instruction positions') parser.add_argument('-S', '--specialized', action='store_true', help='show specialized bytecode') - parser.add_argument('-B', '--block-bg', action='store_true', - help='alternate background per source-line block') parser.add_argument('infile', nargs='?', default='-') args = parser.parse_args(args=args) if args.infile == '-': @@ -1210,8 +1186,7 @@ def main(args=None): source = infile.read() code = compile(source, name, "exec") dis(code, show_caches=args.show_caches, adaptive=args.specialized, - show_offsets=args.show_offsets, show_positions=args.show_positions, - show_block_bg=args.block_bg) + show_offsets=args.show_offsets, show_positions=args.show_positions) if __name__ == "__main__": main() From c3378fbead9fa75579ed2244316fa2e1be34afb5 Mon Sep 17 00:00:00 2001 From: Abduaziz Ziyodov Date: Wed, 19 Aug 2026 01:19:02 +0500 Subject: [PATCH 23/24] feat: make argument detail italic style --- Lib/_colorize.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Lib/_colorize.py b/Lib/_colorize.py index ec5fe31b33079ec..510eef871239ce7 100644 --- a/Lib/_colorize.py +++ b/Lib/_colorize.py @@ -223,7 +223,7 @@ class Dis(ThemeSection): label_fg: str = ANSIColors.BLACK exception_label: str = ANSIColors.CYAN - argument_detail: str = ANSIColors.CYAN + argument_detail: str = "\x1B[3m" op_load: str = ANSIColors.BLUE op_pop: str = ANSIColors.MAGENTA @@ -257,7 +257,6 @@ def color_by_opname(self, opname: str) -> str: ): return self.op_control_flow - return self.reset @dataclass(frozen=True, kw_only=True) From a3d56fe7e86f8454cc8d8618a166fdfaf5db7b19 Mon Sep 17 00:00:00 2001 From: Abduaziz Ziyodov Date: Mon, 24 Aug 2026 00:24:41 +0500 Subject: [PATCH 24/24] feat: change colorization logic(minimal set of colors, align with godbolt) & adjust test cases --- Lib/_colorize.py | 41 ++---------- Lib/dis.py | 15 +++-- Lib/test/test_dis.py | 148 +++++++++++++++++++++++++++++++++---------- 3 files changed, 129 insertions(+), 75 deletions(-) diff --git a/Lib/_colorize.py b/Lib/_colorize.py index 510eef871239ce7..8cde1565f09d638 100644 --- a/Lib/_colorize.py +++ b/Lib/_colorize.py @@ -219,45 +219,18 @@ class Difflib(ThemeSection): @dataclass(frozen=True, kw_only=True) class Dis(ThemeSection): - label_bg: str = ANSIColors.BACKGROUND_CYAN - label_fg: str = ANSIColors.BLACK + disassembly_header: str = ANSIColors.GREEN - exception_label: str = ANSIColors.CYAN - argument_detail: str = "\x1B[3m" + jump_target: str = ANSIColors.GREEN + exception_label: str = ANSIColors.GREEN - op_load: str = ANSIColors.BLUE - op_pop: str = ANSIColors.MAGENTA - op_call_return: str = ANSIColors.YELLOW - op_control_flow: str = ANSIColors.GREEN + opname: str = ANSIColors.BLUE + opname_with_label: str = ANSIColors.GREEN + + arg: str = ANSIColors.YELLOW reset: str = ANSIColors.RESET - def color_by_opname(self, opname: str) -> str: - if opname.startswith("LOAD_"): - return self.op_load - - if opname.startswith("POP_"): - return self.op_pop - - if opname.startswith(("CALL", "RETURN")) or opname in ( - "YIELD_VALUE", - "MAKE_FUNCTION", - "SET_FUNCTION_ATTRIBUTE", - "RESUME", - ): - return self.op_call_return - - if opname.startswith(("JUMP_", "POP_JUMP_", "FOR_ITER")) or opname in ( - "SEND", - "GET_AWAITABLE", - "GET_AITER", - "GET_ANEXT", - "END_ASYNC_FOR", - "CLEANUP_THROW", - ): - return self.op_control_flow - - return self.reset @dataclass(frozen=True, kw_only=True) class FancyCompleter(ThemeSection): diff --git a/Lib/dis.py b/Lib/dis.py index 164e99db3f95c2a..5ece6113a4cf6d5 100644 --- a/Lib/dis.py +++ b/Lib/dis.py @@ -525,7 +525,8 @@ def print_instruction_line(self, instr, mark_as_current): # Column: Label if instr.label is not None: lbl = f"L{instr.label}:" - fields.append(f"{lbl:>{label_width}}") + padded = f"{lbl:>{label_width}}" + fields.append(f"{theme.jump_target}{padded}{theme.reset}") else: fields.append(' ' * label_width) # Column: Instruction offset from start of code sequence @@ -537,17 +538,18 @@ def print_instruction_line(self, instr, mark_as_current): else: fields.append(' ') # Column: Opcode name - fields.append(f"{theme.color_by_opname(instr.opname)}{instr.opname.ljust(_OPNAME_WIDTH)}{theme.reset}") + opname_color = theme.opname_with_label if instr.label is not None else theme.opname + fields.append(f"{opname_color}{instr.opname.ljust(_OPNAME_WIDTH)}{theme.reset}") # Column: Opcode argument if instr.arg is not None: # If opname is longer than _OPNAME_WIDTH, we allow it to overflow into # the space reserved for oparg. This results in fewer misaligned opargs # in the disassembly output. opname_excess = max(0, len(instr.opname) - _OPNAME_WIDTH) - fields.append(repr(instr.arg).rjust(_OPARG_WIDTH - opname_excess)) + fields.append(f"{theme.arg}{repr(instr.arg)}{theme.reset}".rjust(_OPARG_WIDTH - opname_excess)) # Column: Opcode argument details if instr.argrepr: - fields.append(f'{theme.argument_detail}(' + instr.argrepr + f'){theme.reset}') + fields.append('(' + instr.argrepr + ')') print(' '.join(fields).rstrip(), file=self.file) def print_exception_table(self, exception_entries): @@ -591,6 +593,7 @@ def get_label_for_offset(self, offset): return self.labels_map.get(offset, None) def get_argval_argrepr(self, op, arg, offset): + theme = _get_dis_theme() get_name = None if self.names is None else self.names.__getitem__ argval = None argrepr = '' @@ -629,7 +632,7 @@ def get_argval_argrepr(self, op, arg, offset): lbl = self.get_label_for_offset(argval) assert lbl is not None preposition = "from" if deop == END_ASYNC_FOR else "to" - argrepr = f"{preposition} L{lbl}" + argrepr = f"{preposition} {theme.jump_target}L{lbl}{theme.reset}" elif deop in (LOAD_FAST_LOAD_FAST, LOAD_FAST_BORROW_LOAD_FAST_BORROW, STORE_FAST_LOAD_FAST, STORE_FAST_STORE_FAST): arg1 = arg >> 4 arg2 = arg & 15 @@ -870,7 +873,7 @@ def _disassemble_recursive(co, *, file=None, depth=None, show_caches=False, adap for x in co.co_consts: if hasattr(x, 'co_code'): print(file=file) - print(f"{theme.label_bg}{theme.label_fg}Disassembly of {x!r}:{theme.reset}", file=file) + print(f"Disassembly of {theme.disassembly_header}{x!r}{theme.reset}:", file=file) _disassemble_recursive( x, file=file, depth=depth, show_caches=show_caches, adaptive=adaptive, show_offsets=show_offsets, diff --git a/Lib/test/test_dis.py b/Lib/test/test_dis.py index 93d520eb9096a69..54dd76bc5415c49 100644 --- a/Lib/test/test_dis.py +++ b/Lib/test/test_dis.py @@ -2680,6 +2680,7 @@ def test_specialized_code(self): for flag in ['-S', '--specialized']: self.check_output(source, expect, flag) + @force_colorized_test_class class DisColoredTests(unittest.TestCase): def get_colored_output(self, func): @@ -2690,55 +2691,132 @@ def get_colored_output(self, func): return output.getvalue() - def assertOpColored(self, output, opname, color): - self.assertIn( - f"{color}{opname}", output, - f"{opname} should be colored with {color!r}" - ) + def _check_colored(self, output, opname, color, as_not_colored): + # allow spaces, ANSI colors etc. + inter_word_pattern = r"(?:\s|\x1b\[[0-9;]*m)*" - def test_load_ops_colored(self): - def f(a): - return a - out = self.get_colored_output(f) - self.assertOpColored(out, "LOAD_FAST", theme.op_load) + tokens = opname.split() + escaped_tokens = [re.escape(token) for token in tokens] + joined_opname = inter_word_pattern.join(escaped_tokens) - def test_call_return_ops_colored(self): - def f(): - return 1 - out = self.get_colored_output(f) - self.assertOpColored(out, "RETURN_VALUE", theme.op_call_return) - self.assertOpColored(out, "RESUME", theme.op_call_return) + pattern = re.escape(color) + inter_word_pattern + joined_opname + + if as_not_colored: + self.assertNotRegex( + output, + pattern, + f"{opname} should NOT be colored with {color!r}", + ) + else: + self.assertRegex( + output, pattern, f"{opname} should be colored with {color!r}" + ) - def test_pop_ops_colored(self): + def assertOpColoredAs(self, output, opname, color): + self._check_colored(output, opname, color, as_not_colored=False) + + def assertOpNotColoredAs(self, output, opname, wrong_color): + self._check_colored(output, opname, wrong_color, as_not_colored=True) + + def test_opname_and_arg_colored(self): def f(a): - print(a) + return a + out = self.get_colored_output(f) - self.assertOpColored(out, "POP_TOP", theme.op_pop) + self.assertOpColoredAs(out, "LOAD_FAST_BORROW", theme.opname) + self.assertOpColoredAs(out, "RETURN_VALUE", theme.opname) + self.assertOpColoredAs(out, "0", theme.arg) def test_control_flow_ops_colored(self): def f(a): for _ in a: pass + out = self.get_colored_output(f) - self.assertOpColored(out, "FOR_ITER", theme.op_control_flow) - self.assertOpColored(out, "JUMP_BACKWARD", theme.op_control_flow) - def test_argrepr_colored(self): + self.assertOpNotColoredAs(out, "FOR_ITER", theme.opname) + self.assertOpNotColoredAs(out, "END_FOR", theme.opname) + + self.assertOpColoredAs(out, "FOR_ITER", theme.opname_with_label) + self.assertOpColoredAs(out, "END_FOR", theme.opname_with_label) + + opnames = ( + "RESUME", + "LOAD_FAST", + "GET_ITER", + "STORE_FAST", + "JUMP_BACKWARD", + "POP_ITER", + "LOAD_COMMON_CONSTANT", + "RETURN_VALUE", + ) + + for opname in opnames: + self.assertOpColoredAs(out, opname, theme.opname) + + def test_jump_targets_colored(self): + # sample code from: + # https://github.com/python/cpython/pull/144208#issuecomment-5375286176 + def f(a, c): + _t2.d if ( + _t2 := ( + _t1 + if (_t1 := a.b if a is not None else None) is not None + else c + ) + ) is not None else None + + out = self.get_colored_output(f) + + for n in range(1, 6): + self.assertOpColoredAs(out, f"L{n}:", theme.jump_target) + self.assertIn(f"(to {theme.jump_target}L{n}{theme.reset})", out) + + cases = ( + "L1: LOAD_COMMON_CONSTANT", + "L2: COPY", + "L3: LOAD_FAST", + "L4: COPY", + "L5: LOAD_COMMON_CONSTANT", + ) + + for part in cases: + self.assertOpColoredAs(out, part, theme.jump_target) + + def test_exception_table_colored(self): def f(a): - print(a) + try: + a + except Exception: + pass + else: + return a + out = self.get_colored_output(f) - self.assertIn(f"{theme.argument_detail}(", out) - - def test_color_by_opname_coverage(self): - self.assertEqual(theme.color_by_opname("LOAD_FAST"), theme.op_load) - self.assertEqual(theme.color_by_opname("LOAD_GLOBAL"), theme.op_load) - self.assertEqual(theme.color_by_opname("POP_TOP"), theme.op_pop) - self.assertEqual(theme.color_by_opname("CALL"), theme.op_call_return) - self.assertEqual(theme.color_by_opname("RETURN_VALUE"), theme.op_call_return) - self.assertEqual(theme.color_by_opname("RESUME"), theme.op_call_return) - self.assertEqual(theme.color_by_opname("FOR_ITER"), theme.op_control_flow) - self.assertEqual(theme.color_by_opname("JUMP_BACKWARD"), theme.op_control_flow) - self.assertEqual(theme.color_by_opname("BINARY_OP"), theme.reset) # uncolored + + cases = ( + ("L1", "L2", "L3"), + ("L3", "L4", "L8"), + ("L5", "L6", "L8"), + ("L7", "L8", "L8"), + ) + + def assertExceptionTableRow(pairs, out): + p1, p2, p3 = pairs + part = f"{theme.jump_target}{p1}{theme.reset} to {theme.jump_target}{p2}{theme.reset} -> {theme.jump_target}{p3}{theme.reset}" + self.assertIn(part, out) + + for pairs in cases: + assertExceptionTableRow(pairs, out) + + cases = ( + "L3: PUSH_EXC_INFO", + "L6: POP_EXCEPT", + "L7: RERAISE", + ) + + for part in cases: + self.assertOpColoredAs(out, part, theme.jump_target) if __name__ == "__main__": unittest.main()