diff --git a/Doc/library/dis.rst b/Doc/library/dis.rst index f82dc40e0931fe..d8797151ff2f10 100644 --- a/Doc/library/dis.rst +++ b/Doc/library/dis.rst @@ -138,7 +138,7 @@ operation is being performed, so the intermediate analysis object isn't useful: Added *file* parameter. -.. function:: dis(x=None, *, file=None) +.. function:: dis(x=None, *, file=None, recursive=False) Disassemble the *x* object. *x* can denote either a module, a class, a method, a function, a generator, a code object, a string of source code or @@ -152,9 +152,16 @@ operation is being performed, so the intermediate analysis object isn't useful: The disassembly is written as text to the supplied *file* argument if provided and to ``sys.stdout`` otherwise. + If *recursive* is true, also recursively disassemble internal code objects + (the code of comprehensions, generator expressions and local functions, and + the code used for building local classes). + .. versionchanged:: 3.4 Added *file* parameter. + .. versionchanged:: 3.7 + Added *recursive* parameter. + .. function:: distb(tb=None, *, file=None) @@ -169,8 +176,8 @@ operation is being performed, so the intermediate analysis object isn't useful: Added *file* parameter. -.. function:: disassemble(code, lasti=-1, *, file=None) - disco(code, lasti=-1, *, file=None) +.. function:: disassemble(code, lasti=-1, *, file=None, recursive=False) + disco(code, lasti=-1, *, file=None, recursive=False) Disassemble a code object, indicating the last instruction if *lasti* was provided. The output is divided in the following columns: @@ -189,9 +196,16 @@ operation is being performed, so the intermediate analysis object isn't useful: The disassembly is written as text to the supplied *file* argument if provided and to ``sys.stdout`` otherwise. + If *recursive* is true, also recursively disassemble internal code objects + (the code of comprehensions, generator expressions and local functions, and + the code used for building local classes). + .. versionchanged:: 3.4 Added *file* parameter. + .. versionchanged:: 3.7 + Added *recursive* parameter. + .. function:: get_instructions(x, *, first_line=None) diff --git a/Doc/whatsnew/3.7.rst b/Doc/whatsnew/3.7.rst index 761c85fd22084b..4de2de1bc7ff4a 100644 --- a/Doc/whatsnew/3.7.rst +++ b/Doc/whatsnew/3.7.rst @@ -124,6 +124,14 @@ contextlib :func:`contextlib.asynccontextmanager` has been added. (Contributed by Jelle Zijlstra in :issue:`29679`.) +dis +--- + +The :func:`~dis.dis` and :func:`~dis.disassemble` functions now are able to +disassemble internal code objects (the code of comprehensions, generator +expressions and local functions, and the code used for building local +classes). (Contributed by Serhiy Storchaka in :issue:`11822`.) + distutils --------- diff --git a/Lib/dis.py b/Lib/dis.py index f3c18a5fde483d..44719af621feda 100644 --- a/Lib/dis.py +++ b/Lib/dis.py @@ -31,7 +31,7 @@ def _try_compile(source, name): c = compile(source, name, 'exec') return c -def dis(x=None, *, file=None): +def dis(x=None, *, file=None, recursive=False): """Disassemble classes, methods, functions, generators, or code. With no argument, disassemble the last traceback. @@ -52,16 +52,16 @@ def dis(x=None, *, file=None): if isinstance(x1, _have_code): print("Disassembly of %s:" % name, file=file) try: - dis(x1, file=file) + dis(x1, file=file, recursive=recursive) 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, recursive=recursive) elif isinstance(x, (bytes, bytearray)): # Raw bytecode _disassemble_bytes(x, file=file) elif isinstance(x, str): # Source code - _disassemble_str(x, file=file) + _disassemble_str(x, file=file, recursive=recursive) else: raise TypeError("don't know how to disassemble %s objects" % type(x).__name__) @@ -331,12 +331,18 @@ def _get_instructions_bytes(code, varnames=None, names=None, constants=None, arg, argval, argrepr, offset, starts_line, is_jump_target) -def disassemble(co, lasti=-1, *, file=None): +def disassemble(co, lasti=-1, *, file=None, recursive=False): """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) + if recursive: + for x in co.co_consts: + if hasattr(x, 'co_code'): + print(file=file) + print("Disassembly of %r:" % (x,), file=file) + disassemble(x, file=file, recursive=True) def _disassemble_bytes(code, lasti=-1, varnames=None, names=None, constants=None, cells=None, linestarts=None, @@ -368,9 +374,9 @@ def _disassemble_bytes(code, lasti=-1, varnames=None, names=None, print(instr._disassemble(lineno_width, is_current_instr, offset_width), file=file) -def _disassemble_str(source, *, file=None): +def _disassemble_str(source, **kwargs): """Compile the source string, then disassemble the code object.""" - disassemble(_try_compile(source, ''), file=file) + disassemble(_try_compile(source, ''), **kwargs) disco = disassemble # XXX For backwards compatibility @@ -495,12 +501,13 @@ def _test(): import argparse parser = argparse.ArgumentParser() + parser.add_argument('-r', '--recursive', action='store_true') parser.add_argument('infile', type=argparse.FileType(), nargs='?', default='-') args = parser.parse_args() with args.infile as infile: source = infile.read() code = compile(source, args.infile.name, "exec") - dis(code) + dis(code, recursive=args.recursive) if __name__ == "__main__": _test() diff --git a/Lib/test/test_dis.py b/Lib/test/test_dis.py index e614b718ee35f0..132412431ef17d 100644 --- a/Lib/test/test_dis.py +++ b/Lib/test/test_dis.py @@ -331,16 +331,47 @@ def _fstring(a, b, c, d): def _g(x): yield x +def _h(y): + def foo(x): + '''funcdoc''' + return x+y + return foo + +dis_nested = """\ +%3d 0 LOAD_CLOSURE 0 (y) + 2 BUILD_TUPLE 1 + 4 LOAD_CONST 1 () + 6 LOAD_CONST 2 ('_h..foo') + 8 MAKE_FUNCTION 8 + 10 STORE_FAST 1 (foo) + +%3d 12 LOAD_FAST 1 (foo) + 14 RETURN_VALUE + +Disassembly of : +%3d 0 LOAD_FAST 0 (x) + 2 LOAD_DEREF 0 (y) + 4 BINARY_ADD + 6 RETURN_VALUE +""" % (_h.__code__.co_firstlineno + 1, + __file__, + _h.__code__.co_firstlineno + 1, + _h.__code__.co_firstlineno + 4, + __file__, + _h.__code__.co_firstlineno + 1, + _h.__code__.co_firstlineno + 3, +) + class DisTests(unittest.TestCase): - def get_disassembly(self, func, lasti=-1, wrapper=True): + def get_disassembly(self, func, lasti=-1, wrapper=True, **kwargs): # 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, **kwargs) else: - dis.disassemble(func, lasti) + dis.disassemble(func, lasti, **kwargs) return output.getvalue() def get_disassemble_as_string(self, func, lasti=-1): @@ -502,15 +533,25 @@ def test_dis_traceback(self): def test_dis_object(self): self.assertRaises(TypeError, dis.dis, object()) + def test_disassemble_recursive(self): + dis = self.get_disassembly(_h, recursive=True) + dis = self.strip_addresses(dis) + self.assertEqual(dis, dis_nested) + + dis = self.get_disassembly(_h.__code__, wrapper=False, recursive=True) + dis = self.strip_addresses(dis) + self.assertEqual(dis, dis_nested) + + class DisWithFileTests(DisTests): # Run the tests again, using the file arg instead of print - def get_disassembly(self, func, lasti=-1, wrapper=True): + def get_disassembly(self, func, lasti=-1, wrapper=True, **kwargs): output = io.StringIO() if wrapper: - dis.dis(func, file=output) + dis.dis(func, file=output, **kwargs) else: - dis.disassemble(func, lasti, file=output) + dis.disassemble(func, lasti, file=output, **kwargs) return output.getvalue() diff --git a/Misc/NEWS b/Misc/NEWS index 6f90175bf7713f..881bcf4f536cc9 100644 --- a/Misc/NEWS +++ b/Misc/NEWS @@ -341,6 +341,9 @@ Extension Modules Library ------- +- bpo-11822: The dis.dis() and dis.disassemble() functions now are able to + disassemble internal code objects. + - bpo-16500: Allow registering at-fork handlers. - bpo-30470: Deprecate invalid ctypes call protection on Windows. Patch by