Skip to content

Commit b1d3bd2

Browse files
authored
gh-123165: make dis functions render positions on demand (#123168)
1 parent 94036e4 commit b1d3bd2

File tree

5 files changed

+209
-37
lines changed

5 files changed

+209
-37
lines changed

Doc/library/dis.rst

Lines changed: 40 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,10 @@ interpreter.
5656
for jump targets and exception handlers. The ``-O`` command line
5757
option and the ``show_offsets`` argument were added.
5858

59+
.. versionchanged:: 3.14
60+
The :option:`-P <dis --show-positions>` command-line option
61+
and the ``show_positions`` argument were added.
62+
5963
Example: Given the function :func:`!myfunc`::
6064

6165
def myfunc(alist):
@@ -85,7 +89,7 @@ The :mod:`dis` module can be invoked as a script from the command line:
8589

8690
.. code-block:: sh
8791
88-
python -m dis [-h] [-C] [-O] [infile]
92+
python -m dis [-h] [-C] [-O] [-P] [infile]
8993
9094
The following options are accepted:
9195

@@ -103,6 +107,10 @@ The following options are accepted:
103107

104108
Show offsets of instructions.
105109

110+
.. cmdoption:: -P, --show-positions
111+
112+
Show positions of instructions in the source code.
113+
106114
If :file:`infile` is specified, its disassembled code will be written to stdout.
107115
Otherwise, disassembly is performed on compiled source code received from stdin.
108116

@@ -116,7 +124,8 @@ The bytecode analysis API allows pieces of Python code to be wrapped in a
116124
code.
117125

118126
.. class:: Bytecode(x, *, first_line=None, current_offset=None,\
119-
show_caches=False, adaptive=False, show_offsets=False)
127+
show_caches=False, adaptive=False, show_offsets=False,\
128+
show_positions=False)
120129

