Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PEP 678: Update proposal with .add_note() and acknowledgements section #2331

Merged
merged 5 commits into from
Feb 20, 2022
Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
PEP 678: updated proposal
Zac-HD committed Feb 16, 2022
commit 305956fdb821826cf05bc7470ed811c3e39840f1
127 changes: 70 additions & 57 deletions pep-0678.rst
Original file line number Diff line number Diff line change
@@ -17,9 +17,10 @@ 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, 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 PEP proposes to add ``BaseException.with_note(note, *, replace=False)``, a
Zac-HD marked this conversation as resolved.
Show resolved Hide resolved
``.__notes__`` attribute holding a tuple of zero or more notes so added, and to
update the builtin traceback formatting code to include notes in the formatted
traceback following the exception string.

This is particularly useful in relation to :pep:`654` ``ExceptionGroup``\ s,
which make previous workarounds ineffective or confusing. Use cases have been
@@ -41,15 +42,19 @@ example,
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.

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, 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 if assigned, is automatically displayed in
formatted tracebacks.
therefore propose to add:

- a new method ``BaseException.with_note(note, *, replace=False)``,
Zac-HD marked this conversation as resolved.
Show resolved Hide resolved
- ``BaseException.__notes__``, a read-only field which is a tuple of zero or
more notes strings, and
Zac-HD marked this conversation as resolved.
Show resolved Hide resolved
- support in the builtin traceback formatting code such that notes are
displayed in the formatted traceback following the exception string.


Example usage
@@ -58,7 +63,7 @@ Example usage
>>> try:
... raise TypeError('bad type')
... except Exception as e:
... e.__note__ = 'Add some information'
... e.add_note('Add some information')
... raise
...
Traceback (most recent call last):
@@ -114,36 +119,39 @@ 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
Tracking multiple notes as a tuple, rather than by concatenating strings when
notes are added, is intended to maintain the distinction between strings
which might be translated by packages such as ``friendly-traceback``.
Zac-HD marked this conversation as resolved.
Show resolved Hide resolved

However, ``__notes__`` is *not* intended to carry structured data. If your
note is for use by a program rather than display to a human, `we recommend
<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
As a rule of thumb, we suggest that you should 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
Zac-HD marked this conversation as resolved.
Show resolved Hide resolved
``__note__`` when you are collecting multiple exception objects to handle
``.add_note()`` when you are collecting multiple exception objects to handle
together or later. [1]_


Specification
=============

``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.

Assigning a new string value overrides an existing note; if concatenation is
desired users are responsible for implementing it with e.g.::
``BaseException`` gains a new read-only field ``__notes__``, an initially empty
Zac-HD marked this conversation as resolved.
Show resolved Hide resolved
tuple of strings, and a new method ``.add_note(note: str | None, *, replace:
Zac-HD marked this conversation as resolved.
Show resolved Hide resolved
bool=False)``. If ``note`` is not ``None``, it is added to the exception's
notes which appear in the standard traceback after the exception string. If
``replace`` is true, all previously existing notes are removed before the new
one is added. To clear all notes, use ``add_note(None, replace=True)``. A
:exc:`TypeError` is raise if ``note`` is neither a string nor ``None``.
Zac-HD marked this conversation as resolved.
Show resolved Hide resolved

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__``, or to
attempt to delete the attribute.
When an exception with one or more notes is displayed, each note is displayed
on a new line starting immediately after the exception message.
Zac-HD marked this conversation as resolved.
Show resolved Hide resolved

``BaseExceptionGroup.subgroup`` and ``BaseExceptionGroup.split`` copy the
``__note__`` of the original exception group to the parts.
``__notes__`` of the original exception group to the parts.


Backwards Compatibility
@@ -153,27 +161,30 @@ 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
<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 ``__notes__``.

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.

While ``add_note()`` is a fairly common method name, our Google and GitHub
searches did not find any exception classes which define such a method.
Zac-HD marked this conversation as resolved.
Show resolved Hide resolved


How to Teach This
=================

The ``__note__`` attribute will be documented as part of the language standard,
and explained as part of `the "Errors and Exceptions" tutorial
<https://github.com/python/cpython/pull/30441>`__.
The ``add_note()`` method and ``__notes__`` attribute will be documented as
part of the language standard, and explained as part of `the "Errors and
Exceptions" tutorial <https://github.com/python/cpython/pull/30441>`__.


