PEP-654: allow subclassing of exception groups (#1872)

This commit is contained in:
Irit Katriel 2021-03-18 16:56:49 +00:00 committed by GitHub
parent 96c1a37657
commit e31aab38ed
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
1 changed files with 67 additions and 5 deletions

View File

@ -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
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~