Skip to content

Commit 7363be3

Browse files
committed
[GR-23272] Implement unraisable exceptions
PullRequest: graalpython/1049
2 parents 8c427ae + b992c64 commit 7363be3

File tree

8 files changed

+199
-39
lines changed

8 files changed

+199
-39
lines changed

graalpython/com.oracle.graal.python.test/src/tests/cpyext/__init__.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@
3939

4040
import sys
4141
import os
42+
from io import StringIO
43+
4244
from importlib import invalidate_caches
4345
from string import Formatter
4446
__dir__ = __file__.rpartition("/")[0]
@@ -289,7 +291,7 @@ def file_not_empty(path):
289291

290292
class CPyExtFunction():
291293

292-
def __init__(self, pfunc, parameters, template=c_template, cmpfunc=None, **kwargs):
294+
def __init__(self, pfunc, parameters, template=c_template, cmpfunc=None, stderr_validator=None, **kwargs):
293295
self.template = template
294296
self.pfunc = pfunc
295297
self.parameters = parameters
@@ -306,6 +308,7 @@ def __init__(self, pfunc, parameters, template=c_template, cmpfunc=None, **kwarg
306308
kwargs["resultspec"] = kwargs["resultspec"] if "resultspec" in kwargs else "O"
307309
self.formatargs = kwargs
308310
self.cmpfunc = cmpfunc or self.do_compare
311+
self.stderr_validator = stderr_validator
309312

310313
def do_compare(self, x, y):
311314
if isinstance(x, BaseException):
@@ -359,11 +362,18 @@ def test(self):
359362
cargs = self.parameters()
360363
pargs = self.parameters()
361364
for i in range(len(cargs)):
362-
cresult = presult = None
365+
real_stderr = sys.stderr
366+
sys.stderr = StringIO()
363367
try:
364368
cresult = ctest(cargs[i])
365369
except BaseException as e:
366370
cresult = e
371+
else:
372+
if self.stderr_validator:
373+
s = sys.stderr.getvalue()
374+
assert self.stderr_validator(cargs[i], s), f"captured stderr didn't match expectations. Stderr: {s}"
375+
finally:
376+
sys.stderr = real_stderr
367377
try:
368378
presult = self.pfunc(pargs[i])
369379
except BaseException as e:

graalpython/com.oracle.graal.python.test/src/tests/cpyext/test_err.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,7 @@ def compile_module(self, name):
213213
argspec='i',
214214
arguments=["int n"],
215215
callfunction="wrap_PyErr_PrintEx",
216+
stderr_validator=lambda args, stderr: 'unknown key whatsoever' in stderr and 'Traceback' not in stderr,
216217
cmpfunc=unhandled_error_compare
217218
)
218219

@@ -339,11 +340,20 @@ def compile_module(self, name):
339340
test_PyErr_WriteUnraisable = CPyExtFunctionVoid(
340341
lambda args: None,
341342
lambda: (
343+
(None,),
342344
("hello",),
343345
),
344346
resultspec="O",
345347
argspec='O',
346348
arguments=["PyObject* obj"],
349+
code="""void wrap_PyErr_WriteUnraisable(PyObject* object) {
350+
PyErr_SetString(PyExc_RuntimeError, "unraisable exception");
351+
if (object == Py_None)
352+
object = NULL;
353+
PyErr_WriteUnraisable(object);
354+
}""",
355+
callfunction="wrap_PyErr_WriteUnraisable",
356+
stderr_validator=lambda args, stderr: 'unraisable exception' in stderr,
347357
cmpfunc=unhandled_error_compare
348358
)
349359

graalpython/com.oracle.graal.python.test/src/tests/unittest_tags/test_yield_from.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
*graalpython.lib-python.3.test.test_yield_from.TestPEP380Operation.test_attempted_yield_from_loop
22
*graalpython.lib-python.3.test.test_yield_from.TestPEP380Operation.test_attempting_to_send_to_non_generator
3+
*graalpython.lib-python.3.test.test_yield_from.TestPEP380Operation.test_broken_getattr_handling
34
*graalpython.lib-python.3.test.test_yield_from.TestPEP380Operation.test_catching_exception_from_subgen_and_returning
45
*graalpython.lib-python.3.test.test_yield_from.TestPEP380Operation.test_close_with_cleared_frame
56
*graalpython.lib-python.3.test.test_yield_from.TestPEP380Operation.test_conversion_of_sendNone_to_next

graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/PythonCextBuiltins.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,7 @@
201201
import com.oracle.graal.python.nodes.PNodeWithContext;
202202
import com.oracle.graal.python.nodes.PRaiseNode;
203203
import com.oracle.graal.python.nodes.SpecialMethodNames;
204+
import com.oracle.graal.python.nodes.WriteUnraisableNode;
204205
import com.oracle.graal.python.nodes.argument.keywords.ExecuteKeywordStarargsNode.ExpandKeywordStarargsNode;
205206
import com.oracle.graal.python.nodes.argument.positional.ExecutePositionalStarargsNode;
206207
import com.oracle.graal.python.nodes.attributes.HasInheritedAttributeNode;
@@ -686,6 +687,18 @@ Object run(Object typ, PBaseException val, Object tb) {
686687
}
687688
}
688689

690+
@Builtin(name = "PyTruffle_WriteUnraisable", minNumOfPositionalArgs = 2)
691+
@GenerateNodeFactory
692+
abstract static class PyTruffleWriteUnraisable extends PythonBuiltinNode {
693+
694+
@Specialization
695+
Object run(PBaseException exception, Object object,
696+
@Cached WriteUnraisableNode writeUnraisableNode) {
697+
writeUnraisableNode.execute(null, exception, null, (object instanceof PNone) ? PNone.NONE : object);
698+
return PNone.NO_VALUE;
699+
}
700+
}
701+
689702
@Builtin(name = "PyUnicode_FromString", minNumOfPositionalArgs = 1)
690703
@GenerateNodeFactory
691704
abstract static class PyUnicodeFromStringNode extends PythonBuiltinNode {
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
/*
2+
* Copyright (c) 2020, Oracle and/or its affiliates. All rights reserved.
3+
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4+
*
5+
* The Universal Permissive License (UPL), Version 1.0
6+
*
7+
* Subject to the condition set forth below, permission is hereby granted to any
8+
* person obtaining a copy of this software, associated documentation and/or
9+
* data (collectively the "Software"), free of charge and under any and all
10+
* copyright rights in the Software, and any and all patent rights owned or
11+
* freely licensable by each licensor hereunder covering either (i) the
12+
* unmodified Software as contributed to or provided by such licensor, or (ii)
13+
* the Larger Works (as defined below), to deal in both
14+
*
15+
* (a) the Software, and
16+
*
17+
* (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if
18+
* one is included with the Software each a "Larger Work" to which the Software
19+
* is contributed by such licensors),
20+
*
21+
* without restriction, including without limitation the rights to copy, create
22+
* derivative works of, display, perform, and distribute the Software and make,
23+
* use, sell, offer for sale, import, export, have made, and have sold the
24+
* Software and the Larger Work(s), and to sublicense the foregoing rights on
25+
* either these or other terms.
26+
*
27+
* This license is subject to the following condition:
28+
*
29+
* The above copyright notice and either this complete permission notice or at a
30+
* minimum a reference to the UPL must be included in all copies or substantial
31+
* portions of the Software.
32+
*
33+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
34+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
35+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
36+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
37+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
38+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
39+
* SOFTWARE.
40+
*/
41+
package com.oracle.graal.python.nodes;
42+
43+
import com.oracle.graal.python.PythonLanguage;
44+
import com.oracle.graal.python.builtins.objects.PNone;
45+
import com.oracle.graal.python.builtins.objects.exception.GetExceptionTracebackNode;
46+
import com.oracle.graal.python.builtins.objects.exception.PBaseException;
47+
import com.oracle.graal.python.builtins.objects.module.PythonModule;
48+
import com.oracle.graal.python.builtins.objects.object.PythonObjectLibrary;
49+
import com.oracle.graal.python.nodes.attributes.GetAttributeNode;
50+
import com.oracle.graal.python.nodes.call.CallNode;
51+
import com.oracle.graal.python.runtime.PythonContext;
52+
import com.oracle.graal.python.runtime.exception.PException;
53+
import com.oracle.graal.python.runtime.object.PythonObjectFactory;
54+
import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary;
55+
import com.oracle.truffle.api.TruffleLanguage.ContextReference;
56+
import com.oracle.truffle.api.dsl.Cached;
57+
import com.oracle.truffle.api.dsl.CachedContext;
58+
import com.oracle.truffle.api.dsl.ImportStatic;
59+
import com.oracle.truffle.api.dsl.Specialization;
60+
import com.oracle.truffle.api.frame.VirtualFrame;
61+
import com.oracle.truffle.api.library.CachedLibrary;
62+
import com.oracle.truffle.api.nodes.Node;
63+
64+
@ImportStatic(BuiltinNames.class)
65+
public abstract class WriteUnraisableNode extends Node {
66+
static final String UNRAISABLE_HOOK_ARGUMENTS_CLASS = "__UnraisableHookArgs";
67+
68+
public abstract void execute(VirtualFrame frame, PBaseException exception, String message, Object object);
69+
70+
@Specialization(limit = "1")
71+
static void writeUnraisable(VirtualFrame frame, PBaseException exception, String message, Object object,
72+
@CachedContext(PythonLanguage.class) ContextReference<PythonContext> contextRef,
73+
@CachedLibrary("exception") PythonObjectLibrary lib,
74+
@Cached PythonObjectFactory factory,
75+
@Cached GetExceptionTracebackNode getExceptionTracebackNode,
76+
@Cached("create(UNRAISABLEHOOK)") GetAttributeNode getUnraisableHook,
77+
@Cached CallNode callUnraisableHook,
78+
@Cached("create(UNRAISABLE_HOOK_ARGUMENTS_CLASS)") GetAttributeNode getArgumentsFactory,
79+
@Cached CallNode callArgumentsFactory) {
80+
try {
81+
PythonModule sysModule = contextRef.get().getCore().lookupBuiltinModule("sys");
82+
Object unraisablehook = getUnraisableHook.executeObject(frame, sysModule);
83+
Object argumentsFactory = getArgumentsFactory.executeObject(frame, sysModule);
84+
Object exceptionType = lib.getLazyPythonClass(exception);
85+
Object traceback = getExceptionTracebackNode.execute(frame, exception);
86+
if (traceback == null) {
87+
traceback = PNone.NONE;
88+
}
89+
Object messageObj = PNone.NONE;
90+
if (message != null) {
91+
messageObj = formatMessage(message);
92+
}
93+
Object hookArguments = callArgumentsFactory.execute(frame, argumentsFactory,
94+
factory.createTuple(new Object[]{exceptionType, exception, traceback, messageObj, object != null ? object : PNone.NONE}));
95+
callUnraisableHook.execute(frame, unraisablehook, hookArguments);
96+
} catch (PException e) {
97+
ignoreException(message);
98+
}
99+
}
100+
101+
@TruffleBoundary
102+
private static void ignoreException(String message) {
103+
if (message != null) {
104+
System.err.println(formatMessage(message));
105+
} else {
106+
System.err.println("Exception ignored in sys.unraisablehook");
107+
}
108+
}
109+
110+
@TruffleBoundary
111+
private static Object formatMessage(String message) {
112+
return "Exception ignored " + message;
113+
}
114+
115+
public static WriteUnraisableNode create() {
116+
return WriteUnraisableNodeGen.create();
117+
}
118+
}

graalpython/com.oracle.graal.python/src/com/oracle/graal/python/nodes/generator/YieldFromNode.java

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
import com.oracle.graal.python.builtins.objects.exception.PBaseException;
4646
import com.oracle.graal.python.builtins.objects.function.PArguments;
4747
import com.oracle.graal.python.builtins.objects.traceback.GetTracebackNode;
48+
import com.oracle.graal.python.nodes.WriteUnraisableNode;
4849
import com.oracle.graal.python.nodes.attributes.GetAttributeNode;
4950
import com.oracle.graal.python.nodes.call.CallNode;
5051
import com.oracle.graal.python.nodes.control.GetIteratorExpressionNode.GetIteratorNode;
@@ -76,6 +77,7 @@ public class YieldFromNode extends AbstractYieldNode implements GeneratorControl
7677
@Child private CallNode callSendNode;
7778

7879
@Child private GetTracebackNode getTracebackNode;
80+
@Child private WriteUnraisableNode writeUnraisableNode;
7981

8082
@Child private IsBuiltinClassProfile stopIterProfile1 = IsBuiltinClassProfile.create();
8183
@Child private IsBuiltinClassProfile stopIterProfile2 = IsBuiltinClassProfile.create();
@@ -143,8 +145,7 @@ public Object execute(VirtualFrame frame) {
143145
close = getGetCloseNode().executeObject(frame, _i);
144146
} catch (PException pe) {
145147
if (!hasNoCloseProfile.profileException(pe, PythonBuiltinClassType.AttributeError)) {
146-
// TODO msimacek: CPython writes the exception (!=AttributeError) as
147-
// unraisable and discards it
148+
ensureWriteUnraisable().execute(frame, pe.setCatchingFrameAndGetEscapedException(frame), null, _i);
148149
}
149150
}
150151
if (close != null) {
@@ -286,6 +287,14 @@ private GetTracebackNode ensureGetTracebackNode() {
286287
return getTracebackNode;
287288
}
288289

290+
private WriteUnraisableNode ensureWriteUnraisable() {
291+
if (writeUnraisableNode == null) {
292+
CompilerDirectives.transferToInterpreterAndInvalidate();
293+
writeUnraisableNode = insert(WriteUnraisableNode.create());
294+
}
295+
return writeUnraisableNode;
296+
}
297+
289298
public void setIteratorSlot(int slot) {
290299
this.iteratorSlot = slot;
291300
}

graalpython/lib-graalpython/python_cext.py

Lines changed: 8 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1243,38 +1243,13 @@ def _handle_system_exit():
12431243
def PyErr_WriteUnraisable(obj):
12441244
fetched = PyErr_Fetch()
12451245
typ, val, tb = fetched if fetched is not native_null else (None, None, None)
1246-
try:
1247-
if sys.stderr is None:
1248-
return
1249-
1250-
if obj:
1251-
obj_str_arg = None
1252-
try:
1253-
obj_str_arg = repr(obj)
1254-
except:
1255-
obj_str_arg = "<object repr() failed>"
1256-
sys.stderr.write("Exception ignored in: %s\n" % obj_str_arg)
1257-
1258-
try:
1259-
import tb
1260-
tb.print_tb(tb, file=sys.stderr)
1261-
except:
1262-
pass
1263-
1264-
if not typ:
1265-
return
1266-
1267-
if typ.__module__ is None or typ.__name__ is None:
1268-
sys.stderr.write("<unknown>")
1269-
1270-
str_exc = None
1271-
try:
1272-
str_exc = str(obj)
1273-
except:
1274-
str_exc = "<exception str() failed>"
1275-
sys.stderr.write("%s.%s: %s" % (typ.__module__, typ.__name__, str_exc))
1276-
except:
1277-
pass
1246+
if val is None:
1247+
# This means an invalid call, but this function is not supposed to raise exceptions
1248+
return
1249+
if tb is native_null:
1250+
tb = None
1251+
val.__traceback__ = tb
1252+
PyTruffle_WriteUnraisable(val, obj)
12781253

12791254

12801255
def _is_exception_class(exc):
@@ -1534,4 +1509,4 @@ def PyEval_GetBuiltins():
15341509

15351510

15361511
def sequence_clear():
1537-
return 0
1512+
return 0

graalpython/lib-graalpython/sys.py

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,16 @@ def make_hash_info_class():
135135
del make_hash_info_class
136136

137137

138+
def make_unraisable_hook_args_class():
139+
from _descriptor import make_named_tuple_class
140+
return make_named_tuple_class(
141+
"UnraisableHookArgs",
142+
["exc_type", "exc_value", "exc_traceback", "err_msg", "object"],
143+
)
144+
__UnraisableHookArgs = make_unraisable_hook_args_class()
145+
del make_unraisable_hook_args_class
146+
147+
138148
meta_path = []
139149
path_hooks = []
140150
path_importer_cache = {}
@@ -205,8 +215,22 @@ def __print_traceback__(typ, value, tb):
205215

206216
def make_unraisablehook():
207217
def __unraisablehook__(unraisable, /):
208-
# We don't currently use it and there's no way to construct the parameter in python code (internal type)
209-
pass
218+
try:
219+
if unraisable.object:
220+
try:
221+
r = repr(unraisable.object)
222+
except Exception:
223+
r = "<object repr() failed>"
224+
if unraisable.err_msg:
225+
print(f"{unraisable.err_msg}: {r}", file=stderr)
226+
else:
227+
print(f"Exception ignored in: {r}", file=stderr)
228+
elif unraisable.err_msg:
229+
print(f"{unraisable.err_msg}:", file=stderr)
230+
except BaseException:
231+
# let it fall through to the exception printer
232+
pass
233+
__excepthook__(unraisable.exc_type, unraisable.exc_value, unraisable.exc_traceback)
210234

211235
return __unraisablehook__
212236

0 commit comments

Comments
 (0)