diff --git a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/PythonBuiltinClassType.java b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/PythonBuiltinClassType.java index 86e67d02c2..2c0da66626 100644 --- a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/PythonBuiltinClassType.java +++ b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/PythonBuiltinClassType.java @@ -987,7 +987,13 @@ accepted by asctime(), mktime() and strftime(). May be considered as a UnraisableHookArgs Type used to pass arguments to sys.unraisablehook.""")), + PExceptHookArgs( + "_ExceptHookArgs", + PTuple, + newBuilder().publishInModule(J__THREAD).slots(StructSequenceBuiltins.SLOTS, InstantiableStructSequenceBuiltins.SLOTS).doc(""" + _ExceptHookArgs + Type used to pass arguments to _thread._excepthook.""")), PSSLSession("SSLSession", PythonObject, newBuilder().publishInModule(J__SSL).disallowInstantiation()), PSSLContext("_SSLContext", PythonObject, newBuilder().publishInModule(J__SSL).basetype().slots(SSLContextBuiltins.SLOTS)), PSSLSocket("_SSLSocket", PythonObject, newBuilder().publishInModule(J__SSL).basetype()), diff --git a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/ThreadModuleBuiltins.java b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/ThreadModuleBuiltins.java index 82dfdce7dd..22da1f9d4c 100644 --- a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/ThreadModuleBuiltins.java +++ b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/ThreadModuleBuiltins.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2018, 2026, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * The Universal Permissive License (UPL), Version 1.0 @@ -43,6 +43,8 @@ import static com.oracle.graal.python.builtins.objects.thread.AbstractPythonLock.TIMEOUT_MAX; import static com.oracle.graal.python.nodes.BuiltinNames.J_EXIT; import static com.oracle.graal.python.nodes.BuiltinNames.J__THREAD; +import static com.oracle.graal.python.nodes.BuiltinNames.T_STDERR; +import static com.oracle.graal.python.nodes.BuiltinNames.T___EXCEPTHOOK__; import static com.oracle.graal.python.nodes.BuiltinNames.T__THREAD; import static com.oracle.graal.python.util.PythonUtils.tsLiteral; @@ -57,11 +59,20 @@ import com.oracle.graal.python.builtins.PythonBuiltinClassType; import com.oracle.graal.python.builtins.PythonBuiltins; import com.oracle.graal.python.builtins.objects.PNone; +import com.oracle.graal.python.builtins.objects.common.SequenceStorageNodes; +import com.oracle.graal.python.builtins.objects.exception.PBaseException; import com.oracle.graal.python.builtins.objects.function.PKeyword; import com.oracle.graal.python.builtins.objects.module.PythonModule; +import com.oracle.graal.python.builtins.objects.object.PythonObject; import com.oracle.graal.python.builtins.objects.thread.PLock; import com.oracle.graal.python.builtins.objects.thread.PThread; +import com.oracle.graal.python.builtins.objects.tuple.PTuple; +import com.oracle.graal.python.builtins.objects.tuple.StructSequence; +import com.oracle.graal.python.builtins.objects.type.TypeNodes; import com.oracle.graal.python.lib.PyNumberAsSizeNode; +import com.oracle.graal.python.lib.PyObjectLookupAttr; +import com.oracle.graal.python.lib.PyObjectSetAttr; +import com.oracle.graal.python.lib.PyObjectStrAsTruffleStringNode; import com.oracle.graal.python.nodes.ErrorMessages; import com.oracle.graal.python.nodes.PRaiseNode; import com.oracle.graal.python.nodes.WriteUnraisableNode; @@ -76,11 +87,13 @@ import com.oracle.graal.python.nodes.function.builtins.PythonUnaryClinicBuiltinNode; import com.oracle.graal.python.nodes.function.builtins.clinic.ArgumentClinicProvider; import com.oracle.graal.python.nodes.object.BuiltinClassProfiles.IsBuiltinObjectProfile; +import com.oracle.graal.python.nodes.object.GetClassNode; import com.oracle.graal.python.runtime.GilNode; import com.oracle.graal.python.runtime.PythonContext; import com.oracle.graal.python.runtime.exception.PException; import com.oracle.graal.python.runtime.exception.PythonThreadKillException; import com.oracle.graal.python.runtime.object.PFactory; +import com.oracle.graal.python.runtime.sequence.storage.SequenceStorage; import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; import com.oracle.truffle.api.TruffleLanguage; import com.oracle.truffle.api.TruffleThreadBuilder; @@ -97,6 +110,15 @@ @CoreFunctions(defineModule = J__THREAD) public final class ThreadModuleBuiltins extends PythonBuiltins { + public static final StructSequence.BuiltinTypeDescriptor EXCEPTHOOK_ARGS_DESC = new StructSequence.BuiltinTypeDescriptor( + PythonBuiltinClassType.PExceptHookArgs, + 4, + new String[]{ + "exc_type", "exc_value", "exc_traceback", "thread"}, + new String[]{ + "Exception type", "Exception value", "Exception traceback", + "Exception thread"}); + @Override protected List> getNodeFactories() { return ThreadModuleBuiltinsFactory.getFactories(); @@ -106,6 +128,7 @@ protected List> getNodeFa public void initialize(Python3Core core) { addBuiltinConstant("error", core.lookupType(PythonBuiltinClassType.RuntimeError)); addBuiltinConstant("TIMEOUT_MAX", TIMEOUT_MAX); + StructSequence.initType(core, EXCEPTHOOK_ARGS_DESC); core.lookupBuiltinModule(T__THREAD).setModuleState(0); super.initialize(core); } @@ -173,6 +196,92 @@ static long getStackSize(VirtualFrame frame, Object stackSizeObj, } } + @Builtin(name = "_excepthook", minNumOfPositionalArgs = 2, declaresExplicitSelf = true) + @GenerateNodeFactory + abstract static class GetThreadExceptHookNode extends PythonBinaryBuiltinNode { + @Specialization + Object getExceptHook(@SuppressWarnings("unused") PythonModule self, + Object exceptHookArgs, + @Bind Node inliningTarget, + @Cached PRaiseNode raiseNode, + @Cached CallNode callNode, + @Cached PyObjectLookupAttr lookupAttr, + @Cached PyObjectSetAttr setAttr, + @Cached PyObjectStrAsTruffleStringNode strNode) { + + Object argsType = GetClassNode.GetPythonObjectClassNode.executeUncached((PythonObject) exceptHookArgs); + if (!TypeNodes.IsSameTypeNode.executeUncached(argsType, PythonBuiltinClassType.PExceptHookArgs)) { + throw PRaiseNode.getUncached().raise(raiseNode, PythonBuiltinClassType.TypeError, ErrorMessages.ARG_TYPE_MUST_BE, "_thread.excepthook", "ExceptHookArgs"); + } + SequenceStorage seq = ((PTuple) exceptHookArgs).getSequenceStorage(); + if (seq.length() != 4) { + throw PRaiseNode.getUncached().raise(raiseNode, PythonBuiltinClassType.TypeError, ErrorMessages.TAKES_EXACTLY_D_ARGUMENTS_D_GIVEN, 4, seq.length()); + } + + Object excType = SequenceStorageNodes.GetItemScalarNode.executeUncached(seq, 0); + + if (TypeNodes.IsSameTypeNode.executeUncached(excType, PythonBuiltinClassType.SystemExit)) { + return PNone.NONE; + } + Object excValue = SequenceStorageNodes.GetItemScalarNode.executeUncached(seq, 1); + Object excTraceback = SequenceStorageNodes.GetItemScalarNode.executeUncached(seq, 2); + Object thread = SequenceStorageNodes.GetItemScalarNode.executeUncached(seq, 3); + + TruffleString name; + + Object nameAttr = lookupAttr.execute(null, inliningTarget, thread, tsLiteral("_name")); + if (nameAttr != null && nameAttr != PNone.NONE && nameAttr != PNone.NO_VALUE) { + name = strNode.execute(null, inliningTarget, nameAttr); + } else { + Object getIdentBuiltin = lookupAttr.execute(null, inliningTarget, thread, tsLiteral("get_ident")); + Object ident = callNode.executeWithoutFrame(getIdentBuiltin); + name = ident != null ? strNode.execute(null, inliningTarget, ident) : tsLiteral(""); + } + + Object sysMod = getContext().getSysModule(); + Object stdErr = lookupAttr.execute(null, inliningTarget, sysMod, T_STDERR); + + boolean stdErrInvalid = stdErr == null || stdErr == PNone.NONE || stdErr == PNone.NO_VALUE; + + if (stdErrInvalid) { + if (thread != null && thread != PNone.NONE && thread != PNone.NO_VALUE) { + stdErr = lookupAttr.execute(null, inliningTarget, thread, tsLiteral("_stderr")); + } + if (stdErr == null || stdErr == PNone.NONE || stdErr == PNone.NO_VALUE) { + return PNone.NONE; + } + } + + Object write = lookupAttr.execute(null, inliningTarget, stdErr, tsLiteral("write")); + Object flush = lookupAttr.execute(null, inliningTarget, stdErr, tsLiteral("flush")); + + callNode.executeWithoutFrame(write, tsLiteral("Exception in thread ")); + callNode.executeWithoutFrame(write, name); + callNode.executeWithoutFrame(write, tsLiteral(":\n")); + callNode.executeWithoutFrame(flush); + + Object sysExcepthook = lookupAttr.execute(null, inliningTarget, sysMod, T___EXCEPTHOOK__); + if (sysExcepthook != PNone.NO_VALUE && sysExcepthook != PNone.NONE) { + if (!stdErrInvalid) { + callNode.executeWithoutFrame(sysExcepthook, excType, excValue, excTraceback); + } else { + Object oldStdErr = lookupAttr.execute(null, inliningTarget, sysMod, T_STDERR); + try { + setAttr.execute(inliningTarget, sysMod, T_STDERR, stdErr); + callNode.executeWithoutFrame(sysExcepthook, excType, excValue, excTraceback); + } finally { + setAttr.execute(inliningTarget, sysMod, T_STDERR, oldStdErr == PNone.NO_VALUE ? PNone.NONE : oldStdErr); + } + } + callNode.executeWithoutFrame(flush); + } else if (excValue instanceof PBaseException) { + callNode.executeWithoutFrame(write, strNode.execute(null, inliningTarget, excValue)); + callNode.executeWithoutFrame(flush); + } + return PNone.NONE; + } + } + @Builtin(name = "start_new_thread", minNumOfPositionalArgs = 2, maxNumOfPositionalArgs = 3) @Builtin(name = "start_new", minNumOfPositionalArgs = 2, maxNumOfPositionalArgs = 3) @GenerateNodeFactory diff --git a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/tuple/InstantiableStructSequenceBuiltins.java b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/tuple/InstantiableStructSequenceBuiltins.java index 1f6054487c..30912d0891 100644 --- a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/tuple/InstantiableStructSequenceBuiltins.java +++ b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/tuple/InstantiableStructSequenceBuiltins.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * The Universal Permissive License (UPL), Version 1.0 @@ -90,7 +90,8 @@ PythonBuiltinClassType.PIntInfo, PythonBuiltinClassType.PHashInfo, PythonBuiltinClassType.PThreadInfo, - PythonBuiltinClassType.PUnraisableHookArgs}) + PythonBuiltinClassType.PUnraisableHookArgs, + PythonBuiltinClassType.PExceptHookArgs}) public class InstantiableStructSequenceBuiltins extends PythonBuiltins { public static final TpSlots SLOTS = InstantiableStructSequenceBuiltinsSlotsGen.SLOTS; diff --git a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/tuple/StructSequenceBuiltins.java b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/tuple/StructSequenceBuiltins.java index 8644f8e07a..8ecf4975b3 100644 --- a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/tuple/StructSequenceBuiltins.java +++ b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/tuple/StructSequenceBuiltins.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * The Universal Permissive License (UPL), Version 1.0 @@ -116,7 +116,8 @@ PythonBuiltinClassType.PIntInfo, PythonBuiltinClassType.PHashInfo, PythonBuiltinClassType.PThreadInfo, - PythonBuiltinClassType.PUnraisableHookArgs}) + PythonBuiltinClassType.PUnraisableHookArgs, + PythonBuiltinClassType.PExceptHookArgs}) public final class StructSequenceBuiltins extends PythonBuiltins { public static final TpSlots SLOTS = StructSequenceBuiltinsSlotsGen.SLOTS; diff --git a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/type/TypeNodes.java b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/type/TypeNodes.java index 944f037bfa..5d93adacb8 100644 --- a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/type/TypeNodes.java +++ b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/type/TypeNodes.java @@ -2603,7 +2603,7 @@ private static int getBuiltinTypeItemsize(PythonBuiltinClassType cls) { case PInt, Boolean -> 4; case PAsyncGenerator, PFlags, PHashInfo, PTuple, PCoroutine, PGenerator, PThreadInfo, PMemoryView, PStatResult, PUnameResult, PStructTime, PFloatInfo, PStatvfsResult, PIntInfo, PFrame, - PTerminalSize, PUnraisableHookArgs -> 8; + PTerminalSize, PUnraisableHookArgs, PExceptHookArgs -> 8; case PythonClass -> 40; default -> 0; }; diff --git a/graalpython/lib-python/3/test/test_threading.py b/graalpython/lib-python/3/test/test_threading.py index a6b29bfc23..784a441b0a 100644 --- a/graalpython/lib-python/3/test/test_threading.py +++ b/graalpython/lib-python/3/test/test_threading.py @@ -1653,20 +1653,19 @@ def outer(): def test_print_exception(self): script = r"""if True: import threading - import time - running = False + started = threading.Event() + stop = threading.Event() + def run(): - global running - running = True - while running: - time.sleep(0.01) + started.set() + stop.wait() 1/0 + t = threading.Thread(target=run) t.start() - while not running: - time.sleep(0.01) - running = False + started.wait() + stop.set() t.join() """ rc, out, err = assert_python_ok("-c", script) @@ -1681,25 +1680,23 @@ def test_print_exception_stderr_is_none_1(self): script = r"""if True: import sys import threading - import time - running = False + started = threading.Event() + stop = threading.Event() + def run(): - global running - running = True - while running: - time.sleep(0.01) + started.set() + stop.wait() 1/0 + t = threading.Thread(target=run) t.start() - while not running: - time.sleep(0.01) + started.wait() sys.stderr = None - running = False + stop.set() t.join() """ rc, out, err = assert_python_ok("-c", script) - self.assertEqual(out, b'') err = err.decode() self.assertIn("Exception in thread", err) self.assertIn("Traceback (most recent call last):", err) @@ -1710,21 +1707,20 @@ def test_print_exception_stderr_is_none_2(self): script = r"""if True: import sys import threading - import time - running = False + started = threading.Event() + stop = threading.Event() + def run(): - global running - running = True - while running: - time.sleep(0.01) + started.set() + stop.wait() 1/0 + sys.stderr = None t = threading.Thread(target=run) t.start() - while not running: - time.sleep(0.01) - running = False + started.wait() + stop.set() t.join() """ rc, out, err = assert_python_ok("-c", script)