121130
Analyse the bytecode corresponding to a function, generator, asynchronous
122131
generator, coroutine, method, string of source code, or a code object (as
@@ -144,6 +153,9 @@ code.
144153
If *show_offsets* is ``True``, :meth:`.dis` will include instruction
145154
offsets in the output.
146155

156+
If *show_positions* is ``True``, :meth:`.dis` will include instruction
157+
source code positions in the output.
158+
147159
.. classmethod:: from_traceback(tb, *, show_caches=False)
148160

149161
Construct a :class:`Bytecode` instance from the given traceback, setting
@@ -173,6 +185,12 @@ code.
173185
.. versionchanged:: 3.11
174186
Added the *show_caches* and *adaptive* parameters.
175187

188+
.. versionchanged:: 3.13
189+
Added the *show_offsets* parameter
190+
191+
.. versionchanged:: 3.14
192+
Added the *show_positions* parameter.
193+
176194
Example:
177195

178196
.. doctest::
@@ -226,7 +244,8 @@ operation is being performed, so the intermediate analysis object isn't useful:
226244
Added *file* parameter.
227245

228246

229-
.. function:: dis(x=None, *, file=None, depth=None, show_caches=False, adaptive=False)
247+
.. function:: dis(x=None, *, file=None, depth=None, show_caches=False,\
248+
adaptive=False, show_offsets=False, show_positions=False)
230249

231250
Disassemble the *x* object. *x* can denote either a module, a class, a
232251
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:
265284
.. versionchanged:: 3.11
266285
Added the *show_caches* and *adaptive* parameters.
267286

287+
.. versionchanged:: 3.13
288+
Added the *show_offsets* parameter.
289+
290+
.. versionchanged:: 3.14
291+
Added the *show_positions* parameter.
268292

269-
.. function:: distb(tb=None, *, file=None, show_caches=False, adaptive=False,
270-
show_offset=False)
293+
.. function:: distb(tb=None, *, file=None, show_caches=False, adaptive=False,\
294+
show_offset=False, show_positions=False)
271295

272296
Disassemble the top-of-stack function of a traceback, using the last
273297
traceback if none was passed. The instruction causing the exception is
@@ -285,14 +309,19 @@ operation is being performed, so the intermediate analysis object isn't useful:
285309
.. versionchanged:: 3.13
286310
Added the *show_offsets* parameter.
287311

312+
.. versionchanged:: 3.14
313+
Added the *show_positions* parameter.
314+
288315
.. function:: disassemble(code, lasti=-1, *, file=None, show_caches=False, adaptive=False)
289-
disco(code, lasti=-1, *, file=None, show_caches=False, adaptive=False,
290-
show_offsets=False)
316+
disco(code, lasti=-1, *, file=None, show_caches=False, adaptive=False,\
317+
show_offsets=False, show_positions=False)
291318
292319
Disassemble a code object, indicating the last instruction if *lasti* was
293320
provided. The output is divided in the following columns:
294321

295-
#. the line number, for the first instruction of each line
322+
#. the source code location of the instruction. Complete location information
323+
is shown if *show_positions* is true. Otherwise (the default) only the
324+
line number is displayed.
296325
#. the current instruction, indicated as ``-->``,
297326
#. a labelled instruction, indicated with ``>>``,
298327
#. the address of the instruction,
@@ -315,6 +344,9 @@ operation is being performed, so the intermediate analysis object isn't useful:
315344
.. versionchanged:: 3.13
316345
Added the *show_offsets* parameter.
317346

347+
.. versionchanged:: 3.14
348+
Added the *show_positions* parameter.
349+
318350
.. function:: get_instructions(x, *, first_line=None, show_caches=False, adaptive=False)
319351

320352
Return an iterator over the instructions in the supplied function, method,

Doc/whatsnew/3.14.rst

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,22 @@ ast
110110

111111
(Contributed by Bénédikt Tran in :gh:`121141`.)
112112

113+
dis
114+
---
115+
116+
* Added support for rendering full source location information of
117+
:class:`instructions <dis.Instruction>`, rather than only the line number.
118+
This feature is added to the following interfaces via the ``show_positions``
119+
keyword argument:
120+
121+
- :class:`dis.Bytecode`,
122+
- :func:`dis.dis`, :func:`dis.distb`, and
123+
- :func:`dis.disassemble`.
124+
125+
This feature is also exposed via :option:`dis --show-positions`.
126+
127+
(Contributed by Bénédikt Tran in :gh:`123165`.)
128+
113129
fractions
114130
---------
115131

Lib/dis.py

Lines changed: 72 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ def _try_compile(source, name):
8080
return compile(source, name, 'exec')
8181

8282
def dis(x=None, *, file=None, depth=None, show_caches=False, adaptive=False,
83-
show_offsets=False):
83+
show_offsets=False, show_positions=False):
8484
"""Disassemble classes, methods, functions, and other compiled objects.
8585
8686
With no argument, disassemble the last traceback.
@@ -91,7 +91,7 @@ def dis(x=None, *, file=None, depth=None, show_caches=False, adaptive=False,
9191
"""
9292
if x is None:
9393
distb(file=file, show_caches=show_caches, adaptive=adaptive,
94-
show_offsets=show_offsets)
94+
show_offsets=show_offsets, show_positions=show_positions)
9595
return
9696
# Extract functions from methods.
9797
if hasattr(x, '__func__'):
@@ -112,12 +112,12 @@ def dis(x=None, *, file=None, depth=None, show_caches=False, adaptive=False,
112112
if isinstance(x1, _have_code):
113113
print("Disassembly of %s:" % name, file=file)
114114
try:
115-
dis(x1, file=file, depth=depth, show_caches=show_caches, adaptive=adaptive, show_offsets=show_offsets)
115+
dis(x1, file=file, depth=depth, show_caches=show_caches, adaptive=adaptive, show_offsets=show_offsets, show_positions=show_positions)
116116
except TypeError as msg:
117117
print("Sorry:", msg, file=file)
118118
print(file=file)
119119
elif hasattr(x, 'co_code'): # Code object
120-
_disassemble_recursive(x, file=file, depth=depth, show_caches=show_caches, adaptive=adaptive, show_offsets=show_offsets)
120+
_disassemble_recursive(x, file=file, depth=depth, show_caches=show_caches, adaptive=adaptive, show_offsets=show_offsets, show_positions=show_positions)
121121
elif isinstance(x, (bytes, bytearray)): # Raw bytecode
122122
labels_map = _make_labels_map(x)
123123
label_width = 4 + len(str(len(labels_map)))
@@ -128,12 +128,12 @@ def dis(x=None, *, file=None, depth=None, show_caches=False, adaptive=False,
128128
arg_resolver = ArgResolver(labels_map=labels_map)
129129
_disassemble_bytes(x, arg_resolver=arg_resolver, formatter=formatter)
130130
elif isinstance(x, str): # Source code
131-
_disassemble_str(x, file=file, depth=depth, show_caches=show_caches, adaptive=adaptive, show_offsets=show_offsets)
131+
_disassemble_str(x, file=file, depth=depth, show_caches=show_caches, adaptive=adaptive, show_offsets=show_offsets, show_positions=show_positions)
132132
else:
133133
raise TypeError("don't know how to disassemble %s objects" %
134134
type(x).__name__)
135135

136-
def distb(tb=None, *, file=None, show_caches=False, adaptive=False, show_offsets=False):
136+
def distb(tb=None, *, file=None, show_caches=False, adaptive=False, show_offsets=False, show_positions=False):
137137
"""Disassemble a traceback (default: last traceback)."""
138138
if tb is None:
139139
try:
@@ -144,7 +144,7 @@ def distb(tb=None, *, file=None, show_caches=False, adaptive=False, show_offsets
144144
except AttributeError:
145145
raise RuntimeError("no last traceback to disassemble") from None
146146
while tb.tb_next: tb = tb.tb_next
147-
disassemble(tb.tb_frame.f_code, tb.tb_lasti, file=file, show_caches=show_caches, adaptive=adaptive, show_offsets=show_offsets)
147+
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)
148148

149149
# The inspect module interrogates this dictionary to build its
150150
# list of CO_* constants. It is also used by pretty_flags to
@@ -427,21 +427,25 @@ def __str__(self):
427427
class Formatter:
428428

429429
def __init__(self, file=None, lineno_width=0, offset_width=0, label_width=0,
430-
line_offset=0, show_caches=False):
430+
line_offset=0, show_caches=False, *, show_positions=False):
431431
"""Create a Formatter
432432
433433
*file* where to write the output
434-
*lineno_width* sets the width of the line number field (0 omits it)
434+
*lineno_width* sets the width of the source location field (0 omits it).
435+
Should be large enough for a line number or full positions (depending
436+
on the value of *show_positions*).
435437
*offset_width* sets the width of the instruction offset field
436438
*label_width* sets the width of the label field
437439
*show_caches* is a boolean indicating whether to display cache lines
438-
440+
*show_positions* is a boolean indicating whether full positions should
441+
be reported instead of only the line numbers.
439442
"""
440443
self.file = file
441444
self.lineno_width = lineno_width
442445
self.offset_width = offset_width
443446
self.label_width = label_width
444447
self.show_caches = show_caches
448+
self.show_positions = show_positions
445449

446450
def print_instruction(self, instr, mark_as_current=False):
447451
self.print_instruction_line(instr, mark_as_current)
@@ -474,15 +478,27 @@ def print_instruction_line(self, instr, mark_as_current):
474478
print(file=self.file)
475479

476480
fields = []
477-
# Column: Source code line number
481+
# Column: Source code locations information
478482
if lineno_width:
479-
if instr.starts_line:
480-
lineno_fmt = "%%%dd" if instr.line_number is not None else "%%%ds"
481-
lineno_fmt = lineno_fmt % lineno_width
482-
lineno = _NO_LINENO if instr.line_number is None else instr.line_number
483-
fields.append(lineno_fmt % lineno)
483+
if self.show_positions:
484+
# reporting positions instead of just line numbers
485+
if instr_positions := instr.positions:
486+
if all(p is None for p in instr_positions):
487+
positions_str = _NO_LINENO
488+
else:
489+
ps = tuple('?' if p is None else p for p in instr_positions)
490+
positions_str = f"{ps[0]}:{ps[2]}-{ps[1]}:{ps[3]}"
491+
fields.append(f'{positions_str:{lineno_width}}')
492+
else:
493+
fields.append(' ' * lineno_width)
484494
else:
485-
fields.append(' ' * lineno_width)
495+
if instr.starts_line:
496+
lineno_fmt = "%%%dd" if instr.line_number is not None else "%%%ds"
497+
lineno_fmt = lineno_fmt % lineno_width
498+
lineno = _NO_LINENO if instr.line_number is None else instr.line_number
499+
fields.append(lineno_fmt % lineno)
500+
else:
501+
fields.append(' ' * lineno_width)
486502
# Column: Label
487503
if instr.label is not None:
488504
lbl = f"L{instr.label}:"
@@ -769,17 +785,22 @@ def _get_instructions_bytes(code, linestarts=None, line_offset=0, co_positions=N
769785

770786

771787
def disassemble(co, lasti=-1, *, file=None, show_caches=False, adaptive=False,
772-
show_offsets=False):
788+
show_offsets=False, show_positions=False):
773789
"""Disassemble a code object."""
774790
linestarts = dict(findlinestarts(co))
775791
exception_entries = _parse_exception_table(co)
792+
if show_positions:
793+
lineno_width = _get_positions_width(co)
794+
else:
795+
lineno_width = _get_lineno_width(linestarts)
776796
labels_map = _make_labels_map(co.co_code, exception_entries=exception_entries)
777797
label_width = 4 + len(str(len(labels_map)))
778798
formatter = Formatter(file=file,
779-
lineno_width=_get_lineno_width(linestarts),
799+
lineno_width=lineno_width,
780800
offset_width=len(str(max(len(co.co_code) - 2, 9999))) if show_offsets else 0,
781801
label_width=label_width,
782-
show_caches=show_caches)
802+
show_caches=show_caches,
803+
show_positions=show_positions)
783804
arg_resolver = ArgResolver(co_consts=co.co_consts,
784805
names=co.co_names,
785806
varname_from_oparg=co._varname_from_oparg,
@@ -788,8 +809,8 @@ def disassemble(co, lasti=-1, *, file=None, show_caches=False, adaptive=False,
788809
exception_entries=exception_entries, co_positions=co.co_positions(),
789810
original_code=co.co_code, arg_resolver=arg_resolver, formatter=formatter)
790811

791-
def _disassemble_recursive(co, *, file=None, depth=None, show_caches=False, adaptive=False, show_offsets=False):
792-
disassemble(co, file=file, show_caches=show_caches, adaptive=adaptive, show_offsets=show_offsets)
812+
def _disassemble_recursive(co, *, file=None, depth=None, show_caches=False, adaptive=False, show_offsets=False, show_positions=False):
813+
disassemble(co, file=file, show_caches=show_caches, adaptive=adaptive, show_offsets=show_offsets, show_positions=show_positions)
793814
if depth is None or depth > 0:
794815
if depth is not None:
795816
depth = depth - 1
@@ -799,7 +820,7 @@ def _disassemble_recursive(co, *, file=None, depth=None, show_caches=False, adap
799820
print("Disassembly of %r:" % (x,), file=file)
800821
_disassemble_recursive(
801822
x, file=file, depth=depth, show_caches=show_caches,
802-
adaptive=adaptive, show_offsets=show_offsets
823+
adaptive=adaptive, show_offsets=show_offsets, show_positions=show_positions
803824
)
804825

805826

@@ -832,6 +853,22 @@ def _get_lineno_width(linestarts):
832853
lineno_width = len(_NO_LINENO)
833854
return lineno_width
834855

856+
def _get_positions_width(code):
857+
# Positions are formatted as 'LINE:COL-ENDLINE:ENDCOL ' (note trailing space).
858+
# A missing component appears as '?', and when all components are None, we
859+
# render '_NO_LINENO'. thus the minimum width is 1 + len(_NO_LINENO).
860+
#
861+
# If all values are missing, positions are not printed (i.e. positions_width = 0).
862+
has_value = False
863+
values_width = 0
864+
for positions in code.co_positions():
865+
has_value |= any(isinstance(p, int) for p in positions)
866+
width = sum(1 if p is None else len(str(p)) for p in positions)
867+
values_width = max(width, values_width)
868+
if has_value:
869+
# 3 = number of separators in a normal format
870+
return 1 + max(len(_NO_LINENO), 3 + values_width)
871+
return 0
835872

836873
def _disassemble_bytes(code, lasti=-1, linestarts=None,
837874
*, line_offset=0, exception_entries=(),
@@ -978,7 +1015,7 @@ class Bytecode:
9781015
9791016
Iterating over this yields the bytecode operations as Instruction instances.
9801017
"""
981-
def __init__(self, x, *, first_line=None, current_offset=None, show_caches=False, adaptive=False, show_offsets=False):
1018+
def __init__(self, x, *, first_line=None, current_offset=None, show_caches=False, adaptive=False, show_offsets=False, show_positions=False):
9821019
self.codeobj = co = _get_code_object(x)
9831020
if first_line is None:
9841021
self.first_line = co.co_firstlineno
@@ -993,6 +1030,7 @@ def __init__(self, x, *, first_line=None, current_offset=None, show_caches=False
9931030
self.show_caches = show_caches
9941031
self.adaptive = adaptive
9951032
self.show_offsets = show_offsets
1033+
self.show_positions = show_positions
9961034

9971035
def __iter__(self):
9981036
co = self.codeobj
@@ -1036,16 +1074,19 @@ def dis(self):
10361074
with io.StringIO() as output:
10371075
code = _get_code_array(co, self.adaptive)
10381076
offset_width = len(str(max(len(code) - 2, 9999))) if self.show_offsets else 0
1039-
1040-
1077+
if self.show_positions:
1078+
lineno_width = _get_positions_width(co)
1079+
else:
1080+
lineno_width = _get_lineno_width(self._linestarts)
10411081
labels_map = _make_labels_map(co.co_code, self.exception_entries)
10421082
label_width = 4 + len(str(len(labels_map)))
10431083
formatter = Formatter(file=output,
1044-
lineno_width=_get_lineno_width(self._linestarts),
1084+
lineno_width=lineno_width,
10451085
offset_width=offset_width,
10461086
label_width=label_width,
10471087
line_offset=self._line_offset,
1048-
show_caches=self.show_caches)
1088+
show_caches=self.show_caches,
1089+
show_positions=self.show_positions)
10491090

10501091
arg_resolver = ArgResolver(co_consts=co.co_consts,
10511092
names=co.co_names,
@@ -1071,6 +1112,8 @@ def main():
10711112
help='show inline caches')
10721113
parser.add_argument('-O', '--show-offsets', action='store_true',
10731114
help='show instruction offsets')
1115+
parser.add_argument('-P', '--show-positions', action='store_true',
1116+
help='show instruction positions')
10741117
parser.add_argument('infile', nargs='?', default='-')
10751118
args = parser.parse_args()
10761119
if args.infile == '-':
@@ -1081,7 +1124,7 @@ def main():
10811124
with open(args.infile, 'rb') as infile:
10821125
source = infile.read()
10831126
code = compile(source, name, "exec")
1084-
dis(code, show_caches=args.show_caches, show_offsets=args.show_offsets)
1127+
dis(code, show_caches=args.show_caches, show_offsets=args.show_offsets, show_positions=args.show_positions)
10851128

10861129
if __name__ == "__main__":
10871130
main()

0 commit comments

Comments
 (0)