PEP 415: Implement suppression of __context__ display with an exception attribute

This replaces the original PEP 409 implementation. See #14133.
This commit is contained in:
Benjamin Peterson 2012-05-14 22:09:31 -07:00
parent d91dc62379
commit d5a1c44455
12 changed files with 80 additions and 79 deletions

View File

@ -471,10 +471,6 @@ Exception Objects
set by ``raise ... from ...``) associated with the exception as a new set by ``raise ... from ...``) associated with the exception as a new
reference, as accessible from Python through :attr:`__cause__`. 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) .. 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 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*. instance or :const:`None`. This steals a reference to *ctx*.
If the cause is set to :const:`None` the default exception display :attr:`__suppress_context__` is implicitly set to ``True`` by this function.
routines will not display this exception's context, and will not follow the
chain any further.
.. _unicodeexceptions: .. _unicodeexceptions:

View File

@ -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 new exception is not handled the traceback that is eventually displayed will
include the originating exception(s) and the final exception. include the originating exception(s) and the final exception.
This implicit exception chain can be made explicit by using :keyword:`from` This implicit exception chain can be made explicit by using :keyword:`from` with
with :keyword:`raise`. The single argument to :keyword:`from` must be an :keyword:`raise`. The single argument to :keyword:`from` must be an exception
exception or :const:`None`, and it will be set as :attr:`__cause__` on the or ``None``. It will be set as :attr:`__cause__` on the raised exception.
raised exception. If :attr:`__cause__` is an exception it will be displayed Setting :attr:`__cause__` implicitly sets the :attr:`__suppress_context__` to
instead of :attr:`__context__`; if :attr:`__cause__` is None, ``True``. If :attr:`__cause__` is an exception, it will be displayed. If
:attr:`__context__` will not be displayed by the default exception handling :attr:`__cause__` is present or :attr:`__suppress_context__` has a true value,
code. (Note: the default value for :attr:`__context__` is :const:`None`, :attr:`__context__` will not be displayed.
while the default value for :attr:`__cause__` is :const:`Ellipsis`.)
In either case, the default exception handling code will not display In either case, the default exception handling code will not display any of the
any of the remaining links in the :attr:`__context__` chain if remaining links in the :attr:`__context__` chain if :attr:`__cause__` has been
:attr:`__cause__` has been set. set.
Base classes Base classes

View File

@ -2996,11 +2996,10 @@ It is written as ``None``.
The Ellipsis Object The Ellipsis Object
------------------- -------------------
This object is commonly used by slicing (see :ref:`slicings`), but may also This object is commonly used by slicing (see :ref:`slicings`). It supports no
be used in other situations where a sentinel value other than :const:`None` special operations. There is exactly one ellipsis object, named
is needed. It supports no special operations. There is exactly one ellipsis :const:`Ellipsis` (a built-in name). ``type(Ellipsis)()`` produces the
object, named :const:`Ellipsis` (a built-in name). ``type(Ellipsis)()`` :const:`Ellipsis` singleton.
produces the :const:`Ellipsis` singleton.
It is written as ``Ellipsis`` or ``...``. It is written as ``Ellipsis`` or ``...``.

View File

