From 0fdc1ddff7d7f4399654d23cb487c12ea7f5fe05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Tue, 20 Aug 2024 12:44:47 +0200 Subject: [PATCH 01/30] add the `show_positions` formal parameter to `dis` functions --- Lib/dis.py | 44 +++++++++++++++++++++++++++----------------- 1 file changed, 27 insertions(+), 17 deletions(-) diff --git a/Lib/dis.py b/Lib/dis.py index bb922b786f5307..379133fab3825a 100644 --- a/Lib/dis.py +++ b/Lib/dis.py @@ -80,7 +80,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_offsets=False, show_positions=False): """Disassemble classes, methods, functions, and other compiled objects. With no argument, disassemble the last traceback. @@ -91,7 +91,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_offsets=show_offsets, show_positions=show_positions) return # Extract functions from methods. if hasattr(x, '__func__'): @@ -112,28 +112,29 @@ def dis(x=None, *, file=None, depth=None, show_caches=False, adaptive=False, if isinstance(x1, _have_code): print("Disassembly of %s:" % name, file=file) try: - dis(x1, file=file, depth=depth, show_caches=show_caches, adaptive=adaptive, show_offsets=show_offsets) + dis(x1, file=file, depth=depth, show_caches=show_caches, adaptive=adaptive, show_offsets=show_offsets, show_positions=show_positions) 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) + _disassemble_recursive(x, file=file, depth=depth, show_caches=show_caches, adaptive=adaptive, show_offsets=show_offsets, show_positions=show_positions) 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_positions=False) 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) + _disassemble_str(x, file=file, depth=depth, show_caches=show_caches, adaptive=adaptive, show_offsets=show_offsets, show_positions=show_positions) 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): +def distb(tb=None, *, file=None, show_caches=False, adaptive=False, show_offsets=False, show_positions=False): """Disassemble a traceback (default: last traceback).""" if tb is None: try: @@ -144,7 +145,7 @@ def distb(tb=None, *, file=None, show_caches=False, adaptive=False, show_offsets except AttributeError: 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) + 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) # The inspect module interrogates this dictionary to build its # list of CO_* constants. It is also used by pretty_flags to @@ -427,7 +428,7 @@ def __str__(self): class Formatter: def __init__(self, file=None, lineno_width=0, offset_width=0, label_width=0, - line_offset=0, show_caches=False): + line_offset=0, show_caches=False, show_positions=False): """Create a Formatter *file* where to write the output @@ -435,13 +436,17 @@ def __init__(self, file=None, lineno_width=0, offset_width=0, label_width=0, *offset_width* sets the width of the instruction offset field *label_width* sets the width of the label field *show_caches* is a boolean indicating whether to display cache lines + *show_positions* is a boolean indicate whether to display positions + If *show_positions* is true, *lineno_width* should take into account + the width that positions would take. """ self.file = file self.lineno_width = lineno_width self.offset_width = offset_width self.label_width = label_width self.show_caches = show_caches + self.show_positions = show_positions def print_instruction(self, instr, mark_as_current=False): self.print_instruction_line(instr, mark_as_current) @@ -769,7 +774,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_offsets=False, show_positions=False): """Disassemble a code object.""" linestarts = dict(findlinestarts(co)) exception_entries = _parse_exception_table(co) @@ -779,7 +784,8 @@ def disassemble(co, lasti=-1, *, file=None, show_caches=False, adaptive=False, lineno_width=_get_lineno_width(linestarts), 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_caches=show_caches, + show_positions=show_positions) arg_resolver = ArgResolver(co_consts=co.co_consts, names=co.co_names, varname_from_oparg=co._varname_from_oparg, @@ -788,8 +794,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): - disassemble(co, file=file, show_caches=show_caches, adaptive=adaptive, show_offsets=show_offsets) +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) if depth is None or depth > 0: if depth is not None: depth = depth - 1 @@ -799,7 +805,7 @@ def _disassemble_recursive(co, *, file=None, depth=None, show_caches=False, adap print("Disassembly of %r:" % (x,), file=file) _disassemble_recursive( x, file=file, depth=depth, show_caches=show_caches, - adaptive=adaptive, show_offsets=show_offsets + adaptive=adaptive, show_offsets=show_offsets, show_positions=show_positions ) @@ -978,7 +984,7 @@ class Bytecode: Iterating over this yields the bytecode operations as Instruction instances. """ - def __init__(self, x, *, first_line=None, current_offset=None, show_caches=False, adaptive=False, show_offsets=False): + def __init__(self, x, *, first_line=None, current_offset=None, show_caches=False, adaptive=False, show_offsets=False, show_positions=False): self.codeobj = co = _get_code_object(x) if first_line is None: self.first_line = co.co_firstlineno @@ -993,6 +999,7 @@ def __init__(self, x, *, first_line=None, current_offset=None, show_caches=False self.show_caches = show_caches self.adaptive = adaptive self.show_offsets = show_offsets + self.show_positions = show_positions def __iter__(self): co = self.codeobj @@ -1045,7 +1052,8 @@ def dis(self): offset_width=offset_width, label_width=label_width, line_offset=self._line_offset, - show_caches=self.show_caches) + show_caches=self.show_caches, + show_positions=show_positions) arg_resolver = ArgResolver(co_consts=co.co_consts, names=co.co_names, @@ -1071,6 +1079,8 @@ def main(): help='show inline caches') parser.add_argument('-O', '--show-offsets', action='store_true', help='show instruction offsets') + parser.add_argument('-K', '--show-positions', action='store_true', + help='show instruction positions') parser.add_argument('infile', nargs='?', default='-') args = parser.parse_args() if args.infile == '-': @@ -1081,7 +1091,7 @@ def main(): with open(args.infile, 'rb') as infile: source = infile.read() code = compile(source, name, "exec") - dis(code, show_caches=args.show_caches, show_offsets=args.show_offsets) + dis(code, show_caches=args.show_caches, show_offsets=args.show_offsets, show_positions=args.show_positions) if __name__ == "__main__": main() From 3157e24452290c8c65e5449bdbea9b13ad878c0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Tue, 20 Aug 2024 13:01:03 +0200 Subject: [PATCH 02/30] add dummy `_get_positions_width` function --- Lib/dis.py | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/Lib/dis.py b/Lib/dis.py index 379133fab3825a..2dbbf484f0e3fa 100644 --- a/Lib/dis.py +++ b/Lib/dis.py @@ -124,8 +124,7 @@ def dis(x=None, *, file=None, depth=None, show_caches=False, adaptive=False, 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_positions=False) + 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 @@ -427,26 +426,23 @@ def __str__(self): 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): + def __init__(self, file=None, lineno_width=0, offset_width=0, positions_width=0, label_width=0, + line_offset=0, show_caches=False): """Create a Formatter *file* where to write the output *lineno_width* sets the width of the line number field (0 omits it) *offset_width* sets the width of the instruction offset field + *positions_width* sets the width of the instruction positions field (0 omits it) *label_width* sets the width of the label field *show_caches* is a boolean indicating whether to display cache lines - *show_positions* is a boolean indicate whether to display positions - - If *show_positions* is true, *lineno_width* should take into account - the width that positions would take. """ self.file = file self.lineno_width = lineno_width self.offset_width = offset_width + self.positions_width = positions_width self.label_width = label_width self.show_caches = show_caches - self.show_positions = show_positions def print_instruction(self, instr, mark_as_current=False): self.print_instruction_line(instr, mark_as_current) @@ -778,14 +774,15 @@ def disassemble(co, lasti=-1, *, file=None, show_caches=False, adaptive=False, """Disassemble a code object.""" linestarts = dict(findlinestarts(co)) exception_entries = _parse_exception_table(co) + positions_width = _get_positions_width(co) if show_positions else 0 labels_map = _make_labels_map(co.co_code, exception_entries=exception_entries) label_width = 4 + len(str(len(labels_map))) formatter = Formatter(file=file, lineno_width=_get_lineno_width(linestarts), offset_width=len(str(max(len(co.co_code) - 2, 9999))) if show_offsets else 0, + positions_width=positions_width, label_width=label_width, - show_caches=show_caches, - show_positions=show_positions) + show_caches=show_caches) arg_resolver = ArgResolver(co_consts=co.co_consts, names=co.co_names, varname_from_oparg=co._varname_from_oparg, @@ -838,6 +835,8 @@ def _get_lineno_width(linestarts): lineno_width = len(_NO_LINENO) return lineno_width +def _get_positions_width(code): + return 0 def _disassemble_bytes(code, lasti=-1, linestarts=None, *, line_offset=0, exception_entries=(), @@ -1043,17 +1042,17 @@ def dis(self): with io.StringIO() as output: code = _get_code_array(co, self.adaptive) offset_width = len(str(max(len(code) - 2, 9999))) if self.show_offsets else 0 - + positions_width = _get_positions_width(co) if self.show_positions else 0 labels_map = _make_labels_map(co.co_code, self.exception_entries) label_width = 4 + len(str(len(labels_map))) formatter = Formatter(file=output, lineno_width=_get_lineno_width(self._linestarts), offset_width=offset_width, + positions_width=positions_width, label_width=label_width, line_offset=self._line_offset, - show_caches=self.show_caches, - show_positions=show_positions) + show_caches=self.show_caches) arg_resolver = ArgResolver(co_consts=co.co_consts, names=co.co_names, From a8eab11b0003a32555bbffcb258d2920bc2a7af7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Tue, 20 Aug 2024 13:25:34 +0200 Subject: [PATCH 03/30] allow to show code's positions in `dis` --- Lib/dis.py | 46 +++++++++++++++++++++++++++++++++++++--------- 1 file changed, 37 insertions(+), 9 deletions(-) diff --git a/Lib/dis.py b/Lib/dis.py index 2dbbf484f0e3fa..64b56cc30419b8 100644 --- a/Lib/dis.py +++ b/Lib/dis.py @@ -436,6 +436,8 @@ def __init__(self, file=None, lineno_width=0, offset_width=0, positions_width=0, *positions_width* sets the width of the instruction positions field (0 omits it) *label_width* sets the width of the label field *show_caches* is a boolean indicating whether to display cache lines + + If *positions_width* is specified, *lineno_width* is ignored. """ self.file = file self.lineno_width = lineno_width @@ -465,10 +467,11 @@ 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.""" lineno_width = self.lineno_width + positions_width = self.positions_width offset_width = self.offset_width label_width = self.label_width - new_source_line = (lineno_width > 0 and + new_source_line = ((lineno_width > 0 or positions_width > 0) and instr.starts_line and instr.offset > 0) if new_source_line: @@ -476,14 +479,24 @@ def print_instruction_line(self, instr, mark_as_current): fields = [] # Column: Source code line number - if lineno_width: - if instr.starts_line: - lineno_fmt = "%%%dd" if instr.line_number is not None else "%%%ds" - lineno_fmt = lineno_fmt % lineno_width - lineno = _NO_LINENO if instr.line_number is None else instr.line_number - fields.append(lineno_fmt % lineno) + if lineno_width or positions_width: + if positions_width: + # reporting positions instead of just line numbers + assert lineno_width > 0 + if instr_positions := instr.positions: + ps = tuple('?' if p is None else p for p in instr_positions) + positions_str = "%s:%s-%s:%s" % ps + fields.append(f'{positions_str:{positions_width}}') + else: + fields.append(' ' * positions_width) else: - fields.append(' ' * lineno_width) + if instr.starts_line: + lineno_fmt = "%%%dd" if instr.line_number is not None else "%%%ds" + lineno_fmt = lineno_fmt % lineno_width + lineno = _NO_LINENO if instr.line_number is None else instr.line_number + fields.append(lineno_fmt % lineno) + else: + fields.append(' ' * lineno_width) # Column: Label if instr.label is not None: lbl = f"L{instr.label}:" @@ -821,7 +834,7 @@ def _make_labels_map(original_code, exception_entries=()): e.target_label = labels_map[e.target] return labels_map -_NO_LINENO = ' --' +_NO_LINENO = ' --' def _get_lineno_width(linestarts): if linestarts is None: @@ -836,6 +849,21 @@ def _get_lineno_width(linestarts): return lineno_width def _get_positions_width(code): + # Positions are formatted as 'LINE:COL-ENDLINE:ENDCOL' with an additional + # whitespace after the end column. If one of the component is missing, we + # will print ? instead, thus the minimum width is 8 = 1 + len('?:?-?:?'), + # except if all positions are undefined, in which case positions are not + # printed (i.e. positions_width = 0). + has_value = True + values_width = 0 + for positions in code.co_positions(): + if not has_value and any(isinstance(p) for p in positions): + has_value = True + width = sum(1 if p is None else len(str(p)) for p in positions) + values_width = max(width, values_width) + if has_value: + # 3 = number of separators in a normal format + return 1 + max(7, 3 + values_width) return 0 def _disassemble_bytes(code, lasti=-1, linestarts=None, From 352c16ed5e496f664ebc8add941f893b9e9000c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Tue, 20 Aug 2024 13:37:22 +0200 Subject: [PATCH 04/30] cosmetic amendments --- Lib/dis.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/dis.py b/Lib/dis.py index 64b56cc30419b8..6fe55b86cbb5bf 100644 --- a/Lib/dis.py +++ b/Lib/dis.py @@ -834,7 +834,7 @@ def _make_labels_map(original_code, exception_entries=()): e.target_label = labels_map[e.target] return labels_map -_NO_LINENO = ' --' +_NO_LINENO = ' --' def _get_lineno_width(linestarts): if linestarts is None: From 6cd65cfdb3af2d5cbe67d4d64d0df3a1efe7518b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Tue, 20 Aug 2024 13:46:12 +0200 Subject: [PATCH 05/30] fix detection --- Lib/dis.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/dis.py b/Lib/dis.py index 6fe55b86cbb5bf..08e0310a6ccce5 100644 --- a/Lib/dis.py +++ b/Lib/dis.py @@ -854,10 +854,10 @@ def _get_positions_width(code): # will print ? instead, thus the minimum width is 8 = 1 + len('?:?-?:?'), # except if all positions are undefined, in which case positions are not # printed (i.e. positions_width = 0). - has_value = True + has_value = False values_width = 0 for positions in code.co_positions(): - if not has_value and any(isinstance(p) for p in positions): + if not has_value and any(isinstance(p, int) for p in positions): has_value = True width = sum(1 if p is None else len(str(p)) for p in positions) values_width = max(width, values_width) From aed2959b4fec788168169d6f4bb522770c8ab25a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Tue, 20 Aug 2024 13:46:15 +0200 Subject: [PATCH 06/30] add test --- Lib/test/test_dis.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/Lib/test/test_dis.py b/Lib/test/test_dis.py index 80f66c168bab60..1f6779e5b69fb4 100644 --- a/Lib/test/test_dis.py +++ b/Lib/test/test_dis.py @@ -127,6 +127,22 @@ def _f(a): _f.__code__.co_firstlineno + 1, _f.__code__.co_firstlineno + 2) +dis_f_with_positions = f"""\ +%-14s RESUME 0 + +%-14s LOAD_GLOBAL 1 (print + NULL) +%-14s LOAD_FAST 0 (a) +%-14s CALL 1 +%-14s POP_TOP + +%-14s RETURN_CONST 1 (1) +""" % tuple(map('%s:%s-%s:%s'.__mod__, [ + tuple('?' if __p is None else __p for __p in __instruction.positions) + for __instruction in dis._get_instructions_bytes( + dis._get_code_array(_f.__code__, True), + co_positions=_f.__code__.co_positions(), + ) +])) dis_f_co_code = """\ RESUME 0 @@ -950,6 +966,9 @@ def test_dis(self): def test_dis_with_offsets(self): self.do_disassembly_test(_f, dis_f_with_offsets, show_offsets=True) + def test_dis_with_positions(self): + self.do_disassembly_test(_f, dis_f_with_positions, show_positions=True) + def test_bug_708901(self): self.do_disassembly_test(bug708901, dis_bug708901) From 2edff6f29552f9c4e6d7c7718c9501048c3771f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Tue, 20 Aug 2024 14:19:00 +0200 Subject: [PATCH 07/30] simplify tests --- Lib/test/test_dis.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Lib/test/test_dis.py b/Lib/test/test_dis.py index 1f6779e5b69fb4..199d7d8b7c7f0f 100644 --- a/Lib/test/test_dis.py +++ b/Lib/test/test_dis.py @@ -139,8 +139,7 @@ def _f(a): """ % tuple(map('%s:%s-%s:%s'.__mod__, [ tuple('?' if __p is None else __p for __p in __instruction.positions) for __instruction in dis._get_instructions_bytes( - dis._get_code_array(_f.__code__, True), - co_positions=_f.__code__.co_positions(), + _f.__code__.co_code, co_positions=_f.__code__.co_positions(), ) ])) From 1dd8d676d4c209d342b04ad2caaf6bbd75e8c77e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Tue, 20 Aug 2024 14:36:52 +0200 Subject: [PATCH 08/30] add What's New entry --- Doc/whatsnew/3.14.rst | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/Doc/whatsnew/3.14.rst b/Doc/whatsnew/3.14.rst index 088f70d9e9fad4..7b6fa09d6dfe77 100644 --- a/Doc/whatsnew/3.14.rst +++ b/Doc/whatsnew/3.14.rst @@ -110,6 +110,21 @@ ast (Contributed by Bénédikt Tran in :gh:`121141`.) +dis +--- + +* Added support for rendering :class:`instruction positions ` + when available. This feature is available on the following interfaces via + the ``show_positions`` flag: + + - :class:`dis.Bytecode`, + - :func:`dis.dis`, :func:`dis.distb`, and + - :func:`dis.disassemble`. + + This feature is also exposed via :option:`dis --show-positions`. + + (Contributed by Bénédikt Tran in :gh:`123165`.) + fractions --------- From 4e5241302cb5284facbd9657558411e84d94e6ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Tue, 20 Aug 2024 14:36:59 +0200 Subject: [PATCH 09/30] blurb --- .../next/Library/2024-08-20-14-22-49.gh-issue-123165.vOZZOA.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Library/2024-08-20-14-22-49.gh-issue-123165.vOZZOA.rst diff --git a/Misc/NEWS.d/next/Library/2024-08-20-14-22-49.gh-issue-123165.vOZZOA.rst b/Misc/NEWS.d/next/Library/2024-08-20-14-22-49.gh-issue-123165.vOZZOA.rst new file mode 100644 index 00000000000000..05728adc0be388 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2024-08-20-14-22-49.gh-issue-123165.vOZZOA.rst @@ -0,0 +1 @@ +Add support for rendering :class:`~dis.Positions` in :mod:`dis`. From 7a608e7c6a6e2244ca281d6992de0763aea1bd19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Tue, 20 Aug 2024 14:37:02 +0200 Subject: [PATCH 10/30] add docs --- Doc/library/dis.rst | 43 +++++++++++++++++++++++++++++++++++++------ 1 file changed, 37 insertions(+), 6 deletions(-) diff --git a/Doc/library/dis.rst b/Doc/library/dis.rst index a770ad87ab02d3..dcf748a2886428 100644 --- a/Doc/library/dis.rst +++ b/Doc/library/dis.rst @@ -56,6 +56,10 @@ interpreter. for jump targets and exception handlers. The ``-O`` command line option and the ``show_offsets`` argument were added. + .. versionchanged:: 3.14 + Add the :option:`-K ` command-line option + and the ``show_positions`` argument. + Example: Given the function :func:`!myfunc`:: def myfunc(alist): @@ -85,7 +89,7 @@ The :mod:`dis` module can be invoked as a script from the command line: .. code-block:: sh - python -m dis [-h] [-C] [-O] [infile] + python -m dis [-h] [-C] [-O] [-K] [infile] The following options are accepted: @@ -103,6 +107,10 @@ The following options are accepted: Show offsets of instructions. +.. cmdoption:: -K, --show-positions + + Show positions of instructions. + If :file:`infile` is specified, its disassembled code will be written to stdout. Otherwise, disassembly is performed on compiled source code received from stdin. @@ -116,7 +124,8 @@ The bytecode analysis API allows pieces of Python code to be wrapped in a code. .. class:: Bytecode(x, *, first_line=None, current_offset=None,\ - show_caches=False, adaptive=False, show_offsets=False) + show_caches=False, adaptive=False, show_offsets=False, + show_positions=False) Analyse the bytecode corresponding to a function, generator, asynchronous generator, coroutine, method, string of source code, or a code object (as @@ -144,6 +153,9 @@ code. If *show_offsets* is ``True``, :meth:`.dis` will include instruction offsets in the output. + If *show_positions* is ``True``, :meth:`.dis` will include instruction + positions in the output. + .. classmethod:: from_traceback(tb, *, show_caches=False) Construct a :class:`Bytecode` instance from the given traceback, setting @@ -173,6 +185,12 @@ code. .. versionchanged:: 3.11 Added the *show_caches* and *adaptive* parameters. + .. versionchanged:: 3.13 + Added the *show_offsets* parameter + + .. versionchanged:: 3.14 + Added the *show_positions* parameter. + Example: .. doctest:: @@ -226,7 +244,8 @@ operation is being performed, so the intermediate analysis object isn't useful: Added *file* parameter. -.. function:: dis(x=None, *, file=None, depth=None, show_caches=False, adaptive=False) +.. function:: dis(x=None, *, file=None, depth=None, show_caches=False, + adaptive=False, show_offsets=False, show_positions=False) Disassemble the *x* object. *x* can denote either a module, a class, a method, a function, a generator, an asynchronous generator, a coroutine, @@ -265,9 +284,14 @@ operation is being performed, so the intermediate analysis object isn't useful: .. versionchanged:: 3.11 Added the *show_caches* and *adaptive* parameters. + .. versionchanged:: 3.13 + Added the *show_offsets* parameter. + + .. versionchanged:: 3.14 + Added the *show_positions* parameter. .. function:: distb(tb=None, *, file=None, show_caches=False, adaptive=False, - show_offset=False) + show_offset=False, show_positions=False) Disassemble the top-of-stack function of a traceback, using the last traceback if none was passed. The instruction causing the exception is @@ -285,14 +309,18 @@ operation is being performed, so the intermediate analysis object isn't useful: .. versionchanged:: 3.13 Added the *show_offsets* parameter. + .. versionchanged:: 3.14 + Added the *show_positions* parameter. + .. function:: disassemble(code, lasti=-1, *, file=None, show_caches=False, adaptive=False) disco(code, lasti=-1, *, file=None, show_caches=False, adaptive=False, - show_offsets=False) + show_offsets=False, show_positions=False) Disassemble a code object, indicating the last instruction if *lasti* was provided. The output is divided in the following columns: - #. the line number, for the first instruction of each line + #. the line number, for the first instruction of each line, or the + instruction positions if *show_positions* is true. #. the current instruction, indicated as ``-->``, #. a labelled instruction, indicated with ``>>``, #. the address of the instruction, @@ -315,6 +343,9 @@ operation is being performed, so the intermediate analysis object isn't useful: .. versionchanged:: 3.13 Added the *show_offsets* parameter. + .. versionchanged:: 3.14 + Added the *show_positions* parameter. + .. function:: get_instructions(x, *, first_line=None, show_caches=False, adaptive=False) Return an iterator over the instructions in the supplied function, method, From 9c7559726dc6ea3eba10da9bfa77e672d51c4397 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Tue, 20 Aug 2024 14:47:20 +0200 Subject: [PATCH 11/30] fixup! rst --- Doc/library/dis.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Doc/library/dis.rst b/Doc/library/dis.rst index dcf748a2886428..f898319b84b8e2 100644 --- a/Doc/library/dis.rst +++ b/Doc/library/dis.rst @@ -124,7 +124,7 @@ The bytecode analysis API allows pieces of Python code to be wrapped in a code. .. class:: Bytecode(x, *, first_line=None, current_offset=None,\ - show_caches=False, adaptive=False, show_offsets=False, + show_caches=False, adaptive=False, show_offsets=False,\ show_positions=False) Analyse the bytecode corresponding to a function, generator, asynchronous @@ -290,7 +290,7 @@ operation is being performed, so the intermediate analysis object isn't useful: .. versionchanged:: 3.14 Added the *show_positions* parameter. -.. function:: distb(tb=None, *, file=None, show_caches=False, adaptive=False, +.. function:: distb(tb=None, *, file=None, show_caches=False, adaptive=False,\ show_offset=False, show_positions=False) Disassemble the top-of-stack function of a traceback, using the last @@ -313,7 +313,7 @@ operation is being performed, so the intermediate analysis object isn't useful: Added the *show_positions* parameter. .. function:: disassemble(code, lasti=-1, *, file=None, show_caches=False, adaptive=False) - disco(code, lasti=-1, *, file=None, show_caches=False, adaptive=False, + disco(code, lasti=-1, *, file=None, show_caches=False, adaptive=False,\ show_offsets=False, show_positions=False) Disassemble a code object, indicating the last instruction if *lasti* was From e14740a21c0cc511e229f6d9c10e385a3cf9a545 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Tue, 20 Aug 2024 14:48:08 +0200 Subject: [PATCH 12/30] fixup! rst --- Doc/library/dis.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/library/dis.rst b/Doc/library/dis.rst index f898319b84b8e2..9d5d73cb4b7b6e 100644 --- a/Doc/library/dis.rst +++ b/Doc/library/dis.rst @@ -314,7 +314,7 @@ operation is being performed, so the intermediate analysis object isn't useful: .. function:: disassemble(code, lasti=-1, *, file=None, show_caches=False, adaptive=False) disco(code, lasti=-1, *, file=None, show_caches=False, adaptive=False,\ - show_offsets=False, show_positions=False) + show_offsets=False, show_positions=False) Disassemble a code object, indicating the last instruction if *lasti* was provided. The output is divided in the following columns: From fe3242448da49a0307821584d688cf69f9280b33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Tue, 20 Aug 2024 15:01:48 +0200 Subject: [PATCH 13/30] fixup! rst --- Doc/library/dis.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/library/dis.rst b/Doc/library/dis.rst index 9d5d73cb4b7b6e..feb51c71b35c2a 100644 --- a/Doc/library/dis.rst +++ b/Doc/library/dis.rst @@ -244,7 +244,7 @@ operation is being performed, so the intermediate analysis object isn't useful: Added *file* parameter. -.. function:: dis(x=None, *, file=None, depth=None, show_caches=False, +.. function:: dis(x=None, *, file=None, depth=None, show_caches=False,\ adaptive=False, show_offsets=False, show_positions=False) Disassemble the *x* object. *x* can denote either a module, a class, a From a0dce9cd8e7d50f7fd13e0ebe81a82dfe8be80ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Tue, 20 Aug 2024 15:02:10 +0200 Subject: [PATCH 14/30] change style --- Doc/library/dis.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/library/dis.rst b/Doc/library/dis.rst index feb51c71b35c2a..5c2ec251beaead 100644 --- a/Doc/library/dis.rst +++ b/Doc/library/dis.rst @@ -57,7 +57,7 @@ interpreter. option and the ``show_offsets`` argument were added. .. versionchanged:: 3.14 - Add the :option:`-K ` command-line option + Added the :option:`-K ` command-line option and the ``show_positions`` argument. Example: Given the function :func:`!myfunc`:: From b8c1bc860be634e38afd71b056db8480f1dcfb22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Tue, 20 Aug 2024 17:53:17 +0200 Subject: [PATCH 15/30] address review --- Doc/library/dis.rst | 4 ++-- Doc/whatsnew/3.14.rst | 2 +- Lib/dis.py | 48 ++++++++++++++++++++----------------------- 3 files changed, 25 insertions(+), 29 deletions(-) diff --git a/Doc/library/dis.rst b/Doc/library/dis.rst index 5c2ec251beaead..02ace9ad68e0a4 100644 --- a/Doc/library/dis.rst +++ b/Doc/library/dis.rst @@ -57,7 +57,7 @@ interpreter. option and the ``show_offsets`` argument were added. .. versionchanged:: 3.14 - Added the :option:`-K ` command-line option + Added the :option:`-P ` command-line option and the ``show_positions`` argument. Example: Given the function :func:`!myfunc`:: @@ -107,7 +107,7 @@ The following options are accepted: Show offsets of instructions. -.. cmdoption:: -K, --show-positions +.. cmdoption:: -P, --show-positions Show positions of instructions. diff --git a/Doc/whatsnew/3.14.rst b/Doc/whatsnew/3.14.rst index 7b6fa09d6dfe77..89578cef5dfc2e 100644 --- a/Doc/whatsnew/3.14.rst +++ b/Doc/whatsnew/3.14.rst @@ -114,7 +114,7 @@ dis --- * Added support for rendering :class:`instruction positions ` - when available. This feature is available on the following interfaces via + when available. This feature is added to the following interfaces via the ``show_positions`` flag: - :class:`dis.Bytecode`, diff --git a/Lib/dis.py b/Lib/dis.py index 08e0310a6ccce5..082ce9b8b1fcf7 100644 --- a/Lib/dis.py +++ b/Lib/dis.py @@ -426,8 +426,8 @@ def __str__(self): class Formatter: - def __init__(self, file=None, lineno_width=0, offset_width=0, positions_width=0, label_width=0, - line_offset=0, show_caches=False): + def __init__(self, file=None, lineno_width=0, offset_width=0, label_width=0, + line_offset=0, show_caches=False, *, positions_width=0): """Create a Formatter *file* where to write the output @@ -479,24 +479,23 @@ def print_instruction_line(self, instr, mark_as_current): fields = [] # Column: Source code line number - if lineno_width or positions_width: - if positions_width: - # reporting positions instead of just line numbers - assert lineno_width > 0 - if instr_positions := instr.positions: - ps = tuple('?' if p is None else p for p in instr_positions) - positions_str = "%s:%s-%s:%s" % ps - fields.append(f'{positions_str:{positions_width}}') - else: - fields.append(' ' * positions_width) + if positions_width: + # reporting positions instead of just line numbers + assert lineno_width > 0 + if instr_positions := instr.positions: + ps = tuple('?' if p is None else p for p in instr_positions) + positions_str = "%s:%s-%s:%s" % ps + fields.append(f'{positions_str:{positions_width}}') else: - if instr.starts_line: - lineno_fmt = "%%%dd" if instr.line_number is not None else "%%%ds" - lineno_fmt = lineno_fmt % lineno_width - lineno = _NO_LINENO if instr.line_number is None else instr.line_number - fields.append(lineno_fmt % lineno) - else: - fields.append(' ' * lineno_width) + fields.append(' ' * positions_width) + elif lineno_width: + if instr.starts_line: + lineno_fmt = "%%%dd" if instr.line_number is not None else "%%%ds" + lineno_fmt = lineno_fmt % lineno_width + lineno = _NO_LINENO if instr.line_number is None else instr.line_number + fields.append(lineno_fmt % lineno) + else: + fields.append(' ' * lineno_width) # Column: Label if instr.label is not None: lbl = f"L{instr.label}:" @@ -849,16 +848,13 @@ def _get_lineno_width(linestarts): return lineno_width def _get_positions_width(code): - # Positions are formatted as 'LINE:COL-ENDLINE:ENDCOL' with an additional - # whitespace after the end column. If one of the component is missing, we - # will print ? instead, thus the minimum width is 8 = 1 + len('?:?-?:?'), - # except if all positions are undefined, in which case positions are not - # printed (i.e. positions_width = 0). + # Positions are formatted as 'LINE:COL-ENDLINE:ENDCOL ' (note trailing space). + # A missing component appears as '?', so the minimum width is 8 = 1 + len('?:?-?:?'). + # If all values are missing, positions are not printed (i.e. positions_width = 0). has_value = False values_width = 0 for positions in code.co_positions(): - if not has_value and any(isinstance(p, int) for p in positions): - has_value = True + has_value |= any(isinstance(p, int) for p in positions) width = sum(1 if p is None else len(str(p)) for p in positions) values_width = max(width, values_width) if has_value: From 954a312ffd2f5e89b5b457cace4dd289c57cab1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Tue, 20 Aug 2024 19:17:51 +0200 Subject: [PATCH 16/30] fix rendering --- Lib/dis.py | 3 +- Lib/test/test_dis.py | 72 ++++++++++++++++++++++++++++++++++++++------ 2 files changed, 64 insertions(+), 11 deletions(-) diff --git a/Lib/dis.py b/Lib/dis.py index 082ce9b8b1fcf7..47b170544b7a4b 100644 --- a/Lib/dis.py +++ b/Lib/dis.py @@ -481,10 +481,9 @@ def print_instruction_line(self, instr, mark_as_current): # Column: Source code line number if positions_width: # reporting positions instead of just line numbers - assert lineno_width > 0 if instr_positions := instr.positions: ps = tuple('?' if p is None else p for p in instr_positions) - positions_str = "%s:%s-%s:%s" % ps + positions_str = "%s:%s-%s:%s" % (ps[0], ps[2], ps[1], ps[3]) fields.append(f'{positions_str:{positions_width}}') else: fields.append(' ' * positions_width) diff --git a/Lib/test/test_dis.py b/Lib/test/test_dis.py index 199d7d8b7c7f0f..37c3eb13caac82 100644 --- a/Lib/test/test_dis.py +++ b/Lib/test/test_dis.py @@ -127,7 +127,7 @@ def _f(a): _f.__code__.co_firstlineno + 1, _f.__code__.co_firstlineno + 2) -dis_f_with_positions = f"""\ +dis_f_with_positions_format = f"""\ %-14s RESUME 0 %-14s LOAD_GLOBAL 1 (print + NULL) @@ -136,12 +136,7 @@ def _f(a): %-14s POP_TOP %-14s RETURN_CONST 1 (1) -""" % tuple(map('%s:%s-%s:%s'.__mod__, [ - tuple('?' if __p is None else __p for __p in __instruction.positions) - for __instruction in dis._get_instructions_bytes( - _f.__code__.co_code, co_positions=_f.__code__.co_positions(), - ) -])) +""" dis_f_co_code = """\ RESUME 0 @@ -965,8 +960,67 @@ def test_dis(self): def test_dis_with_offsets(self): self.do_disassembly_test(_f, dis_f_with_offsets, show_offsets=True) - def test_dis_with_positions(self): - self.do_disassembly_test(_f, dis_f_with_positions, show_positions=True) + @requires_debug_ranges() + def test_dis_with_all_positions(self): + def format_instr_positions(instr): + values = tuple('?' if p is None else p for p in instr.positions) + return '%s:%s-%s:%s' % (values[0], values[2], values[1], values[3]) + + instrs = list(dis.get_instructions(_f)) + for instr in instrs: + with self.subTest(instr=instr): + self.assertTrue(all(p is not None for p in instr.positions)) + positions = tuple(map(format_instr_positions, instrs)) + expected = dis_f_with_positions_format % positions + self.do_disassembly_test(_f, expected, show_positions=True) + + @requires_debug_ranges() + def test_dis_with_some_positions(self): + def f(): + pass + + PY_CODE_LOCATION_INFO_NO_COLUMNS = 13 + PY_CODE_LOCATION_INFO_WITH_COLUMNS = 14 + + f.__code__ = f.__code__.replace( + co_stacksize=1, + co_firstlineno=42, + co_code=bytes([ + dis.opmap["RESUME"], 0, + dis.opmap["RETURN_CONST"], 0, + ]), + co_linetable=bytes([ + (1 << 7) + | (PY_CODE_LOCATION_INFO_NO_COLUMNS << 3) + | (1 - 1), # 1 code unit (RESUME) + 0, # start line offset is 0 + (1 << 7) + | (PY_CODE_LOCATION_INFO_WITH_COLUMNS << 3) + | (1 - 1), # 1 code unit (RETURN CONST) + 0, # start line offset is 0 + 0, # end line offset is 0 + 1, # 1-based start column (reported as COL - 1) + 5, # 1-based end column (reported as ENDCOL - 1) + ] + )) + expect = '\n'.join([ + '42:?-42:? RESUME 0', + '42:0-42:4 RETURN_CONST 0 (None)', + '', + ]) + self.do_disassembly_test(f, expect, show_positions=True) + + def test_dis_with_no_positions(self): + def f(): + pass + + f.__code__ = f.__code__.replace(co_linetable=b'') + expect = '\n'.join([ + ' RESUME 0', + ' RETURN_CONST 0 (None)', + '', + ]) + self.do_disassembly_test(f, expect, show_positions=True) def test_bug_708901(self): self.do_disassembly_test(bug708901, dis_bug708901) From e90899015aa8c1cf59d71f1a6f1fc21dfa1f094f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Tue, 20 Aug 2024 19:22:02 +0200 Subject: [PATCH 17/30] fix docs --- Doc/library/dis.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/library/dis.rst b/Doc/library/dis.rst index 02ace9ad68e0a4..a662878da955f5 100644 --- a/Doc/library/dis.rst +++ b/Doc/library/dis.rst @@ -89,7 +89,7 @@ The :mod:`dis` module can be invoked as a script from the command line: .. code-block:: sh - python -m dis [-h] [-C] [-O] [-K] [infile] + python -m dis [-h] [-C] [-O] [-P] [infile] The following options are accepted: From df02d44548a4df3d21f12cedaa147c7b422a9ab7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Tue, 20 Aug 2024 19:22:39 +0200 Subject: [PATCH 18/30] fix docstring --- Lib/dis.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/dis.py b/Lib/dis.py index 47b170544b7a4b..9f7f49ff8f9e95 100644 --- a/Lib/dis.py +++ b/Lib/dis.py @@ -433,9 +433,9 @@ def __init__(self, file=None, lineno_width=0, offset_width=0, label_width=0, *file* where to write the output *lineno_width* sets the width of the line number field (0 omits it) *offset_width* sets the width of the instruction offset field - *positions_width* sets the width of the instruction positions field (0 omits it) *label_width* sets the width of the label field *show_caches* is a boolean indicating whether to display cache lines + *positions_width* sets the width of the instruction positions field (0 omits it) If *positions_width* is specified, *lineno_width* is ignored. """ From c64476751c653ff2cddf64b4c9108e0ba06d9c09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Tue, 20 Aug 2024 19:22:53 +0200 Subject: [PATCH 19/30] fix init ordering --- Lib/dis.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/dis.py b/Lib/dis.py index 9f7f49ff8f9e95..899c2825cd4b91 100644 --- a/Lib/dis.py +++ b/Lib/dis.py @@ -442,9 +442,9 @@ def __init__(self, file=None, lineno_width=0, offset_width=0, label_width=0, self.file = file self.lineno_width = lineno_width self.offset_width = offset_width - self.positions_width = positions_width self.label_width = label_width self.show_caches = show_caches + self.positions_width = positions_width def print_instruction(self, instr, mark_as_current=False): self.print_instruction_line(instr, mark_as_current) From 56bcaef698d8be85d444ed9a3156de49d8c79771 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Tue, 20 Aug 2024 19:24:08 +0200 Subject: [PATCH 20/30] update CLI arg --- Lib/dis.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/dis.py b/Lib/dis.py index 899c2825cd4b91..6df72902e9f939 100644 --- a/Lib/dis.py +++ b/Lib/dis.py @@ -1101,7 +1101,7 @@ def main(): help='show inline caches') parser.add_argument('-O', '--show-offsets', action='store_true', help='show instruction offsets') - parser.add_argument('-K', '--show-positions', action='store_true', + parser.add_argument('-P', '--show-positions', action='store_true', help='show instruction positions') parser.add_argument('infile', nargs='?', default='-') args = parser.parse_args() From 4355fc64e63b2de910269abe891873db62434ad8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Wed, 21 Aug 2024 11:32:57 +0200 Subject: [PATCH 21/30] fix representation --- Lib/test/test_dis.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_dis.py b/Lib/test/test_dis.py index 37c3eb13caac82..94f3cf5eeb11de 100644 --- a/Lib/test/test_dis.py +++ b/Lib/test/test_dis.py @@ -997,8 +997,8 @@ def f(): (1 << 7) | (PY_CODE_LOCATION_INFO_WITH_COLUMNS << 3) | (1 - 1), # 1 code unit (RETURN CONST) - 0, # start line offset is 0 - 0, # end line offset is 0 + (0 << 1), # start line offset is 0 (encoded as an svarint) + 0, # end line offset is 0 (varint encoded) 1, # 1-based start column (reported as COL - 1) 5, # 1-based end column (reported as ENDCOL - 1) ] From d5d234308962fa0252a442088728f388d2e0ca36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Wed, 21 Aug 2024 11:41:13 +0200 Subject: [PATCH 22/30] fix f-string --- Lib/dis.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/dis.py b/Lib/dis.py index 6df72902e9f939..3de55d5e2bd86d 100644 --- a/Lib/dis.py +++ b/Lib/dis.py @@ -483,7 +483,7 @@ def print_instruction_line(self, instr, mark_as_current): # reporting positions instead of just line numbers if instr_positions := instr.positions: ps = tuple('?' if p is None else p for p in instr_positions) - positions_str = "%s:%s-%s:%s" % (ps[0], ps[2], ps[1], ps[3]) + positions_str = f"{ps[0]}:{ps[2]}-{ps[1]}:{ps[3]}" fields.append(f'{positions_str:{positions_width}}') else: fields.append(' ' * positions_width) From 760beed12fdab49df57ba233bea05be8ff6e0471 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Wed, 21 Aug 2024 11:43:00 +0200 Subject: [PATCH 23/30] update What's New --- Doc/whatsnew/3.14.rst | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Doc/whatsnew/3.14.rst b/Doc/whatsnew/3.14.rst index 89578cef5dfc2e..ddf087301a8ed1 100644 --- a/Doc/whatsnew/3.14.rst +++ b/Doc/whatsnew/3.14.rst @@ -113,9 +113,10 @@ ast dis --- -* Added support for rendering :class:`instruction positions ` - when available. This feature is added to the following interfaces via - the ``show_positions`` flag: +* Added support for rendering full source location information of + :class:`instructions `, rather than only the line number. + This feature is added to the following interfaces via the ``show_positions`` + flag: - :class:`dis.Bytecode`, - :func:`dis.dis`, :func:`dis.distb`, and From 2356ef5dc58588af20125c431859e57c3d5f1f86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Wed, 21 Aug 2024 11:43:50 +0200 Subject: [PATCH 24/30] update docs --- Doc/library/dis.rst | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/Doc/library/dis.rst b/Doc/library/dis.rst index a662878da955f5..cc8f6363d78be3 100644 --- a/Doc/library/dis.rst +++ b/Doc/library/dis.rst @@ -57,8 +57,8 @@ interpreter. option and the ``show_offsets`` argument were added. .. versionchanged:: 3.14 - Added the :option:`-P ` command-line option - and the ``show_positions`` argument. + The :option:`-P ` command-line option + and the ``show_positions`` argument were added. Example: Given the function :func:`!myfunc`:: @@ -109,7 +109,7 @@ The following options are accepted: .. cmdoption:: -P, --show-positions - Show positions of instructions. + Show positions of instructions in the source code. If :file:`infile` is specified, its disassembled code will be written to stdout. Otherwise, disassembly is performed on compiled source code received from stdin. @@ -154,7 +154,7 @@ code. offsets in the output. If *show_positions* is ``True``, :meth:`.dis` will include instruction - positions in the output. + source code positions in the output. .. classmethod:: from_traceback(tb, *, show_caches=False) @@ -319,8 +319,9 @@ operation is being performed, so the intermediate analysis object isn't useful: Disassemble a code object, indicating the last instruction if *lasti* was provided. The output is divided in the following columns: - #. the line number, for the first instruction of each line, or the - instruction positions if *show_positions* is true. + #. the source code location of the instruction. Complete location information + is shown if *show_positions* is true. Otherwise (the default) only the + line number is displayed. #. the current instruction, indicated as ``-->``, #. a labelled instruction, indicated with ``>>``, #. the address of the instruction, From ca3cf876709b2862be0cdd5cd974f69c957157af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Wed, 21 Aug 2024 12:00:51 +0200 Subject: [PATCH 25/30] unify location rendering --- Lib/dis.py | 87 ++++++++++++++++++++++++++++++++---------------------- 1 file changed, 51 insertions(+), 36 deletions(-) diff --git a/Lib/dis.py b/Lib/dis.py index 3de55d5e2bd86d..e534024d011983 100644 --- a/Lib/dis.py +++ b/Lib/dis.py @@ -426,25 +426,35 @@ def __str__(self): class Formatter: - def __init__(self, file=None, lineno_width=0, offset_width=0, label_width=0, - line_offset=0, show_caches=False, *, positions_width=0): + def __init__(self, file=None, lineno_width=None, offset_width=0, label_width=0, + line_offset=0, show_caches=False, *, locations_width=0, + show_positions=False): """Create a Formatter *file* where to write the output - *lineno_width* sets the width of the line number field (0 omits it) + *lineno_width* sets the width of the line number field (deprecated) *offset_width* sets the width of the instruction offset field *label_width* sets the width of the label field *show_caches* is a boolean indicating whether to display cache lines - *positions_width* sets the width of the instruction positions field (0 omits it) - - If *positions_width* is specified, *lineno_width* is ignored. + *locations_width* sets the width of the instruction locations (0 omits it) + *show_positions* is a boolean indicating whether positions should be + reported instead of line numbers """ + if lineno_width is not None: + import warnings + warnings.warn("The 'lineno_width' parameter is deprecated. It is " + "now ignored and will be removed in Python 3.16. " + "Use 'locations_width' instead.", + DeprecationWarning, stacklevel=2) + self.file = file - self.lineno_width = lineno_width + # keep the attribute in case someone is still using it + self.lineno_width = 0 if lineno_width is None else lineno_width + self.locations_width = locations_width self.offset_width = offset_width self.label_width = label_width self.show_caches = show_caches - self.positions_width = positions_width + self.show_positions = show_positions def print_instruction(self, instr, mark_as_current=False): self.print_instruction_line(instr, mark_as_current) @@ -466,35 +476,35 @@ 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.""" - lineno_width = self.lineno_width - positions_width = self.positions_width + locations_width = self.locations_width offset_width = self.offset_width label_width = self.label_width - new_source_line = ((lineno_width > 0 or positions_width > 0) and + new_source_line = ((locations_width > 0) and instr.starts_line and instr.offset > 0) if new_source_line: print(file=self.file) fields = [] - # Column: Source code line number - if positions_width: - # reporting positions instead of just line numbers - if instr_positions := instr.positions: - ps = tuple('?' if p is None else p for p in instr_positions) - positions_str = f"{ps[0]}:{ps[2]}-{ps[1]}:{ps[3]}" - fields.append(f'{positions_str:{positions_width}}') - else: - fields.append(' ' * positions_width) - elif lineno_width: - if instr.starts_line: - lineno_fmt = "%%%dd" if instr.line_number is not None else "%%%ds" - lineno_fmt = lineno_fmt % lineno_width - lineno = _NO_LINENO if instr.line_number is None else instr.line_number - fields.append(lineno_fmt % lineno) + # Column: Source code locations information + if locations_width: + if self.show_positions: + # reporting positions instead of just line numbers + if instr_positions := instr.positions: + ps = tuple('?' if p is None else p for p in instr_positions) + positions_str = f"{ps[0]}:{ps[2]}-{ps[1]}:{ps[3]}" + fields.append(f'{positions_str:{locations_width}}') + else: + fields.append(' ' * locations_width) else: - fields.append(' ' * lineno_width) + if instr.starts_line: + lineno_fmt = "%%%dd" if instr.line_number is not None else "%%%ds" + lineno_fmt = lineno_fmt % locations_width + lineno = _NO_LINENO if instr.line_number is None else instr.line_number + fields.append(lineno_fmt % lineno) + else: + fields.append(' ' * locations_width) # Column: Label if instr.label is not None: lbl = f"L{instr.label}:" @@ -785,15 +795,18 @@ def disassemble(co, lasti=-1, *, file=None, show_caches=False, adaptive=False, """Disassemble a code object.""" linestarts = dict(findlinestarts(co)) exception_entries = _parse_exception_table(co) - positions_width = _get_positions_width(co) if show_positions else 0 + if show_positions: + locations_width = _get_positions_width(co) + else: + locations_width = _get_lineno_width(linestarts) labels_map = _make_labels_map(co.co_code, exception_entries=exception_entries) label_width = 4 + len(str(len(labels_map))) formatter = Formatter(file=file, - lineno_width=_get_lineno_width(linestarts), offset_width=len(str(max(len(co.co_code) - 2, 9999))) if show_offsets else 0, - positions_width=positions_width, + locations_width=locations_width, label_width=label_width, - show_caches=show_caches) + show_caches=show_caches, + show_positions=show_positions) arg_resolver = ArgResolver(co_consts=co.co_consts, names=co.co_names, varname_from_oparg=co._varname_from_oparg, @@ -1065,17 +1078,19 @@ def dis(self): with io.StringIO() as output: code = _get_code_array(co, self.adaptive) offset_width = len(str(max(len(code) - 2, 9999))) if self.show_offsets else 0 - positions_width = _get_positions_width(co) if self.show_positions else 0 - + if self.show_positions: + locations_width = _get_positions_width(co) + else: + locations_width = _get_lineno_width(self._linestarts) labels_map = _make_labels_map(co.co_code, self.exception_entries) label_width = 4 + len(str(len(labels_map))) formatter = Formatter(file=output, - lineno_width=_get_lineno_width(self._linestarts), offset_width=offset_width, - positions_width=positions_width, + locations_width=locations_width, label_width=label_width, line_offset=self._line_offset, - show_caches=self.show_caches) + show_caches=self.show_caches, + show_positions=self.show_positions) arg_resolver = ArgResolver(co_consts=co.co_consts, names=co.co_names, From 911789545470fe3f95beb5c8852eaf45612719ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Wed, 21 Aug 2024 12:09:11 +0200 Subject: [PATCH 26/30] keep `lineno_width` parameter instead --- Lib/dis.py | 50 +++++++++++++++++++++----------------------------- 1 file changed, 21 insertions(+), 29 deletions(-) diff --git a/Lib/dis.py b/Lib/dis.py index e534024d011983..0b72dabb01343c 100644 --- a/Lib/dis.py +++ b/Lib/dis.py @@ -426,31 +426,23 @@ def __str__(self): class Formatter: - def __init__(self, file=None, lineno_width=None, offset_width=0, label_width=0, - line_offset=0, show_caches=False, *, locations_width=0, - show_positions=False): + def __init__(self, file=None, lineno_width=0, offset_width=0, label_width=0, + line_offset=0, show_caches=False, *, show_positions=False): """Create a Formatter *file* where to write the output - *lineno_width* sets the width of the line number field (deprecated) + *lineno_width* sets the width of the line number field (0 omits it) *offset_width* sets the width of the instruction offset field *label_width* sets the width of the label field *show_caches* is a boolean indicating whether to display cache lines - *locations_width* sets the width of the instruction locations (0 omits it) *show_positions* is a boolean indicating whether positions should be - reported instead of line numbers - """ - if lineno_width is not None: - import warnings - warnings.warn("The 'lineno_width' parameter is deprecated. It is " - "now ignored and will be removed in Python 3.16. " - "Use 'locations_width' instead.", - DeprecationWarning, stacklevel=2) + reported instead of line numbers. + If *show_positions* is true, then *lineno_width* should be computed + accordingly. + """ self.file = file - # keep the attribute in case someone is still using it - self.lineno_width = 0 if lineno_width is None else lineno_width - self.locations_width = locations_width + self.lineno_width = lineno_width self.offset_width = offset_width self.label_width = label_width self.show_caches = show_caches @@ -476,11 +468,11 @@ 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.""" - locations_width = self.locations_width + lineno_width = self.lineno_width offset_width = self.offset_width label_width = self.label_width - new_source_line = ((locations_width > 0) and + new_source_line = (lineno_width > 0 and instr.starts_line and instr.offset > 0) if new_source_line: @@ -488,23 +480,23 @@ def print_instruction_line(self, instr, mark_as_current): fields = [] # Column: Source code locations information - if locations_width: + if lineno_width: if self.show_positions: # reporting positions instead of just line numbers if instr_positions := instr.positions: ps = tuple('?' if p is None else p for p in instr_positions) positions_str = f"{ps[0]}:{ps[2]}-{ps[1]}:{ps[3]}" - fields.append(f'{positions_str:{locations_width}}') + fields.append(f'{positions_str:{lineno_width}}') else: - fields.append(' ' * locations_width) + fields.append(' ' * lineno_width) else: if instr.starts_line: lineno_fmt = "%%%dd" if instr.line_number is not None else "%%%ds" - lineno_fmt = lineno_fmt % locations_width + lineno_fmt = lineno_fmt % lineno_width lineno = _NO_LINENO if instr.line_number is None else instr.line_number fields.append(lineno_fmt % lineno) else: - fields.append(' ' * locations_width) + fields.append(' ' * lineno_width) # Column: Label if instr.label is not None: lbl = f"L{instr.label}:" @@ -796,14 +788,14 @@ def disassemble(co, lasti=-1, *, file=None, show_caches=False, adaptive=False, linestarts = dict(findlinestarts(co)) exception_entries = _parse_exception_table(co) if show_positions: - locations_width = _get_positions_width(co) + lineno_width = _get_positions_width(co) else: - locations_width = _get_lineno_width(linestarts) + lineno_width = _get_lineno_width(linestarts) labels_map = _make_labels_map(co.co_code, exception_entries=exception_entries) label_width = 4 + len(str(len(labels_map))) formatter = Formatter(file=file, + lineno_width=lineno_width, offset_width=len(str(max(len(co.co_code) - 2, 9999))) if show_offsets else 0, - locations_width=locations_width, label_width=label_width, show_caches=show_caches, show_positions=show_positions) @@ -1079,14 +1071,14 @@ def dis(self): code = _get_code_array(co, self.adaptive) offset_width = len(str(max(len(code) - 2, 9999))) if self.show_offsets else 0 if self.show_positions: - locations_width = _get_positions_width(co) + lineno_width = _get_positions_width(co) else: - locations_width = _get_lineno_width(self._linestarts) + lineno_width = _get_lineno_width(self._linestarts) labels_map = _make_labels_map(co.co_code, self.exception_entries) label_width = 4 + len(str(len(labels_map))) formatter = Formatter(file=output, + lineno_width=lineno_width, offset_width=offset_width, - locations_width=locations_width, label_width=label_width, line_offset=self._line_offset, show_caches=self.show_caches, From d48f759fb14beabee796ed0820e91d7f12d7a098 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Wed, 21 Aug 2024 12:37:55 +0200 Subject: [PATCH 27/30] address review --- Doc/whatsnew/3.14.rst | 2 +- Lib/dis.py | 11 +++++------ 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/Doc/whatsnew/3.14.rst b/Doc/whatsnew/3.14.rst index ddf087301a8ed1..a34dc639ad2a94 100644 --- a/Doc/whatsnew/3.14.rst +++ b/Doc/whatsnew/3.14.rst @@ -116,7 +116,7 @@ dis * Added support for rendering full source location information of :class:`instructions `, rather than only the line number. This feature is added to the following interfaces via the ``show_positions`` - flag: + keyword argument: - :class:`dis.Bytecode`, - :func:`dis.dis`, :func:`dis.distb`, and diff --git a/Lib/dis.py b/Lib/dis.py index 0b72dabb01343c..69c4138776f3bd 100644 --- a/Lib/dis.py +++ b/Lib/dis.py @@ -431,15 +431,14 @@ def __init__(self, file=None, lineno_width=0, offset_width=0, label_width=0, """Create a Formatter *file* where to write the output - *lineno_width* sets the width of the line number field (0 omits it) + *lineno_width* sets the width of the source location field (0 omits it). + Should be large enough for a line number or full positions (depending + on the value of *show_positions*). *offset_width* sets the width of the instruction offset field *label_width* sets the width of the label field *show_caches* is a boolean indicating whether to display cache lines - *show_positions* is a boolean indicating whether positions should be - reported instead of line numbers. - - If *show_positions* is true, then *lineno_width* should be computed - accordingly. + *show_positions* is a boolean indicating whether full positions should + be reported instead of only the line numbers. """ self.file = file self.lineno_width = lineno_width From 860a43679e16399723097932a80dc450bffe6910 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Wed, 21 Aug 2024 12:48:47 +0200 Subject: [PATCH 28/30] improve test coverage --- Lib/test/test_dis.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/Lib/test/test_dis.py b/Lib/test/test_dis.py index 94f3cf5eeb11de..028873d18be986 100644 --- a/Lib/test/test_dis.py +++ b/Lib/test/test_dis.py @@ -981,31 +981,39 @@ def f(): PY_CODE_LOCATION_INFO_NO_COLUMNS = 13 PY_CODE_LOCATION_INFO_WITH_COLUMNS = 14 + PY_CODE_LOCATION_INFO_NO_LOCATION = 15 f.__code__ = f.__code__.replace( co_stacksize=1, co_firstlineno=42, co_code=bytes([ dis.opmap["RESUME"], 0, + dis.opmap["NOP"], 0, dis.opmap["RETURN_CONST"], 0, ]), co_linetable=bytes([ (1 << 7) | (PY_CODE_LOCATION_INFO_NO_COLUMNS << 3) | (1 - 1), # 1 code unit (RESUME) - 0, # start line offset is 0 + (1 << 1), # start line offset is 0 (encoded as an svarint) + (1 << 7) + | (PY_CODE_LOCATION_INFO_NO_LOCATION << 3) + | (1 - 1), # 1 code unit (NOP) (1 << 7) | (PY_CODE_LOCATION_INFO_WITH_COLUMNS << 3) | (1 - 1), # 1 code unit (RETURN CONST) - (0 << 1), # start line offset is 0 (encoded as an svarint) - 0, # end line offset is 0 (varint encoded) + (2 << 1), # start line offset is 0 (encoded as an svarint) + 3, # end line offset is 0 (varint encoded) 1, # 1-based start column (reported as COL - 1) 5, # 1-based end column (reported as ENDCOL - 1) ] )) expect = '\n'.join([ - '42:?-42:? RESUME 0', - '42:0-42:4 RETURN_CONST 0 (None)', + '43:?-43:? RESUME 0', + '', + '?:?-?:? NOP', + '', + '45:0-48:4 RETURN_CONST 0 (None)', '', ]) self.do_disassembly_test(f, expect, show_positions=True) From 9dc5ed2be26365ae8bfb48fd8e409c6a0832748b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Wed, 21 Aug 2024 12:53:00 +0200 Subject: [PATCH 29/30] cosmetic adjustements --- Lib/dis.py | 7 +++++-- Lib/test/test_dis.py | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/Lib/dis.py b/Lib/dis.py index 69c4138776f3bd..e39f1b7f7998ae 100644 --- a/Lib/dis.py +++ b/Lib/dis.py @@ -483,8 +483,11 @@ def print_instruction_line(self, instr, mark_as_current): if self.show_positions: # reporting positions instead of just line numbers if instr_positions := instr.positions: - ps = tuple('?' if p is None else p for p in instr_positions) - positions_str = f"{ps[0]}:{ps[2]}-{ps[1]}:{ps[3]}" + if all(p is None for p in instr_positions): + positions_str = _NO_LINENO + else: + ps = tuple('?' if p is None else p for p in instr_positions) + positions_str = f"{ps[0]}:{ps[2]}-{ps[1]}:{ps[3]}" fields.append(f'{positions_str:{lineno_width}}') else: fields.append(' ' * lineno_width) diff --git a/Lib/test/test_dis.py b/Lib/test/test_dis.py index 028873d18be986..5ec06d14af6500 100644 --- a/Lib/test/test_dis.py +++ b/Lib/test/test_dis.py @@ -1011,7 +1011,7 @@ def f(): expect = '\n'.join([ '43:?-43:? RESUME 0', '', - '?:?-?:? NOP', + ' -- NOP', '', '45:0-48:4 RETURN_CONST 0 (None)', '', From 8d8b1450092ba7d268da92ea2aab05b20c444c19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Wed, 21 Aug 2024 12:59:08 +0200 Subject: [PATCH 30/30] cosmetic adjustements --- Lib/dis.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Lib/dis.py b/Lib/dis.py index e39f1b7f7998ae..077c4035ca6511 100644 --- a/Lib/dis.py +++ b/Lib/dis.py @@ -855,7 +855,9 @@ def _get_lineno_width(linestarts): def _get_positions_width(code): # Positions are formatted as 'LINE:COL-ENDLINE:ENDCOL ' (note trailing space). - # A missing component appears as '?', so the minimum width is 8 = 1 + len('?:?-?:?'). + # A missing component appears as '?', and when all components are None, we + # render '_NO_LINENO'. thus the minimum width is 1 + len(_NO_LINENO). + # # If all values are missing, positions are not printed (i.e. positions_width = 0). has_value = False values_width = 0 @@ -865,7 +867,7 @@ def _get_positions_width(code): values_width = max(width, values_width) if has_value: # 3 = number of separators in a normal format - return 1 + max(7, 3 + values_width) + return 1 + max(len(_NO_LINENO), 3 + values_width) return 0 def _disassemble_bytes(code, lasti=-1, linestarts=None,