Commit ab7bf214 authored by Nick Coghlan's avatar Nick Coghlan

Close issue #6210: Implement PEP 409

parent cda6b6d6
...@@ -62,6 +62,7 @@ docs@python.org), and we'll be glad to correct the problem. ...@@ -62,6 +62,7 @@ docs@python.org), and we'll be glad to correct the problem.
* Stefan Franke * Stefan Franke
* Jim Fulton * Jim Fulton
* Peter Funk * Peter Funk
* Ethan Furman
* Lele Gaifax * Lele Gaifax
* Matthew Gallagher * Matthew Gallagher
* Gabriel Genellina * Gabriel Genellina
......
...@@ -421,17 +421,24 @@ Exception Objects ...@@ -421,17 +421,24 @@ Exception Objects
.. c:function:: PyObject* PyException_GetCause(PyObject *ex) .. c:function:: PyObject* PyException_GetCause(PyObject *ex)
Return the cause (another exception instance set by ``raise ... from ...``) Return the cause (either an exception instance, or :const:`None`,
associated with the exception as a new reference, as accessible from Python set by ``raise ... from ...``) associated with the exception as a new
through :attr:`__cause__`. If there is no cause associated, this returns reference, as accessible from Python through :attr:`__cause__`.
*NULL*.
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)
Set the cause associated with the exception to *ctx*. Use *NULL* to clear 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. it. There is no type check to make sure that *ctx* is either an exception
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
routines will not display this exception's context, and will not follow the
chain any further.
.. _unicodeexceptions: .. _unicodeexceptions:
......
...@@ -34,6 +34,24 @@ programmers are encouraged to at least derive new exceptions from the ...@@ -34,6 +34,24 @@ programmers are encouraged to at least derive new exceptions from the
defining exceptions is available in the Python Tutorial under defining exceptions is available in the Python Tutorial under
:ref:`tut-userexceptions`. :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 Base classes
------------ ------------
......
...@@ -2985,10 +2985,11 @@ It is written as ``None``. ...@@ -2985,10 +2985,11 @@ It is written as ``None``.
The Ellipsis Object The Ellipsis Object
------------------- -------------------
This object is commonly used by slicing (see :ref:`slicings`). It supports no This object is commonly used by slicing (see :ref:`slicings`), but may also
special operations. There is exactly one ellipsis object, named be used in other situations where a sentinel value other than :const:`None`
:const:`Ellipsis` (a built-in name). ``type(Ellipsis)()`` produces the is needed. It supports no special operations. There is exactly one ellipsis
:const:`Ellipsis` singleton. object, named :const:`Ellipsis` (a built-in name). ``type(Ellipsis)()``
produces the :const:`Ellipsis` singleton.
It is written as ``Ellipsis`` or ``...``. It is written as ``Ellipsis`` or ``...``.
......
...@@ -254,6 +254,9 @@ inspection of exception attributes:: ...@@ -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:`380` - Syntax for Delegating to a Subgenerator
PEP written by Greg Ewing.
PEP 380 adds the ``yield from`` expression, allowing a generator to delegate 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 part of its operations to another generator. This allows a section of code
containing 'yield' to be factored out and placed in another generator. 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 ...@@ -267,6 +270,67 @@ Kelly and Nick Coghlan, documentation by Zbigniew Jędrzejewski-Szmek and
Nick Coghlan) 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 PEP 3155: Qualified name for classes and functions
================================================== ==================================================
......
...@@ -105,6 +105,7 @@ PyAPI_FUNC(PyObject *) PyException_GetTraceback(PyObject *); ...@@ -105,6 +105,7 @@ 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 *);
......
...@@ -387,19 +387,36 @@ class ExceptionTests(unittest.TestCase): ...@@ -387,19 +387,36 @@ class ExceptionTests(unittest.TestCase):
def testChainingAttrs(self): def testChainingAttrs(self):
e = Exception() e = Exception()
self.assertEqual(e.__context__, None) self.assertIsNone(e.__context__)
self.assertEqual(e.__cause__, None) self.assertIs(e.__cause__, Ellipsis)
e = TypeError() e = TypeError()
self.assertEqual(e.__context__, None) self.assertIsNone(e.__context__)
self.assertEqual(e.__cause__, None) self.assertIs(e.__cause__, Ellipsis)
class MyException(EnvironmentError): class MyException(EnvironmentError):
pass pass
e = MyException() e = MyException()
self.assertEqual(e.__context__, None) self.assertIsNone(e.__context__)
self.assertEqual(e.__cause__, None) 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): def testKeywordArgs(self):
# test that builtin exception don't take keyword args, # test that builtin exception don't take keyword args,
......
...@@ -3,12 +3,27 @@ ...@@ -3,12 +3,27 @@
"""Tests for the raise statement.""" """Tests for the raise statement."""
from test import support from test import support, script_helper
import re
import sys import sys
import types import types
import unittest 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(): def get_tb():
try: try:
raise OSError() raise OSError()
...@@ -77,6 +92,16 @@ class TestRaise(unittest.TestCase): ...@@ -77,6 +92,16 @@ class TestRaise(unittest.TestCase):
nested_reraise() nested_reraise()
self.assertRaises(TypeError, 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 test_with_reraise1(self):
def reraise(): def reraise():
try: try:
...@@ -139,6 +164,23 @@ class TestRaise(unittest.TestCase): ...@@ -139,6 +164,23 @@ class TestRaise(unittest.TestCase):
class TestCause(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): def test_invalid_cause(self):
try: try:
raise IndexError from 5 raise IndexError from 5
...@@ -178,6 +220,44 @@ class TestCause(unittest.TestCase): ...@@ -178,6 +220,44 @@ class TestCause(unittest.TestCase):
class TestTraceback(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): def test_sets_traceback(self):
try: try:
raise IndexError() raise IndexError()
......
...@@ -246,6 +246,21 @@ class BaseExceptionReportingTests: ...@@ -246,6 +246,21 @@ class BaseExceptionReportingTests:
self.check_zero_div(blocks[0]) self.check_zero_div(blocks[0])
self.assertIn('inner_raise() # Marker', blocks[2]) 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): def test_cause_and_context(self):
# When both a cause and a context are set, only the cause should be # When both a cause and a context are set, only the cause should be
# displayed and the context should be muted. # displayed and the context should be muted.
......
...@@ -120,14 +120,14 @@ def _iter_chain(exc, custom_tb=None, seen=None): ...@@ -120,14 +120,14 @@ def _iter_chain(exc, custom_tb=None, seen=None):
seen.add(exc) seen.add(exc)
its = [] its = []
cause = exc.__cause__ cause = exc.__cause__
if cause is not None and cause not in seen: if cause is Ellipsis:
its.append(_iter_chain(cause, None, seen))
its.append([(_cause_message, None)])
else:
context = exc.__context__ context = exc.__context__
if context is not None and context not in seen: if context is not None and context not in seen:
its.append(_iter_chain(context, None, seen)) its.append(_iter_chain(context, None, seen))
its.append([(_context_message, None)]) 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__)]) 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:
......
...@@ -338,6 +338,7 @@ Jim Fulton ...@@ -338,6 +338,7 @@ Jim Fulton
Tadayoshi Funaba Tadayoshi Funaba
Gyro Funch Gyro Funch
Peter Funk Peter Funk
Ethan Furman
Geoff Furnish Geoff Furnish
Ulisses Furquim Ulisses Furquim
Hagen Fürstenau Hagen Fürstenau
......
...@@ -10,6 +10,10 @@ What's New in Python 3.3 Alpha 1? ...@@ -10,6 +10,10 @@ What's New in Python 3.3 Alpha 1?
Core and Builtins 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 - Issue #10181: New memoryview implementation fixes multiple ownership
and lifetime issues of dynamically allocated Py_buffer members (#9990) and lifetime issues of dynamically allocated Py_buffer members (#9990)
as well as crashes (#8305, #7433). Many new features have been added as well as crashes (#8305, #7433). Many new features have been added
......
...@@ -266,28 +266,35 @@ BaseException_get_cause(PyObject *self) { ...@@ -266,28 +266,35 @@ 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_RETURN_NONE; Py_INCREF(Py_Ellipsis);
return Py_Ellipsis;
} }
static int int
BaseException_set_cause(PyObject *self, PyObject *arg) { _PyException_SetCauseChecked(PyObject *self, PyObject *arg) {
if (arg == NULL) { if (arg == Py_Ellipsis) {
PyErr_SetString(PyExc_TypeError, "__cause__ may not be deleted");
return -1;
} else if (arg == Py_None) {
arg = NULL; arg = NULL;
} else if (!PyExceptionInstance_Check(arg)) { } else if (arg != Py_None && !PyExceptionInstance_Check(arg)) {
PyErr_SetString(PyExc_TypeError, "exception cause must be None " PyErr_SetString(PyExc_TypeError, "exception cause must be None, "
"or derive from BaseException"); "Ellipsis or derive from BaseException");
return -1; return -1;
} else { } else {
/* PyException_SetCause steals this reference */ /* PyException_SetCause steals a reference */
Py_INCREF(arg); Py_INCREF(arg);
} }
PyException_SetCause(self, arg); PyException_SetCause(self, arg);
return 0; 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[] = { static PyGetSetDef BaseException_getset[] = {
{"__dict__", PyObject_GenericGetDict, PyObject_GenericSetDict}, {"__dict__", PyObject_GenericGetDict, PyObject_GenericSetDict},
......
...@@ -3567,22 +3567,23 @@ do_raise(PyObject *exc, PyObject *cause) ...@@ -3567,22 +3567,23 @@ 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_DECREF(cause); Py_CLEAR(cause);
} } else {
else if (PyExceptionInstance_Check(cause)) { /* Let "exc.__cause__ = cause" handle all further checks */
fixed_cause = cause; fixed_cause = cause;
cause = NULL; /* Steal the reference */
} }
else { /* We retain ownership of the reference to fixed_cause */
PyErr_SetString(PyExc_TypeError, result = _PyException_SetCauseChecked(value, fixed_cause);
"exception causes must derive from " Py_DECREF(fixed_cause);
"BaseException"); if (result < 0) {
goto raise_error; goto raise_error;
} }
PyException_SetCause(value, fixed_cause);
} }
PyErr_SetObject(type, value); PyErr_SetObject(type, value);
......
...@@ -1698,7 +1698,11 @@ print_exception_recursive(PyObject *f, PyObject *value, PyObject *seen) ...@@ -1698,7 +1698,11 @@ 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) { if (cause && cause == Py_None) {
/* 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();
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment