Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 90b8e7d

Browse files
committed
Close #19378: address flaws in the new dis module APIs
- confusing line_offset parameter -> first_line parameter - systematically test and fix new file parameter - remove redundant Bytecode.show_info() API - rename Bytecode.display_code() to Bytecode.dis() and have it return the multi-line string rather than printing it directly - eliminated some not-so-helpful helpers from the bytecode_helper test support module Also fixed a longstanding defect (worked around in the test suite) where lines emitted by the dis module could include trailing white space. That no longer happens, allowing the formatting tests to be simplified to use plain string comparisons.
1 parent e0881f4 commit 90b8e7d

5 files changed

Lines changed: 168 additions & 123 deletions

File tree

Doc/library/dis.rst

Lines changed: 23 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -44,36 +44,39 @@ The bytecode analysis API allows pieces of Python code to be wrapped in a
4444
:class:`Bytecode` object that provides easy access to details of the
4545
compiled code.
4646

47-
.. class:: Bytecode
47+
.. class:: Bytecode(x, *, first_line=None)
4848

49-
The bytecode operations of a piece of code
49+
Analyse the bytecode corresponding to a function, method, string of
50+
source code, or a code object (as returned by :func:`compile`).
5051

51-
This is a convenient wrapper around many of the functions listed below.
52-
Instantiate it with a function, method, string of code, or a code object
53-
(as returned by :func:`compile`).
52+
This is a convenience wrapper around many of the functions listed below,
53+
most notably :func:`get_instructions`, as iterating over a
54+
:class:`ByteCode` instance yields the bytecode operations as
55+
:class:`Instruction` instances.
5456

55-
Iterating over this yields the bytecode operations as :class:`Instruction`
56-
instances.
57+
If *first_line* is not None, it indicates the line number that should
58+
be reported for the first source line in the disassembled code.
59+
Otherwise, the source line information (if any) is taken directly from
60+
the disassembled code object.
5761

5862
.. data:: codeobj
5963

6064
The compiled code object.
6165

62-
.. method:: display_code(*, file=None)
66+
.. data:: first_line
6367

64-
Print a formatted view of the bytecode operations, like :func:`dis`.
68+
The first source line of the code object (if available)
69+
70+
.. method:: dis()
71+
72+
Return a formatted view of the bytecode operations (the same as
73+
printed by :func:`dis`, but returned as a multi-line string).
6574

6675
.. method:: info()
6776

6877
Return a formatted multi-line string with detailed information about the
6978
code object, like :func:`code_info`.
7079

71-
.. method:: show_info(*, file=None)
72-
73-
Print the information about the code object as returned by :meth:`info`.
74-
75-
.. versionadded:: 3.4
76-
7780
Example::
7881

7982
>>> bytecode = dis.Bytecode(myfunc)
@@ -176,16 +179,18 @@ object isn't useful:
176179
Added ``file`` parameter
177180

178181

179-
.. function:: get_instructions(x, *, line_offset=0)
182+
.. function:: get_instructions(x, *, first_line=None)
180183

181184
Return an iterator over the instructions in the supplied function, method,
182185
source code string or code object.
183186

184187
The iterator generates a series of :class:`Instruction` named tuples
185188
giving the details of each operation in the supplied code.
186189

187-
The given *line_offset* is added to the ``starts_line`` attribute of any
188-
instructions that start a new line.
190+
If *first_line* is not None, it indicates the line number that should
191+
be reported for the first source line in the disassembled code.
192+
Otherwise, the source line information (if any) is taken directly from
193+
the disassembled code object.
189194

190195
.. versionadded:: 3.4
191196

Lib/dis.py

Lines changed: 42 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import sys
44
import types
55
import collections
6+
import io
67

