Skip to content

Commit 1efbf92

Browse files
bpo-11822: Improve disassembly to show embedded code objects. (#1844)
The depth argument limits recursion.
1 parent fdfca5f commit 1efbf92

File tree

5 files changed

+121
-15
lines changed

5 files changed

+121
-15
lines changed

Doc/library/dis.rst

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -138,23 +138,32 @@ operation is being performed, so the intermediate analysis object isn't useful:
138138
Added *file* parameter.
139139

140140

141-
.. function:: dis(x=None, *, file=None)
141+
.. function:: dis(x=None, *, file=None, depth=None)
142142

143143
Disassemble the *x* object. *x* can denote either a module, a class, a
144144
method, a function, a generator, a code object, a string of source code or
145145
a byte sequence of raw bytecode. For a module, it disassembles all functions.
146146
For a class, it disassembles all methods (including class and static methods).
147147
For a code object or sequence of raw bytecode, it prints one line per bytecode
148-
instruction. Strings are first compiled to code objects with the :func:`compile`
148+
instruction. It also recursively disassembles nested code objects (the code
149+
of comprehensions, generator expressions and nested functions, and the code
150+
used for building nested classes).
151+
Strings are first compiled to code objects with the :func:`compile`
149152
built-in function before being disassembled. If no object is provided, this
150153
function disassembles the last traceback.
151154

152155
The disassembly is written as text to the supplied *file* argument if
153156
provided and to ``sys.stdout`` otherwise.
154157

158+
The maximal depth of recursion is limited by *depth* unless it is ``None``.
159+
``depth=0`` means no recursion.
160+
155161
.. versionchanged:: 3.4
156162
Added *file* parameter.
157163

164+
.. versionchanged:: 3.7
165+
Implemented recursive disassembling and added *depth* parameter.
166+
158167

159168
.. function:: distb(tb=None, *, file=None)
160169

Doc/whatsnew/3.7.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,14 @@ contextlib
178178
:func:`contextlib.asynccontextmanager` has been added. (Contributed by
179179
Jelle Zijlstra in :issue:`29679`.)
180180

181+
dis
182+
---
183+
184+
The :func:`~dis.dis` function now is able to
185+
disassemble nested code objects (the code of comprehensions, generator
186+
expressions and nested functions, and the code used for building nested
187+
classes). (Contributed by Serhiy Storchaka in :issue:`11822`.)
188+
181189
distutils
182190
---------
183191

Lib/dis.py

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ def _try_compile(source, name):
3131
c = compile(source, name, 'exec')
3232
return c
3333

34-
def dis(x=None, *, file=None):
34+
def dis(x=None, *, file=None, depth=None):
3535
"""Disassemble classes, methods, functions, generators, or code.
3636
3737
With no argument, disassemble the last traceback.
@@ -52,16 +52,16 @@ def dis(x=None, *, file=None):
5252
if isinstance(x1, _have_code):
5353
print("Disassembly of %s:" % name, file=file)
5454
try:
55-
dis(x1, file=file)
55+
dis(x1, file=file, depth=depth)
5656
except TypeError as msg:
5757
print("Sorry:", msg, file=file)
5858
print(file=file)
5959
elif hasattr(x, 'co_code'): # Code object
60-
disassemble(x, file=file)
60+
_disassemble_recursive(x, file=file, depth=depth)
6161
elif isinstance(x, (bytes, bytearray)): # Raw bytecode
6262
_disassemble_bytes(x, file=file)
6363
elif isinstance(x, str): # Source code
64-
_disassemble_str(x, file=file)
64+
_disassemble_str(x, file=file, depth=depth)
6565
else:
6666
raise TypeError("don't know how to disassemble %s objects" %
6767
type(x).__name__)
@@ -338,6 +338,17 @@ def disassemble(co, lasti=-1, *, file=None):
338338
_disassemble_bytes(co.co_code, lasti, co.co_varnames, co.co_names,
339339
co.co_consts, cell_names, linestarts, file=file)
340340

341+
def _disassemble_recursive(co, *, file=None, depth=None):
342+
disassemble(co, file=file)
343+
if depth is None or depth > 0:
344+
if depth is not None:
345+
depth = depth - 1
346+
for x in co.co_consts:
347+
if hasattr(x, 'co_code'):
348+
print(file=file)
349+
print("Disassembly of %r:" % (x,), file=file)
350+
_disassemble_recursive(x, file=file, depth=depth)
351+
341352
def _disassemble_bytes(code, lasti=-1, varnames=None, names=None,
342353
constants=None, cells=None, linestarts=None,
343354
*, file=None, line_offset=0):
@@ -368,9 +379,9 @@ def _disassemble_bytes(code, lasti=-1, varnames=None, names=None,
368379
print(instr._disassemble(lineno_width, is_current_instr, offset_width),
369380
file=file)
370381

371-
def _disassemble_str(source, *, file=None):
382+
def _disassemble_str(source, **kwargs):
372383
"""Compile the source string, then disassemble the code object."""
373-
disassemble(_try_compile(source, '<dis>'), file=file)
384+
_disassemble_recursive(_try_compile(source, '<dis>'), **kwargs)
374385

375386
disco = disassemble # XXX For backwards compatibility
376387

Lib/test/test_dis.py

Lines changed: 82 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -331,16 +331,77 @@ def _fstring(a, b, c, d):
331331
def _g(x):
332332
yield x
333333

334+
def _h(y):
335+
def foo(x):
336+
'''funcdoc'''
337+
return [x + z for z in y]
338+
return foo
339+
340+
dis_nested_0 = """\
341+
%3d 0 LOAD_CLOSURE 0 (y)
342+
2 BUILD_TUPLE 1
343+
4 LOAD_CONST 1 (<code object foo at 0x..., file "%s", line %d>)
344+
6 LOAD_CONST 2 ('_h.<locals>.foo')
345+
8 MAKE_FUNCTION 8
346+
10 STORE_FAST 1 (foo)
347+
348+
%3d 12 LOAD_FAST 1 (foo)
349+
14 RETURN_VALUE
350+
""" % (_h.__code__.co_firstlineno + 1,
351+
__file__,
352+
_h.__code__.co_firstlineno + 1,
353+
_h.__code__.co_firstlineno + 4,
354+
)
355+
356+
dis_nested_1 = """%s
357+
Disassembly of <code object foo at 0x..., file "%s", line %d>:
358+
%3d 0 LOAD_CLOSURE 0 (x)
359+
2 BUILD_TUPLE 1
360+
4 LOAD_CONST 1 (<code object <listcomp> at 0x..., file "%s", line %d>)
361+
6 LOAD_CONST 2 ('_h.<locals>.foo.<locals>.<listcomp>')
362+
8 MAKE_FUNCTION 8
363+
10 LOAD_DEREF 1 (y)
364+
12 GET_ITER
365+
14 CALL_FUNCTION 1
366+
16 RETURN_VALUE
367+
""" % (dis_nested_0,
368+
__file__,
369+
_h.__code__.co_firstlineno + 1,
370+
_h.__code__.co_firstlineno + 3,
371+
__file__,
372+
_h.__code__.co_firstlineno + 3,
373+
)
374+
375+
dis_nested_2 = """%s
376+
Disassembly of <code object <listcomp> at 0x..., file "%s", line %d>:
377+
%3d 0 BUILD_LIST 0
378+
2 LOAD_FAST 0 (.0)
379+
>> 4 FOR_ITER 12 (to 18)
380+
6 STORE_FAST 1 (z)
381+
8 LOAD_DEREF 0 (x)
382+
10 LOAD_FAST 1 (z)
383+
12 BINARY_ADD
384+
14 LIST_APPEND 2
385+
16 JUMP_ABSOLUTE 4
386+
>> 18 RETURN_VALUE
387+
""" % (dis_nested_1,
388+
__file__,
389+
_h.__code__.co_firstlineno + 3,
390+
_h.__code__.co_firstlineno + 3,
391+
)
392+
334393
class DisTests(unittest.TestCase):
335394

336-
def get_disassembly(self, func, lasti=-1, wrapper=True):
395+
maxDiff = None
396+
397+
def get_disassembly(self, func, lasti=-1, wrapper=True, **kwargs):
337398
# We want to test the default printing behaviour, not the file arg
338399
output = io.StringIO()
339400
with contextlib.redirect_stdout(output):
340401
if wrapper:
341-
dis.dis(func)
402+
dis.dis(func, **kwargs)
342403
else:
343-
dis.disassemble(func, lasti)
404+
dis.disassemble(func, lasti, **kwargs)
344405
return output.getvalue()
345406

346407
def get_disassemble_as_string(self, func, lasti=-1):
@@ -350,7 +411,7 @@ def strip_addresses(self, text):
350411
return re.sub(r'\b0x[0-9A-Fa-f]+\b', '0x...', text)
351412

352413
def do_disassembly_test(self, func, expected):
353-
got = self.get_disassembly(func)
414+
got = self.get_disassembly(func, depth=0)
354415
if got != expected:
355416
got = self.strip_addresses(got)
356417
self.assertEqual(got, expected)
@@ -502,15 +563,29 @@ def test_dis_traceback(self):
502563
def test_dis_object(self):
503564
self.assertRaises(TypeError, dis.dis, object())
504565

566+
def test_disassemble_recursive(self):
567+
def check(expected, **kwargs):
568+
dis = self.get_disassembly(_h, **kwargs)
569+
dis = self.strip_addresses(dis)
570+
self.assertEqual(dis, expected)
571+
572+
check(dis_nested_0, depth=0)
573+
check(dis_nested_1, depth=1)
574+
check(dis_nested_2, depth=2)
575+
check(dis_nested_2, depth=3)
576+
check(dis_nested_2, depth=None)
577+
check(dis_nested_2)
578+
579+
505580
class DisWithFileTests(DisTests):
506581

507582
# Run the tests again, using the file arg instead of print
508-
def get_disassembly(self, func, lasti=-1, wrapper=True):
583+
def get_disassembly(self, func, lasti=-1, wrapper=True, **kwargs):
509584
output = io.StringIO()
510585
if wrapper:
511-
dis.dis(func, file=output)
586+
dis.dis(func, file=output, **kwargs)
512587
else:
513-
dis.disassemble(func, lasti, file=output)
588+
dis.disassemble(func, lasti, file=output, **kwargs)
514589
return output.getvalue()
515590

516591

Misc/NEWS

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -355,6 +355,9 @@ Extension Modules
355355
Library
356356
-------
357357

358+
- bpo-11822: The dis.dis() function now is able to disassemble nested
359+
code objects.
360+
358361
- bpo-30624: selectors does not take KeyboardInterrupt and SystemExit into
359362
account, leaving a fd in a bad state in case of error. Patch by Giampaolo
360363
Rodola'.

0 commit comments

Comments
 (0)