Skip to content

Commit fe2b56a

Browse files
syncosmicncoghlan
authored andcommitted
bpo-31183: dis now handles coroutines & async generators (GH-3077)
Coroutines and async generators use a distinct attribute name for their code objects, so this updates the `dis` module to correctly disassemble objects with those attributes. Due to the increase in the test module length, it also fixes some latent defects in the tests related to how the displayed source line numbers are extracted. https://bugs.python.org/issue31230 is a follow-up issue suggesting we may want to solve this a different way, by instead giving all these object types a common `__code__` attribute, avoiding the need for special casing in the `dis` module.
1 parent 82aff62 commit fe2b56a

File tree

4 files changed

+80
-26
lines changed

4 files changed

+80
-26
lines changed

Doc/library/dis.rst

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,9 @@ code.
5353
.. class:: Bytecode(x, *, first_line=None, current_offset=None)
5454

5555

56-
Analyse the bytecode corresponding to a function, generator, method, string
57-
of source code, or a code object (as returned by :func:`compile`).
56+
Analyse the bytecode corresponding to a function, generator, asynchronous
57+
generator, coroutine, method, string of source code, or a code object (as
58+
returned by :func:`compile`).
5859

5960
This is a convenience wrapper around many of the functions listed below, most
6061
notably :func:`get_instructions`, as iterating over a :class:`Bytecode`
@@ -92,6 +93,9 @@ code.
9293
Return a formatted multi-line string with detailed information about the
9394
code object, like :func:`code_info`.
9495

96+
.. versionchanged:: 3.7
97+
This can now handle coroutine and asynchronous generator objects.
98+
9599
Example::
96100

97101
>>> bytecode = dis.Bytecode(myfunc)
@@ -114,14 +118,18 @@ operation is being performed, so the intermediate analysis object isn't useful:
114118
.. function:: code_info(x)
115119

116120
Return a formatted multi-line string with detailed code object information
117-
for the supplied function, generator, method, source code string or code object.
121+
for the supplied function, generator, asynchronous generator, coroutine,
122+
method, source code string or code object.
118123

119124
Note that the exact contents of code info strings are highly implementation
120125
dependent and they may change arbitrarily across Python VMs or Python
121126
releases.
122127

123128
.. versionadded:: 3.2
124129

130+
.. versionchanged:: 3.7
131+
This can now handle coroutine and asynchronous generator objects.
132+
125133

126134
.. function:: show_code(x, *, file=None)
127135

@@ -141,12 +149,13 @@ operation is being performed, so the intermediate analysis object isn't useful:
141149
.. function:: dis(x=None, *, file=None, depth=None)
142150

