Skip to content

Commit 7a50b27

Browse files
committed
[GR-12144] Usability fixes
PullRequest: graalpython/237
2 parents 4ef9ca7 + da98de0 commit 7a50b27

File tree

17 files changed

+822
-91
lines changed

17 files changed

+822
-91
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,12 @@
33
This changelog summarizes major changes between GraalVM versions of the Python
44
language runtime. The main focus is on user-observable behavior of the engine.
55

6+
## Version 1.0.0 RC9
7+
8+
* Support `help` in the builtin Python shell
9+
* Add `readline` to enable history and autocompletion in the Python shell
10+
* Improve display of foreign array-like objects
11+
612
## Version 1.0.0 RC8
713

814
* Report allocations when the `--memtracer` option is used

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

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,14 @@
4343
import java.io.IOException;
4444
import java.io.InputStream;
4545
import java.nio.charset.StandardCharsets;
46+
import java.util.List;
47+
import java.util.function.BiConsumer;
48+
import java.util.function.BooleanSupplier;
49+
import java.util.function.Consumer;
50+
import java.util.function.Function;
51+
import java.util.function.IntConsumer;
52+
import java.util.function.IntFunction;
53+
import java.util.function.IntSupplier;
4654

4755
import org.graalvm.polyglot.Context;
4856

