Author: Carl Friedrich Bolz-Tereick <cfb...@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) + +@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 _______________________________________________ pypy-commit mailing list pypy-commit@python.org https://mail.python.org/mailman/listinfo/pypy-commit