Skip to content

Fix typings of ExceptionGroup and BaseExceptionGroup #9230

New issue

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

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

Already on GitHub? # to your account

Merged
merged 13 commits into from
Nov 23, 2022
39 changes: 30 additions & 9 deletions stdlib/builtins.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -1938,25 +1938,42 @@ if sys.version_info >= (3, 11):
_ExceptionT_co = TypeVar("_ExceptionT_co", bound=Exception, covariant=True)
_ExceptionT = TypeVar("_ExceptionT", bound=Exception)

# See `check_exception_group.py` for use-cases and comments.
class BaseExceptionGroup(BaseException, Generic[_BaseExceptionT_co]):
def __new__(cls: type[Self], __message: str, __exceptions: Sequence[_BaseExceptionT_co]) -> Self: ...
@property
def message(self) -> str: ...
@property
def exceptions(self) -> tuple[_BaseExceptionT_co | BaseExceptionGroup[_BaseExceptionT_co], ...]: ...
@overload
def subgroup(
self, __condition: type[_ExceptionT] | tuple[type[_ExceptionT], ...]
) -> ExceptionGroup[_ExceptionT] | None: ...
@overload
def subgroup(
self, __condition: type[_BaseExceptionT] | tuple[type[_BaseExceptionT], ...]
) -> BaseExceptionGroup[_BaseExceptionT] | None: ...
@overload
def subgroup(self: Self, __condition: Callable[[_BaseExceptionT_co], bool]) -> Self | None: ...
def subgroup(
self: Self, __condition: Callable[[_BaseExceptionT_co | Self], bool]
) -> BaseExceptionGroup[_BaseExceptionT_co] | None: ...
@overload
def split(
self: Self, __condition: type[_BaseExceptionT] | tuple[type[_BaseExceptionT], ...]
) -> tuple[BaseExceptionGroup[_BaseExceptionT] | None, Self | None]: ...
self, __condition: type[_ExceptionT] | tuple[type[_ExceptionT], ...]
) -> tuple[ExceptionGroup[_ExceptionT] | None, BaseExceptionGroup[_BaseExceptionT_co] | None]: ...
@overload
def split(self: Self, __condition: Callable[[_BaseExceptionT_co], bool]) -> tuple[Self | None, Self | None]: ...
def derive(self: Self, __excs: Sequence[_BaseExceptionT_co]) -> Self: ...
def split(
self, __condition: type[_BaseExceptionT] | tuple[type[_BaseExceptionT], ...]
) -> tuple[BaseExceptionGroup[_BaseExceptionT] | None, BaseExceptionGroup[_BaseExceptionT_co] | None]: ...
@overload
def split(
self: Self, __condition: Callable[[_BaseExceptionT_co | Self], bool]
) -> tuple[BaseExceptionGroup[_BaseExceptionT_co] | None, BaseExceptionGroup[_BaseExceptionT_co] | None]: ...
# In reality it is `NonEmptySequence`:
@overload
def derive(self, __excs: Sequence[_ExceptionT]) -> ExceptionGroup[_ExceptionT]: ...
@overload
def derive(self, __excs: Sequence[_BaseExceptionT]) -> BaseExceptionGroup[_BaseExceptionT]: ...
def __class_getitem__(cls, __item: Any) -> GenericAlias: ...

class ExceptionGroup(BaseExceptionGroup[_ExceptionT_co], Exception):
Expand All @@ -1969,10 +1986,14 @@ if sys.version_info >= (3, 11):
self, __condition: type[_ExceptionT] | tuple[type[_ExceptionT], ...]
) -> ExceptionGroup[_ExceptionT] | None: ...
@overload
def subgroup(self: Self, __condition: Callable[[_ExceptionT_co], bool]) -> Self | None: ...
def subgroup(
self: Self, __condition: Callable[[_ExceptionT_co | Self], bool]
) -> ExceptionGroup[_ExceptionT_co] | None: ...
@overload # type: ignore[override]
def split(
self: Self, __condition: type[_ExceptionT] | tuple[type[_ExceptionT], ...]
) -> tuple[ExceptionGroup[_ExceptionT] | None, Self | None]: ...
self, __condition: type[_ExceptionT] | tuple[type[_ExceptionT], ...]
) -> tuple[ExceptionGroup[_ExceptionT] | None, ExceptionGroup[_ExceptionT_co] | None]: ...
@overload
def split(self: Self, __condition: Callable[[_ExceptionT_co], bool]) -> tuple[Self | None, Self | None]: ...
def split(
self: Self, __condition: Callable[[_ExceptionT_co | Self], bool]
) -> tuple[ExceptionGroup[_ExceptionT_co] | None, ExceptionGroup[_ExceptionT_co] | None]: ...
325 changes: 325 additions & 0 deletions test_cases/stdlib/builtins/check_exception_group-py311.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,325 @@
from __future__ import annotations

