Skip to content

Commit 950eba8

Browse files
committed
Support JLine in input builtin
1 parent dd3bedc commit 950eba8

File tree

4 files changed

+72
-11
lines changed

4 files changed

+72
-11
lines changed

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2017, 2024, Oracle and/or its affiliates. All rights reserved.
2+
* Copyright (c) 2017, 2025, 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
@@ -58,7 +58,7 @@
5858
* The interface to a source of input/output for the context, which may have different
5959
* implementations for different contexts.
6060
*/
61-
abstract class ConsoleHandler {
61+
public abstract class ConsoleHandler {
6262

6363
/**
6464
* Read a line of input, newline is <b>NOT</b> included in result.

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import java.io.IOException;
3434
import java.io.InputStream;
3535
import java.io.OutputStream;
36+
import java.lang.invoke.MethodHandles;
3637
import java.lang.management.ManagementFactory;
3738
import java.nio.file.Files;
3839
import java.nio.file.InvalidPathException;
@@ -56,6 +57,7 @@
5657
import org.graalvm.polyglot.Context;
5758
import org.graalvm.polyglot.Context.Builder;
5859
import org.graalvm.polyglot.Engine;
60+
import org.graalvm.polyglot.HostAccess;
5961
import org.graalvm.polyglot.PolyglotException;
6062
import org.graalvm.polyglot.Source;
6163
import org.graalvm.polyglot.Value;
@@ -826,6 +828,11 @@ protected void launch(Builder contextBuilder) {
826828
contextBuilder.engine(Engine.newBuilder().allowExperimentalOptions(true).options(enginePolyglotOptions).build());
827829
}
828830

831+
if (GraalPythonMain.class.getModule().isNamed()) {
832+
// Needed so that we can access JLine callback via interop
833+
contextBuilder.extendHostAccess(HostAccess.ALL, builder -> builder.useModuleLookup(MethodHandles.lookup()));
834+
}
835+
829836
int rc = 1;
830837
try (Context context = contextBuilder.build()) {
831838
runVersionAction(versionAction, context.getEngine());
@@ -1201,6 +1208,7 @@ private int readEvalPrint(Context context, ConsoleHandler consoleHandler) {
12011208
try {
12021209
setupREPL(context, consoleHandler);
12031210
Value sys = evalInternal(context, "import sys; sys");
1211+
sys.putMember("_console_handler", consoleHandler);
12041212

12051213
while (true) { // processing inputs
12061214
boolean doEcho = doEcho(context);

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2017, 2024, Oracle and/or its affiliates. All rights reserved.
2+
* Copyright (c) 2017, 2025, 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
@@ -72,7 +72,7 @@
7272
import org.graalvm.shadowed.org.jline.terminal.Terminal;
7373
import org.graalvm.shadowed.org.jline.terminal.TerminalBuilder;
7474

75-
class JLineConsoleHandler extends ConsoleHandler {
75+
public class JLineConsoleHandler extends ConsoleHandler {
7676
private final boolean noPrompt;
7777
private final Terminal terminal;
7878
private LineReader reader;

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

Lines changed: 60 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@
2626
package com.oracle.graal.python.builtins.modules;
2727

2828
import static com.oracle.graal.python.builtins.PythonBuiltinClassType.EOFError;
29+
import static com.oracle.graal.python.builtins.PythonBuiltinClassType.KeyboardInterrupt;
30+
import static com.oracle.graal.python.builtins.PythonBuiltinClassType.OSError;
2931
import static com.oracle.graal.python.builtins.PythonBuiltinClassType.RuntimeError;
3032
import static com.oracle.graal.python.builtins.PythonBuiltinClassType.StopIteration;
3133
import static com.oracle.graal.python.builtins.PythonBuiltinClassType.SyntaxError;
@@ -86,6 +88,7 @@
8688
import static com.oracle.graal.python.nodes.BuiltinNames.T___GRAALPYTHON__;
8789
import static com.oracle.graal.python.nodes.PGuards.isNoValue;
8890
import static com.oracle.graal.python.nodes.SpecialAttributeNames.T___DICT__;
91+
import static com.oracle.graal.python.nodes.SpecialMethodNames.T_FILENO;
8992
import static com.oracle.graal.python.nodes.SpecialMethodNames.T___FORMAT__;
9093
import static com.oracle.graal.python.nodes.SpecialMethodNames.T___MRO_ENTRIES__;
9194
import static com.oracle.graal.python.nodes.SpecialMethodNames.T___ROUND__;
@@ -178,6 +181,7 @@
178181
import com.oracle.graal.python.lib.PyEvalGetLocals;
179182
import com.oracle.graal.python.lib.PyIterCheckNode;
180183
import com.oracle.graal.python.lib.PyIterNextNode;
184+
import com.oracle.graal.python.lib.PyLongAsLongNode;
181185
import com.oracle.graal.python.lib.PyMappingCheckNode;
182186
import com.oracle.graal.python.lib.PyNumberAbsoluteNode;
183187
import com.oracle.graal.python.lib.PyNumberAddNode;
@@ -258,7 +262,9 @@
258262
import com.oracle.graal.python.pegparser.sst.ModTy;
259263
import com.oracle.graal.python.pegparser.tokenizer.SourceRange;
260264
import com.oracle.graal.python.runtime.ExecutionContext.IndirectCallContext;
265+
import com.oracle.graal.python.runtime.GilNode;
261266
import com.oracle.graal.python.runtime.IndirectCallData;
267+
import com.oracle.graal.python.runtime.PosixSupportLibrary;
262268
import com.oracle.graal.python.runtime.PythonContext;
263269
import com.oracle.graal.python.runtime.PythonOptions;
264270
import com.oracle.graal.python.runtime.exception.PException;
@@ -298,6 +304,7 @@
298304
import com.oracle.truffle.api.exception.AbstractTruffleException;
299305
import com.oracle.truffle.api.frame.Frame;
300306
import com.oracle.truffle.api.frame.VirtualFrame;
307+
import com.oracle.truffle.api.interop.InteropException;
301308
import com.oracle.truffle.api.interop.InteropLibrary;
302309
import com.oracle.truffle.api.interop.UnsupportedMessageException;
303310
import com.oracle.truffle.api.library.CachedLibrary;
@@ -2628,20 +2635,21 @@ static Object input(VirtualFrame frame, Object prompt,
26282635
}
26292636

26302637
@TruffleBoundary
2631-
private static Object doInput(Object prompt, Node inliningTarget, PythonContext context) {
2638+
@SuppressWarnings("try")
2639+
private static Object doInput(Object prompt, Node node, PythonContext context) {
26322640
PythonModule sysModule = context.getSysModule();
26332641
Object stdin = PyObjectLookupAttr.executeUncached(sysModule, T_STDIN);
26342642
Object stdout = PyObjectLookupAttr.executeUncached(sysModule, T_STDOUT);
26352643
Object stderr = PyObjectLookupAttr.executeUncached(sysModule, T_STDERR);
26362644

26372645
if (stdin instanceof PNone) {
2638-
throw PRaiseNode.raiseStatic(inliningTarget, RuntimeError, ErrorMessages.INPUT_LOST_SYS_S, T_STDIN);
2646+
throw PRaiseNode.raiseStatic(node, RuntimeError, ErrorMessages.INPUT_LOST_SYS_S, T_STDIN);
26392647
}
26402648
if (stdout instanceof PNone) {
2641-
throw PRaiseNode.raiseStatic(inliningTarget, RuntimeError, ErrorMessages.INPUT_LOST_SYS_S, T_STDOUT);
2649+
throw PRaiseNode.raiseStatic(node, RuntimeError, ErrorMessages.INPUT_LOST_SYS_S, T_STDOUT);
26422650
}
26432651
if (stderr instanceof PNone) {
2644-
throw PRaiseNode.raiseStatic(inliningTarget, RuntimeError, ErrorMessages.INPUT_LOST_SYS_S, T_STDERR);
2652+
throw PRaiseNode.raiseStatic(node, RuntimeError, ErrorMessages.INPUT_LOST_SYS_S, T_STDERR);
26452653
}
26462654

26472655
SysModuleBuiltins.AuditNode.auditUncached("builtins.input", prompt != NO_VALUE ? prompt : NONE);
@@ -2652,6 +2660,51 @@ private static Object doInput(Object prompt, Node inliningTarget, PythonContext
26522660
// Ignore
26532661
}
26542662

2663+
Object consoleHandler = PyObjectLookupAttr.executeUncached(sysModule, tsLiteral("_console_handler"));
2664+
if (!(consoleHandler instanceof PNone)) {
2665+
boolean tty = false;
2666+
try {
2667+
long fileno = PyLongAsLongNode.executeUncached(PyObjectCallMethodObjArgs.executeUncached(stdin, T_FILENO));
2668+
if (fileno == 0) {
2669+
tty = PosixSupportLibrary.getUncached().isatty(context.getPosixSupport(), 0);
2670+
}
2671+
} catch (AbstractTruffleException e) {
2672+
// not a tty
2673+
}
2674+
if (tty) {
2675+
InteropLibrary lib = InteropLibrary.getUncached();
2676+
try {
2677+
boolean havePrompt = !(prompt instanceof PNone);
2678+
if (havePrompt) {
2679+
TruffleString promptStr = PyObjectStrAsTruffleStringNode.executeUncached(prompt);
2680+
lib.invokeMember(consoleHandler, "setPrompt", promptStr);
2681+
}
2682+
try (var gil = GilNode.uncachedRelease()) {
2683+
// TODO can we make it interruptible?
2684+
Object line;
2685+
try {
2686+
line = lib.invokeMember(consoleHandler, "readLine", havePrompt);
2687+
} catch (AbstractTruffleException e) {
2688+
try {
2689+
if ("UserInterruptException".equals(lib.asString(lib.getMetaSimpleName(lib.getMetaObject(e))))) {
2690+
throw PRaiseNode.raiseStatic(node, KeyboardInterrupt);
2691+
}
2692+
} catch (InteropException ex) {
2693+
// Fallthrough
2694+
}
2695+
throw PRaiseNode.raiseStatic(node, OSError, ErrorMessages.M, e);
2696+
}
2697+
if (lib.isNull(line)) {
2698+
throw PRaiseNode.raiseStatic(node, EOFError);
2699+
}
2700+
return lib.asTruffleString(line).switchEncodingUncached(TS_ENCODING);
2701+
}
2702+
} catch (InteropException e) {
2703+
// fall back to simple read
2704+
}
2705+
}
2706+
}
2707+
26552708
if (!(prompt instanceof PNone)) {
26562709
Object promptStr = PyObjectStrAsObjectNode.executeUncached(prompt);
26572710
PyObjectCallMethodObjArgs.executeUncached(stdout, T_WRITE, promptStr);
@@ -2667,7 +2720,7 @@ private static Object doInput(Object prompt, Node inliningTarget, PythonContext
26672720
TruffleString strLine = CastToTruffleStringNode.castKnownStringUncached(line);
26682721
int len = strLine.codePointLengthUncached(TS_ENCODING);
26692722
if (len == 0) {
2670-
throw PRaiseNode.raiseStatic(inliningTarget, EOFError, ErrorMessages.EOF_WHEN_READING_A_LINE);
2723+
throw PRaiseNode.raiseStatic(node, EOFError, ErrorMessages.EOF_WHEN_READING_A_LINE);
26712724
}
26722725
int lastChar = strLine.codePointAtIndexUncached(len - 1, TS_ENCODING);
26732726
if (lastChar == '\n') {
@@ -2677,7 +2730,7 @@ private static Object doInput(Object prompt, Node inliningTarget, PythonContext
26772730
} else if (PyBytesCheckNode.executeUncached(line)) {
26782731
byte[] bytesLine = PythonBufferAccessLibrary.getUncached().getCopiedByteArray(BytesNodes.GetBytesStorage.executeUncached(line));
26792732
if (bytesLine.length == 0) {
2680-
throw PRaiseNode.raiseStatic(inliningTarget, EOFError, ErrorMessages.EOF_WHEN_READING_A_LINE);
2733+
throw PRaiseNode.raiseStatic(node, EOFError, ErrorMessages.EOF_WHEN_READING_A_LINE);
26812734
}
26822735
PythonLanguage language = context.getLanguage();
26832736
if (bytesLine[bytesLine.length - 1] == '\n') {
@@ -2686,7 +2739,7 @@ private static Object doInput(Object prompt, Node inliningTarget, PythonContext
26862739
return PFactory.createBytes(language, bytesLine);
26872740
}
26882741
} else {
2689-
throw PRaiseNode.raiseStatic(inliningTarget, TypeError, ErrorMessages.OBJECT_READLINE_RETURNED_NON_STRING);
2742+
throw PRaiseNode.raiseStatic(node, TypeError, ErrorMessages.OBJECT_READLINE_RETURNED_NON_STRING);
26902743
}
26912744
}
26922745
}

0 commit comments

Comments
 (0)