PEP 678: more rejected ideas (GH-2205)
This commit is contained in:
parent
a83e4189ed
commit
854f67698d
141
pep-0678.rst
141
pep-0678.rst
|
@ -15,13 +15,14 @@ Abstract
|
|||
========
|
||||
Exception objects are typically initialized with a message that describes the
|
||||
error which has occurred. Because further information may be available when the
|
||||
exception is caught and re-raised, this PEP proposes to add a ``.__note__``
|
||||
attribute and update the builtin traceback formatting code to include it in
|
||||
the formatted traceback following the exception string.
|
||||
exception is caught and re-raised, or included in an ``ExceptionGroup``, this PEP
|
||||
proposes to add a ``.__note__`` attribute and update the builtin 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, which
|
||||
make previous workarounds ineffective or confusing. Use cases have been identified
|
||||
in the standard library, Hypothesis package, and common code patterns with retries.
|
||||
in the standard library, Hypothesis and ``cattrs`` packages, and common code
|
||||
patterns with retries.
|
||||
|
||||
|
||||
Motivation
|
||||
|
@ -32,17 +33,18 @@ where it is useful to add information after the exception was caught.
|
|||
For example,
|
||||
|
||||
- testing libraries may wish to show the values involved in a failing assertion,
|
||||
or the steps to reproduce a failure (e.g. ``pytest`` and ``hypothesis`` ; example below).
|
||||
- code with retries may wish to note which iteration or timestamp raised which
|
||||
error - especially if re-raising them in an ``ExceptionGroup``
|
||||
or the steps to reproduce a failure (e.g. ``pytest`` and ``hypothesis``; example below).
|
||||
- code which retries an operation on error may wish to associate an iteration, 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
|
||||
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
|
||||
it in sync with the state of raised, and potentially caught or chained, exceptions.
|
||||
This is already error-prone, and made more difficult by :pep:`654` ``ExceptionGroup`` s,
|
||||
This is already error-prone, and made more difficult by :pep:`654` ``ExceptionGroup``\ s,
|
||||
so the time is right for a built-in solution. We therefore propose to add a mutable
|
||||
field ``__note__`` to ``BaseException`` , which can be assigned a string - and
|
||||
field ``__note__`` to ``BaseException``, which can be assigned a string - and
|
||||
if assigned, is automatically displayed in formatted tracebacks.
|
||||
|
||||
|
||||
|
@ -106,10 +108,21 @@ exception includes a note of the minimal failing example::
|
|||
+------------------------------------
|
||||
|
||||
|
||||
Non-goals
|
||||
---------
|
||||
``__note__`` is *not* intended to carry structured data. If your note is for use by
|
||||
a program rather than display to a human, we recommend instead choosing a convention
|
||||
for an attribute like e.g. ``err._parse_errors = ...`` on the error or ``ExceptionGroup`` [1]_ [2]_
|
||||
|
||||
As a rule of thumb, prefer `exception chaining <https://docs.python.org/3/tutorial/errors.html#exception-chaining>`__
|
||||
when the 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. [3]_
|
||||
|
||||
|
||||
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,
|
||||
the note is displayed immediately after the exception.
|
||||
|
||||
|
@ -118,7 +131,7 @@ users are responsible for implementing it with e.g.::
|
|||
|
||||
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 attempt to delete the attribute.
|
||||
|
||||
``BaseExceptionGroup.subgroup`` and ``BaseExceptionGroup.split``
|
||||
|
@ -128,11 +141,11 @@ copy the ``__note__`` of the original exception group to the parts.
|
|||
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 the
|
||||
language specification, with unassigned names reserved for future use and subject
|
||||
to breakage without warning [1]_.
|
||||
to breakage without warning [4]_.
|
||||
|
||||
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
|
||||
Python - the note just won't be displayed with the traceback and exception message.
|
||||
|
||||
|
@ -142,23 +155,23 @@ How to Teach This
|
|||
=================
|
||||
|
||||
The ``__note__`` attribute will be documented as part of the language standard,
|
||||
and explained as part of the tutorial "Errors and Exceptions" [2]_.
|
||||
and explained as part of the tutorial "Errors and Exceptions" [5]_.
|
||||
|
||||
|
||||
|
||||
Reference Implementation
|
||||
========================
|
||||
|
||||
``BaseException.__note__`` was implemented in [3]_ and released in CPython 3.11.0a3,
|
||||
following discussions related to :pep:`654`. [4]_ [5]_ [6]_
|
||||
``BaseException.__note__`` was implemented in [6]_ and released in CPython 3.11.0a3,
|
||||
following discussions related to :pep:`654`. [7]_ [8]_ [9]_
|
||||
|
||||
|
||||
|
||||
Rejected Ideas
|
||||
==============
|
||||
|
||||
Use ``print()`` (or ``logging`` , etc.)
|
||||
---------------------------------------
|
||||
Use ``print()`` (or ``logging``, etc.)
|
||||
--------------------------------------
|
||||
Reporting explanatory or contextual information about an error by printing or logging
|
||||
has historically been an acceptable workaround. However, we dislike the way this
|
||||
separates the content from the exception object it refers to - which can lead to
|
||||
|
@ -174,19 +187,19 @@ eliminates these problems.
|
|||
---------------------------------------
|
||||
An alternative pattern is to use exception chaining: by raising a 'wrapper' exception
|
||||
containing the context or explanation ``from`` the current exception, we avoid the
|
||||
separation challenges from ``print()`` . However, this has two key problems.
|
||||
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
|
||||
downstream code. We consider *always* raising a ``Wrapper`` exception unacceptably
|
||||
inelegant; but because custom exception types might have any number of required
|
||||
arguments we can't always create an instance of the *same* type with our explanation.
|
||||
In cases where the exact exception type is known this can work, such as the standard
|
||||
library ``http.client`` code [7]_, but not for libraries which call user code.
|
||||
library ``http.client`` code [10]_, but not for libraries which call user code.
|
||||
|
||||
Second, exception chaining reports several lines of additional detail, which 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
|
||||
exception chaining, and are unnecessary with ``BaseException.__note__`` :
|
||||
exception chaining, and are unnecessary with ``BaseException.__note__``:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
|
@ -215,6 +228,9 @@ exception chaining, and are unnecessary with ``BaseException.__note__`` :
|
|||
Explanation: # Hence this PEP!
|
||||
You can reproduce this error by ...
|
||||
|
||||
**In cases where these two problems do not apply, we encourage use
|
||||
of exception chaining rather than** ``__note__``.
|
||||
|
||||
|
||||
Subclass Exception and add ``__note__`` downstream
|
||||
--------------------------------------------------
|
||||
|
@ -229,27 +245,84 @@ proposed ``__note__`` semantics, but this would be rarely and inconsistently
|
|||
applicable.
|
||||
|
||||
|
||||
Store notes in ``ExceptionGroup`` s
|
||||
-----------------------------------
|
||||
Store notes in ``ExceptionGroup``\ s
|
||||
------------------------------------
|
||||
Initial discussions proposed making a more focussed change by thinking about how to
|
||||
associate messages with the nested exceptions in ``ExceptionGroup`` s, such as a list
|
||||
of notes or mapping of exceptions to notes. However, this would force a remarkably
|
||||
awkward API and retains a lesser form of the cross-referencing problem discussed
|
||||
under "use ``print()`` " above; if this PEP is rejected we prefer the status quo.
|
||||
under "use ``print()``" above; if this PEP is rejected we prefer the status quo.
|
||||
Finally, of course, ``__note__`` is not only useful with ``ExceptionGroup`` s!
|
||||
|
||||
|
||||
|
||||
Possible Future Enhancements
|
||||
============================
|
||||
|
||||
In addition to rejected alternatives, there have been a range of suggestions which
|
||||
we believe should be deferred to a future version, when we have more experience with
|
||||
the uses (and perhaps misuses) of ``__note__``.
|
||||
|
||||
|
||||
Allow any object, and cast to string for display
|
||||
------------------------------------------------
|
||||
We have not identified any scenario where libraries would want to do anything but either
|
||||
concatenate or replace notes, and so the additional complexity and interoperability
|
||||
challenges do not seem justified.
|
||||
|
||||
Permitting any object would also force any future structured API to change the behaviour
|
||||
of already-legal code, whereas expanding the permitted contents of ``__note__`` from strings
|
||||
to include other objects is fully backwards-compatible. In the absence of any proposed
|
||||
use-case (see also `Non-goals`_), we prefer to begin with a restrictive API that can
|
||||
be relaxed later.
|
||||
|
||||
|
||||
Add a helper function ``contextlib.add_exc_note()``
|
||||
---------------------------------------------------
|
||||
It was suggested 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
|
||||
|
||||
@contextlib.contextmanager
|
||||
def add_exc_note(note: str):
|
||||
try:
|
||||
yield
|
||||
except Exception as err:
|
||||
if err.__note__ is None:
|
||||
err.__note__ = note
|
||||
else:
|
||||
err.__note__ = err.__note__ + "\n\n" + note
|
||||
raise
|
||||
|
||||
with add_exc_note(f"While attempting to frobnicate {item=}"):
|
||||
frobnicate_or_raise(item)
|
||||
|
||||
|
||||
Augment the ``raise`` statement
|
||||
-------------------------------
|
||||
One discussion proposed ``raise Exception() with "note contents"``, but this
|
||||
does not address the original motivation of compatibility with ``ExceptionGroup``.
|
||||
|
||||
Furthermore, we do not believe that the problem we are solving requires or justifies
|
||||
new language syntax.
|
||||
|
||||
References
|
||||
==========
|
||||
|
||||
.. [1] https://docs.python.org/3/reference/lexical_analysis.html#reserved-classes-of-identifiers
|
||||
.. [2] https://github.com/python/cpython/pull/30158
|
||||
.. [3] https://github.com/python/cpython/pull/29880
|
||||
.. [4] https://discuss.python.org/t/accepting-pep-654-exception-groups-and-except/10813/9
|
||||
.. [5] https://github.com/python/cpython/pull/28569#discussion_r721768348
|
||||
.. [6] https://bugs.python.org/issue45607
|
||||
.. [7] https://github.com/python/cpython/blob/69ef1b59983065ddb0b712dac3b04107c5059735/Lib/http/client.py#L596-L597
|
||||
.. [1] https://discuss.python.org/t/accepting-pep-654-exception-groups-and-except/10813/26
|
||||
.. [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!
|
||||
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
|
||||
.. [5] https://github.com/python/cpython/pull/30158
|
||||
.. [6] https://github.com/python/cpython/pull/29880
|
||||
.. [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
|
||||
|
||||
|
||||
|
||||
|
|
Loading…
Reference in New Issue