import sys
from typing import TypeVar
from typing_extensions import assert_type

if sys.version_info >= (3, 11):
# This can be removed later, but right now `flake8` does not know
# about these two classes:
from builtins import BaseExceptionGroup, ExceptionGroup

# BaseExceptionGroup
# ==================
# `BaseExceptionGroup` can work with `BaseException`:
beg = BaseExceptionGroup("x", [SystemExit(), SystemExit()])
assert_type(beg, BaseExceptionGroup[SystemExit])
assert_type(beg.exceptions, tuple[SystemExit | BaseExceptionGroup[SystemExit], ...])

# Covariance works:
_beg1: BaseExceptionGroup[BaseException] = beg

# `BaseExceptionGroup` can work with `Exception`:
beg2 = BaseExceptionGroup("x", [ValueError()])
# FIXME: this is not right, runtime returns `ExceptionGroup` instance instead,
# but I am unable to represent this with types right now.
assert_type(beg2, BaseExceptionGroup[ValueError])

# .subgroup()
# -----------

assert_type(beg.subgroup(KeyboardInterrupt), BaseExceptionGroup[KeyboardInterrupt] | None)
assert_type(beg.subgroup((KeyboardInterrupt,)), BaseExceptionGroup[KeyboardInterrupt] | None)

def is_base_exc(exc: BaseException) -> bool:
return isinstance(exc, BaseException)

def is_specific(exc: SystemExit | BaseExceptionGroup[SystemExit]) -> bool:
return isinstance(exc, SystemExit)

# This one does not have `BaseExceptionGroup` part,
# this is why we treat as an error.
def is_system_exit(exc: SystemExit) -> bool:
return isinstance(exc, SystemExit)

def unrelated_subgroup(exc: KeyboardInterrupt) -> bool:
return False

assert_type(beg.subgroup(is_base_exc), BaseExceptionGroup[SystemExit] | None)
assert_type(beg.subgroup(is_specific), BaseExceptionGroup[SystemExit] | None)
beg.subgroup(is_system_exit) # type: ignore
beg.subgroup(unrelated_subgroup) # type: ignore

# `Exception`` subgroup returns `ExceptionGroup`:
assert_type(beg.subgroup(ValueError), ExceptionGroup[ValueError] | None)
assert_type(beg.subgroup((ValueError,)), ExceptionGroup[ValueError] | None)

# Callable are harder, we don't support cast to `ExceptionGroup` here.
# Because callables might return `True` the first time. And `BaseExceptionGroup`
# will stick, no matter what arguments are.

def is_exception(exc: Exception) -> bool:
return isinstance(exc, Exception)

def is_exception_or_beg(exc: Exception | BaseExceptionGroup[SystemExit]) -> bool:
return isinstance(exc, Exception)

# This is an error because of the `Exception` argument type,
# while `SystemExit` is needed instead.
beg.subgroup(is_exception_or_beg) # type: ignore

# This is an error, because `BaseExceptionGroup` is not an `Exception`
# subclass. It is required.
beg.subgroup(is_exception) # type: ignore

# .split()
# --------

assert_type(
beg.split(KeyboardInterrupt), tuple[BaseExceptionGroup[KeyboardInterrupt] | None, BaseExceptionGroup[SystemExit] | None]
)
assert_type(
beg.split((KeyboardInterrupt,)),
tuple[BaseExceptionGroup[KeyboardInterrupt] | None, BaseExceptionGroup[SystemExit] | None],
)
assert_type(
beg.split(ValueError), # there are no `ValueError` items in there, but anyway
tuple[ExceptionGroup[ValueError] | None, BaseExceptionGroup[SystemExit] | None],
)

excs_to_split: list[ValueError | KeyError | SystemExit] = [ValueError(), KeyError(), SystemExit()]
to_split = BaseExceptionGroup("x", excs_to_split)
assert_type(to_split, BaseExceptionGroup[ValueError | KeyError | SystemExit])

# Ideally the first part should be `ExceptionGroup[ValueError]` (done)
# and the second part should be `BaseExceptionGroup[KeyError | SystemExit]`,
# but we cannot substract type from a union.
# We also cannot change `BaseExceptionGroup` to `ExceptionGroup` even if needed
# in the second part here because of that.
assert_type(
to_split.split(ValueError),
tuple[ExceptionGroup[ValueError] | None, BaseExceptionGroup[ValueError | KeyError | SystemExit] | None],
)

def split_callable1(exc: ValueError | KeyError | SystemExit | BaseExceptionGroup[ValueError | KeyError | SystemExit]) -> bool:
return True