@ -10,7 +10,8 @@ extern "C" {
/* PyException_HEAD defines the initial segment of every exception class. */ /* PyException_HEAD defines the initial segment of every exception class. */
#define PyException_HEAD PyObject_HEAD PyObject *dict;\ #define PyException_HEAD PyObject_HEAD PyObject *dict;\
PyObject *args; PyObject *traceback;\ PyObject *args; PyObject *traceback;\
PyObject *context; PyObject *cause; PyObject *context; PyObject *cause;\
int suppress_context;
typedef struct { typedef struct {
PyException_HEAD PyException_HEAD
@ -114,7 +115,6 @@ PyAPI_FUNC(PyObject *) PyException_GetTraceback(PyObject *);
/* Cause manipulation (PEP 3134) */ /* Cause manipulation (PEP 3134) */
PyAPI_FUNC(PyObject *) PyException_GetCause(PyObject *); PyAPI_FUNC(PyObject *) PyException_GetCause(PyObject *);
PyAPI_FUNC(void) PyException_SetCause(PyObject *, PyObject *); PyAPI_FUNC(void) PyException_SetCause(PyObject *, PyObject *);
PyAPI_FUNC(int) _PyException_SetCauseChecked(PyObject *, PyObject *);
/* Context manipulation (PEP 3134) */ /* Context manipulation (PEP 3134) */
PyAPI_FUNC(PyObject *) PyException_GetContext(PyObject *); PyAPI_FUNC(PyObject *) PyException_GetContext(PyObject *);

View File

@ -388,18 +388,18 @@ class ExceptionTests(unittest.TestCase):
def testChainingAttrs(self): def testChainingAttrs(self):
e = Exception() e = Exception()
self.assertIsNone(e.__context__) self.assertIsNone(e.__context__)
self.assertIs(e.__cause__, Ellipsis) self.assertIsNone(e.__cause__)
e = TypeError() e = TypeError()
self.assertIsNone(e.__context__) self.assertIsNone(e.__context__)
self.assertIs(e.__cause__, Ellipsis) self.assertIsNone(e.__cause__)
class MyException(EnvironmentError): class MyException(EnvironmentError):
pass pass
e = MyException() e = MyException()
self.assertIsNone(e.__context__) self.assertIsNone(e.__context__)
self.assertIs(e.__cause__, Ellipsis) self.assertIsNone(e.__cause__)
def testChainingDescriptors(self): def testChainingDescriptors(self):
try: try:
@ -408,15 +408,16 @@ class ExceptionTests(unittest.TestCase):
e = exc e = exc
self.assertIsNone(e.__context__) self.assertIsNone(e.__context__)
self.assertIs(e.__cause__, Ellipsis) self.assertIsNone(e.__cause__)
self.assertFalse(e.__suppress_context__)
e.__context__ = NameError() e.__context__ = NameError()
e.__cause__ = None e.__cause__ = None
self.assertIsInstance(e.__context__, NameError) self.assertIsInstance(e.__context__, NameError)
self.assertIsNone(e.__cause__) self.assertIsNone(e.__cause__)
self.assertTrue(e.__suppress_context__)
e.__cause__ = Ellipsis e.__suppress_context__ = False
self.assertIs(e.__cause__, Ellipsis) self.assertFalse(e.__suppress_context__)
def testKeywordArgs(self): def testKeywordArgs(self):
# test that builtin exception don't take keyword args, # test that builtin exception don't take keyword args,

View File

@ -174,11 +174,14 @@ class TestCause(unittest.TestCase):
raise ValueError from None raise ValueError from None
except ValueError as exc: except ValueError as exc:
self.assertIsNone(exc.__cause__) self.assertIsNone(exc.__cause__)
raise exc from Ellipsis self.assertTrue(exc.__suppress_context__)
exc.__suppress_context__ = False
raise exc
except ValueError as exc: except ValueError as exc:
e = exc e = exc
self.assertIs(e.__cause__, Ellipsis) self.assertIsNone(e.__cause__)
self.assertFalse(e.__suppress_context__)
self.assertIsInstance(e.__context__, TypeError) self.assertIsInstance(e.__context__, TypeError)
def test_invalid_cause(self): def test_invalid_cause(self):

View File

@ -700,14 +700,14 @@ class SizeofTest(unittest.TestCase):
class C(object): pass class C(object): pass
check(C.__dict__, size(h + 'P')) check(C.__dict__, size(h + 'P'))
# BaseException # BaseException
check(BaseException(), size(h + '5P')) check(BaseException(), size(h + '5Pi'))
# UnicodeEncodeError # UnicodeEncodeError
check(UnicodeEncodeError("", "", 0, 0, ""), size(h + '5P 2P2PP')) check(UnicodeEncodeError("", "", 0, 0, ""), size(h + '5Pi 2P2PP'))
# UnicodeDecodeError # UnicodeDecodeError
# XXX # XXX
# check(UnicodeDecodeError("", "", 0, 0, ""), size(h + '5P2PP')) # check(UnicodeDecodeError("", "", 0, 0, ""), size(h + '5P2PP'))
# UnicodeTranslateError # UnicodeTranslateError
check(UnicodeTranslateError("", 0, 1, ""), size(h + '5P 2P2PP')) check(UnicodeTranslateError("", 0, 1, ""), size(h + '5Pi 2P2PP'))
# ellipses # ellipses
check(Ellipsis, size(h + '')) check(Ellipsis, size(h + ''))
# EncodingMap # EncodingMap

View File

@ -119,15 +119,16 @@ def _iter_chain(exc, custom_tb=None, seen=None):
seen = set() seen = set()
seen.add(exc) seen.add(exc)
its = [] its = []
context = exc.__context__
cause = exc.__cause__ cause = exc.__cause__
if cause is Ellipsis: if cause is not None and cause not in seen:
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:
its.append(_iter_chain(cause, False, seen)) its.append(_iter_chain(cause, False, seen))
its.append([(_cause_message, None)]) 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__)]) its.append([(exc, custom_tb or exc.__traceback__)])
# itertools.chain is in an extension module and may be unavailable # itertools.chain is in an extension module and may be unavailable
for it in its: for it in its:

View File