Reference Implementation
========================

``BaseException.__note__`` was `implemented in <https://github.com/python/cpython/pull/29880>`__ and released in CPython
3.11.0a3, following discussions related to :pep:`654`. [2]_
Following discussions related to :pep:`654` [2]_, an early version of this
proposal was `implemented in <https://github.com/python/cpython/pull/29880>`__
and released in CPython 3.11.0a3, with a mutable string-or-none ``__note__``
attribute.

`CPython PR #31317 <https://github.com/python/cpython/pull/31317>`__
implements ``.add_note()`` and ``__notes__``.


Rejected Ideas
@@ -189,8 +200,8 @@ or merely significant difficulties working out which explanation corresponds to
which error. The new ``ExceptionGroup`` type intensifies these existing
challenges.

Keeping the ``__note__`` attached to the exception object, like the traceback,
eliminates these problems.
Keeping the ``__notes__`` attached to the exception object, like the
traceback, eliminates these problems.
Zac-HD marked this conversation as resolved.
Show resolved Hide resolved


``raise Wrapper(explanation) from err``
@@ -212,7 +223,7 @@ 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.add_note()``:

.. code-block:: python
@@ -242,19 +253,30 @@ exception chaining, and are unnecessary with ``BaseException.__note__``:
You can reproduce this error by ...
**In cases where these two problems do not apply, we encourage use of exception
chaining rather than** ``__note__``.
chaining rather than** ``__notes__``.


Subclass Exception and add ``__note__`` downstream
A mutable ``__note__`` attribute
--------------------------------
The first draft and implementation of this PEP defined a single attribute
``__note__``, which defaulted to ``None`` but could have a string assigned.
This is substantially simpler if, and only if, there is at most one note.

To promote interoperability and support translation of error messages by
libraries such as ``friendly-traceback``, without resorting to dubious parsing
heuristics, we therefore settled on the ``.add_note()``-and-``__notes__` API.
Zac-HD marked this conversation as resolved.
Show resolved Hide resolved
Subclass Exception and add note support downstream
--------------------------------------------------
Traceback printing is built into the C code, and reimplemented in pure Python
in traceback.py. To get ``err.__note__`` printed from a downstream
in traceback.py. To get ``err.__notes__`` printed from a downstream
Zac-HD marked this conversation as resolved.
Show resolved Hide resolved
implementation would *also* require writing custom traceback-printing code;
while this could be shared between projects and reuse some pieces of
traceback.py we prefer to implement this once, upstream.

Custom exception types could implement their ``__str__`` method to include our
proposed ``__note__`` semantics, but this would be rarely and inconsistently
proposed ``__notes__`` semantics, but this would be rarely and inconsistently
applicable.


@@ -265,8 +287,8 @@ 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. Finally, of course, ``__note__`` is not
only useful with ``ExceptionGroup`` s!
is rejected we prefer the status quo. Finally, of course, ``__notes__`` are
not only useful with ``ExceptionGroup``\ s!



@@ -275,22 +297,15 @@ 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__``.
experience with the uses (and perhaps misuses) of ``__notes__``.


Allow any object, and cast to string for display
------------------------------------------------
Allow any object, and convert 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.

We also note that converting an object to a string may raise an exception.
It's more helpful for the traceback to point to the location where the note is
attached to the exception, rather than where the exception and note are being
@@ -307,14 +322,12 @@ as it can be added as an enhancement later.

.. code-block:: python
@contextlib.contextmanager def add_exc_note(note: str):
@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
err.add_note(note)
raise
with add_exc_note(f"While attempting to frobnicate {item=}"):