From d5a1c44455d969968f453f029727bfc45e4ce0a9 Mon Sep 17 00:00:00 2001 From: Benjamin Peterson Date: Mon, 14 May 2012 22:09:31 -0700 Subject: PEP 415: Implement suppression of __context__ display with an exception attribute This replaces the original PEP 409 implementation. See #14133. --- Doc/c-api/exceptions.rst | 8 +------- Doc/library/exceptions.rst | 23 +++++++++++------------ Doc/library/stdtypes.rst | 9 ++++----- Include/pyerrors.h | 4 ++-- Lib/test/test_exceptions.py | 15 ++++++++------- Lib/test/test_raise.py | 7 +++++-- Lib/test/test_sys.py | 6 +++--- Lib/traceback.py | 13 +++++++------ Misc/NEWS | 3 +++ Objects/exceptions.c | 39 ++++++++++++++++++++------------------- Python/ceval.c | 21 ++++++++++++--------- Python/pythonrun.c | 9 +++------ 12 files changed, 79 insertions(+), 78 deletions(-) diff --git a/Doc/c-api/exceptions.rst b/Doc/c-api/exceptions.rst index f181143..9a66b7f 100644 --- a/Doc/c-api/exceptions.rst +++ b/Doc/c-api/exceptions.rst @@ -471,10 +471,6 @@ Exception Objects set by ``raise ... from ...``) associated with the exception as a new reference, as accessible from Python through :attr:`__cause__`. - If there is no cause associated, this returns *NULL* (from Python - ``__cause__ is Ellipsis``). If the cause is :const:`None`, the default - exception display routines stop showing the context chain. - .. c:function:: void PyException_SetCause(PyObject *ex, PyObject *ctx) @@ -482,9 +478,7 @@ Exception Objects it. There is no type check to make sure that *ctx* is either an exception instance or :const:`None`. This steals a reference to *ctx*. - If the cause is set to :const:`None` the default exception display - routines will not display this exception's context, and will not follow the - chain any further. + :attr:`__suppress_context__` is implicitly set to ``True`` by this function. .. _unicodeexceptions: diff --git a/Doc/library/exceptions.rst b/Doc/library/exceptions.rst index 33bc3b0..faf6732 100644 --- a/Doc/library/exceptions.rst +++ b/Doc/library/exceptions.rst @@ -39,18 +39,17 @@ When raising (or re-raising) an exception in an :keyword:`except` clause new exception is not handled the traceback that is eventually displayed will include the originating exception(s) and the final exception. -This implicit exception chain can be made explicit by using :keyword:`from` -with :keyword:`raise`. The single argument to :keyword:`from` must be an -exception or :const:`None`, and it will be set as :attr:`__cause__` on the -raised exception. If :attr:`__cause__` is an exception it will be displayed -instead of :attr:`__context__`; if :attr:`__cause__` is None, -:attr:`__context__` will not be displayed by the default exception handling -code. (Note: the default value for :attr:`__context__` is :const:`None`, -while the default value for :attr:`__cause__` is :const:`Ellipsis`.) - -In either case, the default exception handling code will not display -any of the remaining links in the :attr:`__context__` chain if -:attr:`__cause__` has been set. +This implicit exception chain can be made explicit by using :keyword:`from` with +:keyword:`raise`. The single argument to :keyword:`from` must be an exception +or ``None``. It will be set as :attr:`__cause__` on the raised exception. +Setting :attr:`__cause__` implicitly sets the :attr:`__suppress_context__` to +``True``. If :attr:`__cause__` is an exception, it will be displayed. If +:attr:`__cause__` is present or :attr:`__suppress_context__` has a true value, +:attr:`__context__` will not be displayed. + +In either case, the default exception handling code will not display any of the +remaining links in the :attr:`__context__` chain if :attr:`__cause__` has been +set. Base classes diff --git a/Doc/library/stdtypes.rst b/Doc/library/stdtypes.rst index a34b1cf..bf86d8eb 100644 --- a/Doc/library/stdtypes.rst +++ b/Doc/library/stdtypes.rst @@ -2996,11 +2996,10 @@ It is written as ``None``. The Ellipsis Object ------------------- -This object is commonly used by slicing (see :ref:`slicings`), but may also -be used in other situations where a sentinel value other than :const:`None` -is needed. It supports no special operations. There is exactly one ellipsis -object, named :const:`Ellipsis` (a built-in name). ``type(Ellipsis)()`` -produces the :const:`Ellipsis` singleton. +This object is commonly used by slicing (see :ref:`slicings`). It supports no +special operations. There is exactly one ellipsis object, named +:const:`Ellipsis` (a built-in name). ``type(Ellipsis)()`` produces the +:const:`Ellipsis` singleton. It is written as ``Ellipsis`` or ``...``. diff --git a/Include/pyerrors.h b/Include/pyerrors.h index 1b80ecf..fb6281c 100644 --- a/Include/pyerrors.h +++ b/Include/pyerrors.h @@ -10,7 +10,8 @@ extern "C" { /* PyException_HEAD defines the initial segment of every exception class. */ #define PyException_HEAD PyObject_HEAD PyObject *dict;\ PyObject *args; PyObject *traceback;\ - PyObject *context; PyObject *cause; + PyObject *context; PyObject *cause;\ + int suppress_context; typedef struct { PyException_HEAD @@ -114,7 +115,6 @@ PyAPI_FUNC(PyObject *) PyException_GetTraceback(PyObject *); /* Cause manipulation (PEP 3134) */ PyAPI_FUNC(PyObject *) PyException_GetCause(PyObject *); PyAPI_FUNC(void) PyException_SetCause(PyObject *, PyObject *); -PyAPI_FUNC(int) _PyException_SetCauseChecked(PyObject *, PyObject *); /* Context manipulation (PEP 3134) */ PyAPI_FUNC(PyObject *) PyException_GetContext(PyObject *); diff --git a/Lib/test/test_exceptions.py b/Lib/test/test_exceptions.py index 39ff85f..97762f9 100644 --- a/Lib/test/test_exceptions.py +++ b/Lib/test/test_exceptions.py @@ -388,18 +388,18 @@ class ExceptionTests(unittest.TestCase): def testChainingAttrs(self): e = Exception() self.assertIsNone(e.__context__) - self.assertIs(e.__cause__, Ellipsis) + self.assertIsNone(e.__cause__) e = TypeError() self.assertIsNone(e.__context__) - self.assertIs(e.__cause__, Ellipsis) + self.assertIsNone(e.__cause__) class MyException(EnvironmentError): pass e = MyException() self.assertIsNone(e.__context__) - self.assertIs(e.__cause__, Ellipsis) + self.assertIsNone(e.__cause__) def testChainingDescriptors(self): try: @@ -408,15 +408,16 @@ class ExceptionTests(unittest.TestCase): e = exc self.assertIsNone(e.__context__) - self.assertIs(e.__cause__, Ellipsis) + self.assertIsNone(e.__cause__) + self.assertFalse(e.__suppress_context__) e.__context__ = NameError() e.__cause__ = None self.assertIsInstance(e.__context__, NameError) self.assertIsNone(e.__cause__) - - e.__cause__ = Ellipsis - self.assertIs(e.__cause__, Ellipsis) + self.assertTrue(e.__suppress_context__) + e.__suppress_context__ = False + self.assertFalse(e.__suppress_context__) def testKeywordArgs(self): # test that builtin exception don't take keyword args, diff --git a/Lib/test/test_raise.py b/Lib/test/test_raise.py index 8ae9210..b169867 100644 --- a/Lib/test/test_raise.py +++ b/Lib/test/test_raise.py @@ -174,11 +174,14 @@ class TestCause(unittest.TestCase): raise ValueError from None except ValueError as exc: self.assertIsNone(exc.__cause__) - raise exc from Ellipsis + self.assertTrue(exc.__suppress_context__) + exc.__suppress_context__ = False + raise exc except ValueError as exc: e = exc - self.assertIs(e.__cause__, Ellipsis) + self.assertIsNone(e.__cause__) + self.assertFalse(e.__suppress_context__) self.assertIsInstance(e.__context__, TypeError) def test_invalid_cause(self): diff --git a/Lib/test/test_sys.py b/Lib/test/test_sys.py index 2b9abfc..71dbd29 100644 --- a/Lib/test/test_sys.py +++ b/Lib/test/test_sys.py @@ -700,14 +700,14 @@ class SizeofTest(unittest.TestCase): class C(object): pass check(C.__dict__, size(h + 'P')) # BaseException - check(BaseException(), size(h + '5P')) + check(BaseException(), size(h + '5Pi')) # UnicodeEncodeError - check(UnicodeEncodeError("", "", 0, 0, ""), size(h + '5P 2P2PP')) + check(UnicodeEncodeError("", "", 0, 0, ""), size(h + '5Pi 2P2PP')) # UnicodeDecodeError # XXX # check(UnicodeDecodeError("", "", 0, 0, ""), size(h + '5P2PP')) # UnicodeTranslateError - check(UnicodeTranslateError("", 0, 1, ""), size(h + '5P 2P2PP')) + check(UnicodeTranslateError("", 0, 1, ""), size(h + '5Pi 2P2PP')) # ellipses check(Ellipsis, size(h + '')) # EncodingMap diff --git a/Lib/traceback.py b/Lib/traceback.py index 35858af..eeb9e73 100644 --- a/Lib/traceback.py +++ b/Lib/traceback.py @@ -119,15 +119,16 @@ def _iter_chain(exc, custom_tb=None, seen=None): seen = set() seen.add(exc) its = [] + context = exc.__context__ cause = exc.__cause__ - if cause is Ellipsis: - context = exc.__context__ - if context is not None and context not in seen: - its.append(_iter_chain(context, None, seen)) - its.append([(_context_message, None)]) - elif cause is not None and cause not in seen: + if cause is not None and cause not in seen: its.append(_iter_chain(cause, False, seen)) its.append([(_cause_message, None)]) + elif (context is not None and + not exc.__suppress_context__ and + context not in seen): + its.append(_iter_chain(context, None, seen)) + its.append([(_context_message, None)]) its.append([(exc, custom_tb or exc.__traceback__)]) # itertools.chain is in an extension module and may be unavailable for it in its: diff --git a/Misc/NEWS b/Misc/NEWS index 3b8e3ca..37b319d 100644 --- a/Misc/NEWS +++ b/Misc/NEWS @@ -10,6 +10,9 @@ What's New in Python 3.3.0 Alpha 4? Core and Builtins ----------------- +- Issue #14133 (PEP 415): Implement suppression of __context__ display with an + attribute on BaseException. This replaces the original mechanism of PEP 409. + - Issue #14417: Mutating a dict during lookup now restarts the lookup instead of raising a RuntimeError (undoes issue #14205). diff --git a/Objects/exceptions.c b/Objects/exceptions.c index a11283e..b994862 100644 --- a/Objects/exceptions.c +++ b/Objects/exceptions.c @@ -42,6 +42,7 @@ BaseException_new(PyTypeObject *type, PyObject *args, PyObject *kwds) /* the dict is created on the fly in PyObject_GenericSetAttr */ self->dict = NULL; self->traceback = self->cause = self->context = NULL; + self->suppress_context = 0; self->args = PyTuple_New(0); if (!self->args) { @@ -266,35 +267,28 @@ BaseException_get_cause(PyObject *self) { PyObject *res = PyException_GetCause(self); if (res) return res; /* new reference already returned above */ - Py_INCREF(Py_Ellipsis); - return Py_Ellipsis; + Py_RETURN_NONE; } -int -_PyException_SetCauseChecked(PyObject *self, PyObject *arg) { - if (arg == Py_Ellipsis) { +static int +BaseException_set_cause(PyObject *self, PyObject *arg) { + if (arg == NULL) { + PyErr_SetString(PyExc_TypeError, "__cause__ may not be deleted"); + return -1; + } else if (arg == Py_None) { arg = NULL; - } else if (arg != Py_None && !PyExceptionInstance_Check(arg)) { - PyErr_SetString(PyExc_TypeError, "exception cause must be None, " - "Ellipsis or derive from BaseException"); + } else if (!PyExceptionInstance_Check(arg)) { + PyErr_SetString(PyExc_TypeError, "exception cause must be None " + "or derive from BaseException"); return -1; } else { - /* PyException_SetCause steals a reference */ + /* PyException_SetCause steals this reference */ Py_INCREF(arg); } PyException_SetCause(self, arg); return 0; } -static int -BaseException_set_cause(PyObject *self, PyObject *arg) { - if (arg == NULL) { - PyErr_SetString(PyExc_TypeError, "__cause__ may not be deleted"); - return -1; - } - return _PyException_SetCauseChecked(self, arg); -} - static PyGetSetDef BaseException_getset[] = { {"__dict__", PyObject_GenericGetDict, PyObject_GenericSetDict}, @@ -333,6 +327,7 @@ void PyException_SetCause(PyObject *self, PyObject *cause) { PyObject *old_cause = ((PyBaseExceptionObject *)self)->cause; ((PyBaseExceptionObject *)self)->cause = cause; + ((PyBaseExceptionObject *)self)->suppress_context = 1; Py_XDECREF(old_cause); } @@ -352,6 +347,12 @@ PyException_SetContext(PyObject *self, PyObject *context) { } +static struct PyMemberDef BaseException_members[] = { + {"__suppress_context__", T_BOOL, + offsetof(PyBaseExceptionObject, suppress_context)} +}; + + static PyTypeObject _PyExc_BaseException = { PyVarObject_HEAD_INIT(NULL, 0) "BaseException", /*tp_name*/ @@ -382,7 +383,7 @@ static PyTypeObject _PyExc_BaseException = { 0, /* tp_iter */ 0, /* tp_iternext */ BaseException_methods, /* tp_methods */ - 0, /* tp_members */ + BaseException_members, /* tp_members */ BaseException_getset, /* tp_getset */ 0, /* tp_base */ 0, /* tp_dict */ diff --git a/Python/ceval.c b/Python/ceval.c index b9a006b..718bb32 100644 --- a/Python/ceval.c +++ b/Python/ceval.c @@ -3572,23 +3572,26 @@ do_raise(PyObject *exc, PyObject *cause) if (cause) { PyObject *fixed_cause; - int result; if (PyExceptionClass_Check(cause)) { fixed_cause = PyObject_CallObject(cause, NULL); if (fixed_cause == NULL) goto raise_error; - Py_CLEAR(cause); - } else { - /* Let "exc.__cause__ = cause" handle all further checks */ + Py_DECREF(cause); + } + else if (PyExceptionInstance_Check(cause)) { fixed_cause = cause; - cause = NULL; /* Steal the reference */ } - /* We retain ownership of the reference to fixed_cause */ - result = _PyException_SetCauseChecked(value, fixed_cause); - Py_DECREF(fixed_cause); - if (result < 0) { + else if (cause == Py_None) { + Py_DECREF(cause); + fixed_cause = NULL; + } + else { + PyErr_SetString(PyExc_TypeError, + "exception causes must derive from " + "BaseException"); goto raise_error; } + PyException_SetCause(value, fixed_cause); } PyErr_SetObject(type, value); diff --git a/Python/pythonrun.c b/Python/pythonrun.c index cd3cf5c..d9eb5e7 100644 --- a/Python/pythonrun.c +++ b/Python/pythonrun.c @@ -1761,11 +1761,7 @@ print_exception_recursive(PyObject *f, PyObject *value, PyObject *seen) else if (PyExceptionInstance_Check(value)) { cause = PyException_GetCause(value); context = PyException_GetContext(value); - if (cause && cause == Py_None) { - /* print neither cause nor context */ - ; - } - else if (cause) { + if (cause) { res = PySet_Contains(seen, cause); if (res == -1) PyErr_Clear(); @@ -1776,7 +1772,8 @@ print_exception_recursive(PyObject *f, PyObject *value, PyObject *seen) cause_message, f); } } - else if (context) { + else if (context && + !((PyBaseExceptionObject *)value)->suppress_context) { res = PySet_Contains(seen, context); if (res == -1) PyErr_Clear(); -- cgit v0.12