bpo-32591: Add native coroutine origin tracking (#5250)

* Add coro.cr_origin and sys.set_coroutine_origin_tracking_depth
* Use coroutine origin information in the unawaited coroutine warning
* Stop using set_coroutine_wrapper in asyncio debug mode
* In BaseEventLoop.set_debug, enable debugging in the correct thread
This commit is contained in:
Nathaniel J. Smith 2018-01-21 06:44:07 -08:00 committed by Yury Selivanov
parent 1211c9a989
commit fc2f407829
20 changed files with 485 additions and 100 deletions

View File

@ -34,6 +34,9 @@ provided as convenient choices for the second argument to :func:`getmembers`.
They also help you determine when you can expect to find the following special They also help you determine when you can expect to find the following special
attributes: attributes:
.. this function name is too big to fit in the ascii-art table below
.. |coroutine-origin-link| replace:: :func:`sys.set_coroutine_origin_tracking_depth`
+-----------+-------------------+---------------------------+ +-----------+-------------------+---------------------------+
| Type | Attribute | Description | | Type | Attribute | Description |
+===========+===================+===========================+ +===========+===================+===========================+
@ -215,6 +218,10 @@ attributes:
+-----------+-------------------+---------------------------+ +-----------+-------------------+---------------------------+
| | cr_code | code | | | cr_code | code |
+-----------+-------------------+---------------------------+ +-----------+-------------------+---------------------------+
| | cr_origin | where coroutine was |
| | | created, or ``None``. See |
| | | |coroutine-origin-link| |
+-----------+-------------------+---------------------------+
| builtin | __doc__ | documentation string | | builtin | __doc__ | documentation string |
+-----------+-------------------+---------------------------+ +-----------+-------------------+---------------------------+
| | __name__ | original name of this | | | __name__ | original name of this |
@ -234,6 +241,9 @@ attributes:
The ``__name__`` attribute of generators is now set from the function The ``__name__`` attribute of generators is now set from the function
name, instead of the code name, and it can now be modified. name, instead of the code name, and it can now be modified.
.. versionchanged:: 3.7
Add ``cr_origin`` attribute to coroutines.
.. function:: getmembers(object[, predicate]) .. function:: getmembers(object[, predicate])

View File

@ -675,6 +675,18 @@ always available.
for details.) for details.)
.. function:: get_coroutine_origin_tracking_depth()
Get the current coroutine origin tracking depth, as set by
func:`set_coroutine_origin_tracking_depth`.
.. versionadded:: 3.7
.. note::
This function has been added on a provisional basis (see :pep:`411`
for details.) Use it only for debugging purposes.
.. function:: get_coroutine_wrapper() .. function:: get_coroutine_wrapper()
Returns ``None``, or a wrapper set by :func:`set_coroutine_wrapper`. Returns ``None``, or a wrapper set by :func:`set_coroutine_wrapper`.
@ -686,6 +698,10 @@ always available.
This function has been added on a provisional basis (see :pep:`411` This function has been added on a provisional basis (see :pep:`411`
for details.) Use it only for debugging purposes. for details.) Use it only for debugging purposes.
.. deprecated:: 3.7
The coroutine wrapper functionality has been deprecated, and
will be removed in 3.8. See :issue:`32591` for details.
.. data:: hash_info .. data:: hash_info
@ -1212,6 +1228,26 @@ always available.
This function has been added on a provisional basis (see :pep:`411` This function has been added on a provisional basis (see :pep:`411`
for details.) for details.)
.. function:: set_coroutine_origin_tracking_depth(depth)
Allows enabling or disabling coroutine origin tracking. When
enabled, the ``cr_origin`` attribute on coroutine objects will
contain a tuple of (filename, line number, function name) tuples
describing the traceback where the coroutine object was created,
with the most recent call first. When disabled, ``cr_origin`` will
be None.
To enable, pass a *depth* value greater than zero; this sets the
number of frames whose information will be captured. To disable,
pass set *depth* to zero.
This setting is thread-specific.
.. versionadded:: 3.7
.. note::
This function has been added on a provisional basis (see :pep:`411`
for details.) Use it only for debugging purposes.
.. function:: set_coroutine_wrapper(wrapper) .. function:: set_coroutine_wrapper(wrapper)
@ -1252,6 +1288,10 @@ always available.
This function has been added on a provisional basis (see :pep:`411` This function has been added on a provisional basis (see :pep:`411`
for details.) Use it only for debugging purposes. for details.) Use it only for debugging purposes.
.. deprecated:: 3.7
The coroutine wrapper functionality has been deprecated, and
will be removed in 3.8. See :issue:`32591` for details.
.. function:: _enablelegacywindowsfsencoding() .. function:: _enablelegacywindowsfsencoding()
Changes the default filesystem encoding and errors mode to 'mbcs' and Changes the default filesystem encoding and errors mode to 'mbcs' and

View File

