diff -r 304c61263ae6 Doc/c-api/exceptions.rst --- a/Doc/c-api/exceptions.rst Mon Jun 13 09:24:11 2016 +0300 +++ b/Doc/c-api/exceptions.rst Mon Jun 13 23:34:35 2016 +0300 @@ -578,6 +578,10 @@ Exception Objects it. There is no type check to make sure that *ctx* is an exception instance. This steals a reference to *ctx*. + .. versionchanged:: 3.5.2 + If setting *ctx* introduces an object cycle, the context will be + set to *NULL*. + .. c:function:: PyObject* PyException_GetCause(PyObject *ex) diff -r 304c61263ae6 Doc/library/exceptions.rst --- a/Doc/library/exceptions.rst Mon Jun 13 09:24:11 2016 +0300 +++ b/Doc/library/exceptions.rst Mon Jun 13 23:34:35 2016 +0300 @@ -66,6 +66,18 @@ In either case, the exception itself is exceptions so that the final line of the traceback always shows the last exception that was raised. +.. versionchanged:: 3.5.2 + + An attempt to introduce a object cycle when setting + :attr:`__context__` will trigger a :exc:`RuntimeError` + exception:: + + >>> exc = Exception() + >>> exc.__context__ = exc + Traceback (most recent call last): + File "", line 1, in + RuntimeError: cycle in exception context chain + Base classes ------------ diff -r 304c61263ae6 Lib/test/test_contextlib.py --- a/Lib/test/test_contextlib.py Mon Jun 13 09:24:11 2016 +0300 +++ b/Lib/test/test_contextlib.py Mon Jun 13 23:34:35 2016 +0300 @@ -795,6 +795,40 @@ class TestExitStack(unittest.TestCase): stack.push(cm) self.assertIs(stack._exit_callbacks[-1], cm) + def test_dont_reraise_RuntimeError(self): + """https://bugs.python.org/issue27122""" + class UniqueException(Exception): pass + + @contextmanager + def second(): + try: + yield 1 + except Exception as exc: + raise UniqueException("new exception") from exc + + @contextmanager + def first(): + try: + yield 1 + except Exception as exc: + raise exc + + # The RuntimeError should be caught by second()'s exception + # handler which chain raised a new UniqueException. + with self.assertRaises(RuntimeError) as err_ctx: + with ExitStack() as es_ctx: + es_ctx.enter_context(second()) + es_ctx.enter_context(first()) + raise RuntimeError("please no infinite loop.") + + self.assertEqual(err_ctx.exception.args[0], + "cycle in exception context chain") + self.assertEqual(err_ctx.exception.__context__.args[0], + "please no infinite loop.") + self.assertIsNone(err_ctx.exception.__cause__) + self.assertIsNone(err_ctx.exception.__context__.__context__) + self.assertIsNone(err_ctx.exception.__context__.__cause__) + class TestRedirectStream: diff -r 304c61263ae6 Lib/test/test_exceptions.py --- a/Lib/test/test_exceptions.py Mon Jun 13 09:24:11 2016 +0300 +++ b/Lib/test/test_exceptions.py Mon Jun 13 23:34:35 2016 +0300 @@ -629,6 +629,21 @@ class ExceptionTests(unittest.TestCase): obj = wr() self.assertTrue(obj is None, "%s" % obj) + def testExceptionChainedContext(self): + class Err1(Exception): pass + class Err2(Exception): pass + + err1 = Err1() + with self.assertRaisesRegex(RuntimeError, 'cycle in exception context'): + err1.__context__ = err1 + self.assertIsNone(err1.__context__) + + err2 = Err2() + err2.__context__ = err1 + with self.assertRaisesRegex(RuntimeError, 'cycle in exception context'): + err1.__context__ = err2 + self.assertIsNone(err1.__context__) + def test_exception_target_in_nested_scope(self): # issue 4617: This used to raise a SyntaxError # "can not delete variable 'e' referenced in nested scope" diff -r 304c61263ae6 Objects/exceptions.c --- a/Objects/exceptions.c Mon Jun 13 09:24:11 2016 +0300 +++ b/Objects/exceptions.c Mon Jun 13 23:34:35 2016 +0300 @@ -259,6 +259,10 @@ BaseException_set_context(PyObject *self Py_INCREF(arg); } PyException_SetContext(self, arg); + if (((PyBaseExceptionObject *)self)->context != arg) { + PyErr_SetString(PyExc_RuntimeError, "cycle in exception context chain"); + return -1; + } return 0; } @@ -326,6 +330,7 @@ PyException_GetCause(PyObject *self) { void PyException_SetCause(PyObject *self, PyObject *cause) { + assert(cause == NULL || PyExceptionInstance_Check(cause)); ((PyBaseExceptionObject *)self)->suppress_context = 1; Py_XSETREF(((PyBaseExceptionObject *)self)->cause, cause); } @@ -341,6 +346,15 @@ PyException_GetContext(PyObject *self) { void PyException_SetContext(PyObject *self, PyObject *context) { + PyObject *exc = context; + while (exc != NULL) { + if (exc == self) { + context = NULL; + break; + } + assert(PyExceptionInstance_Check(exc)); + exc = ((PyBaseExceptionObject *)exc)->context; + } Py_XSETREF(((PyBaseExceptionObject *)self)->context, context); }