Skip to content

Commit e7fdfca

Browse files
committed
Add python-implemented ExceptionGroup
1 parent 2d4eec8 commit e7fdfca

File tree

5 files changed

+350
-5
lines changed

5 files changed

+350
-5
lines changed

Lib/test/test_baseexception.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,6 @@ def verify_instance_interface(self, ins):
1818
"%s missing %s attribute" %
1919
(ins.__class__.__name__, attr))
2020

21-
# TODO: RUSTPYTHON
22-
@unittest.expectedFailure
2321
def test_inheritance(self):
2422
# Make sure the inheritance hierarchy matches the documentation
2523
exc_set = set()

Lib/test/test_contextlib.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1271,8 +1271,6 @@ def test_cm_is_reentrant(self):
12711271
1/0
12721272
self.assertTrue(outer_continued)
12731273

1274-
# TODO: RUSTPYTHON
1275-
@unittest.expectedFailure
12761274
def test_exception_groups(self):
12771275
eg_ve = lambda: ExceptionGroup(
12781276
"EG with ValueErrors only",

src/interpreter.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ pub type InitHook = Box<dyn FnOnce(&mut VirtualMachine)>;
1818
/// let mut settings = Settings::default();
1919
/// settings.debug = 1;
2020
/// // You may want to add paths to `rustpython_vm::Settings::path_list` to allow import python libraries.
21+
/// settings.path_list.push("Lib".to_owned()); // add standard library directory
2122
/// settings.path_list.push("".to_owned()); // add current working directory
2223
/// let interpreter = rustpython::InterpreterConfig::new()
2324
/// .settings(settings)
Lines changed: 330 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,330 @@
1+
# Copied from https://github.com/agronholm/ExceptionGroup/blob/1.2.1/src/exceptiongroup/_exceptions.py
2+
# License: https://github.com/agronholm/exceptiongroup/blob/1.2.1/LICENSE
3+
from __future__ import annotations
4+
5+
from collections.abc import Callable, Sequence
6+
from functools import partial
7+
from typing import TYPE_CHECKING, Generic, Type, TypeVar, cast, overload
8+
9+
_BaseExceptionT_co = TypeVar("_BaseExceptionT_co", bound=BaseException, covariant=True)
10+
_BaseExceptionT = TypeVar("_BaseExceptionT", bound=BaseException)
11+
_ExceptionT_co = TypeVar("_ExceptionT_co", bound=Exception, covariant=True)
12+
_ExceptionT = TypeVar("_ExceptionT", bound=Exception)
13+
# using typing.Self would require a typing_extensions dependency on py<3.11
14+
_ExceptionGroupSelf = TypeVar("_ExceptionGroupSelf", bound="ExceptionGroup")
15+
_BaseExceptionGroupSelf = TypeVar("_BaseExceptionGroupSelf", bound="BaseExceptionGroup")
16+
17+
18+
def check_direct_subclass(
19+
exc: BaseException, parents: tuple[type[BaseException]]
20+
) -> bool:
21+
from inspect import getmro # requires rustpython-stdlib
22+
23+
for cls in getmro(exc.__class__)[:-1]:
24+
if cls in parents:
25+
return True
26+
27+
return False
28+
29+
30+
def get_condition_filter(
31+
condition: type[_BaseExceptionT]
32+
| tuple[type[_BaseExceptionT], ...]
33+
| Callable[[_BaseExceptionT_co], bool],
34+
) -> Callable[[_BaseExceptionT_co], bool]:
35+
from inspect import isclass # requires rustpython-stdlib
36+
37+
if isclass(condition) and issubclass(
38+
cast(Type[BaseException], condition), BaseException
39+
):
40+
return partial(check_direct_subclass, parents=(condition,))
41+
elif isinstance(condition, tuple):
42+
if all(isclass(x) and issubclass(x, BaseException) for x in condition):
43+
return partial(check_direct_subclass, parents=condition)
44+
elif callable(condition):
45+
return cast("Callable[[BaseException], bool]", condition)
46+
47+
raise TypeError("expected a function, exception type or tuple of exception types")
48+
49+
50+
def _derive_and_copy_attributes(self, excs):
51+
eg = self.derive(excs)
52+
eg.__cause__ = self.__cause__
53+
eg.__context__ = self.__context__
54+
eg.__traceback__ = self.__traceback__
55+
if hasattr(self, "__notes__"):
56+
# Create a new list so that add_note() only affects one exceptiongroup
57+
eg.__notes__ = list(self.__notes__)
58+
return eg
59+
60+
61+
class BaseExceptionGroup(BaseException, Generic[_BaseExceptionT_co]):
62+
"""A combination of multiple unrelated exceptions."""
63+
64+
def __new__(
65+
cls: type[_BaseExceptionGroupSelf],
66+
__message: str,
67+
__exceptions: Sequence[_BaseExceptionT_co],
68+
) -> _BaseExceptionGroupSelf:
69+
if not isinstance(__message, str):
70+
raise TypeError(f"argument 1 must be str, not {type(__message)}")
71+
if not isinstance(__exceptions, Sequence):
72+
raise TypeError("second argument (exceptions) must be a sequence")
73+
if not __exceptions:
74+
raise ValueError(
75+
"second argument (exceptions) must be a non-empty sequence"
76+
)
77+
78+
for i, exc in enumerate(__exceptions):
79+
if not isinstance(exc, BaseException):
80+
raise ValueError(
81+
f"Item {i} of second argument (exceptions) is not an exception"
82+
)
83+
84+
if cls is BaseExceptionGroup:
85+
if all(isinstance(exc, Exception) for exc in __exceptions):
86+
cls = ExceptionGroup
87+
88+
if issubclass(cls, Exception):
89+
for exc in __exceptions:
90+
if not isinstance(exc, Exception):
91+
if cls is ExceptionGroup:
92+
raise TypeError(
93+
"Cannot nest BaseExceptions in an ExceptionGroup"
94+
)
95+
else:
96+
raise TypeError(
97+
f"Cannot nest BaseExceptions in {cls.__name__!r}"
98+
)
99+
100+
instance = super().__new__(cls, __message, __exceptions)
101+
instance._message = __message
102+
instance._exceptions = __exceptions
103+
return instance
104+
105+
def add_note(self, note: str) -> None:
106+
if not isinstance(note, str):
107+
raise TypeError(
108+
f"Expected a string, got note={note!r} (type {type(note).__name__})"
109+
)
110+
111+
if not hasattr(self, "__notes__"):
112+
self.__notes__: list[str] = []
113+
114+
self.__notes__.append(note)
115+
116+
@property
117+
def message(self) -> str:
118+
return self._message
119+
120+
@property
121+
def exceptions(
122+
self,
123+
) -> tuple[_BaseExceptionT_co | BaseExceptionGroup[_BaseExceptionT_co], ...]:
124+
return tuple(self._exceptions)
125+
126+
@overload
127+
def subgroup(
128+
self, __condition: type[_ExceptionT] | tuple[type[_ExceptionT], ...]
129+
) -> ExceptionGroup[_ExceptionT] | None: ...
130+
131+
@overload
132+
def subgroup(
133+
self, __condition: type[_BaseExceptionT] | tuple[type[_BaseExceptionT], ...]
134+
) -> BaseExceptionGroup[_BaseExceptionT] | None: ...
135+
136+
@overload
137+
def subgroup(
138+
self,
139+
__condition: Callable[[_BaseExceptionT_co | _BaseExceptionGroupSelf], bool],
140+
) -> BaseExceptionGroup[_BaseExceptionT_co] | None: ...
141+
142+
def subgroup(
143+
self,
144+
__condition: type[_BaseExceptionT]
145+
| tuple[type[_BaseExceptionT], ...]
146+
| Callable[[_BaseExceptionT_co | _BaseExceptionGroupSelf], bool],
147+
) -> BaseExceptionGroup[_BaseExceptionT] | None:
148+
condition = get_condition_filter(__condition)
149+
modified = False
150+
if condition(self):
151+
return self
152+
153+
exceptions: list[BaseException] = []
154+
for exc in self.exceptions:
155+
if isinstance(exc, BaseExceptionGroup):
156+
subgroup = exc.subgroup(__condition)
157+
if subgroup is not None:
158+
exceptions.append(subgroup)
159+
160+
if subgroup is not exc:
161+
modified = True
162+
elif condition(exc):
163+
exceptions.append(exc)
164+
else:
165+
modified = True
166+
167+
if not modified:
168+
return self
169+
elif exceptions:
170+
group = _derive_and_copy_attributes(self, exceptions)
171+
return group
172+
else:
173+
return None
174+
175+
@overload
176+
def split(
177+
self, __condition: type[_ExceptionT] | tuple[type[_ExceptionT], ...]
178+
) -> tuple[
179+
ExceptionGroup[_ExceptionT] | None,
180+
BaseExceptionGroup[_BaseExceptionT_co] | None,
181+
]: ...
182+
183+
@overload
184+
def split(
185+
self, __condition: type[_BaseExceptionT] | tuple[type[_BaseExceptionT], ...]
186+
) -> tuple[
187+
BaseExceptionGroup[_BaseExceptionT] | None,
188+
BaseExceptionGroup[_BaseExceptionT_co] | None,
189+
]: ...
190+
191+
@overload
192+
def split(
193+
self,
194+
__condition: Callable[[_BaseExceptionT_co | _BaseExceptionGroupSelf], bool],
195+
) -> tuple[
196+
BaseExceptionGroup[_BaseExceptionT_co] | None,
197+
BaseExceptionGroup[_BaseExceptionT_co] | None,
198+
]: ...
199+
200+
def split(
201+
self,
202+
__condition: type[_BaseExceptionT]
203+
| tuple[type[_BaseExceptionT], ...]
204+
| Callable[[_BaseExceptionT_co], bool],
205+
) -> (
206+
tuple[
207+
ExceptionGroup[_ExceptionT] | None,
208+
BaseExceptionGroup[_BaseExceptionT_co] | None,
209+
]
210+
| tuple[
211+
BaseExceptionGroup[_BaseExceptionT] | None,
212+
BaseExceptionGroup[_BaseExceptionT_co] | None,
213+
]
214+
| tuple[
215+
BaseExceptionGroup[_BaseExceptionT_co] | None,
216+
BaseExceptionGroup[_BaseExceptionT_co] | None,
217+
]
218+
):
219+
condition = get_condition_filter(__condition)
220+
if condition(self):
221+
return self, None
222+
223+
matching_exceptions: list[BaseException] = []
224+
nonmatching_exceptions: list[BaseException] = []
225+
for exc in self.exceptions:
226+
if isinstance(exc, BaseExceptionGroup):
227+
matching, nonmatching = exc.split(condition)
228+
if matching is not None:
229+
matching_exceptions.append(matching)
230+
231+
if nonmatching is not None:
232+
nonmatching_exceptions.append(nonmatching)
233+
elif condition(exc):
234+
matching_exceptions.append(exc)
235+
else:
236+
nonmatching_exceptions.append(exc)
237+
238+
matching_group: _BaseExceptionGroupSelf | None = None
239+
if matching_exceptions:
240+
matching_group = _derive_and_copy_attributes(self, matching_exceptions)
241+
242+
nonmatching_group: _BaseExceptionGroupSelf | None = None
243+
if nonmatching_exceptions:
244+
nonmatching_group = _derive_and_copy_attributes(
245+
self, nonmatching_exceptions
246+
)
247+
248+
return matching_group, nonmatching_group
249+
250+
@overload
251+
def derive(self, __excs: Sequence[_ExceptionT]) -> ExceptionGroup[_ExceptionT]: ...
252+
253+
@overload
254+
def derive(
255+
self, __excs: Sequence[_BaseExceptionT]
256+
) -> BaseExceptionGroup[_BaseExceptionT]: ...
257+
258+
def derive(
259+
self, __excs: Sequence[_BaseExceptionT]
260+
) -> BaseExceptionGroup[_BaseExceptionT]:
261+
return BaseExceptionGroup(self.message, __excs)
262+
263+
def __str__(self) -> str:
264+
suffix = "" if len(self._exceptions) == 1 else "s"
265+
return f"{self.message} ({len(self._exceptions)} sub-exception{suffix})"
266+
267+
def __repr__(self) -> str:
268+
return f"{self.__class__.__name__}({self.message!r}, {self._exceptions!r})"
269+
270+
271+
class ExceptionGroup(BaseExceptionGroup[_ExceptionT_co], Exception):
272+
def __new__(
273+
cls: type[_ExceptionGroupSelf],
274+
__message: str,
275+
__exceptions: Sequence[_ExceptionT_co],
276+
) -> _ExceptionGroupSelf:
277+
return super().__new__(cls, __message, __exceptions)
278+
279+
if TYPE_CHECKING:
280+
281+
@property
282+
def exceptions(
283+
self,
284+
) -> tuple[_ExceptionT_co | ExceptionGroup[_ExceptionT_co], ...]: ...
285+
286+
@overload # type: ignore[override]
287+
def subgroup(
288+
self, __condition: type[_ExceptionT] | tuple[type[_ExceptionT], ...]
289+
) -> ExceptionGroup[_ExceptionT] | None: ...
290+
291+
@overload
292+
def subgroup(
293+
self, __condition: Callable[[_ExceptionT_co | _ExceptionGroupSelf], bool]
294+
) -> ExceptionGroup[_ExceptionT_co] | None: ...
295+
296+
def subgroup(
297+
self,
298+
__condition: type[_ExceptionT]
299+
| tuple[type[_ExceptionT], ...]
300+
| Callable[[_ExceptionT_co], bool],
301+
) -> ExceptionGroup[_ExceptionT] | None:
302+
return super().subgroup(__condition)
303+
304+
@overload
305+
def split(
306+
self, __condition: type[_ExceptionT] | tuple[type[_ExceptionT], ...]
307+
) -> tuple[
308+
ExceptionGroup[_ExceptionT] | None, ExceptionGroup[_ExceptionT_co] | None
309+
]: ...
310+
311+
@overload
312+
def split(
313+
self, __condition: Callable[[_ExceptionT_co | _ExceptionGroupSelf], bool]
314+
) -> tuple[
315+
ExceptionGroup[_ExceptionT_co] | None, ExceptionGroup[_ExceptionT_co] | None
316+
]: ...
317+
318+
def split(
319+
self: _ExceptionGroupSelf,
320+
__condition: type[_ExceptionT]
321+
| tuple[type[_ExceptionT], ...]
322+
| Callable[[_ExceptionT_co], bool],
323+
) -> tuple[
324+
ExceptionGroup[_ExceptionT_co] | None, ExceptionGroup[_ExceptionT_co] | None
325+
]:
326+
return super().split(__condition)
327+
328+
329+
BaseExceptionGroup.__module__ = 'builtins'
330+
ExceptionGroup.__module__ = 'builtins'

vm/src/vm/mod.rs

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -364,8 +364,11 @@ impl VirtualMachine {
364364
}
365365
}
366366