@ -510,6 +510,9 @@ sys
Added :attr:`sys.flags.dev_mode` flag for the new development mode. Added :attr:`sys.flags.dev_mode` flag for the new development mode.
Deprecated :func:`sys.set_coroutine_wrapper` and
:func:`sys.get_coroutine_wrapper`.
time time
---- ----

View File

@ -31,6 +31,8 @@ PyAPI_FUNC(PyObject *) PyEval_CallMethod(PyObject *obj,
#ifndef Py_LIMITED_API #ifndef Py_LIMITED_API
PyAPI_FUNC(void) PyEval_SetProfile(Py_tracefunc, PyObject *); PyAPI_FUNC(void) PyEval_SetProfile(Py_tracefunc, PyObject *);
PyAPI_FUNC(void) PyEval_SetTrace(Py_tracefunc, PyObject *); PyAPI_FUNC(void) PyEval_SetTrace(Py_tracefunc, PyObject *);
PyAPI_FUNC(void) _PyEval_SetCoroutineOriginTrackingDepth(int new_depth);
PyAPI_FUNC(int) _PyEval_GetCoroutineOriginTrackingDepth(void);
PyAPI_FUNC(void) _PyEval_SetCoroutineWrapper(PyObject *); PyAPI_FUNC(void) _PyEval_SetCoroutineWrapper(PyObject *);
PyAPI_FUNC(PyObject *) _PyEval_GetCoroutineWrapper(void); PyAPI_FUNC(PyObject *) _PyEval_GetCoroutineWrapper(void);
PyAPI_FUNC(void) _PyEval_SetAsyncGenFirstiter(PyObject *); PyAPI_FUNC(void) _PyEval_SetAsyncGenFirstiter(PyObject *);

View File

@ -51,6 +51,7 @@ PyAPI_FUNC(void) _PyGen_Finalize(PyObject *self);
#ifndef Py_LIMITED_API #ifndef Py_LIMITED_API
typedef struct { typedef struct {
_PyGenObject_HEAD(cr) _PyGenObject_HEAD(cr)
PyObject *cr_origin;
} PyCoroObject; } PyCoroObject;
PyAPI_DATA(PyTypeObject) PyCoro_Type; PyAPI_DATA(PyTypeObject) PyCoro_Type;

View File

@ -262,6 +262,8 @@ typedef struct _ts {
void (*on_delete)(void *); void (*on_delete)(void *);
void *on_delete_data; void *on_delete_data;
int coroutine_origin_tracking_depth;
PyObject *coroutine_wrapper; PyObject *coroutine_wrapper;
int in_coroutine_wrapper; int in_coroutine_wrapper;

View File

@ -56,6 +56,10 @@ PyErr_WarnExplicitFormat(PyObject *category,
#define PyErr_Warn(category, msg) PyErr_WarnEx(category, msg, 1) #define PyErr_Warn(category, msg) PyErr_WarnEx(category, msg, 1)
#endif #endif
#ifndef Py_LIMITED_API
void _PyErr_WarnUnawaitedCoroutine(PyObject *coro);
#endif
#ifdef __cplusplus #ifdef __cplusplus
} }
#endif #endif

View File

@ -34,6 +34,7 @@ try:
except ImportError: # pragma: no cover except ImportError: # pragma: no cover
ssl = None ssl = None
from . import constants
from . import coroutines from . import coroutines
from . import events from . import events
from . import futures from . import futures
@ -224,7 +225,8 @@ class BaseEventLoop(events.AbstractEventLoop):
self.slow_callback_duration = 0.1 self.slow_callback_duration = 0.1
self._current_handle = None self._current_handle = None
self._task_factory = None self._task_factory = None
self._coroutine_wrapper_set = False self._coroutine_origin_tracking_enabled = False
self._coroutine_origin_tracking_saved_depth = None
if hasattr(sys, 'get_asyncgen_hooks'): if hasattr(sys, 'get_asyncgen_hooks'):
# Python >= 3.6 # Python >= 3.6
@ -382,7 +384,7 @@ class BaseEventLoop(events.AbstractEventLoop):
if events._get_running_loop() is not None: if events._get_running_loop() is not None:
raise RuntimeError( raise RuntimeError(
'Cannot run the event loop while another loop is running') 'Cannot run the event loop while another loop is running')
self._set_coroutine_wrapper(self._debug) self._set_coroutine_origin_tracking(self._debug)
self._thread_id = threading.get_ident() self._thread_id = threading.get_ident()
if self._asyncgens is not None: if self._asyncgens is not None:
old_agen_hooks = sys.get_asyncgen_hooks() old_agen_hooks = sys.get_asyncgen_hooks()
@ -398,7 +400,7 @@ class BaseEventLoop(events.AbstractEventLoop):
self._stopping = False self._stopping = False
self._thread_id = None self._thread_id = None
events._set_running_loop(None) events._set_running_loop(None)
self._set_coroutine_wrapper(False) self._set_coroutine_origin_tracking(False)
if self._asyncgens is not None: if self._asyncgens is not None:
sys.set_asyncgen_hooks(*old_agen_hooks) sys.set_asyncgen_hooks(*old_agen_hooks)
@ -1531,39 +1533,20 @@ class BaseEventLoop(events.AbstractEventLoop):
handle._run() handle._run()
handle = None # Needed to break cycles when an exception occurs. handle = None # Needed to break cycles when an exception occurs.
def _set_coroutine_wrapper(self, enabled): def _set_coroutine_origin_tracking(self, enabled):
try: if bool(enabled) == bool(self._coroutine_origin_tracking_enabled):
set_wrapper = sys.set_coroutine_wrapper
get_wrapper = sys.get_coroutine_wrapper
except AttributeError:
return return
enabled = bool(enabled)
if self._coroutine_wrapper_set == enabled:
return
wrapper = coroutines.debug_wrapper
current_wrapper = get_wrapper()
if enabled: if enabled:
if current_wrapper not in (None, wrapper): self._coroutine_origin_tracking_saved_depth = (
warnings.warn( sys.get_coroutine_origin_tracking_depth())
f"loop.set_debug(True): cannot set debug coroutine " sys.set_coroutine_origin_tracking_depth(
f"wrapper; another wrapper is already set " constants.DEBUG_STACK_DEPTH)
f"{current_wrapper!r}",
RuntimeWarning)
else: else:
set_wrapper(wrapper) sys.set_coroutine_origin_tracking_depth(
self._coroutine_wrapper_set = True self._coroutine_origin_tracking_saved_depth)
else:
if current_wrapper not in (None, wrapper): self._coroutine_origin_tracking_enabled = enabled
warnings.warn(
f"loop.set_debug(False): cannot unset debug coroutine "
f"wrapper; another wrapper was set {current_wrapper!r}",
RuntimeWarning)
else:
set_wrapper(None)
self._coroutine_wrapper_set = False
def get_debug(self): def get_debug(self):
return self._debug return self._debug
@ -1572,4 +1555,4 @@ class BaseEventLoop(events.AbstractEventLoop):
self._debug = enabled self._debug = enabled
if self.is_running(): if self.is_running():
self._set_coroutine_wrapper(enabled) self.call_soon_threadsafe(self._set_coroutine_origin_tracking, enabled)

View File

@ -32,14 +32,6 @@ def _is_debug_mode():
_DEBUG = _is_debug_mode() _DEBUG = _is_debug_mode()
def debug_wrapper(gen):
# This function is called from 'sys.set_coroutine_wrapper'.
# We only wrap here coroutines defined via 'async def' syntax.
# Generator-based coroutines are wrapped in @coroutine
# decorator.
return CoroWrapper(gen, None)
class CoroWrapper: class CoroWrapper:
# Wrapper for coroutine object in _DEBUG mode. # Wrapper for coroutine object in _DEBUG mode.
@ -87,39 +79,16 @@ class CoroWrapper:
return self.gen.gi_code return self.gen.gi_code
def __await__(self): def __await__(self):
cr_await = getattr(self.gen, 'cr_await', None)
if cr_await is not None:
raise RuntimeError(
f"Cannot await on coroutine {self.gen!r} while it's "
f"awaiting for {cr_await!r}")
return self return self
@property @property
def gi_yieldfrom(self): def gi_yieldfrom(self):
return self.gen.gi_yieldfrom return self.gen.gi_yieldfrom
@property
def cr_await(self):
return self.gen.cr_await
@property
def cr_running(self):
return self.gen.cr_running
@property
def cr_code(self):
return self.gen.cr_code
@property
def cr_frame(self):
return self.gen.cr_frame
def __del__(self): def __del__(self):
# Be careful accessing self.gen.frame -- self.gen might not exist. # Be careful accessing self.gen.frame -- self.gen might not exist.
gen = getattr(self, 'gen', None) gen = getattr(self, 'gen', None)
frame = getattr(gen, 'gi_frame', None) frame = getattr(gen, 'gi_frame', None)
if frame is None:
frame = getattr(gen, 'cr_frame', None)
if frame is not None and frame.f_lasti == -1: if frame is not None and frame.f_lasti == -1:
msg = f'{self!r} was never yielded from' msg = f'{self!r} was never yielded from'
tb = getattr(self, '_source_traceback', ()) tb = getattr(self, '_source_traceback', ())
@ -141,8 +110,6 @@ def coroutine(func):
if inspect.iscoroutinefunction(func): if inspect.iscoroutinefunction(func):
# In Python 3.5 that's all we need to do for coroutines # In Python 3.5 that's all we need to do for coroutines
# defined with "async def". # defined with "async def".
# Wrapping in CoroWrapper will happen via
# 'sys.set_coroutine_wrapper' function.
return func return func
if inspect.isgeneratorfunction(func): if inspect.isgeneratorfunction(func):

View File

@ -1,5 +1,6 @@
"""Tests support for new syntax introduced by PEP 492.""" """Tests support for new syntax introduced by PEP 492."""
import sys
import types import types
import unittest import unittest
@ -148,35 +149,14 @@ class CoroutineTests(BaseTest):
data = self.loop.run_until_complete(foo()) data = self.loop.run_until_complete(foo())
self.assertEqual(data, 'spam') self.assertEqual(data, 'spam')
@mock.patch('asyncio.coroutines.logger') def test_debug_mode_manages_coroutine_origin_tracking(self):
def test_async_def_wrapped(self, m_log):
async def foo():
pass
async def start(): async def start():
foo_coro = foo() self.assertTrue(sys.get_coroutine_origin_tracking_depth() > 0)
self.assertRegex(
repr(foo_coro),
r'<CoroWrapper .*\.foo\(\) running at .*pep492.*>')
with support.check_warnings((r'.*foo.*was never',
RuntimeWarning)):
foo_coro = None
support.gc_collect()
self.assertTrue(m_log.error.called)
message = m_log.error.call_args[0][0]
self.assertRegex(message,
r'CoroWrapper.*foo.*was never')
self.assertEqual(sys.get_coroutine_origin_tracking_depth(), 0)
self.loop.set_debug(True) self.loop.set_debug(True)
self.loop.run_until_complete(start()) self.loop.run_until_complete(start())
self.assertEqual(sys.get_coroutine_origin_tracking_depth(), 0)
async def start():
foo_coro = foo()
task = asyncio.ensure_future(foo_coro, loop=self.loop)
self.assertRegex(repr(task), r'Task.*foo.*running')
self.loop.run_until_complete(start())
def test_types_coroutine(self): def test_types_coroutine(self):
def gen(): def gen():
@ -226,9 +206,9 @@ class CoroutineTests(BaseTest):
t.cancel() t.cancel()
self.loop.set_debug(True) self.loop.set_debug(True)
with self.assertRaisesRegex( with self.assertRaises(
RuntimeError, RuntimeError,
r'Cannot await.*test_double_await.*\bafunc\b.*while.*\bsleep\b'): msg='coroutine is being awaited already'):
self.loop.run_until_complete(runner()) self.loop.run_until_complete(runner())

View File

@ -2,6 +2,7 @@ import contextlib
import copy import copy
import inspect import inspect
import pickle import pickle
import re
import sys import sys
import types import types
import unittest import unittest
@ -1974,8 +1975,10 @@ class SysSetCoroWrapperTest(unittest.TestCase):
wrapped = gen wrapped = gen
return gen return gen
with self.assertWarns(DeprecationWarning):
self.assertIsNone(sys.get_coroutine_wrapper()) self.assertIsNone(sys.get_coroutine_wrapper())
with self.assertWarns(DeprecationWarning):
sys.set_coroutine_wrapper(wrap) sys.set_coroutine_wrapper(wrap)
self.assertIs(sys.get_coroutine_wrapper(), wrap) self.assertIs(sys.get_coroutine_wrapper(), wrap)
try: try:
@ -2041,6 +2044,130 @@ class SysSetCoroWrapperTest(unittest.TestCase):
sys.set_coroutine_wrapper(None) sys.set_coroutine_wrapper(None)
class OriginTrackingTest(unittest.TestCase):
def here(self):
info = inspect.getframeinfo(inspect.currentframe().f_back)
return (info.filename, info.lineno)
def test_origin_tracking(self):
orig_depth = sys.get_coroutine_origin_tracking_depth()
try:
async def corofn():
pass
sys.set_coroutine_origin_tracking_depth(0)
self.assertEqual(sys.get_coroutine_origin_tracking_depth(), 0)
with contextlib.closing(corofn()) as coro:
self.assertIsNone(coro.cr_origin)
sys.set_coroutine_origin_tracking_depth(1)
self.assertEqual(sys.get_coroutine_origin_tracking_depth(), 1)
fname, lineno = self.here()
with contextlib.closing(corofn()) as coro:
self.assertEqual(coro.cr_origin,
((fname, lineno + 1, "test_origin_tracking"),))
sys.set_coroutine_origin_tracking_depth(2)
self.assertEqual(sys.get_coroutine_origin_tracking_depth(), 2)
def nested():
return (self.here(), corofn())
fname, lineno = self.here()
((nested_fname, nested_lineno), coro) = nested()
with contextlib.closing(coro):
self.assertEqual(coro.cr_origin,
((nested_fname, nested_lineno, "nested"),
(fname, lineno + 1, "test_origin_tracking")))
# Check we handle running out of frames correctly
sys.set_coroutine_origin_tracking_depth(1000)
with contextlib.closing(corofn()) as coro:
self.assertTrue(2 < len(coro.cr_origin) < 1000)
# We can't set depth negative
with self.assertRaises(ValueError):
sys.set_coroutine_origin_tracking_depth(-1)
# And trying leaves it unchanged
self.assertEqual(sys.get_coroutine_origin_tracking_depth(), 1000)
finally:
sys.set_coroutine_origin_tracking_depth(orig_depth)
def test_origin_tracking_warning(self):
async def corofn():
pass
a1_filename, a1_lineno = self.here()
def a1():
return corofn() # comment in a1
a1_lineno += 2
a2_filename, a2_lineno = self.here()
def a2():
return a1() # comment in a2
a2_lineno += 2
def check(depth, msg):
sys.set_coroutine_origin_tracking_depth(depth)
with warnings.catch_warnings(record=True) as wlist:
a2()
support.gc_collect()
# This might be fragile if other warnings somehow get triggered
# inside our 'with' block... let's worry about that if/when it
# happens.
self.assertTrue(len(wlist) == 1)
self.assertIs(wlist[0].category, RuntimeWarning)
self.assertEqual(msg, str(wlist[0].message))
orig_depth = sys.get_coroutine_origin_tracking_depth()
try:
msg = check(0, f"coroutine '{corofn.__qualname__}' was never awaited")
check(1, "".join([
f"coroutine '{corofn.__qualname__}' was never awaited\n",
"Coroutine created at (most recent call last)\n",
f' File "{a1_filename}", line {a1_lineno}, in a1\n',
f' return corofn() # comment in a1',
]))
check(2, "".join([
f"coroutine '{corofn.__qualname__}' was never awaited\n",
"Coroutine created at (most recent call last)\n",
f' File "{a2_filename}", line {a2_lineno}, in a2\n',
f' return a1() # comment in a2\n',
f' File "{a1_filename}", line {a1_lineno}, in a1\n',
f' return corofn() # comment in a1',
]))
finally:
sys.set_coroutine_origin_tracking_depth(orig_depth)
def test_unawaited_warning_when_module_broken(self):
# Make sure we don't blow up too bad if
# warnings._warn_unawaited_coroutine is broken somehow (e.g. because
# of shutdown problems)
async def corofn():
pass
orig_wuc = warnings._warn_unawaited_coroutine
try:
warnings._warn_unawaited_coroutine = lambda coro: 1/0
with support.captured_stderr() as stream:
corofn()
support.gc_collect()
self.assertIn("Exception ignored in", stream.getvalue())
self.assertIn("ZeroDivisionError", stream.getvalue())
self.assertIn("was never awaited", stream.getvalue())
del warnings._warn_unawaited_coroutine
with support.captured_stderr() as stream:
corofn()
support.gc_collect()
self.assertIn("was never awaited", stream.getvalue())
finally:
warnings._warn_unawaited_coroutine = orig_wuc
@support.cpython_only @support.cpython_only
class CAPITest(unittest.TestCase): class CAPITest(unittest.TestCase):

View File

@ -488,6 +488,29 @@ class catch_warnings(object):
self._module._showwarnmsg_impl = self._showwarnmsg_impl self._module._showwarnmsg_impl = self._showwarnmsg_impl
# Private utility function called by _PyErr_WarnUnawaitedCoroutine
def _warn_unawaited_coroutine(coro):
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")
# Passing source= here means that if the user happens to have tracemalloc
# enabled and tracking where the coroutine was created, the warning will
# contain that traceback. This does mean that if they have *both*
# coroutine origin tracking *and* tracemalloc enabled, they'll get two
# partially-redundant tracebacks. If we wanted to be clever we could
# probably detect this case and avoid it, but for now we don't bother.
warn(msg, category=RuntimeWarning, stacklevel=2, source=coro)
# filters contains a sequence of filter 5-tuples # filters contains a sequence of filter 5-tuples
# The components of the 5-tuple are: # The components of the 5-tuple are:
# - an action: error, ignore, always, default, module, or once # - an action: error, ignore, always, default, module, or once

View File

@ -1486,6 +1486,7 @@ Christopher Smith
Eric V. Smith Eric V. Smith
Gregory P. Smith Gregory P. Smith
Mark Smith Mark Smith
Nathaniel J. Smith
Roy Smith Roy Smith
Ryan Smith-Roberts Ryan Smith-Roberts
Rafal Smotrzyk Rafal Smotrzyk

View File

@ -0,0 +1,4 @@
Added built-in support for tracking the origin of coroutine objects; see
sys.set_coroutine_origin_tracking_depth and CoroutineType.cr_origin. This
replaces the asyncio debug mode's use of coroutine wrapping for native
coroutine objects.

View File

@ -32,6 +32,8 @@ gen_traverse(PyGenObject *gen, visitproc visit, void *arg)
Py_VISIT(gen->gi_code); Py_VISIT(gen->gi_code);
Py_VISIT(gen->gi_name); Py_VISIT(gen->gi_name);
Py_VISIT(gen->gi_qualname); Py_VISIT(gen->gi_qualname);
/* No need to visit cr_origin, because it's just tuples/str/int, so can't
participate in a reference cycle. */
return exc_state_traverse(&gen->gi_exc_state, visit, arg); return exc_state_traverse(&gen->gi_exc_state, visit, arg);
} }
@ -75,9 +77,7 @@ _PyGen_Finalize(PyObject *self)
((PyCodeObject *)gen->gi_code)->co_flags & CO_COROUTINE && ((PyCodeObject *)gen->gi_code)->co_flags & CO_COROUTINE &&
gen->gi_frame->f_lasti == -1) { gen->gi_frame->f_lasti == -1) {
if (!error_value) { if (!error_value) {
PyErr_WarnFormat(PyExc_RuntimeWarning, 1, _PyErr_WarnUnawaitedCoroutine((PyObject *)gen);
"coroutine '%.50S' was never awaited",
gen->gi_qualname);
} }
} }
else { else {
@ -137,6 +137,9 @@ gen_dealloc(PyGenObject *gen)
gen->gi_frame->f_gen = NULL; gen->gi_frame->f_gen = NULL;
Py_CLEAR(gen->gi_frame); Py_CLEAR(gen->gi_frame);
} }
if (((PyCodeObject *)gen->gi_code)->co_flags & CO_COROUTINE) {
Py_CLEAR(((PyCoroObject *)gen)->cr_origin);
}
Py_CLEAR(gen->gi_code); Py_CLEAR(gen->gi_code);
Py_CLEAR(gen->gi_name); Py_CLEAR(gen->gi_name);
Py_CLEAR(gen->gi_qualname); Py_CLEAR(gen->gi_qualname);
@ -990,6 +993,7 @@ static PyMemberDef coro_memberlist[] = {
{"cr_frame", T_OBJECT, offsetof(PyCoroObject, cr_frame), READONLY}, {"cr_frame", T_OBJECT, offsetof(PyCoroObject, cr_frame), READONLY},
{"cr_running", T_BOOL, offsetof(PyCoroObject, cr_running), READONLY}, {"cr_running", T_BOOL, offsetof(PyCoroObject, cr_running), READONLY},
{"cr_code", T_OBJECT, offsetof(PyCoroObject, cr_code), READONLY}, {"cr_code", T_OBJECT, offsetof(PyCoroObject, cr_code), READONLY},
{"cr_origin", T_OBJECT, offsetof(PyCoroObject, cr_origin), READONLY},
{NULL} /* Sentinel */ {NULL} /* Sentinel */
}; };
@ -1158,10 +1162,59 @@ PyTypeObject _PyCoroWrapper_Type = {
0, /* tp_free */ 0, /* tp_free */
}; };
static PyObject *
compute_cr_origin(int origin_depth)
{
PyFrameObject *frame = PyEval_GetFrame();
/* First count how many frames we have */
int frame_count = 0;
for (; frame && frame_count < origin_depth; ++frame_count) {
frame = frame->f_back;
}
/* Now collect them */
PyObject *cr_origin = PyTuple_New(frame_count);
frame = PyEval_GetFrame();
for (int i = 0; i < frame_count; ++i) {
PyObject *frameinfo = Py_BuildValue(
"OiO",
frame->f_code->co_filename,
PyFrame_GetLineNumber(frame),
frame->f_code->co_name);
if (!frameinfo) {
Py_DECREF(cr_origin);
return NULL;
}
PyTuple_SET_ITEM(cr_origin, i, frameinfo);
frame = frame->f_back;
}
return cr_origin;
}
PyObject * PyObject *
PyCoro_New(PyFrameObject *f, PyObject *name, PyObject *qualname) PyCoro_New(PyFrameObject *f, PyObject *name, PyObject *qualname)
{ {
return gen_new_with_qualname(&PyCoro_Type, f, name, qualname); PyObject *coro = gen_new_with_qualname(&PyCoro_Type, f, name, qualname);
if (!coro) {
return NULL;
}
PyThreadState *tstate = PyThreadState_GET();
int origin_depth = tstate->coroutine_origin_tracking_depth;
if (origin_depth == 0) {
((PyCoroObject *)coro)->cr_origin = NULL;
} else {
PyObject *cr_origin = compute_cr_origin(origin_depth);
if (!cr_origin) {
Py_DECREF(coro);
return NULL;
}
((PyCoroObject *)coro)->cr_origin = cr_origin;
}
return coro;
} }

