From ff0d6c2a21e5583329ff3d8e9da9b08c115a6a5a Mon Sep 17 00:00:00 2001 From: gc Date: Fri, 11 Aug 2017 08:00:52 -0700 Subject: [PATCH 01/15] bpo-31183: dis can disassemble async gens/coros Extended issue 21947 to handle async generator and coroutine objects. --- Lib/dis.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/Lib/dis.py b/Lib/dis.py index b990839bcbfe923..bdd34972d47ab1b 100644 --- a/Lib/dis.py +++ b/Lib/dis.py @@ -32,7 +32,8 @@ 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, generators, + asynchronous generators, coroutines, or code. With no argument, disassemble the last traceback. @@ -46,6 +47,10 @@ def dis(x=None, *, file=None, depth=None): x = x.__code__ if hasattr(x, 'gi_code'): # Generator x = x.gi_code + if hasattr(x, 'ag_code'): # Async generator + x = x.ag_code + if hasattr(x, 'cr_code'): # Coroutine + x = x.cr_code if hasattr(x, '__dict__'): # Class or module items = sorted(x.__dict__.items()) for name, x1 in items: @@ -107,13 +112,19 @@ def pretty_flags(flags): return ", ".join(names) def _get_code_object(x): - """Helper to handle methods, functions, generators, strings and raw code objects""" + """Helper to handle methods, functions, generators, + asynchronous generators, coroutines, strings and + raw code objects""" if hasattr(x, '__func__'): # Method x = x.__func__ if hasattr(x, '__code__'): # Function x = x.__code__ if hasattr(x, 'gi_code'): # Generator x = x.gi_code + if hasattr(x, 'ag_code'): # Async generator + x = x.ag_code + if hasattr(x, 'cr_code'): # Coroutine + x = x.cr_code if isinstance(x, str): # Source code x = _try_compile(x, "") if hasattr(x, 'co_code'): # Code object From 1d08a7cc7a95eb48eca444931d839cca9b16f1cd Mon Sep 17 00:00:00 2001 From: gc Date: Fri, 11 Aug 2017 08:03:13 -0700 Subject: [PATCH 02/15] bpo-31183: dis unit tests for async gen/coro Added unit tests based on issue 21947 for async generator and coroutine objects. This required two additional steps: borrowing (and slightly adapting) a context manager written for Trio in order to suppress a leaking-coroutine warning, and fixing a latent bug in test_source_line_in_disassembly (which assumed the source code line would never be > 999. (For consistency, I used the same approach for the explicit line number, even though the number chosen (350) doesn't require it). --- Lib/test/test_dis.py | 63 +++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 59 insertions(+), 4 deletions(-) diff --git a/Lib/test/test_dis.py b/Lib/test/test_dis.py index 254b317e4982437..d0104684fad3d34 100644 --- a/Lib/test/test_dis.py +++ b/Lib/test/test_dis.py @@ -9,6 +9,8 @@ import re import types import contextlib +import warnings +import gc def get_tb(): def _error(): @@ -330,6 +332,12 @@ def _fstring(a, b, c, d): def _g(x): yield x + +async def _ag(x): + yield x + +async def _co(x): + await _ag(x) def _h(y): def foo(x): @@ -390,6 +398,42 @@ def foo(x): _h.__code__.co_firstlineno + 3, ) + + +@contextlib.contextmanager +def ignore_coroutine_never_awaited_warnings(): + """ + The coroutine test leaks a coroutine, and thus triggers the + "RuntimeWarning: coroutine '...' was never awaited" message. This context + manager should be used anywhere this happens to hide those messages, because + (a) when expected they're clutter, (b) on CPython 3.5.x where x < 3, this + warning can trigger a segfault if we run with warnings turned into errors: + https://bugs.python.org/issue27811. + + (Adapted from python-trio/trio/.../test_run.py) + """ + with warnings.catch_warnings(): + warnings.filterwarnings( + "ignore", message="coroutine '.*' was never awaited" + ) + try: + yield + finally: + # Make sure to trigger any coroutine __del__ methods now, before + # we leave the context manager. + + # In the test suite we sometimes want to call gc.collect() to make sure + # that any objects with noisy __del__ methods (e.g. unawaited coroutines) + # get collected before we continue, so their noise doesn't leak into + # unrelated tests. + # + # On PyPy, coroutine objects (for example) can survive at least 1 round of + # garbage collection, because executing their __del__ method to print the + # warning can cause them to be resurrected. So we call collect a few times + # to make sure. + for _ in range(4): + gc.collect() + class DisTests(unittest.TestCase): maxDiff = None @@ -534,6 +578,17 @@ 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 self.assertEqual(gen_disas, gen_func_disas) + + def test_disassemble_async_generator(self): + agen_func_disas = self.get_disassembly(_ag) # Disassemble async generator function + agen_disas = self.get_disassembly(_ag(1)) # Disassemble async generator itself + self.assertEqual(agen_disas, agen_func_disas) + + def test_disassemble_coroutine(self): + coro_func_disas = self.get_disassembly(_co) # Disassemble coroutine function + with ignore_coroutine_never_awaited_warnings(): + coro_disas = self.get_disassembly(_co(1)) # Disassemble coroutine itself + self.assertEqual(coro_disas, coro_func_disas) def test_disassemble_fstring(self): self.do_disassembly_test(_fstring, dis_fstring) @@ -1050,12 +1105,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): From 6700a244c5730864f328cae6bfa2d5f561244f2a Mon Sep 17 00:00:00 2001 From: gc Date: Fri, 11 Aug 2017 08:05:03 -0700 Subject: [PATCH 03/15] bpo-31183: add async gens/coros to dis doc --- Doc/library/dis.rst | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/Doc/library/dis.rst b/Doc/library/dis.rst index bc32380e976bfb8..002c9ea0cf4f1fb 100644 --- a/Doc/library/dis.rst +++ b/Doc/library/dis.rst @@ -53,8 +53,9 @@ code. .. class:: Bytecode(x, *, first_line=None, current_offset=None) - Analyse the bytecode corresponding to a function, generator, method, string - of source code, or a code object (as returned by :func:`compile`). + Analyse the bytecode corresponding to a function, generator, asynchronous + generator, coroutine, method, string of source code, or a code object (as + returned by :func:`compile`). This is a convenience wrapper around many of the functions listed below, most notably :func:`get_instructions`, as iterating over a :class:`Bytecode` @@ -114,7 +115,8 @@ operation is being performed, so the intermediate analysis object isn't useful: .. function:: code_info(x) Return a formatted multi-line string with detailed code object information - for the supplied function, generator, method, source code string or code object. + for the supplied function, generator, asynchronous generator, coroutine, + method, source code string or code object. Note that the exact contents of code info strings are highly implementation dependent and they may change arbitrarily across Python VMs or Python @@ -141,12 +143,13 @@ operation is being performed, so the intermediate analysis object isn't useful: .. function:: dis(x=None, *, file=None, depth=None) 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 - a byte sequence of raw bytecode. For a module, it disassembles all functions. - For a class, it disassembles all methods (including class and static methods). - For a code object or sequence of raw bytecode, it prints one line per bytecode - instruction. It also recursively disassembles nested code objects (the code - of comprehensions, generator expressions and nested functions, and the code + method, a function, a generator, an asynchronous generator, a couroutine, + a code object, a string of source code or a byte sequence of raw bytecode. + For a module, it disassembles all functions. For a class, it disassembles + all methods (including class and static methods). For a code object or + sequence of raw bytecode, it prints one line per bytecode instruction. + It also recursively disassembles nested code objects (the code of + comprehensions, generator expressions and nested functions, and the code used for building nested classes). Strings are first compiled to code objects with the :func:`compile` built-in function before being disassembled. If no object is provided, this From 45f693c8534c6fc54095c0984147546ae62d8885 Mon Sep 17 00:00:00 2001 From: gc Date: Fri, 11 Aug 2017 11:57:22 -0700 Subject: [PATCH 04/15] Fixed whitespace issues (I hope) --- Doc/library/dis.rst | 2 +- Lib/dis.py | 4 ++-- Lib/test/test_dis.py | 12 ++++++------ 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Doc/library/dis.rst b/Doc/library/dis.rst index 002c9ea0cf4f1fb..bea5a6c93473b94 100644 --- a/Doc/library/dis.rst +++ b/Doc/library/dis.rst @@ -146,7 +146,7 @@ operation is being performed, so the intermediate analysis object isn't useful: method, a function, a generator, an asynchronous generator, a couroutine, a code object, a string of source code or a byte sequence of raw bytecode. For a module, it disassembles all functions. For a class, it disassembles - all methods (including class and static methods). For a code object or + all methods (including class and static methods). For a code object or sequence of raw bytecode, it prints one line per bytecode instruction. It also recursively disassembles nested code objects (the code of comprehensions, generator expressions and nested functions, and the code diff --git a/Lib/dis.py b/Lib/dis.py index bdd34972d47ab1b..a428be8d2868ded 100644 --- a/Lib/dis.py +++ b/Lib/dis.py @@ -32,7 +32,7 @@ def _try_compile(source, name): return c def dis(x=None, *, file=None, depth=None): - """Disassemble classes, methods, functions, generators, + """Disassemble classes, methods, functions, generators, asynchronous generators, coroutines, or code. With no argument, disassemble the last traceback. @@ -112,7 +112,7 @@ def pretty_flags(flags): return ", ".join(names) def _get_code_object(x): - """Helper to handle methods, functions, generators, + """Helper to handle methods, functions, generators, asynchronous generators, coroutines, strings and raw code objects""" if hasattr(x, '__func__'): # Method diff --git a/Lib/test/test_dis.py b/Lib/test/test_dis.py index d0104684fad3d34..49223affb055f26 100644 --- a/Lib/test/test_dis.py +++ b/Lib/test/test_dis.py @@ -332,10 +332,10 @@ def _fstring(a, b, c, d): def _g(x): yield x - + async def _ag(x): yield x - + async def _co(x): await _ag(x) @@ -409,7 +409,7 @@ def ignore_coroutine_never_awaited_warnings(): (a) when expected they're clutter, (b) on CPython 3.5.x where x < 3, this warning can trigger a segfault if we run with warnings turned into errors: https://bugs.python.org/issue27811. - + (Adapted from python-trio/trio/.../test_run.py) """ with warnings.catch_warnings(): @@ -433,7 +433,7 @@ def ignore_coroutine_never_awaited_warnings(): # to make sure. for _ in range(4): gc.collect() - + class DisTests(unittest.TestCase): maxDiff = None @@ -578,12 +578,12 @@ 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 self.assertEqual(gen_disas, gen_func_disas) - + def test_disassemble_async_generator(self): agen_func_disas = self.get_disassembly(_ag) # Disassemble async generator function agen_disas = self.get_disassembly(_ag(1)) # Disassemble async generator itself self.assertEqual(agen_disas, agen_func_disas) - + def test_disassemble_coroutine(self): coro_func_disas = self.get_disassembly(_co) # Disassemble coroutine function with ignore_coroutine_never_awaited_warnings(): From ec7d10ae27d061d4641a5e83c757e1c5eec140bc Mon Sep 17 00:00:00 2001 From: gc Date: Sun, 13 Aug 2017 08:15:40 -0700 Subject: [PATCH 05/15] Fixed brittleness in test_source_line_in_disassembly. The initial version broke if the number of digits in the line where simple() was defined increased beyond 3, which happened during the patch. After discussion in the first PR, I went with what seemed to be the clearest general method and kept consistency between the actual case and the explicit case (although the explicit case would only break if the magic number changed). This should now allow arbitrary whitespace on either side and any number of digits in the line number. The .lstrip().partition() chain is repeated, obviously, but it didn't seem worth refactoring into a function since this is "just" test code. --- Lib/test/test_dis.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/Lib/test/test_dis.py b/Lib/test/test_dis.py index 49223affb055f26..44633ad12b7a6b7 100644 --- a/Lib/test/test_dis.py +++ b/Lib/test/test_dis.py @@ -1105,12 +1105,14 @@ 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 (split extracts the line no) - actual = dis.Bytecode(simple).dis().split(" ")[0] - expected = "{:>3}".format(simple.__code__.co_firstlineno) + # Use the line in the source code + actual = dis.Bytecode(simple).dis() + actual = actual.lstrip(" ").partition(" ")[0] # extract the line num + expected = str(simple.__code__.co_firstlineno) self.assertEqual(actual, expected) # Use an explicit first line number (split extracts the line no) - actual = dis.Bytecode(simple, first_line=350).dis().split(" ")[0] + actual = dis.Bytecode(simple, first_line=350).dis() + actual = actual.lstrip(" ").partition(" ")[0] # extract the line num self.assertEqual(actual, "350") def test_info(self): From 33523c5dd354d3739571efe379f5d2dc98e07d84 Mon Sep 17 00:00:00 2001 From: gc Date: Sun, 13 Aug 2017 08:32:28 -0700 Subject: [PATCH 06/15] avoid coroutine warning by explicitly closing coroutine; clean up --- Lib/test/test_dis.py | 62 +++++++++----------------------------------- 1 file changed, 12 insertions(+), 50 deletions(-) diff --git a/Lib/test/test_dis.py b/Lib/test/test_dis.py index 44633ad12b7a6b7..01360826915b1df 100644 --- a/Lib/test/test_dis.py +++ b/Lib/test/test_dis.py @@ -9,8 +9,6 @@ import re import types import contextlib -import warnings -import gc def get_tb(): def _error(): @@ -399,41 +397,6 @@ def foo(x): ) - -@contextlib.contextmanager -def ignore_coroutine_never_awaited_warnings(): - """ - The coroutine test leaks a coroutine, and thus triggers the - "RuntimeWarning: coroutine '...' was never awaited" message. This context - manager should be used anywhere this happens to hide those messages, because - (a) when expected they're clutter, (b) on CPython 3.5.x where x < 3, this - warning can trigger a segfault if we run with warnings turned into errors: - https://bugs.python.org/issue27811. - - (Adapted from python-trio/trio/.../test_run.py) - """ - with warnings.catch_warnings(): - warnings.filterwarnings( - "ignore", message="coroutine '.*' was never awaited" - ) - try: - yield - finally: - # Make sure to trigger any coroutine __del__ methods now, before - # we leave the context manager. - - # In the test suite we sometimes want to call gc.collect() to make sure - # that any objects with noisy __del__ methods (e.g. unawaited coroutines) - # get collected before we continue, so their noise doesn't leak into - # unrelated tests. - # - # On PyPy, coroutine objects (for example) can survive at least 1 round of - # garbage collection, because executing their __del__ method to print the - # warning can cause them to be resurrected. So we call collect a few times - # to make sure. - for _ in range(4): - gc.collect() - class DisTests(unittest.TestCase): maxDiff = None @@ -575,19 +538,20 @@ 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 object self.assertEqual(gen_disas, gen_func_disas) def test_disassemble_async_generator(self): - agen_func_disas = self.get_disassembly(_ag) # Disassemble async generator function - agen_disas = self.get_disassembly(_ag(1)) # Disassemble async generator itself + agen_func_disas = self.get_disassembly(_ag) # Async generator function + agen_disas = self.get_disassembly(_ag(1)) # Async generator object self.assertEqual(agen_disas, agen_func_disas) def test_disassemble_coroutine(self): - coro_func_disas = self.get_disassembly(_co) # Disassemble coroutine function - with ignore_coroutine_never_awaited_warnings(): - coro_disas = self.get_disassembly(_co(1)) # Disassemble coroutine itself + 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): @@ -1105,14 +1069,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() - actual = actual.lstrip(" ").partition(" ")[0] # extract the line num - expected = str(simple.__code__.co_firstlineno) + # 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 (split extracts the line no) - actual = dis.Bytecode(simple, first_line=350).dis() - actual = actual.lstrip(" ").partition(" ")[0] # extract the line num + actual = dis.Bytecode(simple, first_line=350).dis().split(" ")[0] self.assertEqual(actual, "350") def test_info(self): From 7b3d19af629fab2d5deb89bf64d36e764d9cec2b Mon Sep 17 00:00:00 2001 From: gc Date: Sun, 13 Aug 2017 08:35:54 -0700 Subject: [PATCH 07/15] fix TypeError if compiled coroutine is actually run --- Lib/test/test_dis.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_dis.py b/Lib/test/test_dis.py index 01360826915b1df..57fa410de522033 100644 --- a/Lib/test/test_dis.py +++ b/Lib/test/test_dis.py @@ -335,7 +335,8 @@ async def _ag(x): yield x async def _co(x): - await _ag(x) + async for item in _ag(x): + pass def _h(y): def foo(x): From f3a90556e7a1735eac0631e98f54ada6df47750d Mon Sep 17 00:00:00 2001 From: gc Date: Sun, 13 Aug 2017 08:42:58 -0700 Subject: [PATCH 08/15] improve docstrings --- Lib/dis.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Lib/dis.py b/Lib/dis.py index a428be8d2868ded..9cf392195750638 100644 --- a/Lib/dis.py +++ b/Lib/dis.py @@ -32,11 +32,13 @@ def _try_compile(source, name): return c def dis(x=None, *, file=None, depth=None): - """Disassemble classes, methods, functions, generators, - asynchronous generators, coroutines, or code. + """Disassemble classes, methods, functions, other compiled objects, or code. - 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) @@ -112,9 +114,7 @@ def pretty_flags(flags): return ", ".join(names) def _get_code_object(x): - """Helper to handle methods, functions, generators, - asynchronous generators, coroutines, strings and - raw code objects""" + """Helper to handle methods, compiled or raw code objects, and strings.""" if hasattr(x, '__func__'): # Method x = x.__func__ if hasattr(x, '__code__'): # Function From 7f925468bbf3b4e99cb84e62ddf1b5645e5de156 Mon Sep 17 00:00:00 2001 From: gc Date: Sun, 13 Aug 2017 08:55:45 -0700 Subject: [PATCH 09/15] clarify control flow and improve comments This isn't DRY and should probably be refactored so that dis() calls _get_code_object() (or so that both call a simpler helper method) but that's a different patch. --- Lib/dis.py | 31 +++++++++++++++++++------------ 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/Lib/dis.py b/Lib/dis.py index 9cf392195750638..bf7aa79c003b377 100644 --- a/Lib/dis.py +++ b/Lib/dis.py @@ -43,16 +43,19 @@ def dis(x=None, *, file=None, depth=None): 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 x = x.gi_code - if hasattr(x, 'ag_code'): # Async generator + elif hasattr(x, 'ag_code'): #...an asynchronous generator object, or x = x.ag_code - if hasattr(x, 'cr_code'): # Coroutine + 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: @@ -115,19 +118,23 @@ def pretty_flags(flags): def _get_code_object(x): """Helper to handle methods, compiled or raw code objects, and strings.""" - 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 x = x.gi_code - if hasattr(x, 'ag_code'): # Async generator + elif hasattr(x, 'ag_code'): #...an asynchronous generator object, or x = x.ag_code - if hasattr(x, 'cr_code'): # Coroutine + elif hasattr(x, 'cr_code'): #...a coroutine. x = x.cr_code - if isinstance(x, str): # Source code + # Handle source code. + if isinstance(x, str): x = _try_compile(x, "") - 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__) From edaffa63e12c520df0d0aa1f6b1f5e15bd04c636 Mon Sep 17 00:00:00 2001 From: gc Date: Sun, 13 Aug 2017 09:07:10 -0700 Subject: [PATCH 10/15] add versionchanged tags to doc --- Doc/library/dis.rst | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/Doc/library/dis.rst b/Doc/library/dis.rst index bea5a6c93473b94..fe454f333f24a64 100644 --- a/Doc/library/dis.rst +++ b/Doc/library/dis.rst @@ -87,12 +87,15 @@ code. Return a formatted view of the bytecode operations (the same as printed by :func:`dis.dis`, but returned as a multi-line string). - + .. method:: info() Return a formatted multi-line string with detailed information about the code object, like :func:`code_info`. + .. versionchanged:: 3.7 + This can now handle asynchronous generator and coroutine objects. + Example:: >>> bytecode = dis.Bytecode(myfunc) @@ -123,6 +126,9 @@ operation is being performed, so the intermediate analysis object isn't useful: releases. .. versionadded:: 3.2 + + .. versionchanged:: 3.7 + This can now handle asynchronous generator and coroutine objects. .. function:: show_code(x, *, file=None) @@ -166,6 +172,9 @@ operation is being performed, so the intermediate analysis object isn't useful: .. versionchanged:: 3.7 Implemented recursive disassembling and added *depth* parameter. + + .. versionchanged:: 3.7 + This can now handle asynchronous generator and coroutine objects. .. function:: distb(tb=None, *, file=None) From 32d9d5b1cd48a5e25c2097f310a0bd9a10b9d9c6 Mon Sep 17 00:00:00 2001 From: gc Date: Sun, 13 Aug 2017 09:08:20 -0700 Subject: [PATCH 11/15] expand one more docstring --- Lib/dis.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/dis.py b/Lib/dis.py index bf7aa79c003b377..683c78a4c69bf08 100644 --- a/Lib/dis.py +++ b/Lib/dis.py @@ -461,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. """ From 42dd060b9a88e8e818c6c0d557ecc1cf5df5e493 Mon Sep 17 00:00:00 2001 From: gc Date: Sun, 13 Aug 2017 09:20:22 -0700 Subject: [PATCH 12/15] added blurb --- .../next/Library/2017-08-13-09-17-01.bpo-31183.-2_YGj.rst | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2017-08-13-09-17-01.bpo-31183.-2_YGj.rst diff --git a/Misc/NEWS.d/next/Library/2017-08-13-09-17-01.bpo-31183.-2_YGj.rst b/Misc/NEWS.d/next/Library/2017-08-13-09-17-01.bpo-31183.-2_YGj.rst new file mode 100644 index 000000000000000..ef7a31a8d01a72c --- /dev/null +++ b/Misc/NEWS.d/next/Library/2017-08-13-09-17-01.bpo-31183.-2_YGj.rst @@ -0,0 +1,2 @@ +`dis` now works with asynchronous generator and coroutine objects. Patch by +George Collins based on diagnosis by Luciano Ramalho. From 16b1d233a2f5470e5eee5e99c55c1af9c81253e2 Mon Sep 17 00:00:00 2001 From: gc Date: Sun, 13 Aug 2017 09:28:12 -0700 Subject: [PATCH 13/15] fix whitespace in doc (TODO: improve my workflow) --- Doc/library/dis.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Doc/library/dis.rst b/Doc/library/dis.rst index fe454f333f24a64..6f1630c7bfa0fec 100644 --- a/Doc/library/dis.rst +++ b/Doc/library/dis.rst @@ -87,7 +87,7 @@ code. Return a formatted view of the bytecode operations (the same as printed by :func:`dis.dis`, but returned as a multi-line string). - + .. method:: info() Return a formatted multi-line string with detailed information about the @@ -126,7 +126,7 @@ operation is being performed, so the intermediate analysis object isn't useful: releases. .. versionadded:: 3.2 - + .. versionchanged:: 3.7 This can now handle asynchronous generator and coroutine objects. @@ -172,7 +172,7 @@ operation is being performed, so the intermediate analysis object isn't useful: .. versionchanged:: 3.7 Implemented recursive disassembling and added *depth* parameter. - + .. versionchanged:: 3.7 This can now handle asynchronous generator and coroutine objects. From b969705b7e9123ad429e5a0b5547be0e6ad2d493 Mon Sep 17 00:00:00 2001 From: gc Date: Mon, 14 Aug 2017 23:44:53 -0700 Subject: [PATCH 14/15] cleaned up docs and comments per ncoghlan --- Doc/library/dis.rst | 6 +++--- Lib/dis.py | 2 +- Lib/test/test_dis.py | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Doc/library/dis.rst b/Doc/library/dis.rst index 6f1630c7bfa0fec..7b7c84df776cf99 100644 --- a/Doc/library/dis.rst +++ b/Doc/library/dis.rst @@ -94,7 +94,7 @@ code. code object, like :func:`code_info`. .. versionchanged:: 3.7 - This can now handle asynchronous generator and coroutine objects. + This can now handle coroutine and asynchronous generator objects. Example:: @@ -128,7 +128,7 @@ operation is being performed, so the intermediate analysis object isn't useful: .. versionadded:: 3.2 .. versionchanged:: 3.7 - This can now handle asynchronous generator and coroutine objects. + This can now handle coroutine and asynchronous generator objects. .. function:: show_code(x, *, file=None) @@ -174,7 +174,7 @@ operation is being performed, so the intermediate analysis object isn't useful: Implemented recursive disassembling and added *depth* parameter. .. versionchanged:: 3.7 - This can now handle asynchronous generator and coroutine objects. + This can now handle coroutine and asynchronous generator objects. .. function:: distb(tb=None, *, file=None) diff --git a/Lib/dis.py b/Lib/dis.py index 683c78a4c69bf08..6a2997cfcf77959 100644 --- a/Lib/dis.py +++ b/Lib/dis.py @@ -32,7 +32,7 @@ def _try_compile(source, name): return c def dis(x=None, *, file=None, depth=None): - """Disassemble classes, methods, functions, other compiled objects, or code. + """Disassemble classes, methods, functions, and other compiled objects. With no argument, disassemble the last traceback. diff --git a/Lib/test/test_dis.py b/Lib/test/test_dis.py index 57fa410de522033..cde09a0ff37cb38 100644 --- a/Lib/test/test_dis.py +++ b/Lib/test/test_dis.py @@ -540,12 +540,12 @@ def test_disassemble_class_method(self): def test_disassemble_generator(self): gen_func_disas = self.get_disassembly(_g) # Generator function - gen_disas = self.get_disassembly(_g(1)) # Generator object + 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 object + agen_disas = self.get_disassembly(_ag(1)) # Async generator iterator self.assertEqual(agen_disas, agen_func_disas) def test_disassemble_coroutine(self): From df4dc64c5e5a3e927822078ab9c2c38f007eee98 Mon Sep 17 00:00:00 2001 From: gc Date: Thu, 17 Aug 2017 18:42:42 -0700 Subject: [PATCH 15/15] fix docstring fomatting, tweak line number extraction --- Lib/dis.py | 8 ++++---- Lib/test/test_dis.py | 12 +++++++----- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/Lib/dis.py b/Lib/dis.py index 6a2997cfcf77959..90ddf4f3360324c 100644 --- a/Lib/dis.py +++ b/Lib/dis.py @@ -34,11 +34,11 @@ def _try_compile(source, name): def dis(x=None, *, file=None, depth=None): """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. + 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) diff --git a/Lib/test/test_dis.py b/Lib/test/test_dis.py index cde09a0ff37cb38..bfbbee2794a610b 100644 --- a/Lib/test/test_dis.py +++ b/Lib/test/test_dis.py @@ -1070,12 +1070,14 @@ 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 (split extracts the line no) - actual = dis.Bytecode(simple).dis().split(" ")[0] - expected = "{:>3}".format(simple.__code__.co_firstlineno) + # Use the line in the source code + actual = dis.Bytecode(simple).dis() + actual = actual.strip().partition(" ")[0] # extract the line no + expected = str(simple.__code__.co_firstlineno) self.assertEqual(actual, expected) - # Use an explicit first line number (split extracts the line no) - actual = dis.Bytecode(simple, first_line=350).dis().split(" ")[0] + # Use an explicit first line number + actual = dis.Bytecode(simple, first_line=350).dis() + actual = actual.strip().partition(" ")[0] # extract the line no self.assertEqual(actual, "350") def test_info(self):