367+
let expect_stdlib =
368+
cfg!(feature = "freeze-stdlib") || !self.state.settings.path_list.is_empty();
369+
367370
#[cfg(feature = "encodings")]
368-
if cfg!(feature = "freeze-stdlib") || !self.state.settings.path_list.is_empty() {
371+
if expect_stdlib {
369372
if let Err(e) = self.import_encodings() {
370373
eprintln!(
371374
"encodings initialization failed. Only utf-8 encoding will be supported."
@@ -382,6 +385,21 @@ impl VirtualMachine {
382385
);
383386
}
384387

388+
if expect_stdlib {
389+
// enable python-implemented ExceptionGroup when stdlib exists
390+
let py_core_init = || -> PyResult<()> {
391+
let exception_group = import::import_frozen(self, "_py_exceptiongroup")?;
392+
let base_exception_group = exception_group.get_attr("BaseExceptionGroup", self)?;
393+
self.builtins
394+
.set_attr("BaseExceptionGroup", base_exception_group, self)?;
395+
let exception_group = exception_group.get_attr("ExceptionGroup", self)?;
396+
self.builtins
397+
.set_attr("ExceptionGroup", exception_group, self)?;
398+
Ok(())
399+
};
400+
self.expect_pyresult(py_core_init(), "exceptiongroup initialization failed");
401+
}
402+
385403
self.initialized = true;
386404
}
387405

0 commit comments

Comments
 (0)