Close issue #6210: Implement PEP 409

This commit is contained in:
Nick Coghlan 2012-02-26 17:49:52 +10:00
parent cda6b6d60d
commit ab7bf2143e
15 changed files with 263 additions and 42 deletions

View File

@ -62,6 +62,7 @@ docs@python.org), and we'll be glad to correct the problem.
* Stefan Franke
* Jim Fulton
* Peter Funk
* Ethan Furman
* Lele Gaifax
* Matthew Gallagher
* Gabriel Genellina

View File

@ -421,17 +421,24 @@ Exception Objects
.. c:function:: PyObject* PyException_GetCause(PyObject *ex)
Return the cause (another exception instance 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*.
Return the cause (either an exception instance, or :const:`None`,
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)
Set the cause associated with the exception to *ctx*. Use *NULL* to clear
it. There is no type check to make sure that *ctx* is an exception instance.
This steals a reference to *ctx*.
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.
.. _unicodeexceptions:

View File

@ -34,6 +34,24 @@ programmers are encouraged to at least derive new exceptions from the
defining exceptions is available in the Python Tutorial under
:ref:`tut-userexceptions`.
When raising (or re-raising) an exception in an :keyword:`except` clause
:attr:`__context__` is automatically set to the last exception caught; if the
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 bet 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.
Base classes
------------

View File

@ -2985,10 +2985,11 @@ It is written as ``None``.
The Ellipsis Object
-------------------
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.
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.
It is written as ``Ellipsis`` or ``...``.

View File

@ -254,6 +254,9 @@ inspection of exception attributes::
PEP 380: Syntax for Delegating to a Subgenerator
================================================
:pep:`380` - Syntax for Delegating to a Subgenerator
PEP written by Greg Ewing.
PEP 380 adds the ``yield from`` expression, allowing a generator to delegate
part of its operations to another generator. This allows a section of code
containing 'yield' to be factored out and placed in another generator.
@ -267,6 +270,67 @@ Kelly and Nick Coghlan, documentation by Zbigniew Jędrzejewski-Szmek and
Nick Coghlan)
PEP 409: Suppressing exception context
======================================
:pep:`409` - Suppressing exception context
PEP written by Ethan Furman, implemented by Ethan Furman and Nick Coghlan.
PEP 409 introduces new syntax that allows the display of the chained
exception context to be disabled. This allows cleaner error messages in
applications that convert between exception types::
>>> class D:
... def __init__(self, extra):
... self._extra_attributes = extra
... def __getattr__(self, attr):
... try:
... return self._extra_attributes[attr]
... except KeyError:
... raise AttributeError(attr) from None
...
>>> D({}).x
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 8, in __getattr__
AttributeError: x
Without the ``from None`` suffix to suppress the cause, the original
exception would be displayed by default::
>>> class C:
... def __init__(self, extra):
... self._extra_attributes = extra
... def __getattr__(self, attr):
... try:
... return self._extra_attributes[attr]
... except KeyError:
... raise AttributeError(attr)
...
>>> C({}).x
Traceback (most recent call last):
File "<stdin>", line 6, in __getattr__
KeyError: 'x'
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 8, in __getattr__
AttributeError: x
No debugging capability is lost, as the original exception context remains
available if needed (for example, if an intervening library has incorrectly
suppressed valuable underlying details)::
>>> try:
... D({}).x
... except AttributeError as exc:
... print(repr(exc.__context__))
...
KeyError('x',)
PEP 3155: Qualified name for classes and functions
==================================================

View File

@ -105,6 +105,7 @@ 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

@ -387,19 +387,36 @@ class ExceptionTests(unittest.TestCase):
def testChainingAttrs(self):
e = Exception()
self.assertEqual(e.__context__, None)
self.assertEqual(e.__cause__, None)
self.assertIsNone(e.__context__)
self.assertIs(e.__cause__, Ellipsis)
e = TypeError()
self.assertEqual(e.__context__, None)
self.assertEqual(e.__cause__, None)
self.assertIsNone(e.__context__)
self.assertIs(e.__cause__, Ellipsis)
class MyException(EnvironmentError):
pass
e = MyException()
self.assertEqual(e.__context__, None)
self.assertEqual(e.__cause__, None)
self.assertIsNone(e.__context__)
self.assertIs(e.__cause__, Ellipsis)
def testChainingDescriptors(self):
try:
raise Exception()
except Exception as exc:
e = exc
self.assertIsNone(e.__context__)
self.assertIs(e.__cause__, Ellipsis)
e.__context__ = NameError()
e.__cause__ = None
self.assertIsInstance(e.__context__, NameError)
self.assertIsNone(e.__cause__)
e.__cause__ = Ellipsis
self.assertIs(e.__cause__, Ellipsis)
def testKeywordArgs(self):
# test that builtin exception don't take keyword args,

View File

