]> granicus.if.org Git - python/commitdiff
bpo-22352: Adjust widths in the output of dis.dis() for large line numbers and (...
authorSerhiy Storchaka <storchaka@gmail.com>
Wed, 19 Apr 2017 17:36:31 +0000 (20:36 +0300)
committerGitHub <noreply@github.com>
Wed, 19 Apr 2017 17:36:31 +0000 (20:36 +0300)
instruction offsets.

Add tests for widths of opcode names.

Lib/dis.py
Lib/test/test_dis.py
Misc/NEWS

index f93d5b22d465c87b8a9362526e6cb468603a97df..f3c18a5fde483db53cdc4cd8df63246f5c467a4c 100644 (file)
@@ -175,6 +175,9 @@ _Instruction.offset.__doc__ = "Start index of operation within bytecode sequence
 _Instruction.starts_line.__doc__ = "Line started by this opcode (if any), otherwise None"
 _Instruction.is_jump_target.__doc__ = "True if other code jumps to here, otherwise False"
 
+_OPNAME_WIDTH = 20
+_OPARG_WIDTH = 5
+
 class Instruction(_Instruction):
     """Details for a bytecode operation
 
@@ -189,11 +192,12 @@ class Instruction(_Instruction):
          is_jump_target - True if other code jumps to here, otherwise False
     """
 
-    def _disassemble(self, lineno_width=3, mark_as_current=False):
+    def _disassemble(self, lineno_width=3, mark_as_current=False, offset_width=4):
         """Format instruction details for inclusion in disassembly output
 
         *lineno_width* sets the width of the line number field (0 omits it)
         *mark_as_current* inserts a '-->' marker arrow as part of the line
+        *offset_width* sets the width of the instruction offset field
         """
         fields = []
         # Column: Source code line number
@@ -214,12 +218,12 @@ class Instruction(_Instruction):
         else:
             fields.append('  ')
         # Column: Instruction offset from start of code sequence
-        fields.append(repr(self.offset).rjust(4))
+        fields.append(repr(self.offset).rjust(offset_width))
         # Column: Opcode name
-        fields.append(self.opname.ljust(20))
+        fields.append(self.opname.ljust(_OPNAME_WIDTH))
         # Column: Opcode argument
         if self.arg is not None:
-            fields.append(repr(self.arg).rjust(5))
+            fields.append(repr(self.arg).rjust(_OPARG_WIDTH))
             # Column: Opcode argument details
             if self.argrepr:
                 fields.append('(' + self.argrepr + ')')
@@ -339,8 +343,19 @@ def _disassemble_bytes(code, lasti=-1, varnames=None, names=None,
                        *, file=None, line_offset=0):
     # Omit the line number column entirely if we have no line number info
     show_lineno = linestarts is not None
-    # TODO?: Adjust width upwards if max(linestarts.values()) >= 1000?
-    lineno_width = 3 if show_lineno else 0
+    if show_lineno:
+        maxlineno = max(linestarts.values()) + line_offset
+        if maxlineno >= 1000:
+            lineno_width = len(str(maxlineno))
+        else:
+            lineno_width = 3
+    else:
+        lineno_width = 0
+    maxoffset = len(code) - 2
+    if maxoffset >= 10000:
+        offset_width = len(str(maxoffset))
+    else:
+        offset_width = 4
     for instr in _get_instructions_bytes(code, varnames, names,
                                          constants, cells, linestarts,
                                          line_offset=line_offset):
@@ -350,7 +365,8 @@ def _disassemble_bytes(code, lasti=-1, varnames=None, names=None,
         if new_source_line:
             print(file=file)
         is_current_instr = instr.offset == lasti
-        print(instr._disassemble(lineno_width, is_current_instr), file=file)
+        print(instr._disassemble(lineno_width, is_current_instr, offset_width),
+              file=file)
 
 def _disassemble_str(source, *, file=None):
     """Compile the source string, then disassemble the code object."""
index 980ae16c652741af7b3fa3c6f98266bbb6b15640..e614b718ee35f09960c82257d9ba894ce82bf798 100644 (file)
@@ -175,6 +175,13 @@ _BIG_LINENO_FORMAT = """\
               6 RETURN_VALUE
 """
 
+_BIG_LINENO_FORMAT2 = """\
+%4d           0 LOAD_GLOBAL              0 (spam)
+               2 POP_TOP
+               4 LOAD_CONST               0 (None)
+               6 RETURN_VALUE
+"""
+
 dis_module_expected_results = """\
 Disassembly of f:
   4           0 LOAD_CONST               0 (None)
@@ -360,6 +367,17 @@ class DisTests(unittest.TestCase):
         self.assertEqual(dis.opmap["EXTENDED_ARG"], dis.EXTENDED_ARG)
         self.assertEqual(dis.opmap["STORE_NAME"], dis.HAVE_ARGUMENT)
 
+    def test_widths(self):
+        for opcode, opname in enumerate(dis.opname):
+            if opname in ('BUILD_MAP_UNPACK_WITH_CALL',
+                          'BUILD_TUPLE_UNPACK_WITH_CALL'):
+                continue
+            with self.subTest(opname=opname):
+                width = dis._OPNAME_WIDTH
+                if opcode < dis.HAVE_ARGUMENT:
+                    width += 1 + dis._OPARG_WIDTH
+                self.assertLessEqual(len(opname), width)
+
     def test_dis(self):
         self.do_disassembly_test(_f, dis_f)
 
@@ -387,13 +405,45 @@ class DisTests(unittest.TestCase):
             self.do_disassembly_test(func(i), expected)
 
         # Test some larger ranges too
-        for i in range(300, 5000, 10):
+        for i in range(300, 1000, 10):
             expected = _BIG_LINENO_FORMAT % (i + 2)
             self.do_disassembly_test(func(i), expected)
 
+        for i in range(1000, 5000, 10):
+            expected = _BIG_LINENO_FORMAT2 % (i + 2)
+            self.do_disassembly_test(func(i), expected)
+
         from test import dis_module
         self.do_disassembly_test(dis_module, dis_module_expected_results)
 
+    def test_big_offsets(self):
+        def func(count):
+            namespace = {}
+            func = "def foo(x):\n " + ";".join(["x = x + 1"] * count) + "\n return x"
+            exec(func, namespace)
+            return namespace['foo']
+
+        def expected(count, w):
+            s = ['''\
+           %*d LOAD_FAST                0 (x)
+           %*d LOAD_CONST               1 (1)
+           %*d BINARY_ADD
+           %*d STORE_FAST               0 (x)
+''' % (w, 8*i, w, 8*i + 2, w, 8*i + 4, w, 8*i + 6)
+                 for i in range(count)]
+            s += ['''\
+
+  3        %*d LOAD_FAST                0 (x)
+           %*d RETURN_VALUE
+''' % (w, 8*count, w, 8*count + 2)]
+            s[0] = '  2' + s[0][3:]
+            return ''.join(s)
+
+        for i in range(1, 5):
+            self.do_disassembly_test(func(i), expected(i, 4))
+        self.do_disassembly_test(func(1249), expected(1249, 4))
+        self.do_disassembly_test(func(1250), expected(1250, 5))
+
     def test_disassemble_str(self):
         self.do_disassembly_test(expr_str, dis_expr_str)
         self.do_disassembly_test(simple_stmt_str, dis_simple_stmt_str)
index 4dc7b088b7d480618f61fb2eae81f422b2e9c59e..e57faacf615daa14f88873b7b04327a8f64d8bab 100644 (file)
--- a/Misc/NEWS
+++ b/Misc/NEWS
@@ -313,6 +313,9 @@ Extension Modules
 Library
 -------
 
+- bpo-22352: Column widths in the output of dis.dis() are now adjusted for
+  large line numbers and instruction offsets.
+
 - bpo-30061: Fixed crashes in IOBase methods __next__() and readlines() when
   readline() or __next__() respectively return non-sizeable object.
   Fixed possible other errors caused by not checking results of PyObject_Size(),