View File

@ -1153,6 +1153,53 @@ exit:
return ret; return ret;
} }
void
_PyErr_WarnUnawaitedCoroutine(PyObject *coro)
{
/* First, we attempt to funnel the warning through
warnings._warn_unawaited_coroutine.
This could raise an exception, due to:
- a bug
- some kind of shutdown-related brokenness
- succeeding, but with an "error" warning filter installed, so the
warning is converted into a RuntimeWarning exception
In the first two cases, we want to print the error (so we know what it
is!), and then print a warning directly as a fallback. In the last
case, we want to print the error (since it's the warning!), but *not*
do a fallback. And after we print the error we can't check for what
type of error it was (because PyErr_WriteUnraisable clears it), so we
need a flag to keep track.
Since this is called from __del__ context, it's careful to never raise
an exception.
*/
_Py_IDENTIFIER(_warn_unawaited_coroutine);
int warned = 0;
PyObject *fn = get_warnings_attr(&PyId__warn_unawaited_coroutine, 1);
if (fn) {
PyObject *res = PyObject_CallFunctionObjArgs(fn, coro, NULL);
Py_DECREF(fn);
if (res || PyErr_ExceptionMatches(PyExc_RuntimeWarning)) {
warned = 1;
}
Py_XDECREF(res);
}
if (PyErr_Occurred()) {
PyErr_WriteUnraisable(coro);
}
if (!warned) {
PyErr_WarnFormat(PyExc_RuntimeWarning, 1,
"coroutine '%.50S' was never awaited",
((PyCoroObject *)coro)->cr_qualname);
/* Maybe *that* got converted into an exception */
if (PyErr_Occurred()) {
PyErr_WriteUnraisable(coro);
}
}
}
PyDoc_STRVAR(warn_explicit_doc, PyDoc_STRVAR(warn_explicit_doc,
"Low-level inferface to warnings functionality."); "Low-level inferface to warnings functionality.");

