Skip to content

Commit ae58142

Browse files
sobolevnJelleZijlstraAlexWaygood
authored
Fix typings of ExceptionGroup and BaseExceptionGroup (#9230)
Co-authored-by: Jelle Zijlstra <[email protected]> Co-authored-by: AlexWaygood <[email protected]>
1 parent a46283c commit ae58142

File tree

2 files changed

+355
-9
lines changed

2 files changed

+355
-9
lines changed

stdlib/builtins.pyi

+30-9
Original file line numberDiff line numberDiff line change
@@ -1938,25 +1938,42 @@ if sys.version_info >= (3, 11):
19381938
_ExceptionT_co = TypeVar("_ExceptionT_co", bound=Exception, covariant=True)
19391939
_ExceptionT = TypeVar("_ExceptionT", bound=Exception)
19401940

1941+
# See `check_exception_group.py` for use-cases and comments.
19411942
class BaseExceptionGroup(BaseException, Generic[_BaseExceptionT_co]):
19421943
def __new__(cls: type[Self], __message: str, __exceptions: Sequence[_BaseExceptionT_co]) -> Self: ...
19431944
@property
19441945
def message(self) -> str: ...
19451946
@property
19461947
def exceptions(self) -> tuple[_BaseExceptionT_co | BaseExceptionGroup[_BaseExceptionT_co], ...]: ...
19471948
@overload
1949+
def subgroup(
1950+
self, __condition: type[_ExceptionT] | tuple[type[_ExceptionT], ...]
1951+
) -> ExceptionGroup[_ExceptionT] | None: ...
1952+
@overload
19481953
def subgroup(
19491954
self, __condition: type[_BaseExceptionT] | tuple[type[_BaseExceptionT], ...]
19501955
) -> BaseExceptionGroup[_BaseExceptionT] | None: ...
19511956
@overload
1952-
def subgroup(self: Self, __condition: Callable[[_BaseExceptionT_co], bool]) -> Self | None: ...
1957+
def subgroup(
1958+
self: Self, __condition: Callable[[_BaseExceptionT_co | Self], bool]
1959+
) -> BaseExceptionGroup[_BaseExceptionT_co] | None: ...
19531960
@overload
19541961
def split(
1955-
self: Self, __condition: type[_BaseExceptionT] | tuple[type[_BaseExceptionT], ...]
1956-
) -> tuple[BaseExceptionGroup[_BaseExceptionT] | None, Self | None]: ...
1962+
self, __condition: type[_ExceptionT] | tuple[type[_ExceptionT], ...]
1963+
) -> tuple[ExceptionGroup[_ExceptionT] | None, BaseExceptionGroup[_BaseExceptionT_co] | None]: ...
19571964
@overload
1958-
def split(self: Self, __condition: Callable[[_BaseExceptionT_co], bool]) -> tuple[Self | None, Self | None]: ...
1959-
def derive(self: Self, __excs: Sequence[_BaseExceptionT_co]) -> Self: ...
1965+
def split(
1966+
self, __condition: type[_BaseExceptionT] | tuple[type[_BaseExceptionT], ...]
1967+
) -> tuple[BaseExceptionGroup[_BaseExceptionT] | None, BaseExceptionGroup[_BaseExceptionT_co] | None]: ...
1968+
@overload
1969+
def split(
1970+
self: Self, __condition: Callable[[_BaseExceptionT_co | Self], bool]
1971+
) -> tuple[BaseExceptionGroup[_BaseExceptionT_co] | None, BaseExceptionGroup[_BaseExceptionT_co] | None]: ...
1972+
# In reality it is `NonEmptySequence`:
1973+
@overload
1974+
def derive(self, __excs: Sequence[_ExceptionT]) -> ExceptionGroup[_ExceptionT]: ...
1975+
@overload
1976+
def derive(self, __excs: Sequence[_BaseExceptionT]) -> BaseExceptionGroup[_BaseExceptionT]: ...
19601977
def __class_getitem__(cls, __item: Any) -> GenericAlias: ...
19611978

