[pypy-commit] pypy py3.7: implement coroutine origin tracking

cfbolz pypy.commits at gmail.com
Fri Jan 31 14:34:00 EST 2020


Author: Carl Friedrich Bolz-Tereick <cfbolz at gmx.de>
Branch: py3.7
Changeset: r98615:5c1dc1c61dcc
Date: 2020-01-31 20:28 +0100
http://bitbucket.org/pypy/pypy/changeset/5c1dc1c61dcc/

Log:	implement coroutine origin tracking

diff --git a/pypy/interpreter/executioncontext.py b/pypy/interpreter/executioncontext.py
--- a/pypy/interpreter/executioncontext.py
+++ b/pypy/interpreter/executioncontext.py
@@ -45,6 +45,7 @@
         self.w_asyncgen_firstiter_fn = None
         self.w_asyncgen_finalizer_fn = None
         self.contextvar_context = None
+        self.coroutine_origin_tracking_depth = 0
 
     @staticmethod
     def _mark_thread_disappeared(space):
diff --git a/pypy/interpreter/generator.py b/pypy/interpreter/generator.py
--- a/pypy/interpreter/generator.py
+++ b/pypy/interpreter/generator.py
@@ -348,17 +348,42 @@
     "A coroutine object."
     KIND = "coroutine"
 
+    def __init__(self, frame, name=None, qualname=None):
+        GeneratorOrCoroutine.__init__(self, frame, name, qualname)
+        self.w_cr_origin = self.space.w_None
+
+    def capture_origin(self, ec):
+        if not ec.coroutine_origin_tracking_depth:
+            return
+        self._capture_origin(ec)
+
+    def _capture_origin(self, ec):
+        space = self.space
+        frames_w = []
+        frame = ec.gettopframe_nohidden()
+        for i in range(ec.coroutine_origin_tracking_depth):
+            frames_w.append(
+                space.newtuple([
+                    frame.pycode.w_filename,
+                    frame.fget_f_lineno(space),
+                    space.newtext(frame.pycode.co_name)]))
+            frame = ec.getnextframe_nohidden(frame)
+            if frame is None:
+                break
+        self.w_cr_origin = space.newtuple(frames_w)
+
     def descr__await__(self, space):
         return CoroutineWrapper(self)
 
     def _finalize_(self):
         # If coroutine was never awaited on issue a RuntimeWarning.
-        if self.pycode is not None and \
-           self.frame is not None and \
-           self.frame.last_instr == -1:
+        if (self.pycode is not None and
+                self.frame is not None and
+                self.frame.last_instr == -1):
             space = self.space
-            msg = "coroutine '%s' was never awaited" % self.get_qualname()
-            space.warn(space.newtext(msg), space.w_RuntimeWarning)
+            w_mod = space.getbuiltinmodule("_warnings")
+            w_f = space.getattr(w_mod, space.newtext("_warn_unawaited_coroutine"))
+            space.call_function(w_f, self)
         GeneratorOrCoroutine._finalize_(self)
 
 
diff --git a/pypy/interpreter/pyframe.py b/pypy/interpreter/pyframe.py
--- a/pypy/interpreter/pyframe.py
+++ b/pypy/interpreter/pyframe.py
@@ -266,6 +266,7 @@
             gen = Coroutine(self, name, qualname)
             ec = space.getexecutioncontext()
             w_wrapper = ec.w_coroutine_wrapper_fn
+            gen.capture_origin(ec)
         elif flags & pycode.CO_ASYNC_GENERATOR:
             from pypy.interpreter.generator import AsyncGenerator
             gen = AsyncGenerator(self, name, qualname)
diff --git a/pypy/interpreter/test/apptest_coroutine.py b/pypy/interpreter/test/apptest_coroutine.py
--- a/pypy/interpreter/test/apptest_coroutine.py
+++ b/pypy/interpreter/test/apptest_coroutine.py
@@ -840,3 +840,65 @@
         assert finalized == 2
     finally:
         sys.set_asyncgen_hooks(*old_hooks)
+
+def test_coroutine_capture_origin():
+    import contextlib
+
+    def here():
+        f = sys._getframe().f_back
+        return (f.f_code.co_filename, f.f_lineno)
+
+    try:
+        async def corofn():
+            pass
+
+        with contextlib.closing(corofn()) as coro:
+            assert coro.cr_origin is None
+
+        sys.set_coroutine_origin_tracking_depth(1)
+
+        fname, lineno = here()
+        with contextlib.closing(corofn()) as coro:
+            print(coro.cr_origin)
+            assert coro.cr_origin == (
+                (fname, lineno + 1, "test_coroutine_capture_origin"),)
+
+
+        sys.set_coroutine_origin_tracking_depth(2)
+
+        def nested():
+            return (here(), corofn())
+        fname, lineno = here()
+        ((nested_fname, nested_lineno), coro) = nested()
+        with contextlib.closing(coro):
+            print(coro.cr_origin)
+            assert coro.cr_origin == (
+                (nested_fname, nested_lineno, "nested"),
+                (fname, lineno + 1, "test_coroutine_capture_origin"))
+
+        # Check we handle running out of frames correctly
+        sys.set_coroutine_origin_tracking_depth(1000)
+        with contextlib.closing(corofn()) as coro:
+            print(coro.cr_origin)
+            assert 1 <= len(coro.cr_origin) < 1000
+    finally:
+        sys.set_coroutine_origin_tracking_depth(0)
+
+def test_runtime_warning_origin_tracking():
+    import gc, warnings  # XXX: importing warnings is expensive untranslated
+    async def foobaz():
+        pass
+    gc.collect()   # emit warnings from unrelated older tests
+    with warnings.catch_warnings(record=True) as l:
+        foobaz()
+        gc.collect()
+        gc.collect()
+        gc.collect()
+
+    assert len(l) == 1, repr(l)
+    w = l[0].message
+    assert isinstance(w, RuntimeWarning)
+    assert str(w).startswith("coroutine ")
+    assert str(w).endswith("foobaz' was never awaited")
+    assert "test_runtime_warning_origin_tracking" in str(w)
+
diff --git a/pypy/interpreter/typedef.py b/pypy/interpreter/typedef.py
--- a/pypy/interpreter/typedef.py
+++ b/pypy/interpreter/typedef.py
@@ -865,6 +865,7 @@
     cr_frame   = GetSetProperty(Coroutine.descr_gicr_frame),
     cr_code    = interp_attrproperty_w('pycode', cls=Coroutine),
     cr_await=GetSetProperty(Coroutine.descr_delegate),