assert_type(
to_split.split(split_callable1), # Concrete type is ok
tuple[
BaseExceptionGroup[ValueError | KeyError | SystemExit] | None,
BaseExceptionGroup[ValueError | KeyError | SystemExit] | None,
],
)
assert_type(
to_split.split(is_base_exc), # Base class is ok
tuple[
BaseExceptionGroup[ValueError | KeyError | SystemExit] | None,
BaseExceptionGroup[ValueError | KeyError | SystemExit] | None,
],
)
# `Exception` cannot be used: `BaseExceptionGroup` is not a subtype of it.
to_split.split(is_exception) # type: ignore

# .derive()
# ---------

assert_type(beg.derive([ValueError()]), ExceptionGroup[ValueError])
assert_type(beg.derive([KeyboardInterrupt()]), BaseExceptionGroup[KeyboardInterrupt])

# ExceptionGroup
# ==============

# `ExceptionGroup` can work with `Exception`:
excs: list[ValueError | KeyError] = [ValueError(), KeyError()]
eg = ExceptionGroup("x", excs)
assert_type(eg, ExceptionGroup[ValueError | KeyError])
assert_type(eg.exceptions, tuple[ValueError | KeyError | ExceptionGroup[ValueError | KeyError], ...])

# Covariance works:
_eg1: ExceptionGroup[Exception] = eg

# `ExceptionGroup` cannot work with `BaseException`:
ExceptionGroup("x", [SystemExit()]) # type: ignore

# .subgroup()
# -----------

# Our decision is to ban cases like::
#
# >>> eg = ExceptionGroup('x', [ValueError()])
# >>> eg.subgroup(BaseException)
# ExceptionGroup('e', [ValueError()])
#
# are possible in runtime.
# We do it because, it does not make sense for all other base exception types.
# Supporting just `BaseException` looks like an overkill.
eg.subgroup(BaseException) # type: ignore
eg.subgroup((KeyboardInterrupt, SystemExit)) # type: ignore

assert_type(eg.subgroup(Exception), ExceptionGroup[Exception] | None)
assert_type(eg.subgroup(ValueError), ExceptionGroup[ValueError] | None)
assert_type(eg.subgroup((ValueError,)), ExceptionGroup[ValueError] | None)

def subgroup_eg1(exc: ValueError | KeyError | ExceptionGroup[ValueError | KeyError]) -> bool:
return True

def subgroup_eg2(exc: ValueError | KeyError) -> bool:
return True

assert_type(eg.subgroup(subgroup_eg1), ExceptionGroup[ValueError | KeyError] | None)
assert_type(eg.subgroup(is_exception), ExceptionGroup[ValueError | KeyError] | None)
assert_type(eg.subgroup(is_base_exc), ExceptionGroup[ValueError | KeyError] | None)
assert_type(eg.subgroup(is_base_exc), ExceptionGroup[ValueError | KeyError] | None)

# Does not have `ExceptionGroup` part:
eg.subgroup(subgroup_eg2) # type: ignore

# .split()
# --------

assert_type(eg.split(TypeError), tuple[ExceptionGroup[TypeError] | None, ExceptionGroup[ValueError | KeyError] | None])
assert_type(eg.split((TypeError,)), tuple[ExceptionGroup[TypeError] | None, ExceptionGroup[ValueError | KeyError] | None])
assert_type(
eg.split(is_exception), tuple[ExceptionGroup[ValueError | KeyError] | None, ExceptionGroup[ValueError | KeyError] | None]
)
assert_type(
eg.split(is_base_exc),
# is not converted, because `ExceptionGroup` cannot have
# direct `BaseException` subclasses inside.
tuple[ExceptionGroup[ValueError | KeyError] | None, ExceptionGroup[ValueError | KeyError] | None],
)

# It does not include `ExceptionGroup` itself, so it will fail:
def value_or_key_error(exc: ValueError | KeyError) -> bool:
return isinstance(exc, (ValueError, KeyError))

eg.split(value_or_key_error) # type: ignore

# `ExceptionGroup` cannot have direct `BaseException` subclasses inside.
eg.split(BaseException) # type: ignore
eg.split((SystemExit, GeneratorExit)) # type: ignore

# .derive()
# ---------

assert_type(eg.derive([ValueError()]), ExceptionGroup[ValueError])
assert_type(eg.derive([KeyboardInterrupt()]), BaseExceptionGroup[KeyboardInterrupt])

# BaseExceptionGroup Custom Subclass
# ==================================
# In some cases `Self` type can be preserved in runtime,
# but it is impossible to express. That's why we always fallback to
# `BaseExceptionGroup` and `ExceptionGroup`.

_BE = TypeVar("_BE", bound=BaseException)

class CustomBaseGroup(BaseExceptionGroup[_BE]):
...

cb1 = CustomBaseGroup("x", [SystemExit()])
assert_type(cb1, CustomBaseGroup[SystemExit])
cb2 = CustomBaseGroup("x", [ValueError()])
assert_type(cb2, CustomBaseGroup[ValueError])

