Close issue #6210: Implement PEP 409
diff --git a/Doc/ACKS.txt b/Doc/ACKS.txt
index f9e4d3b..b64c650 100644
--- a/Doc/ACKS.txt
+++ b/Doc/ACKS.txt
@@ -62,6 +62,7 @@
    * Stefan Franke
    * Jim Fulton
    * Peter Funk
+   * Ethan Furman
    * Lele Gaifax
    * Matthew Gallagher
    * Gabriel Genellina
diff --git a/Doc/c-api/exceptions.rst b/Doc/c-api/exceptions.rst
index c7252ed..fd7aee7 100644
--- a/Doc/c-api/exceptions.rst
+++ b/Doc/c-api/exceptions.rst
@@ -421,17 +421,24 @@
 
 .. 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:
diff --git a/Doc/library/exceptions.rst b/Doc/library/exceptions.rst
index 3f1a30d..7e3a0c3 100644
--- a/Doc/library/exceptions.rst
+++ b/Doc/library/exceptions.rst
@@ -34,6 +34,24 @@
 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
 ------------
diff --git a/Doc/library/stdtypes.rst b/Doc/library/stdtypes.rst
index be06595..1526a0a 100644
--- a/Doc/library/stdtypes.rst
+++ b/Doc/library/stdtypes.rst
@@ -2985,10 +2985,11 @@
 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 ``...``.
 
diff --git a/Doc/whatsnew/3.3.rst b/Doc/whatsnew/3.3.rst
index 560331f..9023dfa 100644
--- a/Doc/whatsnew/3.3.rst
+++ b/Doc/whatsnew/3.3.rst
@@ -254,6 +254,9 @@
 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 @@
 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
 ==================================================
 
diff --git a/Include/pyerrors.h b/Include/pyerrors.h
index 1bd0442..1e42ebb 100644
--- a/Include/pyerrors.h
+++ b/Include/pyerrors.h
@@ -105,6 +105,7 @@
 /* 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 a7683ac..91d85ef 100644
--- a/Lib/test/test_exceptions.py
+++ b/Lib/test/test_exceptions.py
@@ -387,19 +387,36 @@
 
     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,
diff --git a/Lib/test/test_raise.py b/Lib/test/test_raise.py
index 92c50c7..8ae9210 100644
--- a/Lib/test/test_raise.py
+++ b/Lib/test/test_raise.py
@@ -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 @@
                 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 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 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()
diff --git a/Lib/test/test_traceback.py b/Lib/test/test_traceback.py
index 4752d37..5bce2af 100644
--- a/Lib/test/test_traceback.py
+++ b/Lib/test/test_traceback.py
@@ -246,6 +246,21 @@
         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.
diff --git a/Lib/traceback.py b/Lib/traceback.py
index 8d4e96e..35858af 100644
--- a/Lib/traceback.py
+++ b/Lib/traceback.py
@@ -120,14 +120,14 @@
     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:
diff --git a/Misc/ACKS b/Misc/ACKS
index 48ef080..e9077c3 100644
--- a/Misc/ACKS
+++ b/Misc/ACKS
@@ -338,6 +338,7 @@
 Tadayoshi Funaba
 Gyro Funch
 Peter Funk
+Ethan Furman
 Geoff Furnish
 Ulisses Furquim
 Hagen Fürstenau
diff --git a/Misc/NEWS b/Misc/NEWS
index bc9fe5d..be6f700 100644
--- a/Misc/NEWS
+++ b/Misc/NEWS
@@ -10,6 +10,10 @@
 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
diff --git a/Objects/exceptions.c b/Objects/exceptions.c
index e9522e8..bc43799 100644
--- a/Objects/exceptions.c
+++ b/Objects/exceptions.c
@@ -266,7 +266,24 @@
     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 @@
     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);
 }
 
 
diff --git a/Python/ceval.c b/Python/ceval.c
index 06bff4c..017dc4a 100644
--- a/Python/ceval.c
+++ b/Python/ceval.c
@@ -3567,22 +3567,23 @@
 
     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);
diff --git a/Python/pythonrun.c b/Python/pythonrun.c
index a642c0b..f4e7e7b 100644
--- a/Python/pythonrun.c
+++ b/Python/pythonrun.c
@@ -1698,7 +1698,11 @@
         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();