+    cr_origin  = interp_attrproperty_w('w_cr_origin', cls=Coroutine),
     __name__   = GetSetProperty(Coroutine.descr__name__,
                                 Coroutine.descr_set__name__,
                                 doc="name of the coroutine"),
diff --git a/pypy/module/_warnings/app_warnings.py b/pypy/module/_warnings/app_warnings.py
new file mode 100644
--- /dev/null
+++ b/pypy/module/_warnings/app_warnings.py
@@ -0,0 +1,15 @@
+def _warn_unawaited_coroutine(coro):
+    from _warnings import warn
+    msg_lines = [
+        f"coroutine '{coro.__qualname__}' was never awaited\n"
+    ]
+    if coro.cr_origin is not None:
+        import linecache, traceback
+        def extract():
+            for filename, lineno, funcname in reversed(coro.cr_origin):
+                line = linecache.getline(filename, lineno)
+                yield (filename, lineno, funcname, line)
+        msg_lines.append("Coroutine created at (most recent call last)\n")
+        msg_lines += traceback.format_list(list(extract()))
+    msg = "".join(msg_lines).rstrip("\n")
+    warn(msg, category=RuntimeWarning, stacklevel=2, source=coro)
diff --git a/pypy/module/_warnings/moduledef.py b/pypy/module/_warnings/moduledef.py
--- a/pypy/module/_warnings/moduledef.py
+++ b/pypy/module/_warnings/moduledef.py
@@ -11,6 +11,7 @@
     }
 
     appleveldefs = {
+        '_warn_unawaited_coroutine' : 'app_warnings._warn_unawaited_coroutine',
     }
 
     def setup_after_space_initialization(self):
diff --git a/pypy/module/sys/moduledef.py b/pypy/module/sys/moduledef.py
--- a/pypy/module/sys/moduledef.py
+++ b/pypy/module/sys/moduledef.py
@@ -100,6 +100,9 @@
         'set_asyncgen_hooks'    : 'vm.set_asyncgen_hooks',
 
         'is_finalizing'         : 'vm.is_finalizing',
+
+        'get_coroutine_origin_tracking_depth': 'vm.get_coroutine_origin_tracking_depth',
+        'set_coroutine_origin_tracking_depth': 'vm.set_coroutine_origin_tracking_depth',
         }
 
     if sys.platform == 'win32':
diff --git a/pypy/module/sys/test/test_sysmodule.py b/pypy/module/sys/test/test_sysmodule.py
--- a/pypy/module/sys/test/test_sysmodule.py
+++ b/pypy/module/sys/test/test_sysmodule.py
@@ -812,6 +812,20 @@
         assert cur.firstiter is None
         assert cur.finalizer is None
 
+    def test_coroutine_origin_tracking_depth(self):
+        import sys
+        depth = sys.get_coroutine_origin_tracking_depth()
+        assert depth == 0
+        try:
+            sys.set_coroutine_origin_tracking_depth(6)
+            depth = sys.get_coroutine_origin_tracking_depth()
+            assert depth == 6
+            with raises(ValueError):
+                sys.set_coroutine_origin_tracking_depth(-5)
+        finally:
+            sys.set_coroutine_origin_tracking_depth(0)
+
+
 
 class AppTestSysSettracePortedFromCpython(object):
     def test_sys_settrace(self):
diff --git a/pypy/module/sys/vm.py b/pypy/module/sys/vm.py
--- a/pypy/module/sys/vm.py
+++ b/pypy/module/sys/vm.py
@@ -402,3 +402,26 @@
 
 def is_finalizing(space):
     return space.newbool(space.sys.finalizing)
+
+def get_coroutine_origin_tracking_depth(space):
+    """get_coroutine_origin_tracking_depth()
+        Check status of origin tracking for coroutine objects in this thread.
+    """
+    ec = space.getexecutioncontext()
+    return space.newint(ec.coroutine_origin_tracking_depth)
+
+ at unwrap_spec(depth=int)
+def set_coroutine_origin_tracking_depth(space, depth):
+    """set_coroutine_origin_tracking_depth(depth)
+        Enable or disable origin tracking for coroutine objects in this thread.
+
+        Coroutine objects will track 'depth' frames of traceback information
+        about where they came from, available in their cr_origin attribute.
+
+        Set a depth of 0 to disable.
+    """
+    if depth < 0:
+        raise oefmt(space.w_ValueError,
+                "depth must be >= 0")
+    ec = space.getexecutioncontext()
+    ec.coroutine_origin_tracking_depth = depth


More information about the pypy-commit mailing list