@@ -59,10 +67,20 @@ public abstract class ConsoleHandler {
5967

6068
public abstract void setPrompt(String prompt);
6169

70+
public void addCompleter(@SuppressWarnings("unused") Function<String, List<String>> completer) {
71+
// ignore by default
72+
}
73+
6274
public void setContext(@SuppressWarnings("unused") Context context) {
6375
// ignore by default
6476
}
6577

78+
@SuppressWarnings("unused")
79+
public void setHistory(BooleanSupplier shouldRecord, IntSupplier getSize, Consumer<String> addItem, IntFunction<String> getItem, BiConsumer<Integer, String> setItem, IntConsumer removeItem,
80+
Runnable clear) {
81+
// ignore by default
82+
}
83+
6684
public InputStream createInputStream() {
6785
return new InputStream() {
6886
byte[] buffer = null;

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

Lines changed: 44 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
import org.graalvm.polyglot.PolyglotException.StackFrame;
4949
import org.graalvm.polyglot.Source;
5050
import org.graalvm.polyglot.SourceSection;
51+
import org.graalvm.polyglot.Value;
5152

5253
import jline.console.UserInterruptException;
5354

@@ -58,8 +59,6 @@ public static void main(String[] args) {
5859

5960
private static final String LANGUAGE_ID = "python";
6061
private static final Source QUIT_EOF = Source.newBuilder(LANGUAGE_ID, "import site\nexit()", "<exit-on-eof>").internal(true).buildLiteral();
61-
private static final Source GET_PROMPT = Source.newBuilder(LANGUAGE_ID, "import sys\nsys.ps1", "<prompt>").internal(true).buildLiteral();
62-
private static final Source GET_CONTINUE_PROMPT = Source.newBuilder(LANGUAGE_ID, "import sys\nsys.ps2", "<continue-prompt>").internal(true).buildLiteral();
6362

6463
private ArrayList<String> programArgs = null;
6564
private String commandString = null;
@@ -517,9 +516,14 @@ public ConsoleHandler createConsoleHandler(InputStream inStream, OutputStream ou
517516
public int readEvalPrint(Context context, ConsoleHandler consoleHandler) {
518517
int lastStatus = 0;
519518
try {
519+
setupReadline(context, consoleHandler);
520+
Value sys = context.eval(Source.create(getLanguageId(), "import sys; sys"));
521+
context.eval(Source.create(getLanguageId(), "del sys\ndel site\ndel readline"));
522+
520523
while (true) { // processing inputs
521524
boolean doEcho = doEcho(context);
522-
consoleHandler.setPrompt(doEcho ? getPrompt(context) : null);
525+
consoleHandler.setPrompt(doEcho ? sys.getMember("ps1").asString() : null);
526+
523527
try {
524528
String input = consoleHandler.readLine();
525529
if (input == null) {
@@ -538,7 +542,7 @@ public int readEvalPrint(Context context, ConsoleHandler consoleHandler) {
538542
context.eval(Source.newBuilder(getLanguageId(), sb.toString(), "<stdin>").interactive(true).buildLiteral());
539543
} catch (PolyglotException e) {
540544
if (continuePrompt == null) {
541-
continuePrompt = doEcho ? getContinuePrompt(context) : null;
545+
continuePrompt = doEcho ? sys.getMember("ps2").asString() : null;
542546
}
543547
if (e.isIncompleteSource()) {
544548
// read another line of input
@@ -587,6 +591,42 @@ public int readEvalPrint(Context context, ConsoleHandler consoleHandler) {
587591
}
588592
}
589593

594+
private void setupReadline(Context context, ConsoleHandler consoleHandler) {
595+
// First run nothing to trigger the setup of interactive mode (site import and so on)
596+
context.eval(Source.newBuilder(getLanguageId(), "None", "setup-interactive").interactive(true).buildLiteral());
597+
// Then we can get the readline module and see if any completers were registered and use its
598+
// history feature
599+
final Value readline = context.eval(Source.create(getLanguageId(), "import readline; readline"));
600+
final Value completer = readline.getMember("get_completer").execute();
601+
final Value shouldRecord = readline.getMember("get_auto_history");
602+
final Value addHistory = readline.getMember("add_history");
603+
final Value getHistoryItem = readline.getMember("get_history_item");
604+
final Value setHistoryItem = readline.getMember("replace_history_item");
605+
final Value deleteHistoryItem = readline.getMember("remove_history_item");
606+
final Value clearHistory = readline.getMember("clear_history");
607+
final Value getHistorySize = readline.getMember("get_current_history_length");
608+
consoleHandler.setHistory(
609+
() -> shouldRecord.execute().asBoolean(),
610+
() -> getHistorySize.execute().asInt(),
611+
(item) -> addHistory.execute(item),
612+
(pos) -> getHistoryItem.execute(pos).asString(),
613+
(pos, item) -> setHistoryItem.execute(pos, item),
614+
(pos) -> deleteHistoryItem.execute(pos),
615+
() -> clearHistory.execute());
616+
617+
if (completer.canExecute()) {
618+
consoleHandler.addCompleter((buffer) -> {
619+
List<String> candidates = new ArrayList<>();
620+
Value candidate = completer.execute(buffer, candidates.size());
621+
while (candidate.isString()) {
622+
candidates.add(candidate.asString());
623+
candidate = completer.execute(buffer, candidates.size());
624+
}
625+
return candidates;
626+
});
627+
}
628+
}
629+
590630
private static final class ExitException extends RuntimeException {
591631
private static final long serialVersionUID = 1L;
592632
private final int code;
@@ -599,26 +639,4 @@ private static final class ExitException extends RuntimeException {
599639
private static boolean doEcho(@SuppressWarnings("unused") Context context) {
600640
return true;
601641
}
602-
603-
private static String getPrompt(Context context) {
604-
try {
605-
return context.eval(GET_PROMPT).asString();
606-
} catch (PolyglotException e) {
607-
if (e.isExit()) {
608-
throw new ExitException(e.getExitStatus());
609-
}
610-
throw new RuntimeException("error while retrieving prompt", e);
611-
}
612-
}
613-
614-
private static String getContinuePrompt(Context context) {
615-
try {
616-
return context.eval(GET_CONTINUE_PROMPT).asString();
617-
} catch (PolyglotException e) {
618-
if (e.isExit()) {
619-
throw new ExitException(e.getExitStatus());
620-
}
621-
throw new RuntimeException("error while retrieving continue prompt", e);
622-
}
623-
}
624642
}

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

Lines changed: 163 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -40,16 +40,29 @@
4040
*/
4141
package com.oracle.graal.python.shell;
4242

43+
import java.io.IOException;
44+
import java.io.InputStream;
45+
import java.io.OutputStream;
46+
import java.util.Iterator;
47+
import java.util.List;
48+
import java.util.ListIterator;
49+
import java.util.function.BiConsumer;
50+
import java.util.function.BooleanSupplier;
51+
import java.util.function.Consumer;
52+
import java.util.function.Function;
53+
import java.util.function.IntConsumer;
54+
import java.util.function.IntFunction;
55+
import java.util.function.IntSupplier;
56+
57+
import org.graalvm.polyglot.Context;
58+
4359
import jline.console.ConsoleReader;
4460
import jline.console.UserInterruptException;
4561
import jline.console.completer.CandidateListCompletionHandler;
62+
import jline.console.completer.Completer;
4663
import jline.console.completer.CompletionHandler;
64+
import jline.console.history.History;
4765
import jline.console.history.MemoryHistory;
48-
import org.graalvm.polyglot.Context;
49-
50-
import java.io.IOException;
51-
import java.io.InputStream;
52-
import java.io.OutputStream;
5366

5467
public class JLineConsoleHandler extends ConsoleHandler {
5568
private final ConsoleReader console;
@@ -64,12 +77,157 @@ public JLineConsoleHandler(InputStream inStream, OutputStream outStream, boolean
6477
console.setHistory(history);
6578
console.setHandleUserInterrupt(true);
6679
console.setExpandEvents(false);
80+
console.setCommentBegin("#");
6781
} catch (IOException ex) {
6882
// TODO throw proper exception type
6983
throw new RuntimeException("unexpected error opening console reader", ex);
7084
}
7185
}
7286

87+
@Override
88+
public void addCompleter(Function<String, List<String>> completer) {
89+
console.addCompleter(new Completer() {
90+
public int complete(String buffer, int cursor, List<CharSequence> candidates) {
91+
if (buffer != null) {
92+
candidates.addAll(completer.apply(buffer));
93+
}
94+
return candidates.isEmpty() ? -1 : 0;
95+
}
96+
});
97+
98+
}
99+
100+
@Override
101+
public void setHistory(BooleanSupplier shouldRecord, IntSupplier getSize, Consumer<String> addItem, IntFunction<String> getItem, BiConsumer<Integer, String> setItem, IntConsumer removeItem,
102+
Runnable clear) {
103+
console.setHistory(new History() {
104+
private int pos = getSize.getAsInt();
105+
106+
public int size() {
107+
return getSize.getAsInt();
108+
}
109+
110+
public void set(int arg0, CharSequence arg1) {
111+
setItem.accept(arg0, arg1.toString());
112+
}
113+
114+
public void replace(CharSequence arg0) {
115+
if (pos < 0 || pos >= size()) {
116+
return;
117+
}
118+
setItem.accept(pos, arg0.toString());
119+
}
120+
121+
public CharSequence removeLast() {
122+
int t = size() - 1;
123+
String s = getItem.apply(t);
124+
removeItem.accept(t);
125+
return s;
126+
}
127+
128+
public CharSequence removeFirst() {
129+
int t = size() - 1;
130+
String s = getItem.apply(t);
131+
removeItem.accept(0);
132+
return s;
133+
}
134+
135+
public CharSequence remove(int arg0) {
136+
int t = size() - 1;
137+
String s = getItem.apply(t);
138+
removeItem.accept(arg0);
139+
return s;
140+
}
141+
142+
public boolean previous() {
143+
if (pos >= 0) {
144+
pos--;
145+
return true;
146+
} else {
147+
return false;
148+
}
149+
}
150+
151+
public boolean next() {
152+
if (pos < size()) {
153+
pos++;
154+
return true;
155+
} else {
156+
return false;
157+
}
158+
}
159+
160+
public boolean moveToLast() {
161+
pos = size();
162+
return true;
163+
}
164+
165+
public boolean moveToFirst() {
166+
pos = 0;
167+
return true;
168+
}
169+
170+
public void moveToEnd() {
171+
moveToLast();
172+
}
173+
174+
public boolean moveTo(int arg0) {
175+
pos = arg0;
176+
int size = size();
177+
if (pos < 0 || pos >= size) {
178+
pos = pos % size;
179+
return false;
180+
}
181+
return true;
182+
}
183+
184+
public Iterator<Entry> iterator() {
185+
// TODO Auto-generated method stub
186+
return null;
187+
}
188+
189+
public boolean isEmpty() {
190+
return size() == 0;
191+
}
192+
193+
public int index() {
194+
return pos;
195+
}
196+
197+
public CharSequence get(int arg0) {
198+
return getItem.apply(arg0);
199+
}
200+
201+
public ListIterator<Entry> entries(int arg0) {
202+
// TODO Auto-generated method stub
203+
return null;
204+
}
205+
206+
public ListIterator<Entry> entries() {
207+
// TODO Auto-generated method stub
208+
return null;
209+
}
210+
211+
public CharSequence current() {
212+
if (pos < 0 || pos >= size()) {
213+
return "";
214+
}
215+
return getItem.apply(pos);
216+
}
217+
218+
public void clear() {
219+
clear.run();
220+
}
221+
222+
public void add(CharSequence arg0) {
223+
if (shouldRecord.getAsBoolean()) {
224+
addItem.accept(arg0.toString());
225+
pos = size();
226+
}
227+
}
228+
});
229+
}
230+
73231
@Override
74232
public void setContext(Context context) {
75233
CompletionHandler completionHandler = console.getCompletionHandler();

graalpython/com.oracle.graal.python/src/com/oracle/graal/python/PythonLanguage.java

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
import com.oracle.graal.python.builtins.objects.PNone;
3939
import com.oracle.graal.python.builtins.objects.PythonAbstractObject;
4040
import com.oracle.graal.python.builtins.objects.code.PCode;
41+
import com.oracle.graal.python.builtins.objects.common.HashingStorage;
4142
import com.oracle.graal.python.builtins.objects.function.PArguments;
4243
import com.oracle.graal.python.builtins.objects.function.PBuiltinFunction;
4344
import com.oracle.graal.python.builtins.objects.function.PKeyword;
@@ -231,7 +232,7 @@ protected CallTarget parse(ParsingRequest request) throws Exception {
231232

232233
// if we are running the interpreter, module 'site' is automatically imported
233234
if (source.isInteractive()) {
234-
Truffle.getRuntime().createCallTarget(new TopLevelExceptionHandler(this, doParse(pythonCore, Source.newBuilder(ID, "import site", "<site import>").build()))).call();
235+
runInteractiveStartup(pythonCore);
235236
}
236237
}
237238
RootNode root = doParse(pythonCore, source);
@@ -242,6 +243,16 @@ protected CallTarget parse(ParsingRequest request) throws Exception {
242243
}
243244
}
244245

246+
private void runInteractiveStartup(PythonCore pythonCore) {
247+
PythonContext context = pythonCore.getContext();
248+
HashingStorage sysModules = context.getImportedModules().getDictStorage();
249+
String siteModuleName = "site";
250+
if (!sysModules.hasKey(siteModuleName, HashingStorage.getSlowPathEquivalence(siteModuleName))) {
251+
Source src = Source.newBuilder(ID, "import site\nimport sys\ngetattr(sys, '__interactivehook__', lambda: None)()\n", "<site import>").build();
252+
Truffle.getRuntime().createCallTarget(new TopLevelExceptionHandler(this, doParse(pythonCore, src))).call();
253+
}
254+
}
255+
245256
private RootNode doParse(PythonCore pythonCore, Source source) {
246257
try {
247258
return (RootNode) pythonCore.getParser().parse(source.isInteractive() ? ParserMode.InteractiveStatement : ParserMode.File, pythonCore, source, null);

0 commit comments

Comments
 (0)