From 5b3d374f8f68fd0070d13b01f4860e9e28ea9780 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Lozier?= Date: Sun, 22 Feb 2026 22:05:35 -0500 Subject: [PATCH] Allow raising .NET exception classes --- src/core/IronPython/Runtime/AsyncGenerator.cs | 12 +---- src/core/IronPython/Runtime/Coroutine.cs | 14 +----- .../Runtime/Exceptions/PythonExceptions.cs | 50 +++++++++++-------- src/core/IronPython/Runtime/Generator.cs | 9 +--- .../Runtime/Operations/PythonOps.cs | 16 ++++++ tests/suite/test_exceptions.py | 28 +++++++++++ 6 files changed, 78 insertions(+), 51 deletions(-) diff --git a/src/core/IronPython/Runtime/AsyncGenerator.cs b/src/core/IronPython/Runtime/AsyncGenerator.cs index 8c6e7b607..8e7877208 100644 --- a/src/core/IronPython/Runtime/AsyncGenerator.cs +++ b/src/core/IronPython/Runtime/AsyncGenerator.cs @@ -103,17 +103,7 @@ public object asend(object? value) { /// [LightThrowing] public object athrow(object? type, object? value, object? traceback) { - // Validate shape (mirrors PythonGenerator/PythonCoroutine.throw). - if (type is Exception || type is PythonExceptions.BaseException) { - if (value is not null) - return LightExceptions.Throw(PythonOps.TypeError("instance exception may not have a separate value")); - } else if (type is PythonType pt && typeof(PythonExceptions.BaseException).IsAssignableFrom(pt.UnderlyingSystemType)) { - // ok — class form - } else { - return LightExceptions.Throw(PythonOps.TypeError( - "exceptions must be classes or instances deriving from BaseException, not {0}", - PythonOps.GetPythonTypeName(type))); - } + if (PythonOps.ValidateThrowArguments(type, value) is Exception error) return LightExceptions.Throw(error); Exception ex = PythonOps.MakeExceptionForGenerator(DefaultContext.Default, type, value, traceback, cause: null); _started = true; diff --git a/src/core/IronPython/Runtime/Coroutine.cs b/src/core/IronPython/Runtime/Coroutine.cs index 2e63f4410..1cac125aa 100644 --- a/src/core/IronPython/Runtime/Coroutine.cs +++ b/src/core/IronPython/Runtime/Coroutine.cs @@ -97,18 +97,8 @@ public object send(object? value) { /// [LightThrowing] public object @throw(object? type, object? value, object? traceback) { - // Validate the shape of (type, value, traceback) up front. Mirrors PythonGenerator.@throw - // so we don't mutate any state on bad input. - if (type is Exception || type is PythonExceptions.BaseException) { - if (value is not null) - return LightExceptions.Throw(PythonOps.TypeError("instance exception may not have a separate value")); - } else if (type is PythonType pt && typeof(PythonExceptions.BaseException).IsAssignableFrom(pt.UnderlyingSystemType)) { - // ok — class form, MakeExceptionForGenerator will construct. - } else { - return LightExceptions.Throw(PythonOps.TypeError( - "exceptions must be classes or instances deriving from BaseException, not {0}", - PythonOps.GetPythonTypeName(type))); - } + // Validate up front so we don't mutate any state on bad input. + if (PythonOps.ValidateThrowArguments(type, value) is Exception error) return LightExceptions.Throw(error); // Construct the actual exception object. DefaultContext.Default suffices here — throw()'s // CodeContext role is limited to resolving class-form exception construction, and the diff --git a/src/core/IronPython/Runtime/Exceptions/PythonExceptions.cs b/src/core/IronPython/Runtime/Exceptions/PythonExceptions.cs index f4dd89fab..439538ad6 100644 --- a/src/core/IronPython/Runtime/Exceptions/PythonExceptions.cs +++ b/src/core/IronPython/Runtime/Exceptions/PythonExceptions.cs @@ -27,13 +27,13 @@ namespace IronPython.Runtime.Exceptions { /// - /// Implementation of the Python exceptions module and the IronPython/CLR exception mapping + /// Implementation of the Python exceptions module and the IronPython/CLR exception mapping /// mechanism. The exception module is the parent module for all Python exception classes /// and therefore is built-in to IronPython.dll instead of IronPython.Modules.dll. - /// + /// /// The exception mapping mechanism is exposed as internal surface area available to only /// IronPython / IronPython.Modules.dll. The actual exceptions themselves are all public. - /// + /// /// Because the oddity of the built-in exception types all sharing the same physical layout /// (see also PythonExceptions.BaseException) some classes are defined as classes w/ their /// proper name and some classes are defined as PythonType fields. When a class is defined @@ -324,7 +324,7 @@ internal static class Errno { if error[i] < '0' or error[i] > '9': error = error[:i] break - + if not error: continue @@ -332,12 +332,12 @@ internal static class Errno { if errNo == 22 or errNo >= 10000: continue allErrors.append((justName, error)) - - l = toError.get(errNo) + + l = toError.get(errNo) if l is None: toError[errNo] = l = [] l.append(justName) - + for name, err in allErrors: print('internal const int %s = %s;' % (name, err)) @@ -771,21 +771,31 @@ internal static BaseException CreatePythonThrowable(PythonType type, params obje #nullable enable internal static BaseException CreateBaseExceptionForRaise(CodeContext/*!*/ context, PythonType/*!*/ type, object? value) { - object? pyEx; + object? pyEx = CallForRaise(context, type, value); + + if (pyEx is BaseException be) return be; + + throw PythonOps.TypeError($"calling {PythonOps.ToString(type)} should have returned an instance of BaseException, not {PythonOps.ToString(DynamicHelpers.GetPythonType(pyEx))}"); + } + internal static Exception CreateClrExceptionForRaise(CodeContext/*!*/ context, PythonType/*!*/ type, object? value) { + object? clrEx = CallForRaise(context, type, value); + + if (clrEx is Exception ex) return ex; + + throw PythonOps.TypeError($"calling {PythonOps.ToString(type)} should have returned an instance of Exception, not {PythonOps.ToString(DynamicHelpers.GetPythonType(clrEx))}"); + } + + private static object? CallForRaise(CodeContext/*!*/ context, PythonType/*!*/ type, object? value) { if (PythonOps.IsInstance(value, type)) { - pyEx = value; + return value; } else if (value is PythonTuple pt) { - pyEx = PythonCalls.Call(context, type, pt.ToArray()); + return PythonCalls.Call(context, type, pt.ToArray()); } else if (value != null) { - pyEx = PythonCalls.Call(context, type, value); + return PythonCalls.Call(context, type, value); } else { - pyEx = PythonCalls.Call(context, type); + return PythonCalls.Call(context, type); } - - if (pyEx is BaseException be) return be; - - throw PythonOps.TypeError($"calling {PythonOps.ToString(type)} should have returned an instance of BaseException, not {PythonOps.ToString(DynamicHelpers.GetPythonType(pyEx))}"); } /// @@ -1002,7 +1012,7 @@ public static PythonType CreateSubType(PythonContext/*!*/ context, PythonType ba /// /// Creates a PythonType for a built-in module, where the type may inherit - /// from multiple bases. These types are mutable like normal user types. + /// from multiple bases. These types are mutable like normal user types. /// [PythonHidden] public static PythonType CreateSubType(PythonContext/*!*/ context, PythonType[] baseTypes, Type underlyingType, string name, string module, string documentation, Func exceptionMaker) { @@ -1015,8 +1025,8 @@ public static PythonType CreateSubType(PythonContext/*!*/ context, PythonType[] /// /// Creates a new type for a built-in exception which derives from another Python - /// type. . These types are built-in and immutable like any other normal type. For - /// example StandardError.x = 3 is illegal. This isn't for module exceptions which + /// type. . These types are built-in and immutable like any other normal type. For + /// example StandardError.x = 3 is illegal. This isn't for module exceptions which /// are like user defined types. thread.error.x = 3 is legal. /// private static PythonType CreateSubType(PythonType baseType, string name, Func exceptionMaker) { @@ -1024,7 +1034,7 @@ private static PythonType CreateSubType(PythonType baseType, string name, Func - /// Creates a new type for a built-in exception which is the root concrete type. + /// Creates a new type for a built-in exception which is the root concrete type. /// private static PythonType/*!*/ CreateSubType(PythonType/*!*/ baseType, Type/*!*/ concreteType, Func exceptionMaker) { Assert.NotNull(baseType, concreteType); diff --git a/src/core/IronPython/Runtime/Generator.cs b/src/core/IronPython/Runtime/Generator.cs index 4cb008626..ec57ee040 100644 --- a/src/core/IronPython/Runtime/Generator.cs +++ b/src/core/IronPython/Runtime/Generator.cs @@ -120,14 +120,7 @@ public object @throw(object? type, object? value, object? traceback) { [LightThrowing] private object @throw(object? type, object? value, object? traceback, bool finalizing) { // Do argument validation outside of the generator's body and do not update the state on failure - if (type is Exception || type is PythonExceptions.BaseException) { - if (value is not null) - throw PythonOps.TypeError("instance exception may not have a separate value"); - } else if (type is PythonType pt && typeof(PythonExceptions.BaseException).IsAssignableFrom(pt.UnderlyingSystemType)) { - // ok - } else { - throw PythonOps.TypeError("exceptions must be classes or instances deriving from BaseException, not {0}", PythonOps.GetPythonTypeName(type)); - } + if (PythonOps.ValidateThrowArguments(type, value) is Exception error) return LightExceptions.Throw(error); // Set fields which will then be used by CheckThrowable. // We create the actual exception from inside the generator so that if the exception's __init__ diff --git a/src/core/IronPython/Runtime/Operations/PythonOps.cs b/src/core/IronPython/Runtime/Operations/PythonOps.cs index bdc02b079..7d7a327de 100644 --- a/src/core/IronPython/Runtime/Operations/PythonOps.cs +++ b/src/core/IronPython/Runtime/Operations/PythonOps.cs @@ -2551,6 +2551,20 @@ internal static Exception MakeExceptionForGenerator(CodeContext/*!*/ context, ob return e; } + /// + /// Validates the arguments of generator/coroutine throw(). Returns the TypeError to raise, or null if valid. + /// + internal static Exception? ValidateThrowArguments(object? type, object? value) { + if (type is Exception || type is PythonExceptions.BaseException) { + if (value is not null) return TypeError("instance exception may not have a separate value"); + } else if (type is PythonType pt && (typeof(PythonExceptions.BaseException).IsAssignableFrom(pt.UnderlyingSystemType) || typeof(Exception).IsAssignableFrom(pt.UnderlyingSystemType))) { + // ok + } else { + return TypeError("exceptions must be classes or instances deriving from BaseException, not {0}", GetPythonTypeName(type)); + } + return null; + } + private static Exception MakeExceptionWorker(CodeContext/*!*/ context, object? type, object? value, object? traceback, object? cause, bool suppressContext, bool forRethrow) { Exception throwable; @@ -2560,6 +2574,8 @@ private static Exception MakeExceptionWorker(CodeContext/*!*/ context, object? t if (type is Exception ex) { // TODO: maybe add cause? throwable = ex; + } else if (type is PythonType pt && typeof(Exception).IsAssignableFrom(pt.UnderlyingSystemType)) { + throwable = PythonExceptions.CreateClrExceptionForRaise(context, pt, value); } else { throwable = GetBaseException(context, type, value).CreateClrExceptionWithCause(causeException, contextException, suppressContext); } diff --git a/tests/suite/test_exceptions.py b/tests/suite/test_exceptions.py index a2688c595..7404c25d9 100644 --- a/tests/suite/test_exceptions.py +++ b/tests/suite/test_exceptions.py @@ -1079,4 +1079,32 @@ def f(): self.assertRaises(error, f) + @skipUnlessIronPython() + def test_ipy3_gh1816(self): + import System + + with self.assertRaises(System.InvalidOperationException): + raise System.InvalidOperationException + + with self.assertRaises(SystemError): + raise System.InvalidOperationException + + class MyException(System.Exception): pass + + with self.assertRaises(MyException): + raise MyException + + with self.assertRaises(TypeError): # constructor requires arguments + raise System.Reflection.ReflectionTypeLoadException + + def gen(): + try: + yield + except System.InvalidOperationException as e: + yield e.Message + + g = gen() + next(g) + self.assertEqual(g.throw(System.InvalidOperationException, "abc"), "abc") + run_test(__name__)