From da742ba826721da84140abc785856d4ccc2d787f Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sun, 17 May 2020 22:47:31 -0700 Subject: [PATCH] bpo-31033: Improve the traceback for cancelled asyncio tasks (GH-19951) When an asyncio.Task is cancelled, the exception traceback now starts with where the task was first interrupted. Previously, the traceback only had "depth one." --- Include/internal/pycore_pyerrors.h | 17 +++ Lib/asyncio/futures.py | 26 +++- Lib/asyncio/tasks.py | 26 ++-- Lib/test/test_asyncio/test_tasks.py | 134 +++++++++++++++--- .../2020-05-06-02-33-00.bpo-31033.aX12pw.rst | 2 + Modules/_asynciomodule.c | 84 ++++++++--- Modules/clinic/_asynciomodule.c.h | 44 +++++- Objects/genobject.c | 20 +-- Python/errors.c | 14 ++ setup.py | 3 +- 10 files changed, 290 insertions(+), 80 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2020-05-06-02-33-00.bpo-31033.aX12pw.rst diff --git a/Include/internal/pycore_pyerrors.h b/Include/internal/pycore_pyerrors.h index bae10561c9f..3290a37051e 100644 --- a/Include/internal/pycore_pyerrors.h +++ b/Include/internal/pycore_pyerrors.h @@ -14,6 +14,20 @@ static inline PyObject* _PyErr_Occurred(PyThreadState *tstate) return tstate->curexc_type; } +static inline void _PyErr_ClearExcState(_PyErr_StackItem *exc_state) +{ + PyObject *t, *v, *tb; + t = exc_state->exc_type; + v = exc_state->exc_value; + tb = exc_state->exc_traceback; + exc_state->exc_type = NULL; + exc_state->exc_value = NULL; + exc_state->exc_traceback = NULL; + Py_XDECREF(t); + Py_XDECREF(v); + Py_XDECREF(tb); +} + PyAPI_FUNC(void) _PyErr_Fetch( PyThreadState *tstate, @@ -36,6 +50,9 @@ PyAPI_FUNC(void) _PyErr_SetObject( PyObject *type, PyObject *value); +PyAPI_FUNC(void) _PyErr_ChainStackItem( + _PyErr_StackItem *exc_state); + PyAPI_FUNC(void) _PyErr_Clear(PyThreadState *tstate); PyAPI_FUNC(void) _PyErr_SetNone(PyThreadState *tstate, PyObject *exception); diff --git a/Lib/asyncio/futures.py b/Lib/asyncio/futures.py index 889f3e6eb86..bed4da52fd4 100644 --- a/Lib/asyncio/futures.py +++ b/Lib/asyncio/futures.py @@ -52,6 +52,8 @@ class Future: _loop = None _source_traceback = None _cancel_message = None + # A saved CancelledError for later chaining as an exception context. + _cancelled_exc = None # This field is used for a dual purpose: # - Its presence is a marker to declare that a class implements @@ -124,6 +126,21 @@ class Future: raise RuntimeError("Future object is not initialized.") return loop + def _make_cancelled_error(self): + """Create the CancelledError to raise if the Future is cancelled. + + This should only be called once when handling a cancellation since + it erases the saved context exception value. + """ + if self._cancel_message is None: + exc = exceptions.CancelledError() + else: + exc = exceptions.CancelledError(self._cancel_message) + exc.__context__ = self._cancelled_exc + # Remove the reference since we don't need this anymore. + self._cancelled_exc = None + return exc + def cancel(self, msg=None): """Cancel the future and schedule callbacks. @@ -175,9 +192,8 @@ class Future: the future is done and has an exception set, this exception is raised. """ if self._state == _CANCELLED: - raise exceptions.CancelledError( - '' if self._cancel_message is None else self._cancel_message) - + exc = self._make_cancelled_error() + raise exc if self._state != _FINISHED: raise exceptions.InvalidStateError('Result is not ready.') self.__log_traceback = False @@ -194,8 +210,8 @@ class Future: InvalidStateError. """ if self._state == _CANCELLED: - raise exceptions.CancelledError( - '' if self._cancel_message is None else self._cancel_message) + exc = self._make_cancelled_error() + raise exc if self._state != _FINISHED: raise exceptions.InvalidStateError('Exception is not set.') self.__log_traceback = False diff --git a/Lib/asyncio/tasks.py b/Lib/asyncio/tasks.py index a3a0a33ee03..21b98b6647b 100644 --- a/Lib/asyncio/tasks.py +++ b/Lib/asyncio/tasks.py @@ -270,8 +270,7 @@ class Task(futures._PyFuture): # Inherit Python Task implementation f'_step(): already done: {self!r}, {exc!r}') if self._must_cancel: if not isinstance(exc, exceptions.CancelledError): - exc = exceptions.CancelledError('' - if self._cancel_message is None else self._cancel_message) + exc = self._make_cancelled_error() self._must_cancel = False coro = self._coro self._fut_waiter = None @@ -293,11 +292,9 @@ class Task(futures._PyFuture): # Inherit Python Task implementation else: super().set_result(exc.value) except exceptions.CancelledError as exc: - if exc.args: - cancel_msg = exc.args[0] - else: - cancel_msg = None - super().cancel(msg=cancel_msg) # I.e., Future.cancel(self). + # Save the original exception so we can chain it later. + self._cancelled_exc = exc + super().cancel() # I.e., Future.cancel(self). except (KeyboardInterrupt, SystemExit) as exc: super().set_exception(exc) raise @@ -787,8 +784,7 @@ def gather(*coros_or_futures, loop=None, return_exceptions=False): # Check if 'fut' is cancelled first, as # 'fut.exception()' will *raise* a CancelledError # instead of returning it. - exc = exceptions.CancelledError('' - if fut._cancel_message is None else fut._cancel_message) + exc = fut._make_cancelled_error() outer.set_exception(exc) return else: @@ -804,9 +800,12 @@ def gather(*coros_or_futures, loop=None, return_exceptions=False): for fut in children: if fut.cancelled(): - # Check if 'fut' is cancelled first, as - # 'fut.exception()' will *raise* a CancelledError - # instead of returning it. + # Check if 'fut' is cancelled first, as 'fut.exception()' + # will *raise* a CancelledError instead of returning it. + # Also, since we're adding the exception return value + # to 'results' instead of raising it, don't bother + # setting __context__. This also lets us preserve + # calling '_make_cancelled_error()' at most once. res = exceptions.CancelledError( '' if fut._cancel_message is None else fut._cancel_message) @@ -820,8 +819,7 @@ def gather(*coros_or_futures, loop=None, return_exceptions=False): # If gather is being cancelled we must propagate the # cancellation regardless of *return_exceptions* argument. # See issue 32684. - exc = exceptions.CancelledError('' - if fut._cancel_message is None else fut._cancel_message) + exc = fut._make_cancelled_error() outer.set_exception(exc) else: outer.set_result(results) diff --git a/Lib/test/test_asyncio/test_tasks.py b/Lib/test/test_asyncio/test_tasks.py index 65bee526d2e..63968e2a178 100644 --- a/Lib/test/test_asyncio/test_tasks.py +++ b/Lib/test/test_asyncio/test_tasks.py @@ -10,6 +10,7 @@ import random import re import sys import textwrap +import traceback import types import unittest import weakref @@ -57,6 +58,22 @@ def format_coroutine(qualname, state, src, source_traceback, generator=False): return 'coro=<%s() %s at %s>' % (qualname, state, src) +def get_innermost_context(exc): + """ + Return information about the innermost exception context in the chain. + """ + depth = 0 + while True: + context = exc.__context__ + if context is None: + break + + exc = context + depth += 1 + + return (type(exc), exc.args, depth) + + class Dummy: def __repr__(self): @@ -111,9 +128,10 @@ class BaseTaskTests: self.assertEqual(t._cancel_message, None) t.cancel('my message') + self.assertEqual(t._cancel_message, 'my message') + with self.assertRaises(asyncio.CancelledError): self.loop.run_until_complete(t) - self.assertEqual(t._cancel_message, 'my message') def test_task_cancel_message_setter(self): async def coro(): @@ -123,10 +141,8 @@ class BaseTaskTests: t._cancel_message = 'my new message' self.assertEqual(t._cancel_message, 'my new message') - # Also check that the value is used for cancel(). with self.assertRaises(asyncio.CancelledError): self.loop.run_until_complete(t) - self.assertEqual(t._cancel_message, 'my new message') def test_task_del_collect(self): class Evil: @@ -548,8 +564,8 @@ class BaseTaskTests: def test_cancel_with_message_then_future_result(self): # Test Future.result() after calling cancel() with a message. cases = [ - ((), ('',)), - ((None,), ('',)), + ((), ()), + ((None,), ()), (('my message',), ('my message',)), # Non-string values should roundtrip. ((5,), (5,)), @@ -573,13 +589,17 @@ class BaseTaskTests: with self.assertRaises(asyncio.CancelledError) as cm: loop.run_until_complete(task) exc = cm.exception - self.assertEqual(exc.args, expected_args) + self.assertEqual(exc.args, ()) + + actual = get_innermost_context(exc) + self.assertEqual(actual, + (asyncio.CancelledError, expected_args, 2)) def test_cancel_with_message_then_future_exception(self): # Test Future.exception() after calling cancel() with a message. cases = [ - ((), ('',)), - ((None,), ('',)), + ((), ()), + ((None,), ()), (('my message',), ('my message',)), # Non-string values should roundtrip. ((5,), (5,)), @@ -603,7 +623,11 @@ class BaseTaskTests: with self.assertRaises(asyncio.CancelledError) as cm: loop.run_until_complete(task) exc = cm.exception - self.assertEqual(exc.args, expected_args) + self.assertEqual(exc.args, ()) + + actual = get_innermost_context(exc) + self.assertEqual(actual, + (asyncio.CancelledError, expected_args, 2)) def test_cancel_with_message_before_starting_task(self): loop = asyncio.new_event_loop() @@ -623,7 +647,11 @@ class BaseTaskTests: with self.assertRaises(asyncio.CancelledError) as cm: loop.run_until_complete(task) exc = cm.exception - self.assertEqual(exc.args, ('my message',)) + self.assertEqual(exc.args, ()) + + actual = get_innermost_context(exc) + self.assertEqual(actual, + (asyncio.CancelledError, ('my message',), 2)) def test_cancel_yield(self): with self.assertWarns(DeprecationWarning): @@ -805,6 +833,66 @@ class BaseTaskTests: self.assertTrue(nested_task.cancelled()) self.assertTrue(fut.cancelled()) + def assert_text_contains(self, text, substr): + if substr not in text: + raise RuntimeError(f'text {substr!r} not found in:\n>>>{text}<<<') + + def test_cancel_traceback_for_future_result(self): + # When calling Future.result() on a cancelled task, check that the + # line of code that was interrupted is included in the traceback. + loop = asyncio.new_event_loop() + self.set_event_loop(loop) + + async def nested(): + # This will get cancelled immediately. + await asyncio.sleep(10) + + async def coro(): + task = self.new_task(loop, nested()) + await asyncio.sleep(0) + task.cancel() + await task # search target + + task = self.new_task(loop, coro()) + try: + loop.run_until_complete(task) + except asyncio.CancelledError: + tb = traceback.format_exc() + self.assert_text_contains(tb, "await asyncio.sleep(10)") + # The intermediate await should also be included. + self.assert_text_contains(tb, "await task # search target") + else: + self.fail('CancelledError did not occur') + + def test_cancel_traceback_for_future_exception(self): + # When calling Future.exception() on a cancelled task, check that the + # line of code that was interrupted is included in the traceback. + loop = asyncio.new_event_loop() + self.set_event_loop(loop) + + async def nested(): + # This will get cancelled immediately. + await asyncio.sleep(10) + + async def coro(): + task = self.new_task(loop, nested()) + await asyncio.sleep(0) + task.cancel() + done, pending = await asyncio.wait([task]) + task.exception() # search target + + task = self.new_task(loop, coro()) + try: + loop.run_until_complete(task) + except asyncio.CancelledError: + tb = traceback.format_exc() + self.assert_text_contains(tb, "await asyncio.sleep(10)") + # The intermediate await should also be included. + self.assert_text_contains(tb, + "task.exception() # search target") + else: + self.fail('CancelledError did not occur') + def test_stop_while_run_in_complete(self): def gen(): @@ -2391,15 +2479,14 @@ class BaseTaskTests: def test_cancel_gather_2(self): cases = [ - ((), ('',)), - ((None,), ('',)), + ((), ()), + ((None,), ()), (('my message',), ('my message',)), # Non-string values should roundtrip. ((5,), (5,)), ] for cancel_args, expected_args in cases: with self.subTest(cancel_args=cancel_args): - loop = asyncio.new_event_loop() self.addCleanup(loop.close) @@ -2417,15 +2504,20 @@ class BaseTaskTests: qwe = self.new_task(loop, test()) await asyncio.sleep(0.2) qwe.cancel(*cancel_args) - try: - await qwe - except asyncio.CancelledError as exc: - self.assertEqual(exc.args, expected_args) - else: - self.fail('gather did not propagate the cancellation ' - 'request') + await qwe - loop.run_until_complete(main()) + try: + loop.run_until_complete(main()) + except asyncio.CancelledError as exc: + self.assertEqual(exc.args, ()) + exc_type, exc_args, depth = get_innermost_context(exc) + self.assertEqual((exc_type, exc_args), + (asyncio.CancelledError, expected_args)) + # The exact traceback seems to vary in CI. + self.assertIn(depth, (2, 3)) + else: + self.fail('gather did not propagate the cancellation ' + 'request') def test_exception_traceback(self): # See http://bugs.python.org/issue28843 diff --git a/Misc/NEWS.d/next/Library/2020-05-06-02-33-00.bpo-31033.aX12pw.rst b/Misc/NEWS.d/next/Library/2020-05-06-02-33-00.bpo-31033.aX12pw.rst new file mode 100644 index 00000000000..b1831e5ff8f --- /dev/null +++ b/Misc/NEWS.d/next/Library/2020-05-06-02-33-00.bpo-31033.aX12pw.rst @@ -0,0 +1,2 @@ +When a :class:`asyncio.Task` is cancelled, the exception traceback +now chains all the way back to where the task was first interrupted. diff --git a/Modules/_asynciomodule.c b/Modules/_asynciomodule.c index ff1b2b8b909..1b6a5796824 100644 --- a/Modules/_asynciomodule.c +++ b/Modules/_asynciomodule.c @@ -1,4 +1,5 @@ #include "Python.h" +#include "pycore_pyerrors.h" // _PyErr_ClearExcState() #include // offsetof() @@ -72,7 +73,8 @@ typedef enum { int prefix##_log_tb; \ int prefix##_blocking; \ PyObject *dict; \ - PyObject *prefix##_weakreflist; + PyObject *prefix##_weakreflist; \ + _PyErr_StackItem prefix##_cancelled_exc_state; typedef struct { FutureObj_HEAD(fut) @@ -482,6 +484,7 @@ future_init(FutureObj *fut, PyObject *loop) Py_CLEAR(fut->fut_exception); Py_CLEAR(fut->fut_source_tb); Py_CLEAR(fut->fut_cancel_msg); + _PyErr_ClearExcState(&fut->fut_cancelled_exc_state); fut->fut_state = STATE_PENDING; fut->fut_log_tb = 0; @@ -601,9 +604,7 @@ create_cancelled_error(PyObject *msg) { PyObject *exc; if (msg == NULL || msg == Py_None) { - msg = PyUnicode_FromString(""); - exc = PyObject_CallOneArg(asyncio_CancelledError, msg); - Py_DECREF(msg); + exc = PyObject_CallNoArgs(asyncio_CancelledError); } else { exc = PyObject_CallOneArg(asyncio_CancelledError, msg); } @@ -623,6 +624,7 @@ future_get_result(FutureObj *fut, PyObject **result) { if (fut->fut_state == STATE_CANCELLED) { set_cancelled_error(fut->fut_cancel_msg); + _PyErr_ChainStackItem(&fut->fut_cancelled_exc_state); return -1; } @@ -777,6 +779,7 @@ FutureObj_clear(FutureObj *fut) Py_CLEAR(fut->fut_exception); Py_CLEAR(fut->fut_source_tb); Py_CLEAR(fut->fut_cancel_msg); + _PyErr_ClearExcState(&fut->fut_cancelled_exc_state); Py_CLEAR(fut->dict); return 0; } @@ -793,6 +796,12 @@ FutureObj_traverse(FutureObj *fut, visitproc visit, void *arg) Py_VISIT(fut->fut_source_tb); Py_VISIT(fut->fut_cancel_msg); Py_VISIT(fut->dict); + + _PyErr_StackItem *exc_state = &fut->fut_cancelled_exc_state; + Py_VISIT(exc_state->exc_type); + Py_VISIT(exc_state->exc_value); + Py_VISIT(exc_state->exc_traceback); + return 0; } @@ -858,6 +867,7 @@ _asyncio_Future_exception_impl(FutureObj *self) if (self->fut_state == STATE_CANCELLED) { set_cancelled_error(self->fut_cancel_msg); + _PyErr_ChainStackItem(&self->fut_cancelled_exc_state); return NULL; } @@ -1335,6 +1345,29 @@ FutureObj_get_state(FutureObj *fut, void *Py_UNUSED(ignored)) return ret; } +/*[clinic input] +_asyncio.Future._make_cancelled_error + +Create the CancelledError to raise if the Future is cancelled. + +This should only be called once when handling a cancellation since +it erases the context exception value. +[clinic start generated code]*/ + +static PyObject * +_asyncio_Future__make_cancelled_error_impl(FutureObj *self) +/*[clinic end generated code: output=a5df276f6c1213de input=ac6effe4ba795ecc]*/ +{ + PyObject *exc = create_cancelled_error(self->fut_cancel_msg); + _PyErr_StackItem *exc_state = &self->fut_cancelled_exc_state; + /* Transfer ownership of exc_value from exc_state to exc since we are + done with it. */ + PyException_SetContext(exc, exc_state->exc_value); + exc_state->exc_value = NULL; + + return exc; +} + /*[clinic input] _asyncio.Future._repr_info [clinic start generated code]*/ @@ -1461,6 +1494,7 @@ static PyMethodDef FutureType_methods[] = { _ASYNCIO_FUTURE_CANCELLED_METHODDEF _ASYNCIO_FUTURE_DONE_METHODDEF _ASYNCIO_FUTURE_GET_LOOP_METHODDEF + _ASYNCIO_FUTURE__MAKE_CANCELLED_ERROR_METHODDEF _ASYNCIO_FUTURE__REPR_INFO_METHODDEF {"__class_getitem__", future_cls_getitem, METH_O|METH_CLASS, NULL}, {NULL, NULL} /* Sentinel */ @@ -2232,6 +2266,24 @@ _asyncio_Task_all_tasks_impl(PyTypeObject *type, PyObject *loop) return res; } +/*[clinic input] +_asyncio.Task._make_cancelled_error + +Create the CancelledError to raise if the Task is cancelled. + +This should only be called once when handling a cancellation since +it erases the context exception value. +[clinic start generated code]*/ + +static PyObject * +_asyncio_Task__make_cancelled_error_impl(TaskObj *self) +/*[clinic end generated code: output=55a819e8b4276fab input=52c0e32de8e2f840]*/ +{ + FutureObj *fut = (FutureObj*)self; + return _asyncio_Future__make_cancelled_error_impl(fut); +} + + /*[clinic input] _asyncio.Task._repr_info [clinic start generated code]*/ @@ -2539,6 +2591,7 @@ static PyMethodDef TaskType_methods[] = { _ASYNCIO_TASK_CANCEL_METHODDEF _ASYNCIO_TASK_GET_STACK_METHODDEF _ASYNCIO_TASK_PRINT_STACK_METHODDEF + _ASYNCIO_TASK__MAKE_CANCELLED_ERROR_METHODDEF _ASYNCIO_TASK__REPR_INFO_METHODDEF _ASYNCIO_TASK_GET_NAME_METHODDEF _ASYNCIO_TASK_SET_NAME_METHODDEF @@ -2754,24 +2807,13 @@ task_step_impl(TaskObj *task, PyObject *exc) /* CancelledError */ PyErr_Fetch(&et, &ev, &tb); - PyObject *cancel_msg; - if (ev != NULL && PyExceptionInstance_Check(ev)) { - PyObject *exc_args = ((PyBaseExceptionObject*)ev)->args; - Py_ssize_t size = PyTuple_GET_SIZE(exc_args); - if (size > 0) { - cancel_msg = PyTuple_GET_ITEM(exc_args, 0); - } else { - cancel_msg = NULL; - } - } else { - cancel_msg = ev; - } + FutureObj *fut = (FutureObj*)task; + _PyErr_StackItem *exc_state = &fut->fut_cancelled_exc_state; + exc_state->exc_type = et; + exc_state->exc_value = ev; + exc_state->exc_traceback = tb; - Py_DECREF(et); - Py_XDECREF(tb); - Py_XDECREF(ev); - - return future_cancel((FutureObj*)task, cancel_msg); + return future_cancel(fut, NULL); } /* Some other exception; pop it and call Task.set_exception() */ diff --git a/Modules/clinic/_asynciomodule.c.h b/Modules/clinic/_asynciomodule.c.h index 3f5023c33a5..d3e59a4bc78 100644 --- a/Modules/clinic/_asynciomodule.c.h +++ b/Modules/clinic/_asynciomodule.c.h @@ -271,6 +271,27 @@ _asyncio_Future_get_loop(FutureObj *self, PyObject *Py_UNUSED(ignored)) return _asyncio_Future_get_loop_impl(self); } +PyDoc_STRVAR(_asyncio_Future__make_cancelled_error__doc__, +"_make_cancelled_error($self, /)\n" +"--\n" +"\n" +"Create the CancelledError to raise if the Future is cancelled.\n" +"\n" +"This should only be called once when handling a cancellation since\n" +"it erases the context exception value."); + +#define _ASYNCIO_FUTURE__MAKE_CANCELLED_ERROR_METHODDEF \ + {"_make_cancelled_error", (PyCFunction)_asyncio_Future__make_cancelled_error, METH_NOARGS, _asyncio_Future__make_cancelled_error__doc__}, + +static PyObject * +_asyncio_Future__make_cancelled_error_impl(FutureObj *self); + +static PyObject * +_asyncio_Future__make_cancelled_error(FutureObj *self, PyObject *Py_UNUSED(ignored)) +{ + return _asyncio_Future__make_cancelled_error_impl(self); +} + PyDoc_STRVAR(_asyncio_Future__repr_info__doc__, "_repr_info($self, /)\n" "--\n" @@ -414,6 +435,27 @@ exit: return return_value; } +PyDoc_STRVAR(_asyncio_Task__make_cancelled_error__doc__, +"_make_cancelled_error($self, /)\n" +"--\n" +"\n" +"Create the CancelledError to raise if the Task is cancelled.\n" +"\n" +"This should only be called once when handling a cancellation since\n" +"it erases the context exception value."); + +#define _ASYNCIO_TASK__MAKE_CANCELLED_ERROR_METHODDEF \ + {"_make_cancelled_error", (PyCFunction)_asyncio_Task__make_cancelled_error, METH_NOARGS, _asyncio_Task__make_cancelled_error__doc__}, + +static PyObject * +_asyncio_Task__make_cancelled_error_impl(TaskObj *self); + +static PyObject * +_asyncio_Task__make_cancelled_error(TaskObj *self, PyObject *Py_UNUSED(ignored)) +{ + return _asyncio_Task__make_cancelled_error_impl(self); +} + PyDoc_STRVAR(_asyncio_Task__repr_info__doc__, "_repr_info($self, /)\n" "--\n" @@ -870,4 +912,4 @@ _asyncio__leave_task(PyObject *module, PyObject *const *args, Py_ssize_t nargs, exit: return return_value; } -/*[clinic end generated code: output=6ed4cfda8fc516ad input=a9049054013a1b77]*/ +/*[clinic end generated code: output=0e5c1eb8b692977b input=a9049054013a1b77]*/ diff --git a/Objects/genobject.c b/Objects/genobject.c index fb01e581f8a..40179cdbf7d 100644 --- a/Objects/genobject.c +++ b/Objects/genobject.c @@ -3,6 +3,7 @@ #include "Python.h" #include "pycore_ceval.h" // _PyEval_EvalFrame() #include "pycore_object.h" +#include "pycore_pyerrors.h" // _PyErr_ClearExcState() #include "pycore_pystate.h" // _PyThreadState_GET() #include "frameobject.h" #include "structmember.h" // PyMemberDef @@ -99,21 +100,6 @@ _PyGen_Finalize(PyObject *self) PyErr_Restore(error_type, error_value, error_traceback); } -static inline void -exc_state_clear(_PyErr_StackItem *exc_state) -{ - PyObject *t, *v, *tb; - t = exc_state->exc_type; - v = exc_state->exc_value; - tb = exc_state->exc_traceback; - exc_state->exc_type = NULL; - exc_state->exc_value = NULL; - exc_state->exc_traceback = NULL; - Py_XDECREF(t); - Py_XDECREF(v); - Py_XDECREF(tb); -} - static void gen_dealloc(PyGenObject *gen) { @@ -146,7 +132,7 @@ gen_dealloc(PyGenObject *gen) Py_CLEAR(gen->gi_code); Py_CLEAR(gen->gi_name); Py_CLEAR(gen->gi_qualname); - exc_state_clear(&gen->gi_exc_state); + _PyErr_ClearExcState(&gen->gi_exc_state); PyObject_GC_Del(gen); } @@ -286,7 +272,7 @@ gen_send_ex(PyGenObject *gen, PyObject *arg, int exc, int closing) if (!result || f->f_stacktop == NULL) { /* generator can't be rerun, so release the frame */ /* first clean reference cycle through stored exception traceback */ - exc_state_clear(&gen->gi_exc_state); + _PyErr_ClearExcState(&gen->gi_exc_state); gen->gi_frame->f_gen = NULL; gen->gi_frame = NULL; Py_DECREF(f); diff --git a/Python/errors.c b/Python/errors.c index f856a798eed..3b42c1120b8 100644 --- a/Python/errors.c +++ b/Python/errors.c @@ -512,6 +512,20 @@ _PyErr_ChainExceptions(PyObject *exc, PyObject *val, PyObject *tb) } } +void +_PyErr_ChainStackItem(_PyErr_StackItem *exc_state) +{ + if (exc_state->exc_type == NULL || exc_state->exc_type == Py_None) { + return; + } + Py_INCREF(exc_state->exc_type); + Py_XINCREF(exc_state->exc_value); + Py_XINCREF(exc_state->exc_traceback); + _PyErr_ChainExceptions(exc_state->exc_type, + exc_state->exc_value, + exc_state->exc_traceback); +} + static PyObject * _PyErr_FormatVFromCause(PyThreadState *tstate, PyObject *exception, const char *format, va_list vargs) diff --git a/setup.py b/setup.py index 0f92a9c0108..847cf2641fa 100644 --- a/setup.py +++ b/setup.py @@ -853,7 +853,8 @@ class PyBuildExt(build_ext): # _opcode module self.add(Extension('_opcode', ['_opcode.c'])) # asyncio speedups - self.add(Extension("_asyncio", ["_asynciomodule.c"])) + self.add(Extension("_asyncio", ["_asynciomodule.c"], + extra_compile_args=['-DPy_BUILD_CORE_MODULE'])) # _abc speedups self.add(Extension("_abc", ["_abc.c"])) # _queue module