diff -r 11cf18ec1900 Lib/dis.py --- a/Lib/dis.py Sat Sep 06 22:49:07 2014 +0300 +++ b/Lib/dis.py Sun Sep 07 15:23:45 2014 +1000 @@ -3,6 +3,7 @@ import sys import types import collections +import functools import io from opcode import * @@ -28,7 +29,7 @@ c = compile(source, name, 'exec') return c -def dis(x=None, *, file=None): +def dis(x=None, *, file=None, nested=0): """Disassemble classes, methods, functions, generators, or code. With no argument, disassemble the last traceback. @@ -49,21 +50,21 @@ if isinstance(x1, _have_code): print("Disassembly of %s:" % name, file=file) try: - dis(x1, file=file) + dis(x1, file=file, nested=nested) except TypeError as msg: print("Sorry:", msg, file=file) print(file=file) elif hasattr(x, 'co_code'): # Code object - disassemble(x, file=file) + disassemble(x, file=file, nested=nested) elif isinstance(x, (bytes, bytearray)): # Raw bytecode - _disassemble_bytes(x, file=file) + _disassemble_bytes(x, file=file, nested=nested) elif isinstance(x, str): # Source code - _disassemble_str(x, file=file) + _disassemble_str(x, file=file, nested=nested) else: raise TypeError("don't know how to disassemble %s objects" % type(x).__name__) -def distb(tb=None, *, file=None): +def distb(tb=None, *, file=None, nested=0): """Disassemble a traceback (default: last traceback).""" if tb is None: try: @@ -71,7 +72,7 @@ except AttributeError: raise RuntimeError("no last traceback to disassemble") while tb.tb_next: tb = tb.tb_next - disassemble(tb.tb_frame.f_code, tb.tb_lasti, file=file) + disassemble(tb.tb_frame.f_code, tb.tb_lasti, file=file, nested=nested) # The inspect module interrogates this dictionary to build its # list of CO_* constants. It is also used by pretty_flags to @@ -173,33 +174,63 @@ starts_line - line started by this opcode (if any), otherwise None is_jump_target - True if other code jumps to here, otherwise False """ + # Anatomy of a disassembled line + # 4 chars: spaces or line number + space + # 4 chars: spaces or current line marker ('--> ') + space + # 3 chars: spaces or jump target marker ('>>') + space + # 5 chars: bytecode offset + space + # 21 chars: opcode name + space + # Rest of line: opcode argument and details - def _disassemble(self, lineno_width=3, mark_as_current=False): + _lineno_width = 3 + _opcode_width = 4 + _current_line_marker = '-->' + _jump_target_marker = '>>' + + @classmethod + @functools.lru_cache() + def _get_prefix(cls, show_lineno, nested): + """ Calculate a suitable prefix for display of nested code objects + """ + if not nested: + return '' + prefix_len = len(cls._current_line_marker) + 1 + if show_lineno: + prefix_len += nested * (cls._lineno_width + 1) + prefix_len += nested * (len(cls._jump_target_marker) + 1) + prefix_len += nested * (cls._opcode_width + 1) + return ' ' * prefix_len + + def _disassemble(self, show_lineno=False, mark_as_current=False, + nested=0): """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 + when true, or 3 spaces otherwise (set to None to omit field entirely) + *nested* indicates if this is a nested code object (and how deeply) """ fields = [] # Column: Source code line number - if lineno_width: + if show_lineno: if self.starts_line is not None: - lineno_fmt = "%%%dd" % lineno_width + lineno_fmt = "%%%dd" % self._lineno_width fields.append(lineno_fmt % self.starts_line) else: - fields.append(' ' * lineno_width) + fields.append(' ' * self._lineno_width) # Column: Current instruction indicator - if mark_as_current: - fields.append('-->') - else: - fields.append(' ') + if nested <= 0: + if mark_as_current: + fields.append(self._current_line_marker) + else: + fields.append(' ' * len(self._current_line_marker)) # Column: Jump target marker if self.is_jump_target: - fields.append('>>') + fields.append(self._jump_target_marker) else: - fields.append(' ') + fields.append(' ' * len(self._jump_target_marker)) # Column: Instruction offset from start of code sequence - fields.append(repr(self.offset).rjust(4)) + fields.append(repr(self.offset).rjust(self._opcode_width)) # Column: Opcode name fields.append(self.opname.ljust(20)) # Column: Opcode argument @@ -208,7 +239,8 @@ # Column: Opcode argument details if self.argrepr: fields.append('(' + self.argrepr + ')') - return ' '.join(fields).rstrip() + # Fully disassembled line + return self._get_prefix(show_lineno, nested) +' '.join(fields).rstrip() def get_instructions(x, *, first_line=None): @@ -322,20 +354,19 @@ arg, argval, argrepr, offset, starts_line, is_jump_target) -def disassemble(co, lasti=-1, *, file=None): +def disassemble(co, lasti=-1, *, file=None, nested=0): """Disassemble a code object.""" cell_names = co.co_cellvars + co.co_freevars linestarts = dict(findlinestarts(co)) _disassemble_bytes(co.co_code, lasti, co.co_varnames, co.co_names, - co.co_consts, cell_names, linestarts, file=file) + co.co_consts, cell_names, linestarts, + file=file, nested=nested) def _disassemble_bytes(code, lasti=-1, varnames=None, names=None, constants=None, cells=None, linestarts=None, - *, file=None, line_offset=0): + *, file=None, line_offset=0, nested=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 for instr in _get_instructions_bytes(code, varnames, names, constants, cells, linestarts, line_offset=line_offset): @@ -345,11 +376,21 @@ 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(show_lineno, is_current_instr, nested), + file=file) + # Display nested code objects when referenced + if nested >= 0: + nested_co = instr.argval + if instr.opcode in hasconst and hasattr(nested_co, 'co_name'): + prefix = Instruction._get_prefix(show_lineno, nested+1) + header = "{}Disassembly for {!r}".format(prefix, + nested_co.co_name) + print(header, file=file) + disassemble(nested_co, file=file, nested=nested+1) -def _disassemble_str(source, *, file=None): +def _disassemble_str(source, *, file=None, nested=0): """Compile the source string, then disassemble the code object.""" - disassemble(_try_compile(source, ''), file=file) + disassemble(_try_compile(source, ''), file=file, nested=nested) disco = disassemble # XXX For backwards compatibility diff -r 11cf18ec1900 Lib/test/test_dis.py --- a/Lib/test/test_dis.py Sat Sep 06 22:49:07 2014 +0300 +++ b/Lib/test/test_dis.py Sun Sep 07 15:23:45 2014 +1000 @@ -234,14 +234,16 @@ class DisTests(unittest.TestCase): + maxDiff = None # Usually need the whole diff if anything goes wrong + def get_disassembly(self, func, lasti=-1, wrapper=True): # We want to test the default printing behaviour, not the file arg output = io.StringIO() with contextlib.redirect_stdout(output): if wrapper: - dis.dis(func) + dis.dis(func, nested=-1) else: - dis.disassemble(func, lasti) + dis.disassemble(func, lasti, nested=-1) return output.getvalue() def get_disassemble_as_string(self, func, lasti=-1): @@ -353,9 +355,9 @@ def get_disassembly(self, func, lasti=-1, wrapper=True): output = io.StringIO() if wrapper: - dis.dis(func, file=output) + dis.dis(func, file=output, nested=-1) else: - dis.disassemble(func, lasti, file=output) + dis.disassemble(func, lasti, file=output, nested=-1) return output.getvalue()