View File

@ -4387,6 +4387,21 @@ PyEval_SetTrace(Py_tracefunc func, PyObject *arg)
|| (tstate->c_profilefunc != NULL)); || (tstate->c_profilefunc != NULL));
} }
void
_PyEval_SetCoroutineOriginTrackingDepth(int new_depth)
{
assert(new_depth >= 0);
PyThreadState *tstate = PyThreadState_GET();
tstate->coroutine_origin_tracking_depth = new_depth;
}
int
_PyEval_GetCoroutineOriginTrackingDepth(void)
{
PyThreadState *tstate = PyThreadState_GET();
return tstate->coroutine_origin_tracking_depth;
}
void void
_PyEval_SetCoroutineWrapper(PyObject *wrapper) _PyEval_SetCoroutineWrapper(PyObject *wrapper)
{ {

View File

@ -0,0 +1,66 @@
/*[clinic input]
preserve
[clinic start generated code]*/
PyDoc_STRVAR(sys_set_coroutine_origin_tracking_depth__doc__,
"set_coroutine_origin_tracking_depth($module, /, depth)\n"
"--\n"
"\n"
"Enable or disable origin tracking for coroutine objects in this thread.\n"
"\n"
"Coroutine objects will track \'depth\' frames of traceback information about\n"
"where they came from, available in their cr_origin attribute. Set depth of 0\n"
"to disable.");
#define SYS_SET_COROUTINE_ORIGIN_TRACKING_DEPTH_METHODDEF \
{"set_coroutine_origin_tracking_depth", (PyCFunction)sys_set_coroutine_origin_tracking_depth, METH_FASTCALL|METH_KEYWORDS, sys_set_coroutine_origin_tracking_depth__doc__},
static PyObject *
sys_set_coroutine_origin_tracking_depth_impl(PyObject *module, int depth);
static PyObject *
sys_set_coroutine_origin_tracking_depth(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyObject *kwnames)
{
PyObject *return_value = NULL;
static const char * const _keywords[] = {"depth", NULL};
static _PyArg_Parser _parser = {"i:set_coroutine_origin_tracking_depth", _keywords, 0};
int depth;
if (!_PyArg_ParseStackAndKeywords(args, nargs, kwnames, &_parser,
&depth)) {
goto exit;
}
return_value = sys_set_coroutine_origin_tracking_depth_impl(module, depth);
exit:
return return_value;
}
PyDoc_STRVAR(sys_get_coroutine_origin_tracking_depth__doc__,
"get_coroutine_origin_tracking_depth($module, /)\n"
"--\n"
"\n"
"Check status of origin tracking for coroutine objects in this thread.");
#define SYS_GET_COROUTINE_ORIGIN_TRACKING_DEPTH_METHODDEF \
{"get_coroutine_origin_tracking_depth", (PyCFunction)sys_get_coroutine_origin_tracking_depth, METH_NOARGS, sys_get_coroutine_origin_tracking_depth__doc__},
static int
sys_get_coroutine_origin_tracking_depth_impl(PyObject *module);
static PyObject *
sys_get_coroutine_origin_tracking_depth(PyObject *module, PyObject *Py_UNUSED(ignored))
{
PyObject *return_value = NULL;
int _return_value;
_return_value = sys_get_coroutine_origin_tracking_depth_impl(module);
if ((_return_value == -1) && PyErr_Occurred()) {
goto exit;
}
return_value = PyLong_FromLong((long)_return_value);
exit:
return return_value;
}
/*[clinic end generated code: output=4a3ac42b97d710ff input=a9049054013a1b77]*/

View File

@ -305,6 +305,8 @@ new_threadstate(PyInterpreterState *interp, int init)
tstate->on_delete = NULL; tstate->on_delete = NULL;
tstate->on_delete_data = NULL; tstate->on_delete_data = NULL;
tstate->coroutine_origin_tracking_depth = 0;
tstate->coroutine_wrapper = NULL; tstate->coroutine_wrapper = NULL;
tstate->in_coroutine_wrapper = 0; tstate->in_coroutine_wrapper = 0;

View File

@ -34,6 +34,13 @@ extern void *PyWin_DLLhModule;
extern const char *PyWin_DLLVersionString; extern const char *PyWin_DLLVersionString;
#endif #endif
/*[clinic input]
module sys
[clinic start generated code]*/
/*[clinic end generated code: output=da39a3ee5e6b4b0d input=3726b388feee8cea]*/
#include "clinic/sysmodule.c.h"
_Py_IDENTIFIER(_); _Py_IDENTIFIER(_);
_Py_IDENTIFIER(__sizeof__); _Py_IDENTIFIER(__sizeof__);
_Py_IDENTIFIER(_xoptions); _Py_IDENTIFIER(_xoptions);
@ -710,9 +717,51 @@ sys_setrecursionlimit(PyObject *self, PyObject *args)
Py_RETURN_NONE; Py_RETURN_NONE;
} }
/*[clinic input]
sys.set_coroutine_origin_tracking_depth
depth: int
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 depth of 0
to disable.
[clinic start generated code]*/
static PyObject *
sys_set_coroutine_origin_tracking_depth_impl(PyObject *module, int depth)
/*[clinic end generated code: output=0a2123c1cc6759c5 input=9083112cccc1bdcb]*/
{
if (depth < 0) {
PyErr_SetString(PyExc_ValueError, "depth must be >= 0");
return NULL;
}
_PyEval_SetCoroutineOriginTrackingDepth(depth);
Py_RETURN_NONE;
}
/*[clinic input]
sys.get_coroutine_origin_tracking_depth -> int
Check status of origin tracking for coroutine objects in this thread.
[clinic start generated code]*/
static int
sys_get_coroutine_origin_tracking_depth_impl(PyObject *module)
/*[clinic end generated code: output=3699f7be95a3afb8 input=335266a71205b61a]*/
{
return _PyEval_GetCoroutineOriginTrackingDepth();
}
static PyObject * static PyObject *
sys_set_coroutine_wrapper(PyObject *self, PyObject *wrapper) sys_set_coroutine_wrapper(PyObject *self, PyObject *wrapper)
{ {
if (PyErr_WarnEx(PyExc_DeprecationWarning,
"set_coroutine_wrapper is deprecated", 1) < 0) {
return NULL;
}
if (wrapper != Py_None) { if (wrapper != Py_None) {
if (!PyCallable_Check(wrapper)) { if (!PyCallable_Check(wrapper)) {
PyErr_Format(PyExc_TypeError, PyErr_Format(PyExc_TypeError,
@ -737,6 +786,10 @@ Set a wrapper for coroutine objects."
static PyObject * static PyObject *
sys_get_coroutine_wrapper(PyObject *self, PyObject *args) sys_get_coroutine_wrapper(PyObject *self, PyObject *args)
{ {
if (PyErr_WarnEx(PyExc_DeprecationWarning,
"get_coroutine_wrapper is deprecated", 1) < 0) {
return NULL;
}
PyObject *wrapper = _PyEval_GetCoroutineWrapper(); PyObject *wrapper = _PyEval_GetCoroutineWrapper();
if (wrapper == NULL) { if (wrapper == NULL) {
wrapper = Py_None; wrapper = Py_None;
@ -1512,6 +1565,8 @@ static PyMethodDef sys_methods[] = {
{"call_tracing", sys_call_tracing, METH_VARARGS, call_tracing_doc}, {"call_tracing", sys_call_tracing, METH_VARARGS, call_tracing_doc},
{"_debugmallocstats", sys_debugmallocstats, METH_NOARGS, {"_debugmallocstats", sys_debugmallocstats, METH_NOARGS,
debugmallocstats_doc}, debugmallocstats_doc},
SYS_SET_COROUTINE_ORIGIN_TRACKING_DEPTH_METHODDEF
SYS_GET_COROUTINE_ORIGIN_TRACKING_DEPTH_METHODDEF
{"set_coroutine_wrapper", sys_set_coroutine_wrapper, METH_O, {"set_coroutine_wrapper", sys_set_coroutine_wrapper, METH_O,
set_coroutine_wrapper_doc}, set_coroutine_wrapper_doc},
{"get_coroutine_wrapper", sys_get_coroutine_wrapper, METH_NOARGS, {"get_coroutine_wrapper", sys_get_coroutine_wrapper, METH_NOARGS,