# .subgroup()
# -----------

assert_type(cb1.subgroup(KeyboardInterrupt), BaseExceptionGroup[KeyboardInterrupt] | None)
assert_type(cb2.subgroup((KeyboardInterrupt,)), BaseExceptionGroup[KeyboardInterrupt] | None)

assert_type(cb1.subgroup(ValueError), ExceptionGroup[ValueError] | None)
assert_type(cb2.subgroup((KeyError,)), ExceptionGroup[KeyError] | None)

def cb_subgroup1(exc: SystemExit | CustomBaseGroup[SystemExit]) -> bool:
return True

def cb_subgroup2(exc: ValueError | CustomBaseGroup[ValueError]) -> bool:
return True

assert_type(cb1.subgroup(cb_subgroup1), BaseExceptionGroup[SystemExit] | None)
assert_type(cb2.subgroup(cb_subgroup2), BaseExceptionGroup[ValueError] | None)
cb1.subgroup(cb_subgroup2) # type: ignore
cb2.subgroup(cb_subgroup1) # type: ignore

# .split()
# --------

assert_type(
cb1.split(KeyboardInterrupt), tuple[BaseExceptionGroup[KeyboardInterrupt] | None, BaseExceptionGroup[SystemExit] | None]
)
assert_type(cb1.split(TypeError), tuple[ExceptionGroup[TypeError] | None, BaseExceptionGroup[SystemExit] | None])
assert_type(cb2.split((TypeError,)), tuple[ExceptionGroup[TypeError] | None, BaseExceptionGroup[ValueError] | None])

def cb_split1(exc: SystemExit | CustomBaseGroup[SystemExit]) -> bool:
return True

def cb_split2(exc: ValueError | CustomBaseGroup[ValueError]) -> bool:
return True

assert_type(cb1.split(cb_split1), tuple[BaseExceptionGroup[SystemExit] | None, BaseExceptionGroup[SystemExit] | None])
assert_type(cb2.split(cb_split2), tuple[BaseExceptionGroup[ValueError] | None, BaseExceptionGroup[ValueError] | None])
cb1.split(cb_split2) # type: ignore
cb2.split(cb_split1) # type: ignore

# .derive()
# ---------

# Note, that `Self` type is not preserved in runtime.
assert_type(cb1.derive([ValueError()]), ExceptionGroup[ValueError])
assert_type(cb1.derive([KeyboardInterrupt()]), BaseExceptionGroup[KeyboardInterrupt])
assert_type(cb2.derive([ValueError()]), ExceptionGroup[ValueError])
assert_type(cb2.derive([KeyboardInterrupt()]), BaseExceptionGroup[KeyboardInterrupt])

# ExceptionGroup Custom Subclass
# ==============================

_E = TypeVar("_E", bound=Exception)

class CustomGroup(ExceptionGroup[_E]):
...

CustomGroup("x", [SystemExit()]) # type: ignore
cg1 = CustomGroup("x", [ValueError()])
assert_type(cg1, CustomGroup[ValueError])

# .subgroup()
# -----------

cg1.subgroup(BaseException) # type: ignore
cg1.subgroup((KeyboardInterrupt, SystemExit)) # type: ignore

assert_type(cg1.subgroup(ValueError), ExceptionGroup[ValueError] | None)
assert_type(cg1.subgroup((KeyError,)), ExceptionGroup[KeyError] | None)

def cg_subgroup1(exc: ValueError | CustomGroup[ValueError]) -> bool:
return True

def cg_subgroup2(exc: ValueError) -> bool:
return True

assert_type(cg1.subgroup(cg_subgroup1), ExceptionGroup[ValueError] | None)
cg1.subgroup(cb_subgroup2) # type: ignore

# .split()
# --------

assert_type(cg1.split(TypeError), tuple[ExceptionGroup[TypeError] | None, ExceptionGroup[ValueError] | None])
assert_type(cg1.split((TypeError,)), tuple[ExceptionGroup[TypeError] | None, ExceptionGroup[ValueError] | None])
cg1.split(BaseException) # type: ignore

def cg_split1(exc: ValueError | CustomGroup[ValueError]) -> bool:
return True

def cg_split2(exc: ValueError) -> bool:
return True

assert_type(cg1.split(cg_split1), tuple[ExceptionGroup[ValueError] | None, ExceptionGroup[ValueError] | None])
cg1.split(cg_split2) # type: ignore

# .derive()
# ---------

# Note, that `Self` type is not preserved in runtime.
assert_type(cg1.derive([ValueError()]), ExceptionGroup[ValueError])
assert_type(cg1.derive([KeyboardInterrupt()]), BaseExceptionGroup[KeyboardInterrupt])