@ -3,12 +3,27 @@
"""Tests for the raise statement."""
from test import support
from test import support, script_helper
import re
import sys
import types
import unittest
try:
from resource import setrlimit, RLIMIT_CORE, error as resource_error
except ImportError:
prepare_subprocess = None
else:
def prepare_subprocess():
# don't create core file
try:
setrlimit(RLIMIT_CORE, (0, 0))
except (ValueError, resource_error):
pass
def get_tb():
try:
raise OSError()
@ -77,6 +92,16 @@ class TestRaise(unittest.TestCase):
nested_reraise()
self.assertRaises(TypeError, reraise)
def test_raise_from_None(self):
try:
try:
raise TypeError("foo")
except:
raise ValueError() from None
except ValueError as e:
self.assertTrue(isinstance(e.__context__, TypeError))
self.assertIsNone(e.__cause__)
def test_with_reraise1(self):
def reraise():
try:
@ -139,6 +164,23 @@ class TestRaise(unittest.TestCase):
class TestCause(unittest.TestCase):
def testCauseSyntax(self):
try:
try:
try:
raise TypeError
except Exception:
raise ValueError from None
except ValueError as exc:
self.assertIsNone(exc.__cause__)
raise exc from Ellipsis
except ValueError as exc:
e = exc
self.assertIs(e.__cause__, Ellipsis)
self.assertIsInstance(e.__context__, TypeError)
def test_invalid_cause(self):
try:
raise IndexError from 5
@ -178,6 +220,44 @@ class TestCause(unittest.TestCase):
class TestTraceback(unittest.TestCase):
def get_output(self, code, filename=None):
"""
Run the specified code in Python (in a new child process) and read the
output from the standard error or from a file (if filename is set).
Return the output lines as a list.
"""
options = {}
if prepare_subprocess:
options['preexec_fn'] = prepare_subprocess
process = script_helper.spawn_python('-c', code, **options)
stdout, stderr = process.communicate()
exitcode = process.wait()
output = support.strip_python_stderr(stdout)
output = output.decode('ascii', 'backslashreplace')
if filename:
self.assertEqual(output, '')
with open(filename, "rb") as fp:
output = fp.read()
output = output.decode('ascii', 'backslashreplace')
output = re.sub('Current thread 0x[0-9a-f]+',
'Current thread XXX',
output)
return output.splitlines(), exitcode
def test_traceback_verbiage(self):
code = """
try:
raise ValueError
except:
raise NameError from None
"""
text, exitcode = self.get_output(code)
self.assertEqual(len(text), 3)
self.assertTrue(text[0].startswith('Traceback'))
self.assertTrue(text[1].startswith(' File '))
self.assertTrue(text[2].startswith('NameError'))
def test_sets_traceback(self):
try:
raise IndexError()

View File

@ -246,6 +246,21 @@ class BaseExceptionReportingTests:
self.check_zero_div(blocks[0])
self.assertIn('inner_raise() # Marker', blocks[2])
def test_context_suppression(self):
try:
try:
raise Exception
except:
raise ZeroDivisionError from None
except ZeroDivisionError as _:
e = _
lines = self.get_report(e).splitlines()
self.assertEqual(len(lines), 4)
self.assertTrue(lines[0].startswith('Traceback'))
self.assertTrue(lines[1].startswith(' File'))
self.assertIn('ZeroDivisionError from None', lines[2])
self.assertTrue(lines[3].startswith('ZeroDivisionError'))
def test_cause_and_context(self):
# When both a cause and a context are set, only the cause should be
# displayed and the context should be muted.

View File

@ -120,14 +120,14 @@ def _iter_chain(exc, custom_tb=None, seen=None):
seen.add(exc)
its = []
cause = exc.__cause__
if cause is not None and cause not in seen:
its.append(_iter_chain(cause, None, seen))
its.append([(_cause_message, None)])
else:
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:
its.append(_iter_chain(cause, False, seen))
its.append([(_cause_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

@ -338,6 +338,7 @@ Jim Fulton
Tadayoshi Funaba
Gyro Funch
Peter Funk
Ethan Furman
Geoff Furnish
Ulisses Furquim
Hagen Fürstenau

View File

@ -10,6 +10,10 @@ What's New in Python 3.3 Alpha 1?
Core and Builtins
-----------------
- PEP 409, Issue #6210: "raise X from None" is now supported as a means of
suppressing the display of the chained exception context. The chained
context still remains available as the __context__ attribute.
- Issue #10181: New memoryview implementation fixes multiple ownership
and lifetime issues of dynamically allocated Py_buffer members (#9990)
as well as crashes (#8305, #7433). Many new features have been added

View File

@ -266,7 +266,24 @@ BaseException_get_cause(PyObject *self) {
PyObject *res = PyException_GetCause(self);
if (res)
return res; /* new reference already returned above */
Py_RETURN_NONE;
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;
}
static int
@ -274,18 +291,8 @@ 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);
}
PyException_SetCause(self, arg);
return 0;
return _PyException_SetCauseChecked(self, arg);
}

View File

@ -3567,22 +3567,23 @@ 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_DECREF(cause);
}
else if (PyExceptionInstance_Check(cause)) {
Py_CLEAR(cause);
} else {
/* Let "exc.__cause__ = cause" handle all further checks */
fixed_cause = cause;
cause = NULL; /* Steal the reference */
}
else {
PyErr_SetString(PyExc_TypeError,
"exception causes must derive from "
"BaseException");
/* We retain ownership of the reference to fixed_cause */
result = _PyException_SetCauseChecked(value, fixed_cause);
Py_DECREF(fixed_cause);
if (result < 0) {
goto raise_error;
}
PyException_SetCause(value, fixed_cause);
}
PyErr_SetObject(type, value);

View File

@ -1698,7 +1698,11 @@ print_exception_recursive(PyObject *f, PyObject *value, PyObject *seen)
else if (PyExceptionInstance_Check(value)) {
cause = PyException_GetCause(value);
context = PyException_GetContext(value);
if (cause) {
if (cause && cause == Py_None) {
/* print neither cause nor context */
;
}
else if (cause) {
res = PySet_Contains(seen, cause);
if (res == -1)
PyErr_Clear();