@ -10,6 +10,9 @@ What's New in Python 3.3.0 Alpha 4?
Core and Builtins 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 - Issue #14417: Mutating a dict during lookup now restarts the lookup instead
of raising a RuntimeError (undoes issue #14205). of raising a RuntimeError (undoes issue #14205).

View File

@ -42,6 +42,7 @@ BaseException_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
/* the dict is created on the fly in PyObject_GenericSetAttr */ /* the dict is created on the fly in PyObject_GenericSetAttr */
self->dict = NULL; self->dict = NULL;
self->traceback = self->cause = self->context = NULL; self->traceback = self->cause = self->context = NULL;
self->suppress_context = 0;
self->args = PyTuple_New(0); self->args = PyTuple_New(0);
if (!self->args) { if (!self->args) {
@ -266,24 +267,7 @@ BaseException_get_cause(PyObject *self) {
PyObject *res = PyException_GetCause(self); PyObject *res = PyException_GetCause(self);
if (res) if (res)
return res; /* new reference already returned above */ return res; /* new reference already returned above */
Py_INCREF(Py_Ellipsis); Py_RETURN_NONE;
return Py_Ellipsis;
}
int
_PyException_SetCauseChecked(PyObject *self, PyObject *arg) {
if (arg == Py_Ellipsis) {
arg = NULL;
} else if (arg != Py_None && !PyExceptionInstance_Check(arg)) {
PyErr_SetString(PyExc_TypeError, "exception cause must be None, "
"Ellipsis or derive from BaseException");
return -1;
} else {
/* PyException_SetCause steals a reference */
Py_INCREF(arg);
}
PyException_SetCause(self, arg);
return 0;
} }
static int static int
@ -291,8 +275,18 @@ BaseException_set_cause(PyObject *self, PyObject *arg) {
if (arg == NULL) { if (arg == NULL) {
PyErr_SetString(PyExc_TypeError, "__cause__ may not be deleted"); PyErr_SetString(PyExc_TypeError, "__cause__ may not be deleted");
return -1; return -1;
} else if (arg == Py_None) {
arg = NULL;
} else if (!PyExceptionInstance_Check(arg)) {
PyErr_SetString(PyExc_TypeError, "exception cause must be None "
"or derive from BaseException");
return -1;
} else {
/* PyException_SetCause steals this reference */
Py_INCREF(arg);
} }
return _PyException_SetCauseChecked(self, arg); PyException_SetCause(self, arg);
return 0;
} }
@ -333,6 +327,7 @@ void
PyException_SetCause(PyObject *self, PyObject *cause) { PyException_SetCause(PyObject *self, PyObject *cause) {
PyObject *old_cause = ((PyBaseExceptionObject *)self)->cause; PyObject *old_cause = ((PyBaseExceptionObject *)self)->cause;
((PyBaseExceptionObject *)self)->cause = cause; ((PyBaseExceptionObject *)self)->cause = cause;
((PyBaseExceptionObject *)self)->suppress_context = 1;
Py_XDECREF(old_cause); 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 = { static PyTypeObject _PyExc_BaseException = {
PyVarObject_HEAD_INIT(NULL, 0) PyVarObject_HEAD_INIT(NULL, 0)
"BaseException", /*tp_name*/ "BaseException", /*tp_name*/
@ -382,7 +383,7 @@ static PyTypeObject _PyExc_BaseException = {
0, /* tp_iter */ 0, /* tp_iter */
0, /* tp_iternext */ 0, /* tp_iternext */
BaseException_methods, /* tp_methods */ BaseException_methods, /* tp_methods */
0, /* tp_members */ BaseException_members, /* tp_members */
BaseException_getset, /* tp_getset */ BaseException_getset, /* tp_getset */
0, /* tp_base */ 0, /* tp_base */
0, /* tp_dict */ 0, /* tp_dict */

View File

@ -3572,23 +3572,26 @@ do_raise(PyObject *exc, PyObject *cause)
if (cause) { if (cause) {
PyObject *fixed_cause; PyObject *fixed_cause;
int result;
if (PyExceptionClass_Check(cause)) { if (PyExceptionClass_Check(cause)) {
fixed_cause = PyObject_CallObject(cause, NULL); fixed_cause = PyObject_CallObject(cause, NULL);
if (fixed_cause == NULL) if (fixed_cause == NULL)
goto raise_error; goto raise_error;
Py_CLEAR(cause); Py_DECREF(cause);
} else {
/* Let "exc.__cause__ = cause" handle all further checks */
fixed_cause = cause;
cause = NULL; /* Steal the reference */
} }
/* We retain ownership of the reference to fixed_cause */ else if (PyExceptionInstance_Check(cause)) {
result = _PyException_SetCauseChecked(value, fixed_cause); fixed_cause = 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; goto raise_error;
} }
PyException_SetCause(value, fixed_cause);
} }
PyErr_SetObject(type, value); PyErr_SetObject(type, value);

View File

@ -1761,11 +1761,7 @@ print_exception_recursive(PyObject *f, PyObject *value, PyObject *seen)
else if (PyExceptionInstance_Check(value)) { else if (PyExceptionInstance_Check(value)) {
cause = PyException_GetCause(value); cause = PyException_GetCause(value);
context = PyException_GetContext(value); context = PyException_GetContext(value);
if (cause && cause == Py_None) { if (cause) {
/* print neither cause nor context */
;
}
else if (cause) {
res = PySet_Contains(seen, cause); res = PySet_Contains(seen, cause);
if (res == -1) if (res == -1)
PyErr_Clear(); PyErr_Clear();
@ -1776,7 +1772,8 @@ print_exception_recursive(PyObject *f, PyObject *value, PyObject *seen)
cause_message, f); cause_message, f);
} }
} }
else if (context) { else if (context &&
!((PyBaseExceptionObject *)value)->suppress_context) {
res = PySet_Contains(seen, context); res = PySet_Contains(seen, context);
if (res == -1) if (res == -1)
PyErr_Clear(); PyErr_Clear();