19621979
class ExceptionGroup(BaseExceptionGroup[_ExceptionT_co], Exception):
@@ -1969,10 +1986,14 @@ if sys.version_info >= (3, 11):
19691986
self, __condition: type[_ExceptionT] | tuple[type[_ExceptionT], ...]
19701987
) -> ExceptionGroup[_ExceptionT] | None: ...
19711988
@overload
1972-
def subgroup(self: Self, __condition: Callable[[_ExceptionT_co], bool]) -> Self | None: ...
1989+
def subgroup(
1990+
self: Self, __condition: Callable[[_ExceptionT_co | Self], bool]
1991+
) -> ExceptionGroup[_ExceptionT_co] | None: ...
19731992
@overload # type: ignore[override]
19741993
def split(
1975-
self: Self, __condition: type[_ExceptionT] | tuple[type[_ExceptionT], ...]
1976-
) -> tuple[ExceptionGroup[_ExceptionT] | None, Self | None]: ...
1994+
self, __condition: type[_ExceptionT] | tuple[type[_ExceptionT], ...]
1995+
) -> tuple[ExceptionGroup[_ExceptionT] | None, ExceptionGroup[_ExceptionT_co] | None]: ...
19771996
@overload
1978-
def split(self: Self, __condition: Callable[[_ExceptionT_co], bool]) -> tuple[Self | None, Self | None]: ...
1997+
def split(
1998+
self: Self, __condition: Callable[[_ExceptionT_co | Self], bool]
1999+
) -> tuple[ExceptionGroup[_ExceptionT_co] | None, ExceptionGroup[_ExceptionT_co] | None]: ...
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,325 @@
1+
from __future__ import annotations
2+
3+
import sys
4+
from typing import TypeVar
5+
from typing_extensions import assert_type
6+
7+
if sys.version_info >= (3, 11):
8+
# This can be removed later, but right now `flake8` does not know
9+
# about these two classes:
10+
from builtins import BaseExceptionGroup, ExceptionGroup
11+
12+
# BaseExceptionGroup
13+
# ==================
14+
# `BaseExceptionGroup` can work with `BaseException`:
15+
beg = BaseExceptionGroup("x", [SystemExit(), SystemExit()])
16+
assert_type(beg, BaseExceptionGroup[SystemExit])
17+
assert_type(beg.exceptions, tuple[SystemExit | BaseExceptionGroup[SystemExit], ...])
18+
19+
# Covariance works:
20+
_beg1: BaseExceptionGroup[BaseException] = beg
21+
22+
# `BaseExceptionGroup` can work with `Exception`:
23+
beg2 = BaseExceptionGroup("x", [ValueError()])
24+
# FIXME: this is not right, runtime returns `ExceptionGroup` instance instead,
25+
# but I am unable to represent this with types right now.
26+
assert_type(beg2, BaseExceptionGroup[ValueError])
27+
28+
# .subgroup()
29+
# -----------
30+
31+
assert_type(beg.subgroup(KeyboardInterrupt), BaseExceptionGroup[KeyboardInterrupt] | None)
32+
assert_type(beg.subgroup((KeyboardInterrupt,)), BaseExceptionGroup[KeyboardInterrupt] | None)
33+
34+
def is_base_exc(exc: BaseException) -> bool:
35+
return isinstance(exc, BaseException)
36+
37+
def is_specific(exc: SystemExit | BaseExceptionGroup[SystemExit]) -> bool:
38+
return isinstance(exc, SystemExit)
39+
40+
# This one does not have `BaseExceptionGroup` part,
41+
# this is why we treat as an error.
42+
def is_system_exit(exc: SystemExit) -> bool:
43+
return isinstance(exc, SystemExit)
44+
45+
def unrelated_subgroup(exc: KeyboardInterrupt) -> bool:
46+
return False
47+
48+
assert_type(beg.subgroup(is_base_exc), BaseExceptionGroup[SystemExit] | None)
49+
assert_type(beg.subgroup(is_specific), BaseExceptionGroup[SystemExit] | None)
50+
beg.subgroup(is_system_exit) # type: ignore
51+
beg.subgroup(unrelated_subgroup) # type: ignore
52+
53+
# `Exception`` subgroup returns `ExceptionGroup`:
54+
assert_type(beg.subgroup(ValueError), ExceptionGroup[ValueError] | None)
55+
assert_type(beg.subgroup((ValueError,)), ExceptionGroup[ValueError] | None)
56+
57+
# Callable are harder, we don't support cast to `ExceptionGroup` here.
58+
# Because callables might return `True` the first time. And `BaseExceptionGroup`
59+
# will stick, no matter what arguments are.
60+
61+
def is_exception(exc: Exception) -> bool:
62+
return isinstance(exc, Exception)
63+
64+
def is_exception_or_beg(exc: Exception | BaseExceptionGroup[SystemExit]) -> bool:
65+
return isinstance(exc, Exception)
66+
67+
# This is an error because of the `Exception` argument type,
68+
# while `SystemExit` is needed instead.
69+
beg.subgroup(is_exception_or_beg) # type: ignore
70+
71+
# This is an error, because `BaseExceptionGroup` is not an `Exception`
72+
# subclass. It is required.
73+
beg.subgroup(is_exception) # type: ignore
74+
75+
# .split()
76+
# --------
77+
78+
assert_type(
79+
beg.split(KeyboardInterrupt), tuple[BaseExceptionGroup[KeyboardInterrupt] | None, BaseExceptionGroup[SystemExit] | None]
80+
)
81+
assert_type(
82+
beg.split((KeyboardInterrupt,)),
83+
tuple[BaseExceptionGroup[KeyboardInterrupt] | None, BaseExceptionGroup[SystemExit] | None],
84+
)
85+
assert_type(
86+
beg.split(ValueError), # there are no `ValueError` items in there, but anyway
87+
tuple[ExceptionGroup[ValueError] | None, BaseExceptionGroup[SystemExit] | None],
88+
)
89+
90+
excs_to_split: list[ValueError | KeyError | SystemExit] = [ValueError(), KeyError(), SystemExit()]
91+
to_split = BaseExceptionGroup("x", excs_to_split)
92+
assert_type(to_split, BaseExceptionGroup[ValueError | KeyError | SystemExit])
93+
94+
# Ideally the first part should be `ExceptionGroup[ValueError]` (done)
95+
# and the second part should be `BaseExceptionGroup[KeyError | SystemExit]`,
96+
# but we cannot substract type from a union.
97+
# We also cannot change `BaseExceptionGroup` to `ExceptionGroup` even if needed
98+
# in the second part here because of that.
99+
assert_type(
100+
to_split.split(ValueError),
101+
tuple[ExceptionGroup[ValueError] | None, BaseExceptionGroup[ValueError | KeyError | SystemExit] | None],
102+
)
103+
104+
def split_callable1(exc: ValueError | KeyError | SystemExit | BaseExceptionGroup[ValueError | KeyError | SystemExit]) -> bool:
105+
return True
106+
107+
assert_type(
108+
to_split.split(split_callable1), # Concrete type is ok
109+
tuple[
110+
BaseExceptionGroup[ValueError | KeyError | SystemExit] | None,
111+
BaseExceptionGroup[ValueError | KeyError | SystemExit] | None,
112+
],
113+
)
114+
assert_type(
115+
to_split.split(is_base_exc), # Base class is ok
116+
tuple[
117+
BaseExceptionGroup[ValueError | KeyError | SystemExit] | None,
118+
BaseExceptionGroup[ValueError | KeyError | SystemExit] | None,
119+
],
120+
)
121+
# `Exception` cannot be used: `BaseExceptionGroup` is not a subtype of it.
122+
to_split.split(is_exception) # type: ignore
123+
124+
# .derive()
125+
# ---------
126+
127+
assert_type(beg.derive([ValueError()]), ExceptionGroup[ValueError])
128+
assert_type(beg.derive([KeyboardInterrupt()]), BaseExceptionGroup[KeyboardInterrupt])
129+
130+
# ExceptionGroup
131+
# ==============
132+
133+
# `ExceptionGroup` can work with `Exception`:
134+
excs: list[ValueError | KeyError] = [ValueError(), KeyError()]
135+
eg = ExceptionGroup("x", excs)
136+
assert_type(eg, ExceptionGroup[ValueError | KeyError])
137+
assert_type(eg.exceptions, tuple[ValueError | KeyError | ExceptionGroup[ValueError | KeyError], ...])
138+
139+
# Covariance works:
140+
_eg1: ExceptionGroup[Exception] = eg
141+
142+
# `ExceptionGroup` cannot work with `BaseException`:
143+
ExceptionGroup("x", [SystemExit()]) # type: ignore
144+
145+
# .subgroup()
146+
# -----------
147+
148+
# Our decision is to ban cases like::
149+
#
150+
# >>> eg = ExceptionGroup('x', [ValueError()])
151+
# >>> eg.subgroup(BaseException)
152+
# ExceptionGroup('e', [ValueError()])
153+
#
154+
# are possible in runtime.
155+
# We do it because, it does not make sense for all other base exception types.
156+
# Supporting just `BaseException` looks like an overkill.
157+
eg.subgroup(BaseException) # type: ignore
158+
eg.subgroup((KeyboardInterrupt, SystemExit)) # type: ignore
159+
160+
assert_type(eg.subgroup(Exception), ExceptionGroup[Exception] | None)
161+
assert_type(eg.subgroup(ValueError), ExceptionGroup[ValueError] | None)
162+
assert_type(eg.subgroup((ValueError,)), ExceptionGroup[ValueError] | None)
163+
164+
def subgroup_eg1(exc: ValueError | KeyError | ExceptionGroup[ValueError | KeyError]) -> bool:
165+
return True
166+
167+
def subgroup_eg2(exc: ValueError | KeyError) -> bool:
168+
return True
169+
170+
assert_type(eg.subgroup(subgroup_eg1), ExceptionGroup[ValueError | KeyError] | None)
171+
assert_type(eg.subgroup(is_exception), ExceptionGroup[ValueError | KeyError] | None)
172+
assert_type(eg.subgroup(is_base_exc), ExceptionGroup[ValueError | KeyError] | None)
173+
assert_type(eg.subgroup(is_base_exc), ExceptionGroup[ValueError | KeyError] | None)
174+
175+
# Does not have `ExceptionGroup` part:
176+
eg.subgroup(subgroup_eg2) # type: ignore
177+
178+
# .split()
179+
# --------
180+
181+
assert_type(eg.split(TypeError), tuple[ExceptionGroup[TypeError] | None, ExceptionGroup[ValueError | KeyError] | None])
182+
assert_type(eg.split((TypeError,)), tuple[ExceptionGroup[TypeError] | None, ExceptionGroup[ValueError | KeyError] | None])
183+
assert_type(
184+
eg.split(is_exception), tuple[ExceptionGroup[ValueError | KeyError] | None, ExceptionGroup[ValueError | KeyError] | None]
185+
)
186+
assert_type(
187+
eg.split(is_base_exc),
188+
# is not converted, because `ExceptionGroup` cannot have
189+
# direct `BaseException` subclasses inside.
190+
tuple[ExceptionGroup[ValueError | KeyError] | None, ExceptionGroup[ValueError | KeyError] | None],
191+
)
192+
193+
# It does not include `ExceptionGroup` itself, so it will fail:
194+
def value_or_key_error(exc: ValueError | KeyError) -> bool:
195+
return isinstance(exc, (ValueError, KeyError))
196+
197+
eg.split(value_or_key_error) # type: ignore
198+
199+
# `ExceptionGroup` cannot have direct `BaseException` subclasses inside.
200+
eg.split(BaseException) # type: ignore
201+
eg.split((SystemExit, GeneratorExit)) # type: ignore
202+
203+
# .derive()
204+
# ---------
205+
206+
assert_type(eg.derive([ValueError()]), ExceptionGroup[ValueError])
207+
assert_type(eg.derive([KeyboardInterrupt()]), BaseExceptionGroup[KeyboardInterrupt])
208+
209+
# BaseExceptionGroup Custom Subclass
210+
# ==================================
211+
# In some cases `Self` type can be preserved in runtime,
212+
# but it is impossible to express. That's why we always fallback to
213+
# `BaseExceptionGroup` and `ExceptionGroup`.
214+
215+
_BE = TypeVar("_BE", bound=BaseException)
216+
217+
class CustomBaseGroup(BaseExceptionGroup[_BE]):
218+
...
219+
220+
cb1 = CustomBaseGroup("x", [SystemExit()])
221+
assert_type(cb1, CustomBaseGroup[SystemExit])
222+
cb2 = CustomBaseGroup("x", [ValueError()])
223+
assert_type(cb2, CustomBaseGroup[ValueError])
224+
225+
# .subgroup()
226+
# -----------
227+
228+
assert_type(cb1.subgroup(KeyboardInterrupt), BaseExceptionGroup[KeyboardInterrupt] | None)
229+
assert_type(cb2.subgroup((KeyboardInterrupt,)), BaseExceptionGroup[KeyboardInterrupt] | None)
230+
231+
assert_type(cb1.subgroup(ValueError), ExceptionGroup[ValueError] | None)
232+
assert_type(cb2.subgroup((KeyError,)), ExceptionGroup[KeyError] | None)
233+
234+
def cb_subgroup1(exc: SystemExit | CustomBaseGroup[SystemExit]) -> bool:
235+
return True
236+
237+
def cb_subgroup2(exc: ValueError | CustomBaseGroup[ValueError]) -> bool:
238+
return True
239+
240+
assert_type(cb1.subgroup(cb_subgroup1), BaseExceptionGroup[SystemExit] | None)
241+
assert_type(cb2.subgroup(cb_subgroup2), BaseExceptionGroup[ValueError] | None)
242+
cb1.subgroup(cb_subgroup2) # type: ignore
243+
cb2.subgroup(cb_subgroup1) # type: ignore
244+
245+
# .split()
246+
# --------
247+
248+
assert_type(
249+
cb1.split(KeyboardInterrupt), tuple[BaseExceptionGroup[KeyboardInterrupt] | None, BaseExceptionGroup[SystemExit] | None]
250+
)
251+
assert_type(cb1.split(TypeError), tuple[ExceptionGroup[TypeError] | None, BaseExceptionGroup[SystemExit] | None])
252+
assert_type(cb2.split((TypeError,)), tuple[ExceptionGroup[TypeError] | None, BaseExceptionGroup[ValueError] | None])
253+
254+
def cb_split1(exc: SystemExit | CustomBaseGroup[SystemExit]) -> bool:
255+
return True
256+
257+
def cb_split2(exc: ValueError | CustomBaseGroup[ValueError]) -> bool:
258+
return True
259+
260+
assert_type(cb1.split(cb_split1), tuple[BaseExceptionGroup[SystemExit] | None, BaseExceptionGroup[SystemExit] | None])
261+
assert_type(cb2.split(cb_split2), tuple[BaseExceptionGroup[ValueError] | None, BaseExceptionGroup[ValueError] | None])
262+
cb1.split(cb_split2) # type: ignore
263+
cb2.split(cb_split1) # type: ignore
264+
265+
# .derive()
266+
# ---------
267+
268+
# Note, that `Self` type is not preserved in runtime.
269+
assert_type(cb1.derive([ValueError()]), ExceptionGroup[ValueError])
270+
assert_type(cb1.derive([KeyboardInterrupt()]), BaseExceptionGroup[KeyboardInterrupt])
271+
assert_type(cb2.derive([ValueError()]), ExceptionGroup[ValueError])
272+
assert_type(cb2.derive([KeyboardInterrupt()]), BaseExceptionGroup[KeyboardInterrupt])
273+
274+
# ExceptionGroup Custom Subclass
275+
# ==============================
276+
277+
_E = TypeVar("_E", bound=Exception)
278+
279+
class CustomGroup(ExceptionGroup[_E]):
280+
...
281+
282+
CustomGroup("x", [SystemExit()]) # type: ignore
283+
cg1 = CustomGroup("x", [ValueError()])
284+
assert_type(cg1, CustomGroup[ValueError])
285+
286+
# .subgroup()
287+
# -----------
288+
289+
cg1.subgroup(BaseException) # type: ignore
290+
cg1.subgroup((KeyboardInterrupt, SystemExit)) # type: ignore
291+
292+
assert_type(cg1.subgroup(ValueError), ExceptionGroup[ValueError] | None)
293+
assert_type(cg1.subgroup((KeyError,)), ExceptionGroup[KeyError] | None)
294+
295+
def cg_subgroup1(exc: ValueError | CustomGroup[ValueError]) -> bool:
296+
return True
297+
298+
def cg_subgroup2(exc: ValueError) -> bool:
299+
return True
300+
301+
assert_type(cg1.subgroup(cg_subgroup1), ExceptionGroup[ValueError] | None)
302+
cg1.subgroup(cb_subgroup2) # type: ignore
303+
304+
# .split()
305+
# --------
306+
307+
assert_type(cg1.split(TypeError), tuple[ExceptionGroup[TypeError] | None, ExceptionGroup[ValueError] | None])
308+
assert_type(cg1.split((TypeError,)), tuple[ExceptionGroup[TypeError] | None, ExceptionGroup[ValueError] | None])
309+
cg1.split(BaseException) # type: ignore
310+
311+
def cg_split1(exc: ValueError | CustomGroup[ValueError]) -> bool:
312+
return True
313+
314+
def cg_split2(exc: ValueError) -> bool:
315+
return True
316+
317+
assert_type(cg1.split(cg_split1), tuple[ExceptionGroup[ValueError] | None, ExceptionGroup[ValueError] | None])
318+
cg1.split(cg_split2) # type: ignore
319+
320+
# .derive()
321+
# ---------
322+
323+
# Note, that `Self` type is not preserved in runtime.
324+
assert_type(cg1.derive([ValueError()]), ExceptionGroup[ValueError])
325+
assert_type(cg1.derive([KeyboardInterrupt()]), BaseExceptionGroup[KeyboardInterrupt])

0 commit comments

Comments
 (0)