Skip to content

Commit 625a9de

Browse files
committed
[GR-55263] Fix swallowing __repr__ exceptions in REPL
PullRequest: graalpython/3415
2 parents f207af3 + 01336b5 commit 625a9de

File tree

6 files changed

+222
-19
lines changed

6 files changed

+222
-19
lines changed

graalpython/com.oracle.graal.python.shell/src/com/oracle/graal/python/shell/GraalPythonMain.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1253,7 +1253,7 @@ private int readEvalPrint(Context context, ConsoleHandler consoleHandler) {
12531253
} catch (EOFException e) {
12541254
if (!noSite) {
12551255
try {
1256-
evalInternal(context, "import site; exit()\n");
1256+
context.eval(Source.newBuilder(getLanguageId(), "import site; exit()\n", "<internal>").internal(true).interactive(true).buildLiteral());
12571257
} catch (PolyglotException e2) {
12581258
if (e2.isExit()) {
12591259
// don't use the exit code from the PolyglotException
Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
# Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved.
2+
# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
3+
#
4+
# The Universal Permissive License (UPL), Version 1.0
5+
#
6+
# Subject to the condition set forth below, permission is hereby granted to any
7+
# person obtaining a copy of this software, associated documentation and/or
8+
# data (collectively the "Software"), free of charge and under any and all
9+
# copyright rights in the Software, and any and all patent rights owned or
10+
# freely licensable by each licensor hereunder covering either (i) the
11+
# unmodified Software as contributed to or provided by such licensor, or (ii)
12+
# the Larger Works (as defined below), to deal in both
13+
#
14+
# (a) the Software, and
15+
#
16+
# (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if
17+
# one is included with the Software each a "Larger Work" to which the Software
18+
# is contributed by such licensors),
19+
#
20+
# without restriction, including without limitation the rights to copy, create
21+
# derivative works of, display, perform, and distribute the Software and make,
22+
# use, sell, offer for sale, import, export, have made, and have sold the
23+
# Software and the Larger Work(s), and to sublicense the foregoing rights on
24+
# either these or other terms.
25+
#
26+
# This license is subject to the following condition:
27+
#
28+
# The above copyright notice and either this complete permission notice or at a
29+
# minimum a reference to the UPL must be included in all copies or substantial
30+
# portions of the Software.
31+
#
32+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
33+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
34+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
35+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
36+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
37+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
38+
# SOFTWARE.
39+
40+
import sys
41+
42+
if sys.platform != 'win32' and (sys.implementation.name != 'graalpy' or not __graalpython__.is_managed_launcher()):
43+
import os
44+
import re
45+
import select
46+
import subprocess
47+
import tempfile
48+
import termios
49+
from textwrap import dedent
50+
51+
52+
def validate_repl(stdin, python_args=(), ignore_preamble=True):
53+
env = os.environ.copy()
54+
env['TERM'] = 'ansi'
55+
env['PYTHONIOENCODING'] = 'utf-8'
56+
pty_parent, pty_child = os.openpty()
57+
try:
58+
termios.tcsetwinsize(pty_parent, (60, 80))
59+
proc = subprocess.Popen(
60+
[sys.executable, '-I', *python_args],
61+
env=env,
62+
stdin=pty_child,
63+
stdout=pty_child,
64+
stderr=pty_child,
65+
)
66+
out = ''
67+
input_and_output = []
68+
expected_preamble = ''
69+
in_matches = list(re.finditer(r'^(>>>|\.\.\.) (.*)', stdin, flags=re.MULTILINE))
70+
for i, match in enumerate(in_matches):
71+
if i == 0:
72+
expected_preamble = stdin[:match.start() - 1] if match.start() else ''
73+
input_and_output.append((
74+
match.group(1),
75+
match.group(2),
76+
stdin[match.end():in_matches[i + 1].start() - 1 if i + 1 < len(in_matches) else -1],
77+
))
78+
index = -1
79+
whole_out = ''
80+
while True:
81+
rlist, _, _ = select.select([pty_parent], [], [], 30)
82+
assert pty_parent in rlist, f"Timed out waiting for REPL output. Output: {whole_out}{out}"
83+
out += os.read(pty_parent, 1024).decode('utf-8')
84+
out = out.replace('\r\n', '\n')
85+
out = re.sub(r'\x1b\[(?:\?2004[hl]|\d+[A-G])', '', out)
86+
if out == '>>> ' or out.endswith(('\n>>> ', '\n... ')):
87+
prompt = out[:3]
88+
actual = out[:-5]
89+
if index >= 0:
90+
expected_prompt, current_in, expected_out = input_and_output[index]
91+
assert prompt == expected_prompt
92+
expected = f'{expected_prompt} {current_in}{expected_out}'
93+
else:
94+
expected = expected_preamble
95+
if index >= 0 or not ignore_preamble:
96+
assert actual == expected, f'Actual:\n{actual!r}\nExpected:\n{expected!r}'
97+
index += 1
98+
whole_out += out[:-4]
99+
out = out[-4:]
100+
if index >= len(input_and_output):
101+
os.write(pty_parent, b'\x04') # CTRL-D
102+
proc.wait(timeout=30)
103+
out = os.read(pty_parent, 1024).decode('utf-8')
104+
out = re.sub(r'\x1b\[\?2004[hl]', '', out)
105+
assert not out.strip(), f"Garbage after EOF:\n{out!r}"
106+
return
107+
else:
108+
_, next_in, _ = input_and_output[index]
109+
os.write(pty_parent, next_in.encode('utf-8') + b'\r')
110+
finally:
111+
os.close(pty_child)
112+
os.close(pty_parent)
113+
114+
115+
def test_basic_repl():
116+
validate_repl(dedent("""\
117+
>>> 1023 + 1
118+
1024
119+
>>> None
120+
>>> "hello"
121+
'hello'
122+
>>> _
123+
'hello'
124+
"""))
125+
126+
127+
def test_continuation():
128+
validate_repl(dedent(r'''\
129+
>>> def foo():
130+
... a = 1
131+
... return a
132+
...
133+
>>> class Foo:
134+
... def meth(self):
135+
... return 1
136+
...
137+
>>> from functools import wraps
138+
>>> @wraps
139+
... def foo(fn):
140+
... return fn
141+
...
142+
>>> from contextlib import contextmanager
143+
>>> @contextmanager
144+
... class Foo:
145+
... pass
146+
...
147+
>>> """
148+
... asdf
149+
... """
150+
'\nasdf\n'
151+
'''))
152+
153+
154+
def test_exceptions():
155+
validate_repl(dedent("""\
156+
>>> 1 / 0
157+
Traceback (most recent call last):
158+
File "<stdin>", line 1, in <module>
159+
ZeroDivisionError: division by zero
160+
>>> import sys
161+
>>> sys.last_value
162+
ZeroDivisionError('division by zero')
163+
>>> class BrokenRepr:
164+
... def __repr__(self):
165+
... asdf
166+
...
167+
>>> BrokenRepr()
168+
Traceback (most recent call last):
169+
File "<stdin>", line 1, in <module>
170+
File "<stdin>", line 3, in __repr__
171+
NameError: name 'asdf' is not defined
172+
"""))
173+
174+
175+
def test_inspect_flag():
176+
with tempfile.NamedTemporaryFile('w') as f:
177+
f.write('a = 1\n')
178+
f.flush()
179+
validate_repl(dedent("""\
180+
>>> a
181+
1
182+
"""), python_args=['-i', f.name], ignore_preamble=False)
183+
184+
185+
def test_inspect_flag_exit():
186+
with tempfile.NamedTemporaryFile('w') as f:
187+
f.write('a = 1\nimport sys\nsys.exit(1)\n')
188+
f.flush()
189+
validate_repl(dedent(f"""\
190+
Traceback (most recent call last):
191+
File "{os.path.realpath(f.name)}", line 3, in <module>
192+
sys.exit(1)
193+
SystemExit: 1
194+
>>> a
195+
1
196+
"""), python_args=['-i', f.name], ignore_preamble=False)

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

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1689,23 +1689,13 @@ static Object doHook(VirtualFrame frame, PythonModule sys, Object obj,
16891689
throw raiseNode.get(inliningTarget).raise(RuntimeError, LOST_S, "sys.stdout");
16901690
}
16911691

1692-
boolean reprWriteOk = false;
1693-
boolean unicodeEncodeError = false;
1692+
Object reprVal = null;
16941693
try {
1695-
Object reprVal = objectRepr(frame, inliningTarget, obj, reprAsObjectNode);
1696-
if (reprVal == null) {
1697-
reprWriteOk = false;
1698-
} else {
1699-
reprWriteOk = true;
1700-
fileWriteString(frame, inliningTarget, stdOut, castToStringNode.execute(inliningTarget, reprVal), getAttr, callNode);
1701-
}
1694+
reprVal = objectRepr(frame, inliningTarget, obj, reprAsObjectNode);
17021695
} catch (PException pe) {
17031696
pe.expect(inliningTarget, UnicodeEncodeError, unicodeEncodeErrorProfile);
17041697
// repr(o) is not encodable to sys.stdout.encoding with sys.stdout.errors error
17051698
// handler (which is probably 'strict')
1706-
unicodeEncodeError = true;
1707-
}
1708-
if (!reprWriteOk && unicodeEncodeError) {
17091699
// inlined sysDisplayHookUnencodable
17101700
final TruffleString stdoutEncoding = objectLookupAttrAsString(frame, inliningTarget, stdOut, T_ENCODING, lookupAttr, castToStringNode);
17111701
final Object reprStr = objectRepr(frame, inliningTarget, obj, reprAsObjectNode);
@@ -1720,6 +1710,9 @@ static Object doHook(VirtualFrame frame, PythonModule sys, Object obj,
17201710
fileWriteString(frame, inliningTarget, stdOut, castToStringNode.execute(inliningTarget, str), getAttr, callNode);
17211711
}
17221712
}
1713+
if (reprVal != null) {
1714+
fileWriteString(frame, inliningTarget, stdOut, castToStringNode.execute(inliningTarget, reprVal), getAttr, callNode);
1715+
}
17231716

17241717
fileWriteString(frame, inliningTarget, stdOut, T_NEWLINE, getAttr, callNode);
17251718
setAttr.execute(frame, inliningTarget, builtins, T___, obj);

graalpython/com.oracle.graal.python/src/com/oracle/graal/python/lib/PyTraceBackPrintNode.java

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2021, 2023, Oracle and/or its affiliates. All rights reserved.
2+
* Copyright (c) 2021, 2024, Oracle and/or its affiliates. All rights reserved.
33
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
44
*
55
* The Universal Permissive License (UPL), Version 1.0
@@ -151,11 +151,7 @@ public static Object objectRepr(VirtualFrame frame, Object value) {
151151
}
152152

153153
public static Object objectRepr(VirtualFrame frame, Node inliningTarget, Object value, PyObjectReprAsObjectNode reprAsObjectNode) {
154-
try {
155-
return reprAsObjectNode.execute(frame, inliningTarget, value);
156-
} catch (PException pe) {
157-
return null;
158-
}
154+
return reprAsObjectNode.execute(frame, inliningTarget, value);
159155
}
160156

161157
public static TruffleString castToString(Object value) {
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[[rules]]
2+
patch = "psycopg2-binary.patch"
3+
license = "LGPL-3.0-or-later WITH openssl-exception"
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
diff --git a/psycopg/utils.c b/psycopg/utils.c
2+
index 16be906..c78a24b 100644
3+
--- a/psycopg/utils.c
4+
+++ b/psycopg/utils.c
5+
@@ -392,7 +392,9 @@ psyco_set_error(PyObject *exc, cursorObject *curs, const char *msg)
6+
static int
7+
psyco_is_main_interp(void)
8+
{
9+
-#if PY_VERSION_HEX >= 0x03080000
10+
+#if GRAALVM_PYTHON
11+
+ return 1;
12+
+#elif PY_VERSION_HEX >= 0x03080000
13+
/* tested with Python 3.8.0a2 */
14+
return _PyInterpreterState_Get() == PyInterpreterState_Main();
15+
#else

0 commit comments

Comments
 (0)