From 85d4e98d634c09bfd93ce7edea65f926c9f87ee3 Mon Sep 17 00:00:00 2001 From: Zac Hatfield-Dodds Date: Tue, 15 Apr 2025 00:10:21 -0700 Subject: [PATCH 1/2] PEP 785: why not more attributes --- peps/pep-0785.rst | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/peps/pep-0785.rst b/peps/pep-0785.rst index bfbcd94bdca..91d18c0b488 100644 --- a/peps/pep-0785.rst +++ b/peps/pep-0785.rst @@ -329,6 +329,20 @@ less magical and tempting to use in cases where it would not be appropriate. We could be argued around though, if others prefer this form. +Preserve additional attributes +------------------------------ + +We decided against preserving the ``__cause__`` and ``__suppress_context__`` +attributes, because they are not changed by re-raising the exception, and we +prefer to support ``raise exc from None`` or ``raise exc from cause_exc`` +together with ``with exc.preserve_context():``. + +Similarly, we considered preserving the ``__traceback__`` attribute, and +decided against because the additional ``raise ...`` statement may be an +important clue when understanding some error. If end users wish to pop a +frame from the traceback, they can do with a separate context manager. + + Footnotes ========= From 1255e4da13581be16f0af79f493a8bd3654cbfc4 Mon Sep 17 00:00:00 2001 From: Zac Hatfield-Dodds Date: Tue, 15 Apr 2025 00:24:58 -0700 Subject: [PATCH 2/2] PEP 785: rename to 'leaf_exceptions()' --- peps/pep-0785.rst | 44 ++++++++++++++++++++++++++------------------ 1 file changed, 26 insertions(+), 18 deletions(-) diff --git a/peps/pep-0785.rst b/peps/pep-0785.rst index 91d18c0b488..3e4052ab94c 100644 --- a/peps/pep-0785.rst +++ b/peps/pep-0785.rst @@ -17,7 +17,7 @@ As :pep:`654` :class:`ExceptionGroup` has come into widespread use across the Python community, some common but awkward patterns have emerged. We therefore propose adding two new methods to exception objects: -- :meth:`!BaseExceptionGroup.flat_exceptions`, returning the 'leaf' exceptions as +- :meth:`!BaseExceptionGroup.leaf_exceptions`, returning the 'leaf' exceptions as a list, with each traceback composited from any intermediate groups. - :meth:`!BaseException.preserve_context`, a context manager which @@ -39,10 +39,10 @@ often write code to process or respond to individual leaf exceptions, for example when implementing middleware, error logging, or response handlers in a web framework. -`Searching GitHub`__ found four implementations of :meth:`!flat_exceptions` by +`Searching GitHub`__ found four implementations of :meth:`!leaf_exceptions` by various names in the first sixty hits, of which none handle tracebacks.\ [#numbers]_ The same search found thirteen cases where -:meth:`!.flat_exceptions` could be used. We therefore believe that providing +:meth:`!.leaf_exceptions` could be used. We therefore believe that providing a method on the :class:`BaseException` type with proper traceback preservation will improve error-handling and debugging experiences across the ecosystem. @@ -55,7 +55,7 @@ unwrap ``HTTPException`` if that is the sole leaf of a group: .. code-block:: python except* HTTPException as group: - first, *rest = group.flat_exceptions() # get the whole traceback :-) + first, *rest = group.leaf_exceptions() # get the whole traceback :-) if not rest: raise first raise @@ -78,12 +78,12 @@ readable, and easy-to-use solution for these cases. Specification ============= -A new method ``flat_exceptions()`` will be added to ``BaseExceptionGroup``, with the +A new method ``leaf_exceptions()`` will be added to ``BaseExceptionGroup``, with the following signature: .. code-block:: python - def flat_exceptions(self, *, fix_tracebacks=True) -> list[BaseException]: + def leaf_exceptions(self, *, fix_tracebacks=True) -> list[BaseException]: """ Return a flat list of all 'leaf' exceptions in the group. @@ -118,7 +118,7 @@ Usage example: try: user_code_here() except* HTTPException as group: - first, *rest = group.flat_exceptions() + first, *rest = group.leaf_exceptions() if rest: raise # handled by internal-server-error middleware ... # logging, cache updates, etc. @@ -142,12 +142,12 @@ Backwards Compatibility Adding new methods to built-in classes, especially those as widely used as ``BaseException``, can have substantial impacts. However, GitHub search shows -no collisions for these method names (`zero hits`__ and +no collisions for these method names (`zero hits`__\ [#naming]_ and `three unrelated hits`__ respectively). If user-defined methods with these names exist in private code they will shadow those proposed in the PEP, without changing runtime behavior. -__ https://github.com/search?q=%2F%5C.flat_exceptions%5C%28%2F+language%3APython&type=code +__ https://github.com/search?q=%2F%5C.leaf_exceptions%5C%28%2F+language%3APython&type=code __ https://github.com/search?q=%2F%5C.preserve_context%5C%28%2F+language%3APython&type=code @@ -157,20 +157,20 @@ How to Teach This Working with exception groups is an intermediate-to-advanced topic, unlikely to arise for beginning programmers. We therefore suggest teaching this topic via documentation, and via just-in-time feedback from static analysis tools. -In intermediate classes, we recommend teaching ``.flat_exceptions()`` together +In intermediate classes, we recommend teaching ``.leaf_exceptions()`` together with the ``.split()`` and ``.subgroup()`` methods, and mentioning ``.preserve_context()`` as an advanced option to address specific pain points. Both the API reference and the existing `ExceptionGroup tutorial`__ should be updated to demonstrate and explain the new methods. The tutorial -should include examples of common patterns where ``.flat_exceptions()`` and +should include examples of common patterns where ``.leaf_exceptions()`` and ``.preserve_context()`` help simplify error handling logic. Downstream libraries which often use exception groups could include similar docs. __ https://docs.python.org/3/tutorial/errors.html#raising-and-handling-multiple-unrelated-exceptions We have also designed lint rules for inclusion in ``flake8-async`` which will -suggest using ``.flat_exceptions()`` when iterating over ``group.exceptions`` +suggest using ``.leaf_exceptions()`` when iterating over ``group.exceptions`` or re-raising a leaf exception, and suggest using ``.preserve_context()`` when re-raising a leaf exception inside an ``except*`` block would override any existing context. @@ -186,7 +186,7 @@ on older versions of Python, and can demonstrate the intended semantics. We have found these helper functions quite useful when working with :class:`ExceptionGroup`\ s in a large production codebase. -A ``flat_exceptions()`` helper function +A ``leaf_exceptions()`` helper function --------------------------------------- .. code-block:: python @@ -196,7 +196,7 @@ A ``flat_exceptions()`` helper function from types import TracebackType - def flat_exceptions( + def leaf_exceptions( self: BaseExceptionGroup, *, fix_traceback: bool = True ) -> list[BaseException]: """ @@ -297,11 +297,11 @@ Add ``BaseException.as_group()`` (or group methods) Our survey of ``ExceptionGroup``-related error handling code also observed many cases of duplicated logic to handle both a bare exception, and the same kind of exception inside a group (often incorrectly, motivating -``.flat_exceptions()``). +``.leaf_exceptions()``). We briefly `proposed `__ adding ``.split(...)`` and ``.subgroup(...)`` methods too all exceptions, -before considering ``.flat_exceptions()`` made us feel this was too clumsy. +before considering ``.leaf_exceptions()`` made us feel this was too clumsy. As a cleaner alternative, we sketched out an ``.as_group()`` method: .. code-block:: python @@ -351,7 +351,7 @@ Footnotes `__ for ``for \w+ in [eg]\w*\.exceptions:``, we find: - * Four functions implementing ``flat_exceptions()`` semantics, none of + * Four functions implementing ``leaf_exceptions()`` semantics, none of which preserve tracebacks: (`one `__, `two `__, @@ -368,7 +368,7 @@ Footnotes `six `__) * Seven cases which mishandle nested exception groups, and would thus - benefit from ``flat_exceptions()``. We were surprised to note that only + benefit from ``leaf_exceptions()``. We were surprised to note that only one of these cases could straightforwardly be replaced by use of an ``except*`` clause or ``.subgroup()`` method. (`one `__, @@ -389,6 +389,14 @@ Footnotes We expect that ``except*`` will be widely used in such cases by the time that the methods proposed by this PEP are widely available. +.. [#naming] + The name ``leaf_exceptions()`` was `first proposed`__ in an early + precursor to :pep:`654`. If the prototype had matched ``except*`` + in wrapping bare exceptions in a group, we might even have included + a ``.leaf_exceptions()`` method in that earlier PEP! + +__ https://github.com/python-trio/exceptiongroup/pull/13 + Copyright =========