From e2357e0d03e46254865569a5a3a092d40470eb1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Lozier?= Date: Fri, 6 Nov 2026 23:19:02 -0500 Subject: [PATCH] Enable test_contextlib --- src/core/IronPython/Runtime/AsyncGenerator.cs | 4 +- src/core/IronPython/Runtime/Coroutine.cs | 7 +- .../PythonExceptions.BaseException.cs | 2 + src/core/IronPython/Runtime/Generator.cs | 28 ++--- .../Runtime/Operations/PythonOps.cs | 102 ++++++++++-------- .../Cases/CPythonCasesManifest.ini | 7 -- tests/suite/stdlib/test_contextlib.py | 33 ++++++ tests/suite/stdlib/test_raise.py | 32 ++++++ tests/suite/test_exceptions.py | 14 +++ 9 files changed, 160 insertions(+), 69 deletions(-) create mode 100644 tests/suite/stdlib/test_contextlib.py create mode 100644 tests/suite/stdlib/test_raise.py diff --git a/src/core/IronPython/Runtime/AsyncGenerator.cs b/src/core/IronPython/Runtime/AsyncGenerator.cs index 8e7877208..ec48594cb 100644 --- a/src/core/IronPython/Runtime/AsyncGenerator.cs +++ b/src/core/IronPython/Runtime/AsyncGenerator.cs @@ -103,9 +103,9 @@ public object asend(object? value) { /// [LightThrowing] public object athrow(object? type, object? value, object? traceback) { - if (PythonOps.ValidateThrowArguments(type, value) is Exception error) return LightExceptions.Throw(error); + if (PythonOps.ValidateThrowArguments(type, value, traceback) is Exception error) return LightExceptions.Throw(error); - Exception ex = PythonOps.MakeExceptionForGenerator(DefaultContext.Default, type, value, traceback, cause: null); + Exception ex = PythonOps.MakeExceptionForGenerator(DefaultContext.Default, type, value, traceback); _started = true; _throwSlot.Value = ex; _sendSlot.Value = null; diff --git a/src/core/IronPython/Runtime/Coroutine.cs b/src/core/IronPython/Runtime/Coroutine.cs index 1cac125aa..3d6d6ddff 100644 --- a/src/core/IronPython/Runtime/Coroutine.cs +++ b/src/core/IronPython/Runtime/Coroutine.cs @@ -29,7 +29,7 @@ public sealed class PythonCoroutine : ICodeFormattable, IWeakReferenceable { // _cts — CancellationTokenSource whose Token was passed into AsyncExpression. throw(exc) cancels this. // _cancellationException — StrongBox shared with DriveAsync's `cancellationException` parameter; // when non-null at cancellation time, that exception is surfaced to the body - // instead of OperationCanceledException. + // instead of OperationCanceledException. // // Lazy start: the body is not executed at construction. _factory is the thunk that creates (and thereby starts driving) the Task on // first access; _task caches the materialized Task. Calling an async def is therefore side-effect free until the coroutine is first @@ -98,13 +98,12 @@ public object send(object? value) { [LightThrowing] public object @throw(object? type, object? value, object? traceback) { // 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); + if (PythonOps.ValidateThrowArguments(type, value, traceback) 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 // coroutine's own context will re-bind the chain when the exception unwinds inside the body. - Exception ex = PythonOps.MakeExceptionForGenerator( - DefaultContext.Default, type, value, traceback, cause: null); + Exception ex = PythonOps.MakeExceptionForGenerator(DefaultContext.Default, type, value, traceback); var task = EnsureTask(); if (task.IsCompleted) { diff --git a/src/core/IronPython/Runtime/Exceptions/PythonExceptions.BaseException.cs b/src/core/IronPython/Runtime/Exceptions/PythonExceptions.BaseException.cs index 8d87c9993..012720feb 100644 --- a/src/core/IronPython/Runtime/Exceptions/PythonExceptions.BaseException.cs +++ b/src/core/IronPython/Runtime/Exceptions/PythonExceptions.BaseException.cs @@ -281,6 +281,8 @@ internal set { } } + internal BaseException? RawContext => _context; + public bool __suppress_context__ { get; set; } public TraceBack? __traceback__ { diff --git a/src/core/IronPython/Runtime/Generator.cs b/src/core/IronPython/Runtime/Generator.cs index 1ce01e6a1..61be1e766 100644 --- a/src/core/IronPython/Runtime/Generator.cs +++ b/src/core/IronPython/Runtime/Generator.cs @@ -42,7 +42,7 @@ public sealed class PythonGenerator : IEnumerator, IEnumerator, ICodeFor /// /// Fields set by Throw() to communicate an exception to the yield point. - /// These are plumbed through the generator to become parameters to Raise(...) invoked + /// These are plumbed through the generator to become parameters to Raise(...) invoked /// at the yield suspension point in the generator. /// private object?[]? _excInfo; @@ -131,15 +131,15 @@ public object @throw(object? type, object? value, object? traceback) { /// /// Throw(...) is like Raise(...) being called from the yield point within the generator. - /// Note it must come from inside the generator so that the traceback matches, and so that it can + /// Note it must come from inside the generator so that the traceback matches, and so that it can /// properly cooperate with any try/catch/finallys inside the generator body. - /// + /// /// If the generator catches the exception and yields another value, that is the return value of g.throw(). /// [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 (PythonOps.ValidateThrowArguments(type, value) is Exception error) return LightExceptions.Throw(error); + if (PythonOps.ValidateThrowArguments(type, value, traceback) is Exception error) return LightExceptions.Throw(error); // Pep explicitly says that Throw on a closed generator throws the exception, // and not a StopIteration exception. (This is different than Next()). @@ -172,7 +172,7 @@ private object @throw(object? type, object? value, object? traceback, bool final return ThrowOnClosed(); object ThrowOnClosed() - => LightExceptions.Throw(PythonOps.MakeExceptionForGenerator(Context, type, value, traceback, null)); + => LightExceptions.Throw(PythonOps.MakeExceptionForGenerator(Context, type, value, traceback)); } /// @@ -234,7 +234,7 @@ public object send(object? value) { return lightEh; } - // Generator should not have exited normally. + // Generator should not have exited normally. return LightExceptions.Throw(new RuntimeException("generator ignored GeneratorExit")); } catch (StopIterationException) { // Ignore, clear any stack frames we built up @@ -352,7 +352,7 @@ private void HandleFinalizerException(Exception e) { // PEP says: // If close() raises an exception, a traceback for the exception is printed to sys.stderr // and further ignored; it is not propagated back to the place that - // triggered the garbage collection. + // triggered the garbage collection. // Sample error message from CPython 2.5 looks like: // Exception __main__.MyError: MyError() in ignored @@ -491,7 +491,7 @@ private object NextWorker() { // The generator body here may: // 1. return an item: _next() returns true and 'next' is set to the next item in the enumeration. // 2. Exit normally: _next returns false. - // 3. Exit with a StopIteration exception: for-loops and other enumeration consumers will + // 3. Exit with a StopIteration exception: for-loops and other enumeration consumers will // catch this and terminate the loop without propogating the exception. // 4. Exit via some other unhandled exception: This will close the generator, but the exception still propagates. // _next does not return, so ret is left assigned to false (closed), which we detect in the finally. @@ -557,7 +557,7 @@ private static void AlreadyExecuting() { /// [LightThrowing] internal object? CheckThrowableAndReturnSendValue() { - // Since this method is called from the generator body's execution, the generator must be running + // Since this method is called from the generator body's execution, the generator must be running // and not closed. Debug.Assert(!Closed); @@ -595,9 +595,9 @@ private object ThrowThrowable() { // Clear it so that any future Next()/MoveNext() call doesn't pick up the exception again. _excInfo = null; - // This may invoke user code such as __init__, thus MakeException may throw. - // Since this is invoked from the generator's body, the generator can catch this exception. - return LightExceptions.Throw(PythonOps.MakeExceptionForGenerator(Context, throwableBackup[0], throwableBackup[1], throwableBackup[2], null)); + // This may invoke user code such as __init__, thus MakeException may throw. + // Since this is invoked from the generator's body, the generator can catch this exception. + return LightExceptions.Throw(PythonOps.MakeExceptionForGenerator(Context, throwableBackup[0], throwableBackup[1], throwableBackup[2])); } private void Close() { @@ -628,7 +628,7 @@ private bool Closed { /// /// True if the thread is currently inside the generator (ie, invoking the _next delegate). - /// This can be used to enforce that a generator does not call back into itself. + /// This can be used to enforce that a generator does not call back into itself. /// Pep255 says that a generator should throw a ValueError if called reentrantly. /// private bool Active => Volatile.Read(ref _active) != 0; @@ -719,7 +719,7 @@ public GeneratorFinalizer(PythonGenerator generator) { var gen = Generator; if (gen != null) { Debug.Assert(gen._finalizer == this); - // We set this to null to indicate that the GeneratorFinalizer has been finalized, + // We set this to null to indicate that the GeneratorFinalizer has been finalized, // and should not be reused (ie saved in PythonGenerator._LastFinalizer) gen._finalizer = null; diff --git a/src/core/IronPython/Runtime/Operations/PythonOps.cs b/src/core/IronPython/Runtime/Operations/PythonOps.cs index c99a61de4..0a2ac20ae 100644 --- a/src/core/IronPython/Runtime/Operations/PythonOps.cs +++ b/src/core/IronPython/Runtime/Operations/PythonOps.cs @@ -108,7 +108,7 @@ public static PythonDictionary MakeHomogeneousDictFromItems(object[] data) { return new PythonDictionary(new CommonDictionaryStorage(data, true)); } - public static bool IsCallable(CodeContext/*!*/ context, [NotNullWhen(true)]object? o) { + public static bool IsCallable(CodeContext/*!*/ context, [NotNullWhen(true)] object? o) { // This tells if an object can be called, but does not make a claim about the parameter list. // In 1.x, we could check for certain interfaces like ICallable*, but those interfaces were deprecated // in favor of dynamic sites. @@ -407,7 +407,7 @@ internal static bool IsSubClass(PythonType/*!*/ c, PythonType/*!*/ typeinfo) { return typeinfo.__subclasscheck__(c); } - internal static bool IsSubClass(CodeContext/*!*/ context, PythonType c, [NotNull]object? typeinfo) { + internal static bool IsSubClass(CodeContext/*!*/ context, PythonType c, [NotNull] object? typeinfo) { if (c == null) throw PythonOps.TypeError("issubclass: arg 1 must be a class"); if (typeinfo == null) throw PythonOps.TypeError("issubclass: arg 2 must be a class"); @@ -492,7 +492,7 @@ internal static bool IsInstance(CodeContext/*!*/ context, object? o, PythonTuple } // used by Ironclad - public static bool IsInstance(CodeContext/*!*/ context, object? o, [NotNull]object? typeinfo) { + public static bool IsInstance(CodeContext/*!*/ context, object? o, [NotNull] object? typeinfo) { if (typeinfo == null) throw PythonOps.TypeError("isinstance: arg 2 must be a class, type, or tuple of classes and types"); if (typeinfo is PythonTuple tt) { @@ -521,7 +521,7 @@ private static bool IsInstanceDynamic(object? o, object typeinfo, PythonType odt return false; } - private static bool IsSubclassSlow([NotNullWhen(true)]object? cls, object typeinfo) { + private static bool IsSubclassSlow([NotNullWhen(true)] object? cls, object typeinfo) { if (cls == null) return false; // Same type @@ -1662,7 +1662,7 @@ PythonDictionary pydict when pydict.GetType() == typeof(PythonDictionary) => #if DEBUG Debug.Assert(attrdict._storage.IsMutable); #endif - }; + } return attrdict; } @@ -2252,7 +2252,7 @@ public static object GetEnumeratorObject(CodeContext/*!*/ context, object? o) { throw TypeErrorForNotIterable(o); } - private static bool TryGetEnumeratorObject(CodeContext/*!*/ context, object? o, [NotNullWhen(true)]out object? enumerator) { + private static bool TryGetEnumeratorObject(CodeContext/*!*/ context, object? o, [NotNullWhen(true)] out object? enumerator) { // IronPython defines __getitem__ on PythonType (to support generics and .NET enums) // which would make it "iterable" even though it is not. if (o is PythonType pt && !pt.IsIterable(context)) { @@ -2286,7 +2286,7 @@ public static KeyValuePair ThrowTypeErrorForBadIterati throw PythonOps.TypeError("iteration over non-sequence of type {0}", PythonOps.GetPythonTypeName(enumerable)); } - internal static bool TryGetEnumerator(CodeContext/*!*/ context, [NotNullWhen(true)]object? enumerable, [NotNullWhen(true)]out IEnumerator? enumerator) { + internal static bool TryGetEnumerator(CodeContext/*!*/ context, [NotNullWhen(true)] object? enumerable, [NotNullWhen(true)] out IEnumerator? enumerator) { enumerator = null; // IronPython defines __getitem__ on PythonType (to support generics and .NET enums) @@ -2516,37 +2516,37 @@ public static PythonTuple GetExceptionInfo(CodeContext/*!*/ context) { } /// - /// helper function for re-raised exceptions. + /// Helper for a bare raise: re-raises the exception being handled. /// public static Exception MakeRethrownException(CodeContext/*!*/ context) { - PythonTuple t = GetExceptionInfo(context); - Debug.Assert(t[1] == GetRawContextException()); - Exception e = MakeExceptionWorker(context, t[0], t[1], t[2], null, suppressContext: false, forRethrow: true); - return MakeRethrowExceptionWorker(e); + var current = GetCurrentException(); + if (current is null) throw RuntimeError("No active exception to reraise"); + Exception e = MakeExceptionWorker(context, PythonExceptions.ToPython(current), null, null, withCause: false); + return ExceptionHelpers.UpdateForRethrow(e); } /// - /// helper function for re-raised exception. - /// This entry point is used by 'raise' inside 'with' statement + /// Re-raises the exception of a 'with' block after __exit__ returned false. /// - /// - /// public static Exception MakeRethrowExceptionWorker(Exception e) { e.RemoveTraceBack(); - ExceptionHelpers.UpdateForRethrow(e); - return e; + return ExceptionHelpers.UpdateForRethrow(e); } public static Exception MakeException(CodeContext/*!*/ context, object exception) - => MakeExceptionWorker(context, exception, null, null, null, suppressContext: false, forRethrow: false); + => MakeExceptionWorker(context, exception, null, null, withCause: false); public static Exception MakeExceptionWithCause(CodeContext/*!*/ context, object exception, object cause) - => MakeExceptionWorker(context, exception, null, null, cause, suppressContext: true, forRethrow: false); + => MakeExceptionWorker(context, exception, null, cause, withCause: true); internal static PythonExceptions.BaseException? GetRawContextException() => GetCurrentException()?.GetPythonException(); - internal static Exception MakeExceptionForGenerator(CodeContext/*!*/ context, object? type, object? value, object? traceback, object? cause) { - Exception e = MakeExceptionWorker(context, type, value, traceback, cause, suppressContext: false, forRethrow: false); + internal static Exception MakeExceptionForGenerator(CodeContext/*!*/ context, object? type, object? value, object? traceback) { + // throw() does not chain the exception to the one being handled + Exception e = MakeExceptionWorker(context, type, value, null, withCause: false, keepContext: true); + if (traceback is TraceBack tb) { + e.SetTraceBack(tb); + } e.RemoveFrameList(); return e; } @@ -2554,7 +2554,7 @@ internal static Exception MakeExceptionForGenerator(CodeContext/*!*/ context, ob /// /// Validates the arguments of generator/coroutine throw(). Returns the TypeError to raise, or null if valid. /// - internal static Exception? ValidateThrowArguments(object? type, object? value) { + internal static Exception? ValidateThrowArguments(object? type, object? value, object? traceback) { 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))) { @@ -2562,34 +2562,53 @@ internal static Exception MakeExceptionForGenerator(CodeContext/*!*/ context, ob } else { return TypeError("exceptions must be classes or instances deriving from BaseException, not {0}", GetPythonTypeName(type)); } + if (traceback is not null && traceback is not TraceBack) { + return TypeError("throw() third argument must be a traceback object"); + } return null; } - private static Exception MakeExceptionWorker(CodeContext/*!*/ context, object? type, object? value, object? traceback, object? cause, bool suppressContext, bool forRethrow) { + private static Exception MakeExceptionWorker(CodeContext/*!*/ context, object? type, object? value, object? cause, bool withCause, bool keepContext = false) { Exception throwable; - var causeException = GetCause(context, cause); - var contextException = GetRawContextException(); - 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); - } + var baseException = GetBaseException(context, type, value); - if (traceback != null) { - if (!forRethrow) { - if (!(traceback is TraceBack tb)) throw PythonOps.TypeError("traceback argument must be a traceback object"); + PythonExceptions.BaseException? causeException; + bool suppressContext; + if (withCause) { + causeException = GetCause(context, cause); + suppressContext = true; + } else { + // only raise ... from ... changes the cause + causeException = baseException.__cause__; + suppressContext = baseException.__suppress_context__; + } - throwable.SetTraceBack(tb); + var contextException = GetRawContextException(); + if (keepContext || contextException == baseException) { + // throw() does not change the context, and raising the exception being handled + // (including a bare raise) does not make it its own context + contextException = baseException.RawContext; + } else { + // avoid creating cycles through the context chain + for (var o = contextException; o is not null; o = o.RawContext) { + if (o.RawContext == baseException) { + o.__context__ = null; + break; + } + } } - } else { - throwable.RemoveTraceBack(); + throwable = baseException.CreateClrExceptionWithCause(causeException, contextException, suppressContext); } + throwable.RemoveTraceBack(); + PerfTrack.NoteEvent(PerfTrack.Categories.Exceptions, throwable); return throwable; @@ -2605,7 +2624,7 @@ private static Exception MakeExceptionWorker(CodeContext/*!*/ context, object? t if (cause is PythonExceptions.BaseException be) return be; - throw PythonOps.TypeError("exception causes must derive from BaseException"); + throw TypeError("exception causes must derive from BaseException"); } static PythonExceptions.BaseException GetBaseException(CodeContext context, object? type, object? value) { @@ -2614,7 +2633,7 @@ static PythonExceptions.BaseException GetBaseException(CodeContext context, obje if (type is PythonType pt && typeof(PythonExceptions.BaseException).IsAssignableFrom(pt.UnderlyingSystemType)) return PythonExceptions.CreateBaseExceptionForRaise(context, pt, value); - throw PythonOps.TypeError("exceptions must derive from BaseException"); + throw TypeError("exceptions must derive from BaseException"); } } @@ -2954,7 +2973,7 @@ public static object GetInitSlotMember(CodeContext/*!*/ context, PythonType type /// Helper to determine if the value is a simple numeric type (int or big int or bool) - used for OldInstance /// deprecated form of slicing. /// - public static bool IsNumericObject([NotNullWhen(true)]object? value) { + public static bool IsNumericObject([NotNullWhen(true)] object? value) { return value is int || value is BigInteger || value is Extensible || value is bool; } @@ -3018,8 +3037,7 @@ internal static MethodInfo GetConversionHelper(string name, ConversionResultKind } public static object ConvertToPythonPrimitive(object value) { - return value switch - { + return value switch { float f => (double)f, sbyte sb => (int)sb, byte b => (int)b, @@ -3331,7 +3349,7 @@ public static object MakeCoroutineWrapper(PythonGenerator generator) { Type tp = task.GetType(); var tcs = new TaskCompletionSource(); task.ContinueWith(t => { - if (t.IsCanceled) tcs.SetCanceled(); + if (t.IsCanceled) tcs.SetCanceled(); else if (t.IsFaulted) tcs.SetException(t.Exception!.InnerException ?? t.Exception); else tcs.SetResult(!tp.IsGenericType ? null : (object?)tp.GetProperty("Result")!.GetValue(t)); }, TaskContinuationOptions.ExecuteSynchronously); @@ -4295,7 +4313,7 @@ public static Exception TypeErrorForArgumentCountMismatch(string methodName, int methodName, expectedArgCount, expectedArgCount == 1 ? "" : "s", actualArgCount); } - public static Exception TypeErrorForOptionalArgumentCountMismatch(string methodName, int expectedArgCount, int actualArgCount, bool positional=false) { + public static Exception TypeErrorForOptionalArgumentCountMismatch(string methodName, int expectedArgCount, int actualArgCount, bool positional = false) { return TypeError("{0}() takes at most {1} {2}argument{3} ({4} given)", methodName, expectedArgCount, positional ? "positional " : "", expectedArgCount == 1 ? "" : "s", actualArgCount); } diff --git a/tests/IronPython.Tests/Cases/CPythonCasesManifest.ini b/tests/IronPython.Tests/Cases/CPythonCasesManifest.ini index 192475484..a23776169 100644 --- a/tests/IronPython.Tests/Cases/CPythonCasesManifest.ini +++ b/tests/IronPython.Tests/Cases/CPythonCasesManifest.ini @@ -286,10 +286,6 @@ Reason=AttributeError: 'module' object has no attribute 'symlink' Ignore=true Reason=ImportError: Cannot import name SemLock -[CPython.test_contextlib] -Ignore=true -Reason=Hangs - [CPython.test_copy] IsolationLevel=ENGINE MaxRecursion=100 @@ -774,9 +770,6 @@ Ignore=true [CPython.test_pyexpat] Ignore=true -[CPython.test_raise] -Ignore=true - [CPython.test_random] Ignore=true diff --git a/tests/suite/stdlib/test_contextlib.py b/tests/suite/stdlib/test_contextlib.py new file mode 100644 index 000000000..c68980d55 --- /dev/null +++ b/tests/suite/stdlib/test_contextlib.py @@ -0,0 +1,33 @@ +# Licensed to the .NET Foundation under one or more agreements. +# The .NET Foundation licenses this file to you under the Apache 2.0 License. +# See the LICENSE file in the project root for more information. + +## +## Run selected tests from test_contextlib from StdLib +## + +import sys + +from iptest import is_ironpython, generate_suite, run_test + +import test.test_contextlib + +def load_tests(loader, standard_tests, pattern): + tests = loader.loadTestsFromModule(test.test_contextlib, pattern=pattern) + + if is_ironpython: + failing_tests = [] + if sys.version_info >= (3, 6): + failing_tests += [ + test.test_contextlib.ContextManagerTestCase('test_contextmanager_except_pep479'), # AssertionError: StopIteration was suppressed + test.test_contextlib.ContextManagerTestCase('test_contextmanager_except_stopiter'), # AssertionError: DeprecationWarning not triggered + ] + + skip_tests = [] + + return generate_suite(tests, failing_tests, skip_tests) + + else: + return tests + +run_test(__name__) diff --git a/tests/suite/stdlib/test_raise.py b/tests/suite/stdlib/test_raise.py new file mode 100644 index 000000000..5ab4a19ef --- /dev/null +++ b/tests/suite/stdlib/test_raise.py @@ -0,0 +1,32 @@ +# Licensed to the .NET Foundation under one or more agreements. +# The .NET Foundation licenses this file to you under the Apache 2.0 License. +# See the LICENSE file in the project root for more information. + +## +## Run selected tests from test_raise from StdLib +## + +import sys + +from iptest import is_ironpython, generate_suite, run_test + +import test.test_raise + +def load_tests(loader, standard_tests, pattern): + tests = loader.loadTestsFromModule(test.test_raise, pattern=pattern) + + if is_ironpython: + failing_tests = [ + test.test_raise.TestContext('test_c_exception_raise'), # AssertionError: None is not an instance of + test.test_raise.TestContext('test_context_manager'), # AssertionError: None is not an instance of + test.test_raise.TestTraceback('test_accepts_traceback'), # AssertionError: None != + ] + + skip_tests = [] + + return generate_suite(tests, failing_tests, skip_tests) + + else: + return tests + +run_test(__name__) diff --git a/tests/suite/test_exceptions.py b/tests/suite/test_exceptions.py index 7404c25d9..d8a2c1862 100644 --- a/tests/suite/test_exceptions.py +++ b/tests/suite/test_exceptions.py @@ -1107,4 +1107,18 @@ def gen(): next(g) self.assertEqual(g.throw(System.InvalidOperationException, "abc"), "abc") + def test_context_cycle_broken(self): + a, b = KeyError(), ValueError() + try: + try: + raise a + except KeyError: + raise b + except ValueError: + try: + raise a + except KeyError as e: + self.assertIs(e.__context__, b) + self.assertIsNone(b.__context__) + run_test(__name__)