diff --git a/vm/Lib/python_builtins/_py_exceptiongroup.py b/vm/Lib/python_builtins/_py_exceptiongroup.py deleted file mode 100644 index 91e9354d8a..0000000000 --- a/vm/Lib/python_builtins/_py_exceptiongroup.py +++ /dev/null @@ -1,330 +0,0 @@ -# Copied from https://github.com/agronholm/ExceptionGroup/blob/1.2.1/src/exceptiongroup/_exceptions.py -# License: https://github.com/agronholm/exceptiongroup/blob/1.2.1/LICENSE -from __future__ import annotations - -from collections.abc import Callable, Sequence -from functools import partial -from typing import TYPE_CHECKING, Generic, Type, TypeVar, cast, overload - -_BaseExceptionT_co = TypeVar("_BaseExceptionT_co", bound=BaseException, covariant=True) -_BaseExceptionT = TypeVar("_BaseExceptionT", bound=BaseException) -_ExceptionT_co = TypeVar("_ExceptionT_co", bound=Exception, covariant=True) -_ExceptionT = TypeVar("_ExceptionT", bound=Exception) -# using typing.Self would require a typing_extensions dependency on py<3.11 -_ExceptionGroupSelf = TypeVar("_ExceptionGroupSelf", bound="ExceptionGroup") -_BaseExceptionGroupSelf = TypeVar("_BaseExceptionGroupSelf", bound="BaseExceptionGroup") - - -def check_direct_subclass( - exc: BaseException, parents: tuple[type[BaseException]] -) -> bool: - from inspect import getmro # requires rustpython-stdlib - - for cls in getmro(exc.__class__)[:-1]: - if cls in parents: - return True - - return False - - -def get_condition_filter( - condition: type[_BaseExceptionT] - | tuple[type[_BaseExceptionT], ...] - | Callable[[_BaseExceptionT_co], bool], -) -> Callable[[_BaseExceptionT_co], bool]: - from inspect import isclass # requires rustpython-stdlib - - if isclass(condition) and issubclass( - cast(Type[BaseException], condition), BaseException - ): - return partial(check_direct_subclass, parents=(condition,)) - elif isinstance(condition, tuple): - if all(isclass(x) and issubclass(x, BaseException) for x in condition): - return partial(check_direct_subclass, parents=condition) - elif callable(condition): - return cast("Callable[[BaseException], bool]", condition) - - raise TypeError("expected a function, exception type or tuple of exception types") - - -def _derive_and_copy_attributes(self, excs): - eg = self.derive(excs) - eg.__cause__ = self.__cause__ - eg.__context__ = self.__context__ - eg.__traceback__ = self.__traceback__ - if hasattr(self, "__notes__"): - # Create a new list so that add_note() only affects one exceptiongroup - eg.__notes__ = list(self.__notes__) - return eg - - -class BaseExceptionGroup(BaseException, Generic[_BaseExceptionT_co]): - """A combination of multiple unrelated exceptions.""" - - def __new__( - cls: type[_BaseExceptionGroupSelf], - __message: str, - __exceptions: Sequence[_BaseExceptionT_co], - ) -> _BaseExceptionGroupSelf: - if not isinstance(__message, str): - raise TypeError(f"argument 1 must be str, not {type(__message)}") - if not isinstance(__exceptions, Sequence): - raise TypeError("second argument (exceptions) must be a sequence") - if not __exceptions: - raise ValueError( - "second argument (exceptions) must be a non-empty sequence" - ) - - for i, exc in enumerate(__exceptions): - if not isinstance(exc, BaseException): - raise ValueError( - f"Item {i} of second argument (exceptions) is not an exception" - ) - - if cls is BaseExceptionGroup: - if all(isinstance(exc, Exception) for exc in __exceptions): - cls = ExceptionGroup - - if issubclass(cls, Exception): - for exc in __exceptions: - if not isinstance(exc, Exception): - if cls is ExceptionGroup: - raise TypeError( - "Cannot nest BaseExceptions in an ExceptionGroup" - ) - else: - raise TypeError( - f"Cannot nest BaseExceptions in {cls.__name__!r}" - ) - - instance = super().__new__(cls, __message, __exceptions) - instance._message = __message - instance._exceptions = __exceptions - return instance - - def add_note(self, note: str) -> None: - if not isinstance(note, str): - raise TypeError( - f"Expected a string, got note={note!r} (type {type(note).__name__})" - ) - - if not hasattr(self, "__notes__"): - self.__notes__: list[str] = [] - - self.__notes__.append(note) - - @property - def message(self) -> str: - return self._message - - @property - def exceptions( - self, - ) -> tuple[_BaseExceptionT_co | BaseExceptionGroup[_BaseExceptionT_co], ...]: - return tuple(self._exceptions) - - @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, - __condition: Callable[[_BaseExceptionT_co | _BaseExceptionGroupSelf], bool], - ) -> BaseExceptionGroup[_BaseExceptionT_co] | None: ... - - def subgroup( - self, - __condition: type[_BaseExceptionT] - | tuple[type[_BaseExceptionT], ...] - | Callable[[_BaseExceptionT_co | _BaseExceptionGroupSelf], bool], - ) -> BaseExceptionGroup[_BaseExceptionT] | None: - condition = get_condition_filter(__condition) - modified = False - if condition(self): - return self - - exceptions: list[BaseException] = [] - for exc in self.exceptions: - if isinstance(exc, BaseExceptionGroup): - subgroup = exc.subgroup(__condition) - if subgroup is not None: - exceptions.append(subgroup) - - if subgroup is not exc: - modified = True - elif condition(exc): - exceptions.append(exc) - else: - modified = True - - if not modified: - return self - elif exceptions: - group = _derive_and_copy_attributes(self, exceptions) - return group - else: - return None - - @overload - def split( - self, __condition: type[_ExceptionT] | tuple[type[_ExceptionT], ...] - ) -> tuple[ - ExceptionGroup[_ExceptionT] | None, - BaseExceptionGroup[_BaseExceptionT_co] | None, - ]: ... - - @overload - def split( - self, __condition: type[_BaseExceptionT] | tuple[type[_BaseExceptionT], ...] - ) -> tuple[ - BaseExceptionGroup[_BaseExceptionT] | None, - BaseExceptionGroup[_BaseExceptionT_co] | None, - ]: ... - - @overload - def split( - self, - __condition: Callable[[_BaseExceptionT_co | _BaseExceptionGroupSelf], bool], - ) -> tuple[ - BaseExceptionGroup[_BaseExceptionT_co] | None, - BaseExceptionGroup[_BaseExceptionT_co] | None, - ]: ... - - def split( - self, - __condition: type[_BaseExceptionT] - | tuple[type[_BaseExceptionT], ...] - | Callable[[_BaseExceptionT_co], bool], - ) -> ( - tuple[ - ExceptionGroup[_ExceptionT] | None, - BaseExceptionGroup[_BaseExceptionT_co] | None, - ] - | tuple[ - BaseExceptionGroup[_BaseExceptionT] | None, - BaseExceptionGroup[_BaseExceptionT_co] | None, - ] - | tuple[ - BaseExceptionGroup[_BaseExceptionT_co] | None, - BaseExceptionGroup[_BaseExceptionT_co] | None, - ] - ): - condition = get_condition_filter(__condition) - if condition(self): - return self, None - - matching_exceptions: list[BaseException] = [] - nonmatching_exceptions: list[BaseException] = [] - for exc in self.exceptions: - if isinstance(exc, BaseExceptionGroup): - matching, nonmatching = exc.split(condition) - if matching is not None: - matching_exceptions.append(matching) - - if nonmatching is not None: - nonmatching_exceptions.append(nonmatching) - elif condition(exc): - matching_exceptions.append(exc) - else: - nonmatching_exceptions.append(exc) - - matching_group: _BaseExceptionGroupSelf | None = None - if matching_exceptions: - matching_group = _derive_and_copy_attributes(self, matching_exceptions) - - nonmatching_group: _BaseExceptionGroupSelf | None = None - if nonmatching_exceptions: - nonmatching_group = _derive_and_copy_attributes( - self, nonmatching_exceptions - ) - - return matching_group, nonmatching_group - - @overload - def derive(self, __excs: Sequence[_ExceptionT]) -> ExceptionGroup[_ExceptionT]: ... - - @overload - def derive( - self, __excs: Sequence[_BaseExceptionT] - ) -> BaseExceptionGroup[_BaseExceptionT]: ... - - def derive( - self, __excs: Sequence[_BaseExceptionT] - ) -> BaseExceptionGroup[_BaseExceptionT]: - return BaseExceptionGroup(self.message, __excs) - - def __str__(self) -> str: - suffix = "" if len(self._exceptions) == 1 else "s" - return f"{self.message} ({len(self._exceptions)} sub-exception{suffix})" - - def __repr__(self) -> str: - return f"{self.__class__.__name__}({self.message!r}, {self._exceptions!r})" - - -class ExceptionGroup(BaseExceptionGroup[_ExceptionT_co], Exception): - def __new__( - cls: type[_ExceptionGroupSelf], - __message: str, - __exceptions: Sequence[_ExceptionT_co], - ) -> _ExceptionGroupSelf: - return super().__new__(cls, __message, __exceptions) - - if TYPE_CHECKING: - - @property - def exceptions( - self, - ) -> tuple[_ExceptionT_co | ExceptionGroup[_ExceptionT_co], ...]: ... - - @overload # type: ignore[override] - def subgroup( - self, __condition: type[_ExceptionT] | tuple[type[_ExceptionT], ...] - ) -> ExceptionGroup[_ExceptionT] | None: ... - - @overload - def subgroup( - self, __condition: Callable[[_ExceptionT_co | _ExceptionGroupSelf], bool] - ) -> ExceptionGroup[_ExceptionT_co] | None: ... - - def subgroup( - self, - __condition: type[_ExceptionT] - | tuple[type[_ExceptionT], ...] - | Callable[[_ExceptionT_co], bool], - ) -> ExceptionGroup[_ExceptionT] | None: - return super().subgroup(__condition) - - @overload - def split( - self, __condition: type[_ExceptionT] | tuple[type[_ExceptionT], ...] - ) -> tuple[ - ExceptionGroup[_ExceptionT] | None, ExceptionGroup[_ExceptionT_co] | None - ]: ... - - @overload - def split( - self, __condition: Callable[[_ExceptionT_co | _ExceptionGroupSelf], bool] - ) -> tuple[ - ExceptionGroup[_ExceptionT_co] | None, ExceptionGroup[_ExceptionT_co] | None - ]: ... - - def split( - self: _ExceptionGroupSelf, - __condition: type[_ExceptionT] - | tuple[type[_ExceptionT], ...] - | Callable[[_ExceptionT_co], bool], - ) -> tuple[ - ExceptionGroup[_ExceptionT_co] | None, ExceptionGroup[_ExceptionT_co] | None - ]: - return super().split(__condition) - - -BaseExceptionGroup.__module__ = 'builtins' -ExceptionGroup.__module__ = 'builtins' diff --git a/vm/src/exceptions.rs b/vm/src/exceptions.rs index 708a93fe61..530234c389 100644 --- a/vm/src/exceptions.rs +++ b/vm/src/exceptions.rs @@ -43,6 +43,9 @@ impl PyPayload for PyBaseException { } } +const TRACEBACK_LIMIT: usize = 1000; +const TRACEBACK_RECURSIVE_CUTOFF: usize = 3; // Also hardcoded in traceback.py. + impl VirtualMachine { // Why `impl VirtualMachine`? // These functions are natively free function in CPython - not methods of PyException @@ -131,10 +134,40 @@ impl VirtualMachine { exc: &PyBaseExceptionRef, ) -> Result<(), W::Error> { let vm = self; + + // TODO: Get tracebacklimit from sys and replace limit with that if it exists + let limit = TRACEBACK_LIMIT; if let Some(tb) = exc.traceback.read().clone() { writeln!(output, "Traceback (most recent call last):")?; + let mut tb_list = vec![]; for tb in tb.iter() { - write_traceback_entry(output, &tb)?; + tb_list.push(tb); + } + let mut repeat_counter = 0; + let mut previous_file = "".to_string(); + let mut previous_line = 0; + let mut previous_name = "".to_string(); + // Gets the last `limit` traceback entries + for tb in tb_list.into_iter().rev().take(limit).rev() { + if previous_file != tb.frame.code.source_path.as_str() + || previous_line != tb.lineno.get() + || previous_name != tb.frame.code.obj_name.as_str() + { + if repeat_counter > TRACEBACK_RECURSIVE_CUTOFF { + write_repeat_traceback_entry(output, repeat_counter)?; + } + previous_file = tb.frame.code.source_path.as_str().to_string(); + previous_line = tb.lineno.get(); + previous_name = tb.frame.code.obj_name.as_str().to_string(); + repeat_counter = 0; + } + repeat_counter += 1; + if repeat_counter <= TRACEBACK_RECURSIVE_CUTOFF { + write_traceback_entry(output, &tb)?; + } + } + if repeat_counter > TRACEBACK_RECURSIVE_CUTOFF { + write_repeat_traceback_entry(output, repeat_counter)?; } } @@ -211,9 +244,9 @@ impl VirtualMachine { if let Some(text) = maybe_text { // if text ends with \n, remove it - let rtext = text.as_str().trim_end_matches('\n'); - let l_text = rtext.trim_start_matches([' ', '\n', '\x0c']); // \x0c is \f - let spaces = (rtext.len() - l_text.len()) as isize; + let r_text = text.as_str().trim_end_matches('\n'); + let l_text = r_text.trim_start_matches([' ', '\n', '\x0c']); // \x0c is \f + let spaces = (r_text.len() - l_text.len()) as isize; writeln!(output, " {}", l_text)?; @@ -374,15 +407,26 @@ fn write_traceback_entry( writeln!( output, r##" File "{}", line {}, in {}"##, - filename.trim_start_matches(r"\\?\"), - tb_entry.lineno, - tb_entry.frame.code.obj_name + filename, tb_entry.lineno, tb_entry.frame.code.obj_name )?; print_source_line(output, filename, tb_entry.lineno.get())?; Ok(()) } +fn write_repeat_traceback_entry( + output: &mut W, + repeat_counter: usize, +) -> Result<(), W::Error> { + let count = repeat_counter - TRACEBACK_RECURSIVE_CUTOFF; + writeln!( + output, + r##" [Previous line repeated {} more time{}]"##, + count, + if count == 1 { "" } else { "s" } + ) +} + #[derive(Clone)] pub enum ExceptionCtor { Class(PyTypeRef), @@ -1158,7 +1202,6 @@ pub(crate) fn errno_to_exc_type(_errno: i32, _vm: &VirtualMachine) -> Option<&'s } pub(super) mod types { - use crate::common::lock::PyRwLock; #[cfg_attr(target_arch = "wasm32", allow(unused_imports))] use crate::{ AsObject, PyObjectRef, PyRef, PyResult, VirtualMachine, @@ -1169,15 +1212,16 @@ pub(super) mod types { function::{ArgBytesLike, FuncArgs}, types::{Constructor, Initializer}, }; + use crate::{PyPayload, builtins::PyListRef, common::lock::PyRwLock, convert::IntoObject}; use crossbeam_utils::atomic::AtomicCell; use itertools::Itertools; use rustpython_common::str::UnicodeEscapeCodepoint; // This module is designed to be used as `use builtins::*;`. // Do not add any pub symbols not included in builtins module. - // `PyBaseExceptionRef` is the only exception. pub type PyBaseExceptionRef = PyRef; + pub type PyBaseExceptionGroupRef = PyRef; // Sorted By Hierarchy then alphabetized. @@ -1194,13 +1238,238 @@ pub(super) mod types { #[derive(Debug)] pub struct PySystemExit {} - #[pyexception(name, base = "PyBaseException", ctx = "base_exception_group", impl)] + #[pyexception(name, base = "PyBaseException", ctx = "base_exception_group")] #[derive(Debug)] - pub struct PyBaseExceptionGroup {} + #[allow(unused, dead_code)] + pub struct PyBaseExceptionGroup { + pub(super) traceback: PyRwLock>, + pub(super) cause: PyRwLock>>, + pub(super) context: PyRwLock>>, + pub(super) suppress_context: AtomicCell, + pub(super) args: PyRwLock, + pub(super) message: PyRwLock, + pub(super) exceptions: PyRwLock>, + pub(super) notes: PyRwLock>, + } - #[pyexception(name, base = "PyBaseExceptionGroup", ctx = "exception_group", impl)] + #[pyexception] + impl PyBaseExceptionGroup { + #[pyslot] + fn slot_new(cls: PyTypeRef, args: FuncArgs, vm: &VirtualMachine) -> PyResult { + let (message, exceptions): (PyStrRef, PyObjectRef) = args.bind(vm)?; + let exceptions = exceptions.to_sequence(); + let len = exceptions.length(vm)?; + if len == 0 { + return Err(vm.new_type_error( + "BaseExceptionGroup() requires at least one exception".to_owned(), + )); + } + for i in 0..len { + let item = exceptions.get_item(i as isize, vm)?; + if !item.is_instance(PyBaseException::class(&vm.ctx).into(), vm)? { + return Err(vm.new_value_error(format!( + "Item {i} of second argument (exceptions) is not an exception" + ))); + } + } + + let is_subclass = !(cls.is(PyBaseExceptionGroup::class(&vm.ctx)) + || cls.is(PyExceptionGroup::class(&vm.ctx))); + for i in 0..len { + let item = exceptions.get_item(i as isize, vm)?; + if item.is_instance(PyBaseExceptionGroup::class(&vm.ctx).into(), vm)? { + if is_subclass { + return Err(vm.new_type_error(format!( + "Cannot nest BaseExceptions in {}", + cls.name() + ))); + } else { + return Err(vm.new_type_error( + "Cannot nest BaseExceptions in an ExceptionGroup".to_owned(), + )); + } + } + } + let mut exceptions_vec = Vec::with_capacity(len); + for i in 0..len { + let item = exceptions.get_item(i as isize, vm)?; + exceptions_vec.push(item.clone()); + } + let mut args = Vec::with_capacity(1 + len); + args.push(message.clone().into_object()); + for i in 0..len { + let item = exceptions.get_item(i as isize, vm)?; + args.push(item.clone()); + } + Ok(PyBaseExceptionGroup { + traceback: PyRwLock::new(None), + cause: PyRwLock::new(None), + context: PyRwLock::new(None), + suppress_context: AtomicCell::new(false), + args: PyRwLock::new(vm.ctx.new_tuple(args)), + message: PyRwLock::new(message), + exceptions: PyRwLock::new(exceptions_vec), + notes: PyRwLock::new(None), + } + .into_pyobject(vm)) + } + + #[pygetset] + fn message(&self) -> PyStrRef { + self.message.read().clone() + } + #[pygetset] + fn exceptions(&self, vm: &VirtualMachine) -> PyTupleRef { + let exceptions = self.exceptions.read(); + vm.ctx.new_tuple(exceptions.clone()) + } + + #[pymethod] + fn add_note(&self, note: PyStrRef, vm: &VirtualMachine) -> PyResult<()> { + let mut notes = self.notes.write(); + if notes.is_none() { + *notes = Some(vm.ctx.new_list(vec![])); + } + notes.as_mut().unwrap().append(note.into()); + Ok(()) + } + + #[pymethod(magic)] + fn str(&self, vm: &VirtualMachine) -> PyStrRef { + let msg = self.message.read(); + let num_excs = self.exceptions.read().len(); + let s = format!( + "{msg} ({num_excs} sub-exception{p})", + p = if num_excs == 1 { "" } else { "s" } + ); + vm.ctx.new_str(s) + } + + #[pymethod(magic)] + fn repr(&self, vm: &VirtualMachine) -> PyStrRef { + let msg = self.message.read(); + let num_excs = self.exceptions.read().len(); + // TODO: repr of message + let s = format!("{}({msg}, {num_excs})", Self::class(&vm.ctx).name()); + vm.ctx.new_str(s) + } + } + + #[pyexception(name, base = "PyBaseExceptionGroup", ctx = "exception_group")] #[derive(Debug)] - pub struct PyExceptionGroup {} + pub struct PyExceptionGroup { + pub(super) traceback: PyRwLock>, + pub(super) cause: PyRwLock>>, + pub(super) context: PyRwLock>>, + pub(super) suppress_context: AtomicCell, + pub(super) args: PyRwLock, + pub(super) message: PyRwLock, + pub(super) exceptions: PyRwLock>, + pub(super) notes: PyRwLock>, + } + + #[pyexception] + impl PyExceptionGroup { + #[pyslot] + fn slot_new(cls: PyTypeRef, args: FuncArgs, vm: &VirtualMachine) -> PyResult { + let (message, exceptions): (PyStrRef, PyObjectRef) = args.bind(vm)?; + let exceptions = exceptions.to_sequence(); + let len = exceptions.length(vm)?; + if len == 0 { + return Err(vm.new_type_error( + "ExceptionGroup() requires at least one exception".to_owned(), + )); + } + for i in 0..len { + let item = exceptions.get_item(i as isize, vm)?; + if !item.is_instance(PyBaseException::class(&vm.ctx).into(), vm)? { + return Err(vm.new_value_error(format!( + "Item {i} of second argument (exceptions) is not an exception" + ))); + } + } + + let is_subclass = !(cls.is(PyBaseExceptionGroup::class(&vm.ctx)) + || cls.is(PyExceptionGroup::class(&vm.ctx))); + for i in 0..len { + let item = exceptions.get_item(i as isize, vm)?; + if item.is_instance(PyBaseExceptionGroup::class(&vm.ctx).into(), vm)? { + if is_subclass { + return Err(vm.new_type_error(format!( + "Cannot nest BaseExceptions in {}", + cls.name() + ))); + } else { + return Err(vm.new_type_error( + "Cannot nest BaseExceptions in an ExceptionGroup".to_owned(), + )); + } + } + } + let mut exceptions_vec = Vec::with_capacity(len); + for i in 0..len { + let item = exceptions.get_item(i as isize, vm)?; + exceptions_vec.push(item.clone()); + } + let mut args = Vec::with_capacity(1 + len); + args.push(message.clone().into_object()); + for i in 0..len { + let item = exceptions.get_item(i as isize, vm)?; + args.push(item.clone()); + } + Ok(Self { + traceback: PyRwLock::new(None), + cause: PyRwLock::new(None), + context: PyRwLock::new(None), + suppress_context: AtomicCell::new(false), + args: PyRwLock::new(vm.ctx.new_tuple(args)), + message: PyRwLock::new(message), + exceptions: PyRwLock::new(exceptions_vec), + notes: PyRwLock::new(None), + } + .into_pyobject(vm)) + } + + #[pygetset] + fn message(&self) -> PyStrRef { + self.message.read().clone() + } + #[pygetset] + fn exceptions(&self, vm: &VirtualMachine) -> PyTupleRef { + let exceptions = self.exceptions.read(); + vm.ctx.new_tuple(exceptions.clone()) + } + + #[pymethod] + fn add_note(&self, note: PyStrRef, vm: &VirtualMachine) -> PyResult<()> { + let mut notes = self.notes.write(); + if notes.is_none() { + *notes = Some(vm.ctx.new_list(vec![])); + } + notes.as_mut().unwrap().append(note.into()); + Ok(()) + } + + #[pymethod(magic)] + fn str(&self, vm: &VirtualMachine) -> PyStrRef { + let msg = self.message.read(); + let num_excs = self.exceptions.read().len(); + let s = format!( + "{msg} ({num_excs} sub-exception{p})", + p = if num_excs == 1 { "" } else { "s" } + ); + vm.ctx.new_str(s) + } + + #[pymethod(magic)] + fn repr(&self, vm: &VirtualMachine) -> PyStrRef { + let msg = self.message.read(); + let num_excs = self.exceptions.read().len(); + // TODO: repr of message + let s = format!("{}({msg}, {num_excs})", Self::class(&vm.ctx).name()); + vm.ctx.new_str(s) + } + } #[pyexception(name, base = "PyBaseException", ctx = "generator_exit", impl)] #[derive(Debug)] diff --git a/vm/src/vm/mod.rs b/vm/src/vm/mod.rs index 7baaae7770..cbda0f6050 100644 --- a/vm/src/vm/mod.rs +++ b/vm/src/vm/mod.rs @@ -386,21 +386,6 @@ impl VirtualMachine { ); } - if expect_stdlib { - // enable python-implemented ExceptionGroup when stdlib exists - let py_core_init = || -> PyResult<()> { - let exception_group = import::import_frozen(self, "_py_exceptiongroup")?; - let base_exception_group = exception_group.get_attr("BaseExceptionGroup", self)?; - self.builtins - .set_attr("BaseExceptionGroup", base_exception_group, self)?; - let exception_group = exception_group.get_attr("ExceptionGroup", self)?; - self.builtins - .set_attr("ExceptionGroup", exception_group, self)?; - Ok(()) - }; - self.expect_pyresult(py_core_init(), "exceptiongroup initialization failed"); - } - self.initialized = true; }