From 6ebf5d07c401265c141409859f28659d6ba3fdaa Mon Sep 17 00:00:00 2001 From: Jeong YunWon Date: Tue, 9 Dec 2025 02:39:23 +0900 Subject: [PATCH 1/3] native exception group --- Lib/test/test_exception_group.py | 6 - Lib/test/test_socket.py | 2 - crates/vm/src/exception_group.rs | 469 +++++++++++++++++++++++++++++++ crates/vm/src/exceptions.rs | 35 +-- crates/vm/src/lib.rs | 1 + crates/vm/src/stdlib/builtins.rs | 5 +- crates/vm/src/vm/mod.rs | 20 +- 7 files changed, 486 insertions(+), 52 deletions(-) create mode 100644 crates/vm/src/exception_group.rs diff --git a/Lib/test/test_exception_group.py b/Lib/test/test_exception_group.py index ffe4dc4f35..2b48530a30 100644 --- a/Lib/test/test_exception_group.py +++ b/Lib/test/test_exception_group.py @@ -15,8 +15,6 @@ def test_exception_is_not_generic_type(self): with self.assertRaisesRegex(TypeError, 'Exception'): Exception[OSError] - # TODO: RUSTPYTHON - @unittest.expectedFailure def test_exception_group_is_generic_type(self): E = OSError self.assertIsInstance(ExceptionGroup[E], types.GenericAlias) @@ -810,8 +808,6 @@ def test_split_copies_notes(self): self.assertEqual(match.__notes__, orig_notes + ["match"]) self.assertEqual(rest.__notes__, orig_notes + ["rest"]) - # TODO: RUSTPYTHON - @unittest.expectedFailure def test_split_does_not_copy_non_sequence_notes(self): # __notes__ should be a sequence, which is shallow copied. # If it is not a sequence, the split parts don't get any notes. @@ -821,8 +817,6 @@ def test_split_does_not_copy_non_sequence_notes(self): self.assertFalse(hasattr(match, '__notes__')) self.assertFalse(hasattr(rest, '__notes__')) - # TODO: RUSTPYTHON - @unittest.expectedFailure def test_drive_invalid_return_value(self): class MyEg(ExceptionGroup): def derive(self, excs): diff --git a/Lib/test/test_socket.py b/Lib/test/test_socket.py index ea544f6afa..5ccbfa7ff8 100644 --- a/Lib/test/test_socket.py +++ b/Lib/test/test_socket.py @@ -5360,8 +5360,6 @@ def test_create_connection(self): expected_errnos = socket_helper.get_socket_conn_refused_errs() self.assertIn(cm.exception.errno, expected_errnos) - # TODO: RUSTPYTHON - @unittest.expectedFailure def test_create_connection_all_errors(self): port = socket_helper.find_unused_port() try: diff --git a/crates/vm/src/exception_group.rs b/crates/vm/src/exception_group.rs new file mode 100644 index 0000000000..8d49e486cc --- /dev/null +++ b/crates/vm/src/exception_group.rs @@ -0,0 +1,469 @@ +//! ExceptionGroup implementation for Python 3.11+ +//! +//! This module implements BaseExceptionGroup and ExceptionGroup with multiple inheritance support. + +use crate::builtins::{PyList, PyStrRef, PyTuple, PyTupleRef, PyType, PyTypeRef}; +use crate::function::{ArgIterable, FuncArgs}; +use crate::types::{PyTypeFlags, PyTypeSlots}; +use crate::{ + AsObject, Context, Py, PyObject, PyObjectRef, PyRef, PyResult, TryFromObject, VirtualMachine, +}; + +use crate::exceptions::types::PyBaseException; + +/// Create dynamic ExceptionGroup type with multiple inheritance +fn create_exception_group(ctx: &Context) -> PyRef { + let excs = &ctx.exceptions; + let exception_group_slots = PyTypeSlots { + flags: PyTypeFlags::heap_type_flags() | PyTypeFlags::HAS_DICT, + ..Default::default() + }; + PyType::new_heap( + "ExceptionGroup", + vec![ + excs.base_exception_group.to_owned(), + excs.exception_type.to_owned(), + ], + Default::default(), + exception_group_slots, + ctx.types.type_type.to_owned(), + ctx, + ) + .expect("Failed to create ExceptionGroup type with multiple inheritance") +} + +pub fn exception_group() -> &'static Py { + ::rustpython_vm::common::static_cell! { + static CELL: ::rustpython_vm::builtins::PyTypeRef; + } + CELL.get_or_init(|| create_exception_group(Context::genesis())) +} + +pub(super) mod types { + use super::*; + use crate::PyPayload; + use crate::builtins::PyGenericAlias; + + #[pyexception(name, base = PyBaseException, ctx = "base_exception_group")] + #[derive(Debug)] + pub struct PyBaseExceptionGroup {} + + #[pyexception] + impl PyBaseExceptionGroup { + #[pyclassmethod] + fn __class_getitem__( + cls: PyTypeRef, + args: PyObjectRef, + vm: &VirtualMachine, + ) -> PyGenericAlias { + PyGenericAlias::from_args(cls, args, vm) + } + + #[pyslot] + fn slot_new(cls: PyTypeRef, args: FuncArgs, vm: &VirtualMachine) -> PyResult { + // Validate exactly 2 positional arguments + if args.args.len() != 2 { + return Err(vm.new_type_error(format!( + "BaseExceptionGroup.__new__() takes exactly 2 positional arguments ({} given)", + args.args.len() + ))); + } + + // Validate message is str + let message = args.args[0].clone(); + if !message.fast_isinstance(vm.ctx.types.str_type) { + return Err(vm.new_type_error(format!( + "argument 1 must be str, not {}", + message.class().name() + ))); + } + + // Validate exceptions is a sequence (not set or None) + let exceptions_arg = &args.args[1]; + + // Check for set/frozenset (not a sequence - unordered) + if exceptions_arg.fast_isinstance(vm.ctx.types.set_type) + || exceptions_arg.fast_isinstance(vm.ctx.types.frozenset_type) + { + return Err(vm.new_type_error("second argument (exceptions) must be a sequence")); + } + + // Check for None + if exceptions_arg.is(&vm.ctx.none) { + return Err(vm.new_type_error("second argument (exceptions) must be a sequence")); + } + + let exceptions: Vec = exceptions_arg.try_to_value(vm).map_err(|_| { + vm.new_type_error("second argument (exceptions) must be a sequence") + })?; + + // Validate non-empty + if exceptions.is_empty() { + return Err(vm.new_value_error( + "second argument (exceptions) must be a non-empty sequence".to_owned(), + )); + } + + // Validate all items are BaseException instances + let mut has_non_exception = false; + for (i, exc) in exceptions.iter().enumerate() { + if !exc.fast_isinstance(vm.ctx.exceptions.base_exception_type) { + return Err(vm.new_value_error(format!( + "Item {} of second argument (exceptions) is not an exception", + i + ))); + } + // Check if any exception is not an Exception subclass + // With dynamic ExceptionGroup (inherits from both BaseExceptionGroup and Exception), + // ExceptionGroup instances are automatically instances of Exception + if !exc.fast_isinstance(vm.ctx.exceptions.exception_type) { + has_non_exception = true; + } + } + + // Get the dynamic ExceptionGroup type + let exception_group_type = crate::exception_group::exception_group(); + + // Determine the actual class to use + let actual_cls = if cls.is(exception_group_type) { + // ExceptionGroup cannot contain BaseExceptions that are not Exception + if has_non_exception { + return Err( + vm.new_type_error("Cannot nest BaseExceptions in an ExceptionGroup") + ); + } + cls + } else if cls.is(vm.ctx.exceptions.base_exception_group) { + // Auto-convert to ExceptionGroup if all are Exception subclasses + if !has_non_exception { + exception_group_type.to_owned() + } else { + cls + } + } else { + // User-defined subclass + if has_non_exception && cls.fast_issubclass(vm.ctx.exceptions.exception_type) { + return Err(vm.new_type_error(format!( + "Cannot nest BaseExceptions in '{}'", + cls.name() + ))); + } + cls + }; + + // Create the exception with (message, exceptions_tuple) as args + let exceptions_tuple = vm.ctx.new_tuple(exceptions); + let init_args = vec![message, exceptions_tuple.into()]; + PyBaseException::new(init_args, vm) + .into_ref_with_type(vm, actual_cls) + .map(Into::into) + } + + #[pyslot] + #[pymethod(name = "__init__")] + fn slot_init(_zelf: PyObjectRef, _args: FuncArgs, _vm: &VirtualMachine) -> PyResult<()> { + // CPython's BaseExceptionGroup.__init__ just calls BaseException.__init__ + // which stores args as-is. Since __new__ already set up the correct args + // (message, exceptions_tuple), we don't need to do anything here. + // This also allows subclasses to pass extra arguments to __new__ without + // __init__ complaining about argument count. + Ok(()) + } + + #[pymethod] + fn derive( + zelf: PyRef, + excs: PyObjectRef, + vm: &VirtualMachine, + ) -> PyResult { + let message = zelf.get_arg(0).unwrap_or_else(|| vm.ctx.new_str("").into()); + vm.invoke_exception( + vm.ctx.exceptions.base_exception_group.to_owned(), + vec![message, excs], + ) + .map(|e| e.into()) + } + + #[pymethod] + fn subgroup( + zelf: PyRef, + condition: PyObjectRef, + vm: &VirtualMachine, + ) -> PyResult { + let matcher = get_condition_matcher(&condition, vm)?; + + // If self matches the condition entirely, return self + let zelf_obj: PyObjectRef = zelf.clone().into(); + if matcher.check(&zelf_obj, vm)? { + return Ok(zelf_obj); + } + + let exceptions = get_exceptions_tuple(&zelf, vm)?; + let mut matching: Vec = Vec::new(); + let mut modified = false; + + for exc in exceptions { + if is_base_exception_group(&exc, vm) { + // Recursive call for nested groups + let subgroup_result = vm.call_method(&exc, "subgroup", (condition.clone(),))?; + if !vm.is_none(&subgroup_result) { + matching.push(subgroup_result.clone()); + } + if !subgroup_result.is(&exc) { + modified = true; + } + } else if matcher.check(&exc, vm)? { + matching.push(exc); + } else { + modified = true; + } + } + + if !modified { + return Ok(zelf.clone().into()); + } + + if matching.is_empty() { + return Ok(vm.ctx.none()); + } + + // Create new group with matching exceptions and copy metadata + derive_and_copy_attributes(&zelf, matching, vm) + } + + #[pymethod] + fn split( + zelf: PyRef, + condition: PyObjectRef, + vm: &VirtualMachine, + ) -> PyResult { + let matcher = get_condition_matcher(&condition, vm)?; + + // If self matches the condition entirely + let zelf_obj: PyObjectRef = zelf.clone().into(); + if matcher.check(&zelf_obj, vm)? { + return Ok(vm.ctx.new_tuple(vec![zelf_obj, vm.ctx.none()])); + } + + let exceptions = get_exceptions_tuple(&zelf, vm)?; + let mut matching: Vec = Vec::new(); + let mut rest: Vec = Vec::new(); + + for exc in exceptions { + if is_base_exception_group(&exc, vm) { + let result = vm.call_method(&exc, "split", (condition.clone(),))?; + let result_tuple: PyTupleRef = result.try_into_value(vm)?; + let match_part = result_tuple + .first() + .cloned() + .unwrap_or_else(|| vm.ctx.none()); + let rest_part = result_tuple + .get(1) + .cloned() + .unwrap_or_else(|| vm.ctx.none()); + + if !vm.is_none(&match_part) { + matching.push(match_part); + } + if !vm.is_none(&rest_part) { + rest.push(rest_part); + } + } else if matcher.check(&exc, vm)? { + matching.push(exc); + } else { + rest.push(exc); + } + } + + let match_group = if matching.is_empty() { + vm.ctx.none() + } else { + derive_and_copy_attributes(&zelf, matching, vm)? + }; + + let rest_group = if rest.is_empty() { + vm.ctx.none() + } else { + derive_and_copy_attributes(&zelf, rest, vm)? + }; + + Ok(vm.ctx.new_tuple(vec![match_group, rest_group])) + } + + #[pymethod(name = "__str__")] + fn str(zelf: PyRef, vm: &VirtualMachine) -> PyResult { + let message = zelf + .get_arg(0) + .map(|m| m.str(vm)) + .transpose()? + .map(|s| s.as_str().to_owned()) + .unwrap_or_default(); + + let num_excs = zelf + .get_arg(1) + .and_then(|obj| obj.downcast_ref::().map(|t| t.len())) + .unwrap_or(0); + + let suffix = if num_excs == 1 { "" } else { "s" }; + Ok(format!( + "{} ({} sub-exception{})", + message, num_excs, suffix + )) + } + + #[pymethod] + fn __repr__(zelf: PyObjectRef, vm: &VirtualMachine) -> PyResult { + Self::slot_repr(&zelf, vm) + } + + #[pyslot] + fn slot_repr(zelf: &PyObject, vm: &VirtualMachine) -> PyResult { + let zelf = zelf + .downcast_ref::() + .expect("exception group must be BaseException"); + let class_name = zelf.class().name().to_owned(); + let message = zelf + .get_arg(0) + .map(|m| m.repr(vm)) + .transpose()? + .map(|s| s.as_str().to_owned()) + .unwrap_or_else(|| "''".to_owned()); + + // Format exceptions as list [exc1, exc2, ...] instead of tuple (exc1, exc2, ...) + // CPython displays exceptions in list format even though they're stored as tuple + let exceptions_str = if let Some(exceptions_obj) = zelf.get_arg(1) { + // Get exceptions using ArgIterable for robustness + let iter: ArgIterable = + ArgIterable::try_from_object(vm, exceptions_obj.clone())?; + let mut exc_repr_list = Vec::new(); + for exc in iter.iter(vm)? { + exc_repr_list.push(exc?.repr(vm)?.as_str().to_owned()); + } + format!("[{}]", exc_repr_list.join(", ")) + } else { + "[]".to_owned() + }; + + Ok(vm + .ctx + .new_str(format!("{}({}, {})", class_name, message, exceptions_str))) + } + } + + // Helper functions for ExceptionGroup + fn is_base_exception_group(obj: &PyObjectRef, vm: &VirtualMachine) -> bool { + obj.fast_isinstance(vm.ctx.exceptions.base_exception_group) + } + + fn get_exceptions_tuple( + exc: &PyRef, + vm: &VirtualMachine, + ) -> PyResult> { + let obj = exc + .get_arg(1) + .ok_or_else(|| vm.new_type_error("exceptions must be a tuple"))?; + let tuple = obj + .downcast_ref::() + .ok_or_else(|| vm.new_type_error("exceptions must be a tuple"))?; + Ok(tuple.to_vec()) + } + + enum ConditionMatcher { + Type(PyTypeRef), + Types(Vec), + Callable(PyObjectRef), + } + + fn get_condition_matcher( + condition: &PyObjectRef, + vm: &VirtualMachine, + ) -> PyResult { + // If it's a type and subclass of BaseException + if let Some(typ) = condition.downcast_ref::() + && typ.fast_issubclass(vm.ctx.exceptions.base_exception_type) + { + return Ok(ConditionMatcher::Type(typ.to_owned())); + } + + // If it's a tuple of types + if let Some(tuple) = condition.downcast_ref::() { + let mut types = Vec::new(); + for item in tuple.iter() { + let typ: PyTypeRef = item.clone().try_into_value(vm).map_err(|_| { + vm.new_type_error( + "expected a function, exception type or tuple of exception types", + ) + })?; + if !typ.fast_issubclass(vm.ctx.exceptions.base_exception_type) { + return Err(vm.new_type_error( + "expected a function, exception type or tuple of exception types", + )); + } + types.push(typ); + } + if !types.is_empty() { + return Ok(ConditionMatcher::Types(types)); + } + } + + // If it's callable (but not a type) + if condition.is_callable() && condition.downcast_ref::().is_none() { + return Ok(ConditionMatcher::Callable(condition.clone())); + } + + Err(vm.new_type_error("expected a function, exception type or tuple of exception types")) + } + + impl ConditionMatcher { + fn check(&self, exc: &PyObjectRef, vm: &VirtualMachine) -> PyResult { + match self { + ConditionMatcher::Type(typ) => Ok(exc.fast_isinstance(typ)), + ConditionMatcher::Types(types) => Ok(types.iter().any(|t| exc.fast_isinstance(t))), + ConditionMatcher::Callable(func) => { + let result = func.call((exc.clone(),), vm)?; + result.try_to_bool(vm) + } + } + } + } + + fn derive_and_copy_attributes( + orig: &PyRef, + excs: Vec, + vm: &VirtualMachine, + ) -> PyResult { + // Call derive method to create new group + let excs_seq = vm.ctx.new_list(excs); + let new_group = vm.call_method(orig.as_object(), "derive", (excs_seq,))?; + + // Verify derive returned a BaseExceptionGroup + if !is_base_exception_group(&new_group, vm) { + return Err(vm.new_type_error("derive must return an instance of BaseExceptionGroup")); + } + + // Copy traceback + if let Some(tb) = orig.__traceback__() { + new_group.set_attr("__traceback__", tb, vm)?; + } + + // Copy context + if let Some(ctx) = orig.__context__() { + new_group.set_attr("__context__", ctx, vm)?; + } + + // Copy cause + if let Some(cause) = orig.__cause__() { + new_group.set_attr("__cause__", cause, vm)?; + } + + // Copy notes (if present) - make a copy of the list + if let Ok(notes) = orig.as_object().get_attr("__notes__", vm) + && let Some(notes_list) = notes.downcast_ref::() + { + let notes_copy = vm.ctx.new_list(notes_list.borrow_vec().to_vec()); + new_group.set_attr("__notes__", notes_copy, vm)?; + } + + Ok(new_group) + } +} diff --git a/crates/vm/src/exceptions.rs b/crates/vm/src/exceptions.rs index 1916972a79..9d3d239ad4 100644 --- a/crates/vm/src/exceptions.rs +++ b/crates/vm/src/exceptions.rs @@ -22,6 +22,8 @@ use std::{ io::{self, BufRead, BufReader}, }; +pub use super::exception_group::exception_group; + unsafe impl Traverse for PyBaseException { fn traverse(&self, tracer_fn: &mut TraverseFn<'_>) { self.traceback.traverse(tracer_fn); @@ -445,11 +447,10 @@ impl ExceptionCtor { } } -#[derive(Debug, Clone)] +#[derive(Debug)] pub struct ExceptionZoo { pub base_exception_type: &'static Py, pub base_exception_group: &'static Py, - pub exception_group: &'static Py, pub system_exit: &'static Py, pub keyboard_interrupt: &'static Py, pub generator_exit: &'static Py, @@ -742,7 +743,6 @@ impl ExceptionZoo { // Sorted By Hierarchy then alphabetized. let base_exception_group = PyBaseExceptionGroup::init_builtin_type(); - let exception_group = PyExceptionGroup::init_builtin_type(); let system_exit = PySystemExit::init_builtin_type(); let keyboard_interrupt = PyKeyboardInterrupt::init_builtin_type(); let generator_exit = PyGeneratorExit::init_builtin_type(); @@ -830,7 +830,6 @@ impl ExceptionZoo { Self { base_exception_type, base_exception_group, - exception_group, system_exit, keyboard_interrupt, generator_exit, @@ -917,7 +916,7 @@ impl ExceptionZoo { "message" => ctx.new_readonly_getset("message", excs.base_exception_group, make_arg_getter(0)), "exceptions" => ctx.new_readonly_getset("exceptions", excs.base_exception_group, make_arg_getter(1)), }); - extend_exception!(PyExceptionGroup, ctx, excs.exception_group); + extend_exception!(PySystemExit, ctx, excs.system_exit, { "code" => ctx.new_readonly_getset("code", excs.system_exit, system_exit_code), }); @@ -1220,8 +1219,7 @@ pub(super) mod types { use crate::{ AsObject, PyObjectRef, PyRef, PyResult, VirtualMachine, builtins::{ - PyGenericAlias, PyInt, PyStrRef, PyTupleRef, PyTypeRef, traceback::PyTracebackRef, - tuple::IntoPyTuple, + PyInt, PyStrRef, PyTupleRef, PyTypeRef, traceback::PyTracebackRef, tuple::IntoPyTuple, }, convert::ToPyResult, function::{ArgBytesLike, FuncArgs}, @@ -1231,6 +1229,9 @@ pub(super) mod types { use itertools::Itertools; use rustpython_common::str::UnicodeEscapeCodepoint; + // Re-export exception group types from dedicated module + pub use crate::exception_group::types::PyBaseExceptionGroup; + // 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. @@ -1252,26 +1253,6 @@ pub(super) mod types { #[derive(Debug)] pub struct PySystemExit {} - #[pyexception(name, base = PyBaseException, ctx = "base_exception_group")] - #[derive(Debug)] - pub struct PyBaseExceptionGroup {} - - #[pyexception] - impl PyBaseExceptionGroup { - #[pyclassmethod] - fn __class_getitem__( - cls: PyTypeRef, - args: PyObjectRef, - vm: &VirtualMachine, - ) -> PyGenericAlias { - PyGenericAlias::from_args(cls, args, vm) - } - } - - #[pyexception(name, base = PyBaseExceptionGroup, ctx = "exception_group", impl)] - #[derive(Debug)] - pub struct PyExceptionGroup {} - #[pyexception(name, base = PyBaseException, ctx = "generator_exit", impl)] #[derive(Debug)] pub struct PyGeneratorExit {} diff --git a/crates/vm/src/lib.rs b/crates/vm/src/lib.rs index 94147345a6..923b33d2ac 100644 --- a/crates/vm/src/lib.rs +++ b/crates/vm/src/lib.rs @@ -55,6 +55,7 @@ mod dict_inner; #[cfg(feature = "rustpython-compiler")] pub mod eval; +mod exception_group; pub mod exceptions; pub mod format; pub mod frame; diff --git a/crates/vm/src/stdlib/builtins.rs b/crates/vm/src/stdlib/builtins.rs index fb665dbdef..5d4a28bf18 100644 --- a/crates/vm/src/stdlib/builtins.rs +++ b/crates/vm/src/stdlib/builtins.rs @@ -1070,6 +1070,9 @@ pub fn init_module(vm: &VirtualMachine, module: &Py) { builtins::extend_module(vm, module).unwrap(); let debug_mode: bool = vm.state.settings.optimize == 0; + // Create dynamic ExceptionGroup with multiple inheritance (BaseExceptionGroup + Exception) + let exception_group = crate::exception_group::exception_group(); + extend_module!(vm, module, { "__debug__" => ctx.new_bool(debug_mode), @@ -1110,7 +1113,7 @@ pub fn init_module(vm: &VirtualMachine, module: &Py) { // Exceptions: "BaseException" => ctx.exceptions.base_exception_type.to_owned(), "BaseExceptionGroup" => ctx.exceptions.base_exception_group.to_owned(), - "ExceptionGroup" => ctx.exceptions.exception_group.to_owned(), + "ExceptionGroup" => exception_group.to_owned(), "SystemExit" => ctx.exceptions.system_exit.to_owned(), "KeyboardInterrupt" => ctx.exceptions.keyboard_interrupt.to_owned(), "GeneratorExit" => ctx.exceptions.generator_exit.to_owned(), diff --git a/crates/vm/src/vm/mod.rs b/crates/vm/src/vm/mod.rs index e6217e3e35..073f8aa4a9 100644 --- a/crates/vm/src/vm/mod.rs +++ b/crates/vm/src/vm/mod.rs @@ -373,11 +373,11 @@ impl VirtualMachine { self.print_exception(e); } - let expect_stdlib = + let _expect_stdlib = cfg!(feature = "freeze-stdlib") || !self.state.settings.path_list.is_empty(); #[cfg(feature = "encodings")] - if expect_stdlib { + if _expect_stdlib { if let Err(e) = self.import_encodings() { eprintln!( "encodings initialization failed. Only utf-8 encoding will be supported." @@ -394,20 +394,8 @@ 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"); - } + // ExceptionGroup is now implemented natively in Rust (exceptions.rs) + // No Python fallback needed self.initialized = true; } From cb878a07c9669c43c01a988758df6e9c74386a35 Mon Sep 17 00:00:00 2001 From: Jeong YunWon Date: Tue, 9 Dec 2025 09:34:38 +0900 Subject: [PATCH 2/3] Remove python impl --- .../Lib/python_builtins/_py_exceptiongroup.py | 330 ------------------ crates/vm/src/vm/mod.rs | 3 - 2 files changed, 333 deletions(-) delete mode 100644 crates/vm/Lib/python_builtins/_py_exceptiongroup.py diff --git a/crates/vm/Lib/python_builtins/_py_exceptiongroup.py b/crates/vm/Lib/python_builtins/_py_exceptiongroup.py deleted file mode 100644 index 91e9354d8a..0000000000 --- a/crates/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/crates/vm/src/vm/mod.rs b/crates/vm/src/vm/mod.rs index 073f8aa4a9..0d21d708ac 100644 --- a/crates/vm/src/vm/mod.rs +++ b/crates/vm/src/vm/mod.rs @@ -394,9 +394,6 @@ impl VirtualMachine { ); } - // ExceptionGroup is now implemented natively in Rust (exceptions.rs) - // No Python fallback needed - self.initialized = true; } From ba9c8e23c57c81effe6f861414825a131023e694 Mon Sep 17 00:00:00 2001 From: "Jeong, YunWon" <69878+youknowone@users.noreply.github.com> Date: Tue, 9 Dec 2025 23:02:26 +0900 Subject: [PATCH 3/3] Update crates/vm/src/exception_group.rs --- crates/vm/src/exception_group.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/vm/src/exception_group.rs b/crates/vm/src/exception_group.rs index 8d49e486cc..5eb011960e 100644 --- a/crates/vm/src/exception_group.rs +++ b/crates/vm/src/exception_group.rs @@ -290,8 +290,8 @@ pub(super) mod types { Ok(vm.ctx.new_tuple(vec![match_group, rest_group])) } - #[pymethod(name = "__str__")] - fn str(zelf: PyRef, vm: &VirtualMachine) -> PyResult { + #[pymethod] + fn __str__(zelf: PyRef, vm: &VirtualMachine) -> PyResult { let message = zelf .get_arg(0) .map(|m| m.str(vm))