PEP 678: convert footnotes to inline links and hard wrap to 79 cols (#2314)

This commit is contained in:
Zac Hatfield-Dodds 2022-02-08 09:13:36 -08:00 committed by GitHub
parent 389e8bd43e
commit f2c28db177
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
1 changed files with 128 additions and 118 deletions

View File

@ -15,38 +15,41 @@ Post-History: 2022-01-27
Abstract Abstract
======== ========
Exception objects are typically initialized with a message that describes the Exception objects are typically initialized with a message that describes the
error which has occurred. Because further information may be available when the error which has occurred. Because further information may be available when
exception is caught and re-raised, or included in an ``ExceptionGroup``, this PEP the exception is caught and re-raised, or included in an ``ExceptionGroup``,
proposes to add a ``.__note__`` attribute and update the builtin traceback formatting this PEP proposes to add a ``.__note__`` attribute and update the builtin
code to include it in the formatted traceback following the exception string. traceback formatting code to include it in the formatted traceback following
the exception string.
This is particularly useful in relation to :pep:`654` ``ExceptionGroup``\ s, which This is particularly useful in relation to :pep:`654` ``ExceptionGroup``\ s,
make previous workarounds ineffective or confusing. Use cases have been identified which make previous workarounds ineffective or confusing. Use cases have been
in the standard library, Hypothesis and ``cattrs`` packages, and common code identified in the standard library, Hypothesis and ``cattrs`` packages, and
patterns with retries. common code patterns with retries.
Motivation Motivation
========== ==========
When an exception is created in order to be raised, it is usually initialized When an exception is created in order to be raised, it is usually initialized
with information that describes the error that has occurred. There are cases with information that describes the error that has occurred. There are cases
where it is useful to add information after the exception was caught. where it is useful to add information after the exception was caught. For
For example, example,
- testing libraries may wish to show the values involved in a failing assertion, - testing libraries may wish to show the values involved in a failing
or the steps to reproduce a failure (e.g. ``pytest`` and ``hypothesis``; example below). assertion, or the steps to reproduce a failure (e.g. ``pytest`` and
- code which retries an operation on error may wish to associate an iteration, timestamp, ``hypothesis``; example below).
or other explanation with each of several errors - especially if re-raising them in - code which retries an operation on error may wish to associate an iteration,
an ``ExceptionGroup``. timestamp, or other explanation with each of several errors - especially if
re-raising them in an ``ExceptionGroup``.
- programming environments for novices can provide more detailed descriptions - programming environments for novices can provide more detailed descriptions
of various errors, and tips for resolving them (e.g. ``friendly-traceback``). of various errors, and tips for resolving them (e.g. ``friendly-traceback``).
Existing approaches must pass this additional information around while keeping Existing approaches must pass this additional information around while keeping
it in sync with the state of raised, and potentially caught or chained, exceptions. it in sync with the state of raised, and potentially caught or chained,
This is already error-prone, and made more difficult by :pep:`654` ``ExceptionGroup``\ s, exceptions. This is already error-prone, and made more difficult by :pep:`654`
so the time is right for a built-in solution. We therefore propose to add a mutable ``ExceptionGroup``\ s, so the time is right for a built-in solution. We
field ``__note__`` to ``BaseException``, which can be assigned a string - and therefore propose to add a mutable field ``__note__`` to ``BaseException``,
if assigned, is automatically displayed in formatted tracebacks. which can be assigned a string - and if assigned, is automatically displayed in
formatted tracebacks.
Example usage Example usage
@ -64,11 +67,11 @@ Example usage
Add some information Add some information
>>> >>>
When collecting exceptions into an exception group, we may want When collecting exceptions into an exception group, we may want to add context
to add context information for the individual errors. In the following information for the individual errors. In the following example with
example with `Hypothesis' proposed support for ExceptionGroup `Hypothesis' proposed support for ExceptionGroup
<https://github.com/HypothesisWorks/hypothesis/pull/3191>`__, each <https://github.com/HypothesisWorks/hypothesis/pull/3191>`__, each exception
exception includes a note of the minimal failing example:: includes a note of the minimal failing example::
from hypothesis import given, strategies as st, target from hypothesis import given, strategies as st, target
@ -111,44 +114,50 @@ exception includes a note of the minimal failing example::
Non-goals Non-goals
--------- ---------
``__note__`` is *not* intended to carry structured data. If your note is for use by ``__note__`` is *not* intended to carry structured data. If your note is for
a program rather than display to a human, we recommend instead (or additionally) choosing a convention use by a program rather than display to a human, `we recommend
for an attribute like e.g. ``err._parse_errors = ...`` on the error or ``ExceptionGroup`` [1]_ [2]_ <https://discuss.python.org/t/accepting-pep-654-exception-groups-and-except/10813/26>`__
instead (or additionally) choosing a convention for an attribute, e.g.
``err._parse_errors = ...`` on the error or ``ExceptionGroup``.
As a rule of thumb, prefer `exception chaining <https://docs.python.org/3/tutorial/errors.html#exception-chaining>`__ As a rule of thumb, prefer `exception chaining
when the error is going to be re-raised or handled as an individual error, and prefer <https://docs.python.org/3/tutorial/errors.html#exception-chaining>`__ when the
``__note__`` when you are collecting multiple exception objects to handle together or later. [3]_ error is going to be re-raised or handled as an individual error, and prefer
``__note__`` when you are collecting multiple exception objects to handle
together or later. [1]_
Specification Specification
============= =============
``BaseException`` gains a new mutable attribute ``__note__``, which defaults to ``BaseException`` gains a new mutable attribute ``__note__``, which defaults to
``None`` and may have a string assigned. When an exception with a note is displayed, ``None`` and may have a string assigned. When an exception with a note is
the note is displayed immediately after the exception. displayed, the note is displayed immediately after the exception.
Assigning a new string value overrides an existing note; if concatenation is desired Assigning a new string value overrides an existing note; if concatenation is
users are responsible for implementing it with e.g.:: desired users are responsible for implementing it with e.g.::
e.__note__ = msg if e.__note__ is None else e.__note__ + "\n" + msg e.__note__ = msg if e.__note__ is None else e.__note__ + "\n" + msg
It is an error to assign a non-string-or-``None`` value to ``__note__``, It is an error to assign a non-string-or-``None`` value to ``__note__``, or to
or to attempt to delete the attribute. attempt to delete the attribute.
``BaseExceptionGroup.subgroup`` and ``BaseExceptionGroup.split`` ``BaseExceptionGroup.subgroup`` and ``BaseExceptionGroup.split`` copy the
copy the ``__note__`` of the original exception group to the parts. ``__note__`` of the original exception group to the parts.
Backwards Compatibility Backwards Compatibility
======================= =======================
System-defined or "dunder" names (following the pattern ``__*__``) are part of the System-defined or "dunder" names (following the pattern ``__*__``) are part of
language specification, with unassigned names reserved for future use and subject the language specification, with `unassigned names reserved for future use and
to breakage without warning [4]_. subject to breakage without warning
<https://docs.python.org/3/reference/lexical_analysis.html#reserved-classes-of-identifiers>`__.
We are also unaware of any code which *would* be broken by adding ``__note__``; We are also unaware of any code which *would* be broken by adding ``__note__``;
assigning to a ``.__note__`` attribute already *works* on current versions of assigning to a ``.__note__`` attribute already *works* on current versions of
Python - the note just won't be displayed with the traceback and exception message. Python - the note just won't be displayed with the traceback and exception
message.
@ -156,16 +165,15 @@ How to Teach This
================= =================
The ``__note__`` attribute will be documented as part of the language standard, The ``__note__`` attribute will be documented as part of the language standard,
and explained as part of the tutorial "Errors and Exceptions" [5]_. and explained as part of `the "Errors and Exceptions" tutorial
<https://github.com/python/cpython/pull/30441>`__.
Reference Implementation Reference Implementation
======================== ========================
``BaseException.__note__`` was implemented in [6]_ and released in CPython 3.11.0a3, ``BaseException.__note__`` was `implemented in <https://github.com/python/cpython/pull/29880>`__ and released in CPython
following discussions related to :pep:`654`. [7]_ [8]_ [9]_ 3.11.0a3, following discussions related to :pep:`654`. [2]_
Rejected Ideas Rejected Ideas
@ -173,12 +181,13 @@ Rejected Ideas
Use ``print()`` (or ``logging``, etc.) Use ``print()`` (or ``logging``, etc.)
-------------------------------------- --------------------------------------
Reporting explanatory or contextual information about an error by printing or logging Reporting explanatory or contextual information about an error by printing or
has historically been an acceptable workaround. However, we dislike the way this logging has historically been an acceptable workaround. However, we dislike
separates the content from the exception object it refers to - which can lead to the way this separates the content from the exception object it refers to -
"orphan" reports if the error was caught and handled later, or merely significant which can lead to "orphan" reports if the error was caught and handled later,
difficulties working out which explanation corresponds to which error. or merely significant difficulties working out which explanation corresponds to
The new ``ExceptionGroup`` type intensifies these existing challenges. which error. The new ``ExceptionGroup`` type intensifies these existing
challenges.
Keeping the ``__note__`` attached to the exception object, like the traceback, Keeping the ``__note__`` attached to the exception object, like the traceback,
eliminates these problems. eliminates these problems.
@ -186,19 +195,22 @@ eliminates these problems.
``raise Wrapper(explanation) from err`` ``raise Wrapper(explanation) from err``
--------------------------------------- ---------------------------------------
An alternative pattern is to use exception chaining: by raising a 'wrapper' exception An alternative pattern is to use exception chaining: by raising a 'wrapper'
containing the context or explanation ``from`` the current exception, we avoid the exception containing the context or explanation ``from`` the current exception,
separation challenges from ``print()``. However, this has two key problems. we avoid the separation challenges from ``print()``. However, this has two key
problems.
First, it changes the type of the exception, which is often a breaking change for First, it changes the type of the exception, which is often a breaking change
downstream code. We consider *always* raising a ``Wrapper`` exception unacceptably for downstream code. We consider *always* raising a ``Wrapper`` exception
inelegant; but because custom exception types might have any number of required unacceptably inelegant; but because custom exception types might have any
arguments we can't always create an instance of the *same* type with our explanation. number of required arguments we can't always create an instance of the *same*
In cases where the exact exception type is known this can work, such as the standard type with our explanation. In cases where the exact exception type is known
library ``http.client`` code [10]_, but not for libraries which call user code. this can work, such as the standard library ``http.client`` `code
<https://github.com/python/cpython/blob/69ef1b59983065ddb0b712dac3b04107c5059735/Lib/http/client.py#L596-L597>`__,
but not for libraries which call user code.
Second, exception chaining reports several lines of additional detail, which are Second, exception chaining reports several lines of additional detail, which
distracting for experienced users and can be very confusing for beginners. are distracting for experienced users and can be very confusing for beginners.
For example, six of the eleven lines reported for this simple example relate to For example, six of the eleven lines reported for this simple example relate to
exception chaining, and are unnecessary with ``BaseException.__note__``: exception chaining, and are unnecessary with ``BaseException.__note__``:
@ -220,26 +232,26 @@ exception chaining, and are unnecessary with ``BaseException.__note__``:
File "example.py", line 6, in <module> File "example.py", line 6, in <module>
raise AssertionError(why) raise AssertionError(why)
AssertionError: Failed! AssertionError: Failed!
# These lines are # These lines are
The above exception was the direct cause of the following exception: # confusing for new The above exception was the direct cause of ... # confusing for new
# users, and they # users, and they
Traceback (most recent call last): # only exist due Traceback (most recent call last): # only exist due
File "example.py", line 8, in <module> # to implementation File "example.py", line 8, in <module> # to implementation
raise Explanation(msg) from e # constraints :-( raise Explanation(msg) from e # constraints :-(
Explanation: # Hence this PEP! Explanation: # Hence this PEP!
You can reproduce this error by ... You can reproduce this error by ...
**In cases where these two problems do not apply, we encourage use **In cases where these two problems do not apply, we encourage use of exception
of exception chaining rather than** ``__note__``. chaining rather than** ``__note__``.
Subclass Exception and add ``__note__`` downstream Subclass Exception and add ``__note__`` downstream
-------------------------------------------------- --------------------------------------------------
Traceback printing is built into the C code, and reimplemented in pure Python in Traceback printing is built into the C code, and reimplemented in pure Python
traceback.py. To get ``err.__note__`` printed from a downstream implementation in traceback.py. To get ``err.__note__`` printed from a downstream
would *also* require writing custom traceback-printing code; while this could implementation would *also* require writing custom traceback-printing code;
be shared between projects and reuse some pieces of traceback.py we prefer to while this could be shared between projects and reuse some pieces of
implement this once, upstream. traceback.py we prefer to implement this once, upstream.
Custom exception types could implement their ``__str__`` method to include our Custom exception types could implement their ``__str__`` method to include our
proposed ``__note__`` semantics, but this would be rarely and inconsistently proposed ``__note__`` semantics, but this would be rarely and inconsistently
@ -248,50 +260,54 @@ applicable.
Store notes in ``ExceptionGroup``\ s Store notes in ``ExceptionGroup``\ s
------------------------------------ ------------------------------------
Initial discussions proposed making a more focussed change by thinking about how to Initial discussions proposed making a more focussed change by thinking about
associate messages with the nested exceptions in ``ExceptionGroup`` s, such as a list how to associate messages with the nested exceptions in ``ExceptionGroup`` s,
of notes or mapping of exceptions to notes. However, this would force a remarkably such as a list of notes or mapping of exceptions to notes. However, this would
awkward API and retains a lesser form of the cross-referencing problem discussed force a remarkably awkward API and retains a lesser form of the
under "use ``print()``" above; if this PEP is rejected we prefer the status quo. cross-referencing problem discussed under "use ``print()``" above; if this PEP
Finally, of course, ``__note__`` is not only useful with ``ExceptionGroup`` s! is rejected we prefer the status quo. Finally, of course, ``__note__`` is not
only useful with ``ExceptionGroup`` s!
Possible Future Enhancements Possible Future Enhancements
============================ ============================
In addition to rejected alternatives, there have been a range of suggestions which In addition to rejected alternatives, there have been a range of suggestions
we believe should be deferred to a future version, when we have more experience with which we believe should be deferred to a future version, when we have more
the uses (and perhaps misuses) of ``__note__``. experience with the uses (and perhaps misuses) of ``__note__``.
Allow any object, and cast to string for display Allow any object, and cast to string for display
------------------------------------------------ ------------------------------------------------
We have not identified any scenario where libraries would want to do anything but either We have not identified any scenario where libraries would want to do anything
concatenate or replace notes, and so the additional complexity and interoperability but either concatenate or replace notes, and so the additional complexity and
challenges do not seem justified. interoperability challenges do not seem justified.
Permitting any object would also force any future structured API to change the behaviour Permitting any object would also force any future structured API to change the
of already-legal code, whereas expanding the permitted contents of ``__note__`` from strings behaviour of already-legal code, whereas expanding the permitted contents of
to include other objects is fully backwards-compatible. In the absence of any proposed ``__note__`` from strings to include other objects is fully
use-case (see also `Non-goals`_), we prefer to begin with a restrictive API that can backwards-compatible. In the absence of any proposed use-case (see also
be relaxed later. `Non-goals`_), we prefer to begin with a restrictive API that can be relaxed
later.
We also note that converting an object to a string may raise an exception. It's more helpful We also note that converting an object to a string may raise an exception.
for the traceback to point to the location where the note is attached to the exception, It's more helpful for the traceback to point to the location where the note is
rather than where the exception and note are being formatted for display after propagation. attached to the exception, rather than where the exception and note are being
formatted for display after propagation.
Add a helper function ``contextlib.add_exc_note()`` Add a helper function ``contextlib.add_exc_note()``
--------------------------------------------------- ---------------------------------------------------
It was suggested [11]_ that we add a utility such as the one below to the standard It `was suggested
library. We are open to this idea, but do not see it as a core part of the <https://www.reddit.com/r/Python/comments/rmrvxv/pep_678_enriching_exceptions_with_notes/hptbul1/>`__
proposal of this PEP as it can be added as an enhancement later. that we add a utility such as the one below to the standard library. We are
open to this idea, but do not see it as a core part of the proposal of this PEP
as it can be added as an enhancement later.
.. code-block:: python .. code-block:: python
@contextlib.contextmanager @contextlib.contextmanager def add_exc_note(note: str):
def add_exc_note(note: str):
try: try:
yield yield
except Exception as err: except Exception as err:
@ -308,27 +324,21 @@ proposal of this PEP as it can be added as an enhancement later.
Augment the ``raise`` statement Augment the ``raise`` statement
------------------------------- -------------------------------
One discussion proposed ``raise Exception() with "note contents"``, but this One discussion proposed ``raise Exception() with "note contents"``, but this
does not address the original motivation of compatibility with ``ExceptionGroup``. does not address the original motivation of compatibility with
``ExceptionGroup``.
Furthermore, we do not believe that the problem we are solving requires or justifies Furthermore, we do not believe that the problem we are solving requires or
new language syntax. justifies new language syntax.
References References
========== ==========
.. [1] https://discuss.python.org/t/accepting-pep-654-exception-groups-and-except/10813/26 .. [1] this principle was established in the 2003 mail thread which led to :pep:`3134`,
.. [2] https://bugs.python.org/issue46431
.. [3] this principle was established in the 2003 mail thread which led to :pep:`3134`,
and included a proposal for a group-of-exceptions type! and included a proposal for a group-of-exceptions type!
https://mail.python.org/pipermail/python-dev/2003-January/032492.html https://mail.python.org/pipermail/python-dev/2003-January/032492.html
.. [4] https://docs.python.org/3/reference/lexical_analysis.html#reserved-classes-of-identifiers .. [2] particularly those at https://bugs.python.org/issue45607,
.. [5] https://github.com/python/cpython/pull/30441 https://discuss.python.org/t/accepting-pep-654-exception-groups-and-except/10813/9,
.. [6] https://github.com/python/cpython/pull/29880 https://github.com/python/cpython/pull/28569#discussion_r721768348, and
.. [7] https://discuss.python.org/t/accepting-pep-654-exception-groups-and-except/10813/9
.. [8] https://github.com/python/cpython/pull/28569#discussion_r721768348
.. [9] https://bugs.python.org/issue45607
.. [10] https://github.com/python/cpython/blob/69ef1b59983065ddb0b712dac3b04107c5059735/Lib/http/client.py#L596-L597
.. [11] https://www.reddit.com/r/Python/comments/rmrvxv/pep_678_enriching_exceptions_with_notes/hptbul1/