[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