78
from opcode import *
89
from opcode import __all__ as _opcodes_all
@@ -34,7 +35,7 @@ def dis(x=None, *, file=None):
3435
3536
"""
3637
if x is None:
37-
distb()
38+
distb(file=file)
3839
return
3940
if hasattr(x, '__func__'): # Method
4041
x = x.__func__
@@ -46,7 +47,7 @@ def dis(x=None, *, file=None):
4647
if isinstance(x1, _have_code):
4748
print("Disassembly of %s:" % name, file=file)
4849
try:
49-
dis(x1)
50+
dis(x1, file=file)
5051
except TypeError as msg:
5152
print("Sorry:", msg, file=file)
5253
print(file=file)
@@ -203,21 +204,27 @@ def _disassemble(self, lineno_width=3, mark_as_current=False):
203204
# Column: Opcode argument details
204205
if self.argrepr:
205206
fields.append('(' + self.argrepr + ')')
206-
return ' '.join(fields)
207+
return ' '.join(fields).rstrip()
207208

208209

209-
def get_instructions(x, *, line_offset=0):
210+
def get_instructions(x, *, first_line=None):
210211
"""Iterator for the opcodes in methods, functions or code
211212
212213
Generates a series of Instruction named tuples giving the details of
213214
each operations in the supplied code.
214215
215-
The given line offset is added to the 'starts_line' attribute of any
216-
instructions that start a new line.
216+
If *first_line* is not None, it indicates the line number that should
217+
be reported for the first source line in the disassembled code.
218+
Otherwise, the source line information (if any) is taken directly from
219+
the disassembled code object.
217220
"""
218221
co = _get_code_object(x)
219222
cell_names = co.co_cellvars + co.co_freevars
220223
linestarts = dict(findlinestarts(co))
224+
if first_line is not None:
225+
line_offset = first_line - co.co_firstlineno
226+
else:
227+
line_offset = 0
221228
return _get_instructions_bytes(co.co_code, co.co_varnames, co.co_names,
222229
co.co_consts, cell_names, linestarts,
223230
line_offset)
@@ -320,13 +327,14 @@ def disassemble(co, lasti=-1, *, file=None):
320327

321328
def _disassemble_bytes(code, lasti=-1, varnames=None, names=None,
322329
constants=None, cells=None, linestarts=None,
323-
*, file=None):
330+
*, file=None, line_offset=0):
324331
# Omit the line number column entirely if we have no line number info
325332
show_lineno = linestarts is not None
326333
# TODO?: Adjust width upwards if max(linestarts.values()) >= 1000?
327334
lineno_width = 3 if show_lineno else 0
328335
for instr in _get_instructions_bytes(code, varnames, names,
329-
constants, cells, linestarts):
336+
constants, cells, linestarts,
337+
line_offset=line_offset):
330338
new_source_line = (show_lineno and
331339
instr.starts_line is not None and
332340
instr.offset > 0)
@@ -398,40 +406,44 @@ class Bytecode:
398406
399407
Iterating over this yields the bytecode operations as Instruction instances.
400408
"""
401-
def __init__(self, x):
402-
self.codeobj = _get_code_object(x)
403-
self.cell_names = self.codeobj.co_cellvars + self.codeobj.co_freevars
404-
self.linestarts = dict(findlinestarts(self.codeobj))
405-
self.line_offset = 0
406-
self.original_object = x
409+
def __init__(self, x, *, first_line=None):
410+
self.codeobj = co = _get_code_object(x)
411+
if first_line is None:
412+
self.first_line = co.co_firstlineno
413+
self._line_offset = 0
414+
else:
415+
self.first_line = first_line
416+
self._line_offset = first_line - co.co_firstlineno
417+
self._cell_names = co.co_cellvars + co.co_freevars
418+
self._linestarts = dict(findlinestarts(co))
419+
self._original_object = x
407420

408421
def __iter__(self):
409422
co = self.codeobj
410423
return _get_instructions_bytes(co.co_code, co.co_varnames, co.co_names,
411-
co.co_consts, self.cell_names,
412-
self.linestarts, self.line_offset)
424+
co.co_consts, self._cell_names,
425+
self._linestarts,
426+
line_offset=self._line_offset)
413427

414428
def __repr__(self):
415-
return "{}({!r})".format(self.__class__.__name__, self.original_object)
429+
return "{}({!r})".format(self.__class__.__name__,
430+
self._original_object)
416431

417432
def info(self):
418433
"""Return formatted information about the code object."""
419434
return _format_code_info(self.codeobj)
420435

421-
def show_info(self, *, file=None):
422-
"""Print the information about the code object as returned by info()."""
423-
print(self.info(), file=file)
424-
425-
def display_code(self, *, file=None):
426-
"""Print a formatted view of the bytecode operations.
427-
"""
436+
def dis(self):
437+
"""Return a formatted view of the bytecode operations."""
428438
co = self.codeobj
429-
return _disassemble_bytes(co.co_code, varnames=co.co_varnames,
430-
names=co.co_names, constants=co.co_consts,
431-
cells=self.cell_names,
432-
linestarts=self.linestarts,
433-
file=file
434-
)
439+
with io.StringIO() as output:
440+
_disassemble_bytes(co.co_code, varnames=co.co_varnames,
441+
names=co.co_names, constants=co.co_consts,
442+
cells=self._cell_names,
443+
linestarts=self._linestarts,
444+
line_offset=self._line_offset,
445+
file=output)
446+
return output.getvalue()
435447

436448

437449
def _test():

Lib/test/bytecode_helper.py

Lines changed: 0 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -14,37 +14,6 @@ def get_disassembly_as_string(self, co):
1414
dis.dis(co, file=s)
1515
return s.getvalue()
1616

17-
def assertInstructionMatches(self, instr, expected, *, line_offset=0):
18-
# Deliberately test opname first, since that gives a more
19-
# meaningful error message than testing opcode
20-
self.assertEqual(instr.opname, expected.opname)
21-
self.assertEqual(instr.opcode, expected.opcode)
22-
self.assertEqual(instr.arg, expected.arg)
23-
self.assertEqual(instr.argval, expected.argval)
24-
self.assertEqual(instr.argrepr, expected.argrepr)
25-
self.assertEqual(instr.offset, expected.offset)
26-
if expected.starts_line is None:
27-
self.assertIsNone(instr.starts_line)
28-
else:
29-
self.assertEqual(instr.starts_line,
30-
expected.starts_line + line_offset)
31-
self.assertEqual(instr.is_jump_target, expected.is_jump_target)
32-
33-
34-
def assertBytecodeExactlyMatches(self, x, expected, *, line_offset=0):
35-
"""Throws AssertionError if any discrepancy is found in bytecode
36-
37-
*x* is the object to be introspected
38-
*expected* is a list of dis.Instruction objects
39-
40-
Set *line_offset* as appropriate to adjust for the location of the
41-
object to be disassembled within the test file. If the expected list
42-
assumes the first line is line 1, then an appropriate offset would be
43-
``1 - f.__code__.co_firstlineno``.
44-
"""
45-
actual = dis.get_instructions(x, line_offset=line_offset)
46-
self.assertEqual(list(actual), expected)
47-
4817
def assertInBytecode(self, x, opname, argval=_UNSPECIFIED):
4918
"""Returns instr if op is found, otherwise throws AssertionError"""
5019
for instr in dis.get_instructions(x):

0 commit comments

Comments
 (0)