-
-
Couldn't load subscription status.
- Fork 33.3k
fix bpo-31183: allowing dis to disassemble async generator and coroutine objects
#3077
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 14 commits
ff0d6c2
1d08a7c
6700a24
45f693c
ec7d10a
33523c5
7b3d19a
f3a9055
7f92546
edaffa6
32d9d5b
42dd060
16b1d23
b969705
df4dc64
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -32,20 +32,30 @@ def _try_compile(source, name): | |
| return c | ||
|
|
||
| def dis(x=None, *, file=None, depth=None): | ||
| """Disassemble classes, methods, functions, generators, or code. | ||
| """Disassemble classes, methods, functions, and other compiled objects. | ||
|
|
||
| With no argument, disassemble the last traceback. | ||
| With no argument, disassemble the last traceback. | ||
|
|
||
| Compiled objects currently include generator objects, async generator | ||
| objects, and coroutine objects, all of which store their code object | ||
| in a special attribute. | ||
|
||
| """ | ||
| if x is None: | ||
| distb(file=file) | ||
| return | ||
| if hasattr(x, '__func__'): # Method | ||
| # Extract functions from methods. | ||
| if hasattr(x, '__func__'): | ||
| x = x.__func__ | ||
| if hasattr(x, '__code__'): # Function | ||
| # Extract compiled code objects from... | ||
| if hasattr(x, '__code__'): # ...a function, or | ||
| x = x.__code__ | ||
| if hasattr(x, 'gi_code'): # Generator | ||
| elif hasattr(x, 'gi_code'): #...a generator object, or | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. While I don't think we should hold up this PR for it, this does make we wonder if we should just be exposing a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This struck me as well, so I did a little digging on the history and context. It looks as though Then It seems, then, that there are two questions now:
1 seems easier to answer, although that kind of breaking change was probably more in the spirit of 3.0 than 3.7. I have a couple of dim thoughts on 2 but I'll save them for possible later discussion elsewhere. This also raises the question of whether at least some of the other There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. At least some of the others are deliberately different because they refer to slightly different things (e.g. cr_await vs gi_yieldfrom). Anyway, I've added a note about that to the refactoring issue: https://bugs.python.org/issue31197#msg300291 |
||
| x = x.gi_code | ||
| elif hasattr(x, 'ag_code'): #...an asynchronous generator object, or | ||
| x = x.ag_code | ||
| elif hasattr(x, 'cr_code'): #...a coroutine. | ||
| x = x.cr_code | ||
| # Perform the disassembly. | ||
| if hasattr(x, '__dict__'): # Class or module | ||
| items = sorted(x.__dict__.items()) | ||
| for name, x1 in items: | ||
|
|
@@ -107,16 +117,24 @@ def pretty_flags(flags): | |
| return ", ".join(names) | ||
|
|
||
| def _get_code_object(x): | ||
| """Helper to handle methods, functions, generators, strings and raw code objects""" | ||
| if hasattr(x, '__func__'): # Method | ||
| """Helper to handle methods, compiled or raw code objects, and strings.""" | ||
| # Extract functions from methods. | ||
| if hasattr(x, '__func__'): | ||
| x = x.__func__ | ||
| if hasattr(x, '__code__'): # Function | ||
| # Extract compiled code objects from... | ||
| if hasattr(x, '__code__'): # ...a function, or | ||
| x = x.__code__ | ||
| if hasattr(x, 'gi_code'): # Generator | ||
| elif hasattr(x, 'gi_code'): #...a generator object, or | ||
| x = x.gi_code | ||
| if isinstance(x, str): # Source code | ||
| elif hasattr(x, 'ag_code'): #...an asynchronous generator object, or | ||
| x = x.ag_code | ||
| elif hasattr(x, 'cr_code'): #...a coroutine. | ||
| x = x.cr_code | ||
| # Handle source code. | ||
| if isinstance(x, str): | ||
| x = _try_compile(x, "<disassembly>") | ||
| if hasattr(x, 'co_code'): # Code object | ||
| # By now, if we don't have a code object, we can't disassemble x. | ||
| if hasattr(x, 'co_code'): | ||
| return x | ||
| raise TypeError("don't know how to disassemble %s objects" % | ||
| type(x).__name__) | ||
|
|
@@ -443,8 +461,8 @@ def findlinestarts(code): | |
| class Bytecode: | ||
| """The bytecode operations of a piece of code | ||
|
|
||
| Instantiate this with a function, method, string of code, or a code object | ||
| (as returned by compile()). | ||
| Instantiate this with a function, method, other compiled object, string of | ||
| code, or a code object (as returned by compile()). | ||
|
|
||
| Iterating over this yields the bytecode operations as Instruction instances. | ||
| """ | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -331,6 +331,13 @@ def _fstring(a, b, c, d): | |
| def _g(x): | ||
| yield x | ||
|
|
||
| async def _ag(x): | ||
| yield x | ||
|
|
||
| async def _co(x): | ||
| async for item in _ag(x): | ||
| pass | ||
|
|
||
| def _h(y): | ||
| def foo(x): | ||
| '''funcdoc''' | ||
|
|
@@ -390,6 +397,7 @@ def foo(x): | |
| _h.__code__.co_firstlineno + 3, | ||
| ) | ||
|
|
||
|
|
||
| class DisTests(unittest.TestCase): | ||
|
|
||
| maxDiff = None | ||
|
|
@@ -531,10 +539,22 @@ def test_disassemble_class_method(self): | |
| self.do_disassembly_test(_C.cm, dis_c_class_method) | ||
|
|
||
| def test_disassemble_generator(self): | ||
| gen_func_disas = self.get_disassembly(_g) # Disassemble generator function | ||
| gen_disas = self.get_disassembly(_g(1)) # Disassemble generator itself | ||
| gen_func_disas = self.get_disassembly(_g) # Generator function | ||
| gen_disas = self.get_disassembly(_g(1)) # Generator iterator | ||
| self.assertEqual(gen_disas, gen_func_disas) | ||
|
|
||
| def test_disassemble_async_generator(self): | ||
| agen_func_disas = self.get_disassembly(_ag) # Async generator function | ||
| agen_disas = self.get_disassembly(_ag(1)) # Async generator iterator | ||
| self.assertEqual(agen_disas, agen_func_disas) | ||
|
|
||
| def test_disassemble_coroutine(self): | ||
| coro_func_disas = self.get_disassembly(_co) # Coroutine function | ||
| coro = _co(1) # Coroutine object | ||
| coro.close() # Avoid a RuntimeWarning (never awaited) | ||
| coro_disas = self.get_disassembly(coro) | ||
| self.assertEqual(coro_disas, coro_func_disas) | ||
|
|
||
| def test_disassemble_fstring(self): | ||
| self.do_disassembly_test(_fstring, dis_fstring) | ||
|
|
||
|
|
@@ -1050,12 +1070,12 @@ def test_explicit_first_line(self): | |
| self.assertEqual(list(actual), expected_opinfo_outer) | ||
|
|
||
| def test_source_line_in_disassembly(self): | ||
| # Use the line in the source code | ||
| actual = dis.Bytecode(simple).dis()[:3] | ||
| # Use the line in the source code (split extracts the line no) | ||
| actual = dis.Bytecode(simple).dis().split(" ")[0] | ||
|
||
| expected = "{:>3}".format(simple.__code__.co_firstlineno) | ||
| self.assertEqual(actual, expected) | ||
| # Use an explicit first line number | ||
| actual = dis.Bytecode(simple, first_line=350).dis()[:3] | ||
| # Use an explicit first line number (split extracts the line no) | ||
| actual = dis.Bytecode(simple, first_line=350).dis().split(" ")[0] | ||
| self.assertEqual(actual, "350") | ||
|
|
||
| def test_info(self): | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| `dis` now works with asynchronous generator and coroutine objects. Patch by | ||
| George Collins based on diagnosis by Luciano Ramalho. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Align the docstring at the left side of
""". Seems this is never specified explicitly (likely because nobody could think of other way of formatting), but all multiline docstrings are aligned at the left side of""".There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Happy to change it, but this convention is inconsistent even within
dis.py: compare e.g._try_compile,Instructionand_get_name_info, which align to the right of""", with_disassemble,get_instructions, andget_instructions_bytes, which use the alignment you prefer. I was matching_try_compilesince it's directly above.git blamesheds no light on this that I can see, since inconsistent alignments were used even within the same patch.Also, I just confirmed that after applying
trimfrom PEP 257 (in 3.x you have to manually add a dummymaxinttosys), all three docstrings below are equivalent. (PEP 257 only mentionsfooandbar).There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We are sometimes inconsistent, but as @serhiy-storchaka notes, the preferred layouts are the ones that align with the opening triple-quote - the last option isn't shown in PEP 257 because we're not supposed to use it :)
The inconsistent ones just get left alone once they're in, since we don't typically do formatting-only edits - we're more likely to fix the indentation as part of a change that needed to update the affected docstring anyway.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Got it. Thanks! Makes sense.