143151
Disassemble the *x* object. *x* can denote either a module, a class, a
144-
method, a function, a generator, a code object, a string of source code or
145-
a byte sequence of raw bytecode. For a module, it disassembles all functions.
146-
For a class, it disassembles all methods (including class and static methods).
147-
For a code object or sequence of raw bytecode, it prints one line per bytecode
148-
instruction. It also recursively disassembles nested code objects (the code
149-
of comprehensions, generator expressions and nested functions, and the code
152+
method, a function, a generator, an asynchronous generator, a couroutine,
153+
a code object, a string of source code or a byte sequence of raw bytecode.
154+
For a module, it disassembles all functions. For a class, it disassembles
155+
all methods (including class and static methods). For a code object or
156+
sequence of raw bytecode, it prints one line per bytecode instruction.
157+
It also recursively disassembles nested code objects (the code of
158+
comprehensions, generator expressions and nested functions, and the code
150159
used for building nested classes).
151160
Strings are first compiled to code objects with the :func:`compile`
152161
built-in function before being disassembled. If no object is provided, this
@@ -164,6 +173,9 @@ operation is being performed, so the intermediate analysis object isn't useful:
164173
.. versionchanged:: 3.7
165174
Implemented recursive disassembling and added *depth* parameter.
166175

176+
.. versionchanged:: 3.7
177+
This can now handle coroutine and asynchronous generator objects.
178+
167179

168180
.. function:: distb(tb=None, *, file=None)
169181

Lib/dis.py

Lines changed: 30 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -32,20 +32,30 @@ def _try_compile(source, name):
3232
return c
3333

3434
def dis(x=None, *, file=None, depth=None):
35-
"""Disassemble classes, methods, functions, generators, or code.
35+
"""Disassemble classes, methods, functions, and other compiled objects.
3636
3737
With no argument, disassemble the last traceback.
3838
39+
Compiled objects currently include generator objects, async generator
40+
objects, and coroutine objects, all of which store their code object
41+
in a special attribute.
3942
"""
4043
if x is None:
4144
distb(file=file)
4245
return
43-
if hasattr(x, '__func__'): # Method
46+
# Extract functions from methods.
47+
if hasattr(x, '__func__'):
4448
x = x.__func__
45-
if hasattr(x, '__code__'): # Function
49+
# Extract compiled code objects from...
50+
if hasattr(x, '__code__'): # ...a function, or
4651
x = x.__code__
47-
if hasattr(x, 'gi_code'): # Generator
52+
elif hasattr(x, 'gi_code'): #...a generator object, or
4853
x = x.gi_code
54+
elif hasattr(x, 'ag_code'): #...an asynchronous generator object, or
55+
x = x.ag_code
56+
elif hasattr(x, 'cr_code'): #...a coroutine.
57+
x = x.cr_code
58+
# Perform the disassembly.
4959
if hasattr(x, '__dict__'): # Class or module
5060
items = sorted(x.__dict__.items())
5161
for name, x1 in items:
@@ -107,16 +117,24 @@ def pretty_flags(flags):
107117
return ", ".join(names)
108118

109119
def _get_code_object(x):
110-
"""Helper to handle methods, functions, generators, strings and raw code objects"""
111-
if hasattr(x, '__func__'): # Method
120+
"""Helper to handle methods, compiled or raw code objects, and strings."""
121+
# Extract functions from methods.
122+
if hasattr(x, '__func__'):
112123
x = x.__func__
113-
if hasattr(x, '__code__'): # Function
124+
# Extract compiled code objects from...
125+
if hasattr(x, '__code__'): # ...a function, or
114126
x = x.__code__
115-
if hasattr(x, 'gi_code'): # Generator
127+
elif hasattr(x, 'gi_code'): #...a generator object, or
116128
x = x.gi_code
117-
if isinstance(x, str): # Source code
129+
elif hasattr(x, 'ag_code'): #...an asynchronous generator object, or
130+
x = x.ag_code
131+
elif hasattr(x, 'cr_code'): #...a coroutine.
132+
x = x.cr_code
133+
# Handle source code.
134+
if isinstance(x, str):
118135
x = _try_compile(x, "<disassembly>")
119-
if hasattr(x, 'co_code'): # Code object
136+
# By now, if we don't have a code object, we can't disassemble x.
137+
if hasattr(x, 'co_code'):
120138
return x
121139
raise TypeError("don't know how to disassemble %s objects" %
122140
type(x).__name__)
@@ -443,8 +461,8 @@ def findlinestarts(code):
443461
class Bytecode:
444462
"""The bytecode operations of a piece of code
445463
446-
Instantiate this with a function, method, string of code, or a code object
447-
(as returned by compile()).
464+
Instantiate this with a function, method, other compiled object, string of
465+
code, or a code object (as returned by compile()).
448466
449467
Iterating over this yields the bytecode operations as Instruction instances.
450468
"""

Lib/test/test_dis.py

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -331,6 +331,13 @@ def _fstring(a, b, c, d):
331331
def _g(x):
332332
yield x
333333

334+
async def _ag(x):
335+
yield x
336+
337+
async def _co(x):
338+
async for item in _ag(x):
339+
pass
340+
334341
def _h(y):
335342
def foo(x):
336343
'''funcdoc'''
@@ -390,6 +397,7 @@ def foo(x):
390397
_h.__code__.co_firstlineno + 3,
391398
)
392399

400+
393401
class DisTests(unittest.TestCase):
394402

395403
maxDiff = None
@@ -531,10 +539,22 @@ def test_disassemble_class_method(self):
531539
self.do_disassembly_test(_C.cm, dis_c_class_method)
532540

533541
def test_disassemble_generator(self):
534-
gen_func_disas = self.get_disassembly(_g) # Disassemble generator function
535-
gen_disas = self.get_disassembly(_g(1)) # Disassemble generator itself
542+
gen_func_disas = self.get_disassembly(_g) # Generator function
543+
gen_disas = self.get_disassembly(_g(1)) # Generator iterator
536544
self.assertEqual(gen_disas, gen_func_disas)
537545

546+
def test_disassemble_async_generator(self):
547+
agen_func_disas = self.get_disassembly(_ag) # Async generator function
548+
agen_disas = self.get_disassembly(_ag(1)) # Async generator iterator
549+
self.assertEqual(agen_disas, agen_func_disas)
550+
551+
def test_disassemble_coroutine(self):
552+
coro_func_disas = self.get_disassembly(_co) # Coroutine function
553+
coro = _co(1) # Coroutine object
554+
coro.close() # Avoid a RuntimeWarning (never awaited)
555+
coro_disas = self.get_disassembly(coro)
556+
self.assertEqual(coro_disas, coro_func_disas)
557+
538558
def test_disassemble_fstring(self):
539559
self.do_disassembly_test(_fstring, dis_fstring)
540560

@@ -1051,11 +1071,13 @@ def test_explicit_first_line(self):
10511071

10521072
def test_source_line_in_disassembly(self):
10531073
# Use the line in the source code
1054-
actual = dis.Bytecode(simple).dis()[:3]
1055-
expected = "{:>3}".format(simple.__code__.co_firstlineno)
1074+
actual = dis.Bytecode(simple).dis()
1075+
actual = actual.strip().partition(" ")[0] # extract the line no
1076+
expected = str(simple.__code__.co_firstlineno)
10561077
self.assertEqual(actual, expected)
10571078
# Use an explicit first line number
1058-
actual = dis.Bytecode(simple, first_line=350).dis()[:3]
1079+
actual = dis.Bytecode(simple, first_line=350).dis()
1080+
actual = actual.strip().partition(" ")[0] # extract the line no
10591081
self.assertEqual(actual, "350")
10601082

10611083
def test_info(self):
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
`dis` now works with asynchronous generator and coroutine objects. Patch by
2+
George Collins based on diagnosis by Luciano Ramalho.

0 commit comments

Comments
 (0)