diff --git a/Lib/test/test_baseexception.py b/Lib/test/test_baseexception.py index a7a20dc415..a73711c416 100644 --- a/Lib/test/test_baseexception.py +++ b/Lib/test/test_baseexception.py @@ -18,8 +18,6 @@ def verify_instance_interface(self, ins): "%s missing %s attribute" % (ins.__class__.__name__, attr)) - # TODO: RUSTPYTHON - @unittest.expectedFailure def test_inheritance(self): # Make sure the inheritance hierarchy matches the documentation exc_set = set() diff --git a/Lib/test/test_contextlib.py b/Lib/test/test_contextlib.py index f6478b43e0..91764696ba 100644 --- a/Lib/test/test_contextlib.py +++ b/Lib/test/test_contextlib.py @@ -1271,8 +1271,6 @@ def test_cm_is_reentrant(self): 1/0 self.assertTrue(outer_continued) - # TODO: RUSTPYTHON - @unittest.expectedFailure def test_exception_groups(self): eg_ve = lambda: ExceptionGroup( "EG with ValueErrors only", diff --git a/Lib/test/test_exception_group.py b/Lib/test/test_exception_group.py index 03c721d56f..d0d81490df 100644 --- a/Lib/test/test_exception_group.py +++ b/Lib/test/test_exception_group.py @@ -1,12 +1,9 @@ import collections.abc -import traceback import types import unittest - +from test.support import C_RECURSION_LIMIT class TestExceptionGroupTypeHierarchy(unittest.TestCase): - # TODO: RUSTPYTHON - @unittest.expectedFailure def test_exception_group_types(self): self.assertTrue(issubclass(ExceptionGroup, Exception)) self.assertTrue(issubclass(ExceptionGroup, BaseExceptionGroup)) @@ -38,8 +35,6 @@ def test_bad_EG_construction__too_many_args(self): with self.assertRaisesRegex(TypeError, MSG): ExceptionGroup('eg', [ValueError('too')], [TypeError('many')]) - # TODO: RUSTPYTHON - @unittest.expectedFailure def test_bad_EG_construction__bad_message(self): MSG = 'argument 1 must be str, not ' with self.assertRaisesRegex(TypeError, MSG): @@ -47,8 +42,6 @@ def test_bad_EG_construction__bad_message(self): with self.assertRaisesRegex(TypeError, MSG): ExceptionGroup(None, [ValueError(12)]) - # TODO: RUSTPYTHON - @unittest.expectedFailure def test_bad_EG_construction__bad_excs_sequence(self): MSG = r'second argument \(exceptions\) must be a sequence' with self.assertRaisesRegex(TypeError, MSG): @@ -60,8 +53,6 @@ def test_bad_EG_construction__bad_excs_sequence(self): with self.assertRaisesRegex(ValueError, MSG): ExceptionGroup("eg", []) - # TODO: RUSTPYTHON - @unittest.expectedFailure def test_bad_EG_construction__nested_non_exceptions(self): MSG = (r'Item [0-9]+ of second argument \(exceptions\)' ' is not an exception') @@ -78,16 +69,12 @@ def test_EG_wraps_Exceptions__creates_EG(self): type(ExceptionGroup("eg", excs)), ExceptionGroup) - # TODO: RUSTPYTHON - @unittest.expectedFailure def test_BEG_wraps_Exceptions__creates_EG(self): excs = [ValueError(1), TypeError(2)] self.assertIs( type(BaseExceptionGroup("beg", excs)), ExceptionGroup) - # TODO: RUSTPYTHON - @unittest.expectedFailure def test_EG_wraps_BaseException__raises_TypeError(self): MSG= "Cannot nest BaseExceptions in an ExceptionGroup" with self.assertRaisesRegex(TypeError, MSG): @@ -105,8 +92,6 @@ class MyEG(ExceptionGroup): type(MyEG("eg", [ValueError(12), TypeError(42)])), MyEG) - # TODO: RUSTPYTHON - @unittest.expectedFailure def test_EG_subclass_does_not_wrap_base_exceptions(self): class MyEG(ExceptionGroup): pass @@ -115,8 +100,6 @@ class MyEG(ExceptionGroup): with self.assertRaisesRegex(TypeError, msg): MyEG("eg", [ValueError(12), KeyboardInterrupt(42)]) - # TODO: RUSTPYTHON - @unittest.expectedFailure def test_BEG_and_E_subclass_does_not_wrap_base_exceptions(self): class MyEG(BaseExceptionGroup, ValueError): pass @@ -125,6 +108,21 @@ class MyEG(BaseExceptionGroup, ValueError): with self.assertRaisesRegex(TypeError, msg): MyEG("eg", [ValueError(12), KeyboardInterrupt(42)]) + def test_EG_and_specific_subclass_can_wrap_any_nonbase_exception(self): + class MyEG(ExceptionGroup, ValueError): + pass + + # The restriction is specific to Exception, not "the other base class" + MyEG("eg", [ValueError(12), Exception()]) + + def test_BEG_and_specific_subclass_can_wrap_any_nonbase_exception(self): + class MyEG(BaseExceptionGroup, ValueError): + pass + + # The restriction is specific to Exception, not "the other base class" + MyEG("eg", [ValueError(12), Exception()]) + + def test_BEG_subclass_wraps_anything(self): class MyBEG(BaseExceptionGroup): pass @@ -138,8 +136,6 @@ class MyBEG(BaseExceptionGroup): class StrAndReprTests(unittest.TestCase): - # TODO: RUSTPYTHON - @unittest.expectedFailure def test_ExceptionGroup(self): eg = BaseExceptionGroup( 'flat', [ValueError(1), TypeError(2)]) @@ -160,8 +156,6 @@ def test_ExceptionGroup(self): "ExceptionGroup('flat', " "[ValueError(1), TypeError(2)]), TypeError(2)])") - # TODO: RUSTPYTHON - @unittest.expectedFailure def test_BaseExceptionGroup(self): eg = BaseExceptionGroup( 'flat', [ValueError(1), KeyboardInterrupt(2)]) @@ -184,8 +178,6 @@ def test_BaseExceptionGroup(self): "BaseExceptionGroup('flat', " "[ValueError(1), KeyboardInterrupt(2)])])") - # TODO: RUSTPYTHON - @unittest.expectedFailure def test_custom_exception(self): class MyEG(ExceptionGroup): pass @@ -270,8 +262,6 @@ def test_basics_ExceptionGroup_fields(self): self.assertIsNone(tb.tb_next) self.assertEqual(tb.tb_lineno, tb_linenos[1][i]) - # TODO: RUSTPYTHON - @unittest.expectedFailure def test_fields_are_readonly(self): eg = ExceptionGroup('eg', [TypeError(1), OSError(2)]) @@ -287,8 +277,6 @@ def test_fields_are_readonly(self): class ExceptionGroupTestBase(unittest.TestCase): - # TODO: RUSTPYTHON - @unittest.expectedFailure def assertMatchesTemplate(self, exc, exc_type, template): """ Assert that the exception matches the template @@ -318,7 +306,6 @@ def setUp(self): self.eg = create_simple_eg() self.eg_template = [ValueError(1), TypeError(int), ValueError(2)] - @unittest.skip("TODO: RUSTPYTHON") def test_basics_subgroup_split__bad_arg_type(self): bad_args = ["bad arg", OSError('instance not type'), @@ -330,8 +317,6 @@ def test_basics_subgroup_split__bad_arg_type(self): with self.assertRaises(TypeError): self.eg.split(arg) - - @unittest.skip("TODO: RUSTPYTHON") def test_basics_subgroup_by_type__passthrough(self): eg = self.eg self.assertIs(eg, eg.subgroup(BaseException)) @@ -339,11 +324,9 @@ def test_basics_subgroup_by_type__passthrough(self): self.assertIs(eg, eg.subgroup(BaseExceptionGroup)) self.assertIs(eg, eg.subgroup(ExceptionGroup)) - @unittest.skip("TODO: RUSTPYTHON") def test_basics_subgroup_by_type__no_match(self): self.assertIsNone(self.eg.subgroup(OSError)) - @unittest.skip("TODO: RUSTPYTHON") def test_basics_subgroup_by_type__match(self): eg = self.eg testcases = [ @@ -358,15 +341,12 @@ def test_basics_subgroup_by_type__match(self): self.assertEqual(subeg.message, eg.message) self.assertMatchesTemplate(subeg, ExceptionGroup, template) - @unittest.skip("TODO: RUSTPYTHON") def test_basics_subgroup_by_predicate__passthrough(self): self.assertIs(self.eg, self.eg.subgroup(lambda e: True)) - @unittest.skip("TODO: RUSTPYTHON") def test_basics_subgroup_by_predicate__no_match(self): self.assertIsNone(self.eg.subgroup(lambda e: False)) - @unittest.skip("TODO: RUSTPYTHON") def test_basics_subgroup_by_predicate__match(self): eg = self.eg testcases = [ @@ -386,7 +366,6 @@ def setUp(self): self.eg = create_simple_eg() self.eg_template = [ValueError(1), TypeError(int), ValueError(2)] - @unittest.skip("TODO: RUSTPYTHON") def test_basics_split_by_type__passthrough(self): for E in [BaseException, Exception, BaseExceptionGroup, ExceptionGroup]: @@ -395,14 +374,12 @@ def test_basics_split_by_type__passthrough(self): match, ExceptionGroup, self.eg_template) self.assertIsNone(rest) - @unittest.skip("TODO: RUSTPYTHON") def test_basics_split_by_type__no_match(self): match, rest = self.eg.split(OSError) self.assertIsNone(match) self.assertMatchesTemplate( rest, ExceptionGroup, self.eg_template) - @unittest.skip("TODO: RUSTPYTHON") def test_basics_split_by_type__match(self): eg = self.eg VE = ValueError @@ -427,19 +404,16 @@ def test_basics_split_by_type__match(self): else: self.assertIsNone(rest) - @unittest.skip("TODO: RUSTPYTHON") def test_basics_split_by_predicate__passthrough(self): match, rest = self.eg.split(lambda e: True) self.assertMatchesTemplate(match, ExceptionGroup, self.eg_template) self.assertIsNone(rest) - @unittest.skip("TODO: RUSTPYTHON") def test_basics_split_by_predicate__no_match(self): match, rest = self.eg.split(lambda e: False) self.assertIsNone(match) self.assertMatchesTemplate(rest, ExceptionGroup, self.eg_template) - @unittest.skip("TODO: RUSTPYTHON") def test_basics_split_by_predicate__match(self): eg = self.eg VE = ValueError @@ -465,19 +439,15 @@ def test_basics_split_by_predicate__match(self): class DeepRecursionInSplitAndSubgroup(unittest.TestCase): def make_deep_eg(self): e = TypeError(1) - for i in range(2000): + for i in range(C_RECURSION_LIMIT + 1): e = ExceptionGroup('eg', [e]) return e - # TODO: RUSTPYTHON - @unittest.expectedFailure def test_deep_split(self): e = self.make_deep_eg() with self.assertRaises(RecursionError): e.split(TypeError) - # TODO: RUSTPYTHON - @unittest.expectedFailure def test_deep_subgroup(self): e = self.make_deep_eg() with self.assertRaises(RecursionError): @@ -501,7 +471,7 @@ def leaf_generator(exc, tbs=None): class LeafGeneratorTest(unittest.TestCase): # The leaf_generator is mentioned in PEP 654 as a suggestion - # on how to iterate over leaf nodes of an EG. It is also + # on how to iterate over leaf nodes of an EG. Is is also # used below as a test utility. So we test it here. def test_leaf_generator(self): @@ -654,8 +624,6 @@ def tb_linenos(tbs): class NestedExceptionGroupSplitTest(ExceptionGroupSplitTestBase): - # TODO: RUSTPYTHON - @unittest.expectedFailure def test_split_by_type(self): class MyExceptionGroup(ExceptionGroup): pass @@ -755,8 +723,6 @@ def level3(i): self.assertMatchesTemplate(match, ExceptionGroup, [eg_template[0]]) self.assertMatchesTemplate(rest, ExceptionGroup, [eg_template[1]]) - # TODO: RUSTPYTHON - @unittest.expectedFailure def test_split_BaseExceptionGroup(self): def exc(ex): try: @@ -797,8 +763,6 @@ def exc(ex): self.assertMatchesTemplate( rest, ExceptionGroup, [ValueError(1)]) - # TODO: RUSTPYTHON - @unittest.expectedFailure def test_split_copies_notes(self): # make sure each exception group after a split has its own __notes__ list eg = ExceptionGroup("eg", [ValueError(1), TypeError(2)]) @@ -830,11 +794,23 @@ 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): + return 42 + + eg = MyEg('eg', [TypeError(1), ValueError(2)]) + msg = "derive must return an instance of BaseExceptionGroup" + with self.assertRaisesRegex(TypeError, msg): + eg.split(TypeError) + with self.assertRaisesRegex(TypeError, msg): + eg.subgroup(TypeError) + class NestedExceptionGroupSubclassSplitTest(ExceptionGroupSplitTestBase): - # TODO: RUSTPYTHON - @unittest.expectedFailure def test_split_ExceptionGroup_subclass_no_derive_no_new_override(self): class EG(ExceptionGroup): pass @@ -877,8 +853,6 @@ class EG(ExceptionGroup): self.assertMatchesTemplate(match, ExceptionGroup, [[TypeError(2)]]) self.assertMatchesTemplate(rest, ExceptionGroup, [ValueError(1)]) - # TODO: RUSTPYTHON - @unittest.expectedFailure def test_split_BaseExceptionGroup_subclass_no_derive_new_override(self): class EG(BaseExceptionGroup): def __new__(cls, message, excs, unused): @@ -921,8 +895,6 @@ def __new__(cls, message, excs, unused): match, BaseExceptionGroup, [KeyboardInterrupt(2)]) self.assertMatchesTemplate(rest, ExceptionGroup, [ValueError(1)]) - # TODO: RUSTPYTHON - @unittest.expectedFailure def test_split_ExceptionGroup_subclass_derive_and_new_overrides(self): class EG(ExceptionGroup): def __new__(cls, message, excs, code): diff --git a/Lib/typing.py b/Lib/typing.py index 086d0f3f95..75ec2a6a2e 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -22,7 +22,6 @@ from abc import abstractmethod, ABCMeta import collections import collections.abc -import contextlib import functools import operator import re as stdlib_re # Avoid confusion with the re we export. @@ -2138,8 +2137,13 @@ class Other(Leaf): # Error reported by type checker KeysView = _alias(collections.abc.KeysView, 1) ItemsView = _alias(collections.abc.ItemsView, 2) ValuesView = _alias(collections.abc.ValuesView, 1) -ContextManager = _alias(contextlib.AbstractContextManager, 1, name='ContextManager') -AsyncContextManager = _alias(contextlib.AbstractAsyncContextManager, 1, name='AsyncContextManager') +try: + # XXX: RUSTPYTHON; contextlib support for wasm + import contextlib + ContextManager = _alias(contextlib.AbstractContextManager, 1, name='ContextManager') + AsyncContextManager = _alias(contextlib.AbstractAsyncContextManager, 1, name='AsyncContextManager') +except ImportError: + pass Dict = _alias(dict, 2, inst=False, name='Dict') DefaultDict = _alias(collections.defaultdict, 2, name='DefaultDict') OrderedDict = _alias(collections.OrderedDict, 2) diff --git a/src/interpreter.rs b/src/interpreter.rs index 35710ae829..b79a1a0ffb 100644 --- a/src/interpreter.rs +++ b/src/interpreter.rs @@ -18,6 +18,7 @@ pub type InitHook = Box; /// let mut settings = Settings::default(); /// settings.debug = 1; /// // You may want to add paths to `rustpython_vm::Settings::path_list` to allow import python libraries. +/// settings.path_list.push("Lib".to_owned()); // add standard library directory /// settings.path_list.push("".to_owned()); // add current working directory /// let interpreter = rustpython::InterpreterConfig::new() /// .settings(settings) diff --git a/vm/Lib/python_builtins/_py_exceptiongroup.py b/vm/Lib/python_builtins/_py_exceptiongroup.py new file mode 100644 index 0000000000..91e9354d8a --- /dev/null +++ b/vm/Lib/python_builtins/_py_exceptiongroup.py @@ -0,0 +1,330 @@ +# 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/vm/mod.rs b/vm/src/vm/mod.rs index 493789f510..b47c986cae 100644 --- a/vm/src/vm/mod.rs +++ b/vm/src/vm/mod.rs @@ -364,8 +364,11 @@ impl VirtualMachine { } } + let expect_stdlib = + cfg!(feature = "freeze-stdlib") || !self.state.settings.path_list.is_empty(); + #[cfg(feature = "encodings")] - if cfg!(feature = "freeze-stdlib") || !self.state.settings.path_list.is_empty() { + if expect_stdlib { if let Err(e) = self.import_encodings() { eprintln!( "encodings initialization failed. Only utf-8 encoding will be supported." @@ -382,6 +385,21 @@ 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; } diff --git a/wasm/demo/.envrc b/wasm/demo/.envrc new file mode 100644 index 0000000000..928b937d32 --- /dev/null +++ b/wasm/demo/.envrc @@ -0,0 +1,2 @@ +export NODE_OPTIONS=--openssl-legacy-provider +export PATH=$PATH:`pwd`/../../geckodriver diff --git a/wasm/demo/package.json b/wasm/demo/package.json index 53ed264ea7..487f46fbc1 100644 --- a/wasm/demo/package.json +++ b/wasm/demo/package.json @@ -25,7 +25,7 @@ "dev": "webpack serve", "build": "webpack", "dist": "webpack --mode production", - "test": "webpack --mode production && cd ../tests && pytest" + "test": "webpack --mode production && cd ../tests && pytest -v" }, "repository": { "type": "git", diff --git a/wasm/demo/src/index.js b/wasm/demo/src/index.js index db3554c481..1af847d59d 100644 --- a/wasm/demo/src/index.js +++ b/wasm/demo/src/index.js @@ -21,7 +21,14 @@ import('rustpython') }) .catch((e) => { console.error('Error importing `rustpython`:', e); - document.getElementById('error').textContent = e; + let errorDetails = e.toString(); + if (window.__RUSTPYTHON_ERROR) { + errorDetails += '\nRustPython Error: ' + window.__RUSTPYTHON_ERROR; + } + if (window.__RUSTPYTHON_ERROR_STACK) { + errorDetails += '\nStack: ' + window.__RUSTPYTHON_ERROR_STACK; + } + document.getElementById('error').textContent = errorDetails; }); const fixedHeightEditor = EditorView.theme({ diff --git a/wasm/tests/conftest.py b/wasm/tests/conftest.py index ecb5b0007b..84a2530575 100644 --- a/wasm/tests/conftest.py +++ b/wasm/tests/conftest.py @@ -102,6 +102,12 @@ def wdriver(request): driver._print_panic() driver.quit() raise + except Exception as e: + print(f"Error waiting for page to load: {e}") + # Check the page source to see what's loaded + print("Page source:", driver.page_source[:500]) + driver.quit() + raise yield driver