From e31aab38edc0c371f710e6dd67ebdf6b09dc7b4a Mon Sep 17 00:00:00 2001 From: Irit Katriel Date: Thu, 18 Mar 2021 16:56:49 +0000 Subject: [PATCH] PEP-654: allow subclassing of exception groups (#1872) --- pep-0654.rst | 72 ++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 67 insertions(+), 5 deletions(-) diff --git a/pep-0654.rst b/pep-0654.rst index a82cd1f75..d9e7b60ba 100644 --- a/pep-0654.rst +++ b/pep-0654.rst @@ -156,14 +156,13 @@ use the class name. For brevity, we will use ``ExceptionGroup`` in code examples that are relevant to both. Since an exception group can be nested, it represents a tree of exceptions, -where the leaves are plain exceptions and each internal node represent a time +where the leaves are plain exceptions and each internal node represents a time at which the program grouped some unrelated exceptions into a new group and -raised them together. The ``ExceptionGroup`` and ``BaseExceptionGroup`` -classes are final, i.e., they cannot be further subclassed. +raised them together. The ``BaseExceptionGroup.subgroup(condition)`` method gives us a way to obtain -an exception group that has the same metadata (cause, context, traceback) as -the original group, and the same nested structure of groups, but +an exception group that has the same metadata (message, cause, context, +traceback) as the original group, and the same nested structure of groups, but contains only those exceptions for which the condition is true: .. code-block:: @@ -267,6 +266,69 @@ as a shorthand for matching that type: ``eg.split(T)`` divides ``eg`` into the subgroup of leaf exceptions that match the type ``T``, and the subgroup of those that do not (using the same check as ``except`` for a match). +Subclassing Exception Groups +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +It is possible to subclass exception groups, but when doing that it is +usually necessary to specify how ``subgroup()`` and ``split()`` should +create new instances for the matching or non-matching part of the partition. +``BaseExceptionGroup`` exposes an instance method ``derive(self, excs)`` +which is called whenever ``subgroup`` and ``split`` need to create a new +exception group. The parameter ``excs`` is the sequence of exceptions to +include in the new group. Since ``derive`` has access to self, it can +copy data from it to the new object. For example, if we need an exception +group subclass that has an additional error code field, we can do this: + +.. code-block:: + + class MyExceptionGroup(ExceptionGroup): + def __new__(cls, message, excs, errcode): + obj = super().__new__(cls, message, excs) + obj.errcode = errcode + return obj + + def derive(self, excs): + return MyExceptionGroup(self.message, excs, self.errcode) + + +Note that we override ``__new__`` rather than ``__init__``; this is because +``BaseExceptionGroup.__new__`` needs to inspect the constructor arguments, and +its signature is different from that of the subclass. Note also that our +``derive`` function does not copy the ``__context__``, ``__cause__`` and +``__traceback__`` fields, because ``subgroup`` and ``split`` do that for us. + +With the class defined above, we have the following: + +.. code-block:: + + >>> eg = MyExceptionGroup("eg", [TypeError(1), ValueError(2)], 42) + >>> + >>> match, rest = eg.split(ValueError) + >>> print(f'match: {match!r}: {match.errcode}') + match: MyExceptionGroup('eg', [ValueError(2)], 42): 42 + >>> print(f'rest: {rest!r}: {rest.errcode}') + rest: MyExceptionGroup('eg', [TypeError(1)], 42): 42 + >>> + +If we do not override ``derive``, then split calls the one defined +on ``BaseExceptionGroup``, which returns an instance of ``ExceptionGroup`` +if all contained exceptions are of type ``Exception``, and +``BaseExceptionGroup`` otherwise. For example: + +.. code-block:: + + >>> class MyExceptionGroup(BaseExceptionGroup): + ... pass + ... + >>> eg = MyExceptionGroup("eg", [ValueError(1), KeyboardInterrupt(2)]) + >>> match, rest = eg.split(ValueError) + >>> print(f'match: {match!r}') + match: ExceptionGroup('eg', [ValueError(1)]) + >>> print(f'rest: {rest!r}') + rest: BaseExceptionGroup('eg', [KeyboardInterrupt(2)]) + >>> + + The Traceback of an Exception Group ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~