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
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:

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
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`.)
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.
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

View File

@ -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 ``...``.

View File

@ -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 *);

View File

@ -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,

View File

@ -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):

View File

@ -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

View File

@ -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:

View File

@ -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).

View File

@ -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,24 +267,7 @@ 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;
}
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;
Py_RETURN_NONE;
}
static int
@ -291,8 +275,18 @@ 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 (!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) {
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 */

View File

@ -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 */
fixed_cause = cause;
cause = NULL; /* Steal the reference */
Py_DECREF(cause);
}
/* We retain ownership of the reference to fixed_cause */
result = _PyException_SetCauseChecked(value, fixed_cause);
Py_DECREF(fixed_cause);
if (result < 0) {
else if (PyExceptionInstance_Check(cause)) {
fixed_cause = cause;
}
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);

View File

@ -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();