[Jython-checkins] jython: Upgrade to JLine2 and add tab completion support

jim.baker jython-checkins at python.org
Sat Jan 17 05:50:36 CET 2015


https://hg.python.org/jython/rev/ca92ffdff93f
changeset:   7532:ca92ffdff93f
user:        Jim Baker <jim.baker at rackspace.com>
date:        Fri Jan 16 21:49:07 2015 -0700
summary:
  Upgrade to JLine2 and add tab completion support

Fixes getpass.getpass so that user input is not echoed.

Should provide better support for help on Windows in a future fix
(http://bugs.jython.org/issue2209)

Adds tab completion support, much like Python 3.4
(http://bugs.python.org/issue5845). A further fix is needed to see if
we can get tab insertion after whitespace. Note that JLine2 really
wants the tab key to map to a completer.

Fixes http://bugs.jython.org/issue2092

files:
  Lib/getpass.py                                   |   23 +-
  Lib/readline.py                                  |   49 +---
  Lib/site.py                                      |    9 +
  build.xml                                        |    6 +-
  extlibs/jline-1.0.jar                            |  Bin 
  extlibs/jline-2.12.jar                           |  Bin 
  src/org/python/core/Py.java                      |   18 +-
  src/org/python/util/InteractiveConsole.java      |    8 +
  src/org/python/util/InteractiveInterpreter.java  |    2 +-
  src/org/python/util/JLineConsole.java            |  105 +++------
  src/org/python/util/jline-keybindings.properties |   64 ------
  11 files changed, 97 insertions(+), 187 deletions(-)


diff --git a/Lib/getpass.py b/Lib/getpass.py
--- a/Lib/getpass.py
+++ b/Lib/getpass.py
@@ -25,23 +25,14 @@
 
     Restore terminal settings at end.
     """
-    if stream is None:
-        stream = sys.stdout
     try:
-        terminal = sys._jy_console.reader.terminal
+        reader = sys._jy_console.reader
     except:
         return default_getpass(prompt)
-
-    echoed = terminal.getEcho()
-    terminal.disableEcho()
-    try:
-        passwd = _raw_input(prompt, stream)
-    finally:
-        if echoed:
-           terminal.enableEcho()
-
-    stream.write('\n')
-    return passwd
+    if stream is not None:
+        stream.write(prompt)
+        prompt = ''
+    return reader.readLine(prompt, '\0').encode(sys._jy_console.encoding)
 
 
 def unix_getpass(prompt='Password: ', stream=None):
@@ -148,9 +139,7 @@
             from EasyDialogs import AskPassword
         except ImportError:
             if os.name == 'java':
-                # disable this option for now, this does not reliably work
-                # getpass = jython_getpass
-                getpass = default_getpass
+                getpass = jython_getpass
         else:
             getpass = AskPassword
     else:
diff --git a/Lib/readline.py b/Lib/readline.py
--- a/Lib/readline.py
+++ b/Lib/readline.py
@@ -1,9 +1,14 @@
-from __future__ import with_statement
 import os.path
 import sys
 from warnings import warn
 
-import java.lang.reflect.Array
+try:
+    # jarjar-ed version
+    from org.python.jline.console.history import MemoryHistory
+except ImportError:
+    # dev version from extlibs
+    from jline.console.history import MemoryHistory
+
 
 __all__ = ['add_history', 'clear_history', 'get_begidx', 'get_completer',
            'get_completer_delims', 'get_current_history_length',
@@ -18,7 +23,7 @@
     _console = sys._jy_console
     _reader = _console.reader
 except AttributeError:
-    raise ImportError("Cannot access JLineConsole reader")
+    raise ImportError("Cannot access JLine2 setup")
 
 _history_list = None
 
@@ -51,21 +56,7 @@
 _setup_history()
 
 def parse_and_bind(string):
-    if string == "tab: complete":
-        try:
-            keybindings_field = _reader.class.getDeclaredField("keybindings")
-            keybindings_field.setAccessible(True)
-            keybindings = keybindings_field.get(_reader)
-            COMPLETE = _reader.KEYMAP_NAMES.get('COMPLETE')
-            if java.lang.reflect.Array.getShort(keybindings, 9) != COMPLETE:
-                java.lang.reflect.Array.setShort(keybindings, 9, COMPLETE)
-        except:
-            warn("Cannot bind tab key to complete. You need to do this in a .jlinebindings.properties file instead", SecurityWarning, stacklevel=2)
-    else:
-        warn("Cannot bind key %s. You need to do this in a .jlinebindings.properties file instead" % (string,), NotImplementedWarning, stacklevel=2)
-
-def get_line_buffer():
-    return str(_reader.cursorBuffer.buffer)
+    pass
 
 def insert_text(string):
     _reader.putString(string)
@@ -87,7 +78,7 @@
 def write_history_file(filename="~/.history"):
     expanded = os.path.expanduser(filename)
     with open(expanded, 'w') as f:
-        for line in _reader.history.historyList:
+        for line in _reader.history.entries():
             f.write(line)
             f.write("\n")
 
@@ -95,7 +86,7 @@
     _reader.history.clear()
 
 def add_history(line):
-    _reader.history.addToHistory(line)
+    _reader.history.add(line)
 
 def get_history_length():
     return _reader.history.maxSize
@@ -104,32 +95,26 @@
     _reader.history.maxSize = length
 
 def get_current_history_length():
-    return len(_reader.history.historyList)
+    return _reader.history.size()
 
 def get_history_item(index):
     # JLine indexes from 0 while readline indexes from 1 (at least in test_readline)
     if index>0:
-        return _reader.history.historyList[index-1]
+        return _reader.history.get(index-1)
     else:
         return None
 
 def remove_history_item(pos):
-    if _history_list:
-        _history_list.remove(pos)
-    else:
-        warn("Cannot remove history item at position: %s" % (pos,), SecurityWarning, stacklevel=2)
+    _reader.history.remove(pos)
 
 def replace_history_item(pos, line):
-    if _history_list:
-        _history_list.set(pos, line)
-    else:
-        warn("Cannot replace history item at position: %s" % (pos,), SecurityWarning, stacklevel=2)
+    _reader.history.set(pos, line)
 
 def redisplay():
     _reader.redrawLine()
 
 def set_startup_hook(function=None):
-    _console.startupHook = function
+    _console.startup_hook = function
 
 def set_pre_input_hook(function=None):
     warn("set_pre_input_hook %s" % (function,), NotImplementedWarning, stacklevel=2)
@@ -161,7 +146,7 @@
                 break
         return start
 
-    _reader.addCompletor(complete_handler)
+    _reader.addCompleter(complete_handler)
 
 
 def get_completer():
diff --git a/Lib/site.py b/Lib/site.py
--- a/Lib/site.py
+++ b/Lib/site.py
@@ -66,6 +66,15 @@
 _is_jython = sys.platform.startswith("java")
 if _is_jython:
     _ModuleType = type(os)
+    # Follow what Python 3.4 has done, http://bugs.python.org/issue5845
+    # FIXME add tab-as-indent support if preceding is whitespace
+    try:
+        import readline
+    except ImportError:
+        pass
+    else:
+        import rlcompleter
+        readline.parse_and_bind("tab: complete")
 
 # Prefixes for site-packages; add additional prefixes like /usr/local here
 PREFIXES = [sys.prefix, sys.exec_prefix]
diff --git a/build.xml b/build.xml
--- a/build.xml
+++ b/build.xml
@@ -178,7 +178,7 @@
             <pathelement path="${extlibs.dir}/jnr-netdb-1.1.4.jar"/>
             <pathelement path="${extlibs.dir}/jnr-posix-3.0.9.jar"/>
             <pathelement path="${extlibs.dir}/jnr-constants-0.8.6.jar"/>
-            <pathelement path="${extlibs.dir}/jline-1.0.jar"/>
+            <pathelement path="${extlibs.dir}/jline-2.12.jar"/>
             <pathelement path="${extlibs.dir}/netty-buffer-4.0.25.Final.jar"/>
             <pathelement path="${extlibs.dir}/netty-codec-4.0.25.Final.jar"/>
             <pathelement path="${extlibs.dir}/netty-common-4.0.25.Final.jar"/>
@@ -629,10 +629,8 @@
             <rule pattern="org.apache.xerces.**" result="org.python.apache.xerces. at 1"/>
             <rule pattern="org.apache.wml.**" result="org.python.apache.wml. at 1"/>
             <rule pattern="org.apache.html.**" result="org.python.apache.html. at 1"/>
-            <zipfileset src="extlibs/jline-1.0.jar"/>
-            <!--
+            <zipfileset src="extlibs/jline-2.12.jar"/>
             <rule pattern="jline.**" result="org.python.jline. at 1"/>
-            -->
             <manifest>
                 <attribute name="Main-Class" value="org.python.util.jython" />
                 <attribute name="Built-By" value="${user.name}" />
diff --git a/extlibs/jline-1.0.jar b/extlibs/jline-1.0.jar
deleted file mode 100644
index d25927961d4246484403f772a6cebf9badaf857c..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
GIT binary patch
[stripped]
diff --git a/extlibs/jline-2.12.jar b/extlibs/jline-2.12.jar
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..41676a1e210dec6f80cb730830b69dab202a0852
GIT binary patch
[stripped]
diff --git a/src/org/python/core/Py.java b/src/org/python/core/Py.java
--- a/src/org/python/core/Py.java
+++ b/src/org/python/core/Py.java
@@ -24,11 +24,13 @@
 import java.util.Set;
 
 import com.google.common.base.CharMatcher;
+import jline.console.UserInterruptException;
 import jnr.constants.Constant;
 import jnr.constants.platform.Errno;
 import jnr.posix.POSIX;
 import jnr.posix.POSIXFactory;
 
+import jnr.posix.util.Platform;
 import org.python.antlr.base.mod;
 import org.python.core.adapter.ClassicPyObjectAdapter;
 import org.python.core.adapter.ExtensiblePyObjectAdapter;
@@ -144,6 +146,14 @@
     
     public static PyException OSError(Constant errno, PyObject filename) {
         int value = errno.intValue();
+        // see https://github.com/jruby/jruby/commit/947c661e46683ea82f8016dde9d3fa597cd10e56
+        // for rationale to do this mapping, but in a nutshell jnr-constants is automatically
+        // generated from header files, so that's not the right place to do this mapping,
+        // but for Posix compatibility reasons both CPython andCRuby do this mapping;
+        // except CPython chooses EEXIST instead of CRuby's ENOENT
+        if (Platform.IS_WINDOWS && (value == 20047 || value == Errno.ESRCH.intValue())) {
+            value = Errno.EEXIST.intValue();
+        }
         // Pass to strerror because jnr-constants currently lacks Errno descriptions on
         // Windows, and strerror falls back to Linux's
         PyObject args = new PyTuple(Py.newInteger(value), PosixModule.strerror(value), filename);
@@ -172,9 +182,9 @@
         return new PyException(Py.RuntimeError, message);
     }
     public static PyObject KeyboardInterrupt;
-    /*public static PyException KeyboardInterrupt(String message) {
-    return new PyException(Py.KeyboardInterrupt, message);
-    }*/
+    public static PyException KeyboardInterrupt(String message) {
+        return new PyException(Py.KeyboardInterrupt, message);
+    }
     public static PyObject FloatingPointError;
 
     public static PyException FloatingPointError(String message) {
@@ -527,6 +537,8 @@
             return Py.RuntimeError("maximum recursion depth exceeded (Java StackOverflowError)");
         } else if (t instanceof OutOfMemoryError) {
             memory_error((OutOfMemoryError) t);
+        } else if (t instanceof UserInterruptException) {
+            return Py.KeyboardInterrupt("");
         }
         PyObject exc = PyJavaType.wrapJavaObject(t);
         PyException pyex = new PyException(exc.getType(), exc);
diff --git a/src/org/python/util/InteractiveConsole.java b/src/org/python/util/InteractiveConsole.java
--- a/src/org/python/util/InteractiveConsole.java
+++ b/src/org/python/util/InteractiveConsole.java
@@ -134,6 +134,14 @@
                 }
                 write("\n");
                 break;
+            } catch (Throwable t) {
+                // catch jline.console.UserInterruptException, rethrow as a KeyboardInterrupt
+                throw Py.JavaError(t);
+                // One would expect that it would be possible to then catch the KeyboardInterrupt at the
+                // bottom of this loop, however, for some reason the control-C restores the input text,
+                // so simply doing
+                // resetbuffer(); more = false;
+                // is not sufficient
             }
             more = push(line);
         }
diff --git a/src/org/python/util/InteractiveInterpreter.java b/src/org/python/util/InteractiveInterpreter.java
--- a/src/org/python/util/InteractiveInterpreter.java
+++ b/src/org/python/util/InteractiveInterpreter.java
@@ -147,7 +147,7 @@
         Py.stderr.write(data);
     }
 
-    public StringBuffer buffer = new StringBuffer();
+    public StringBuilder buffer = new StringBuilder();
     public String filename = "<stdin>";
 
     public void resetbuffer() {
diff --git a/src/org/python/util/JLineConsole.java b/src/org/python/util/JLineConsole.java
--- a/src/org/python/util/JLineConsole.java
+++ b/src/org/python/util/JLineConsole.java
@@ -5,20 +5,19 @@
 import java.io.File;
 import java.io.FileDescriptor;
 import java.io.FileInputStream;
-import java.io.FileNotFoundException;
 import java.io.FilterInputStream;
 import java.io.IOException;
 import java.io.InputStream;
-import java.io.OutputStreamWriter;
 import java.io.PrintStream;
-import java.io.PrintWriter;
-import java.io.Writer;
+import java.net.MalformedURLException;
+import java.net.URL;
 import java.util.Arrays;
 import java.util.List;
 
-import jline.ConsoleReader;
-import jline.Terminal;
+import jline.console.ConsoleKeys;
+import jline.console.ConsoleReader;
 import jline.WindowsTerminal;
+import jline.console.history.FileHistory;
 import jnr.constants.platform.Errno;
 
 import org.python.core.PlainConsole;
@@ -76,6 +75,22 @@
         // ... not "jline.UnixTerminal.input.encoding" as you might think, not even in JLine2
     }
 
+    private static class HistoryCloser implements Runnable {
+        FileHistory history;
+        public HistoryCloser(FileHistory history) {
+            this.history = history;
+        }
+
+        @Override
+        public void run() {
+            try {
+                history.flush();
+            } catch (IOException e) {
+                // could not save console history, but quietly ignore in this case
+            }
+        }
+    }
+
     /**
      * {@inheritDoc}
      * <p>
@@ -85,34 +100,25 @@
      */
     @Override
     public void install() {
-        Terminal.setupTerminal();
-
         String userHomeSpec = System.getProperty("user.home", ".");
 
         // Configure a ConsoleReader (the object that does most of the line editing).
         try {
-            /*
-             * Wrap System.out in the specified encoding. jline.ConsoleReader.readLine() echoes the
-             * line through this Writer.
-             */
-            Writer out = new PrintWriter(new OutputStreamWriter(System.out, encoding));
-
-            // Get the key bindings (built in ones treat TAB Pythonically).
-            InputStream bindings = getBindings(userHomeSpec, getClass().getClassLoader());
-
             // Create the reader as unbuffered as possible
             InputStream in = new FileInputStream(FileDescriptor.in);
-            reader = new ConsoleReader(in, out, bindings);
+            reader = new ConsoleReader("jython", in, System.out, null, encoding);
+            reader.setKeyMap("jython");
+            reader.setHandleUserInterrupt(true);
 
             // We find the bell too noisy
             reader.setBellEnabled(false);
 
             /*
-             * Everybody else, using sys.stdout or java.lang.System.out gets to write on a special
+             * Everybody else, using sys.stdout or java.lang.System.out, gets to write on a special
              * PrintStream that keeps the last incomplete line in case it turns out to be a console
              * prompt.
              */
-            outWrapper = new ConsoleOutputStream(System.out, reader.getTermwidth());
+            outWrapper = new ConsoleOutputStream(System.out, reader.getTerminal().getWidth());
             System.setOut(new PrintStream(outWrapper, true, encoding));
 
         } catch (IOException e) {
@@ -122,7 +128,9 @@
         // Access and load (if possible) the line history.
         try {
             File historyFile = new File(userHomeSpec, ".jline-jython.history");
-            reader.getHistory().setHistoryFile(historyFile);
+            FileHistory history = new FileHistory(historyFile);
+            Runtime.getRuntime().addShutdownHook(new Thread(new HistoryCloser(history)));
+            reader.setHistory(history);
         } catch (IOException e) {
             // oh well, no history from file
         }
@@ -185,14 +193,19 @@
                     startup_hook.__call__();
                 }
 
+                try {
+                    // Resumption from control-Z suspension may occur without JLine telling us
+                    // Work around by putting the terminal into a well-known state before
+                    // each read line, if possible
+                    reader.getTerminal().init();
+                } catch (Exception exc) {}
+
                 // Send the cursor to the start of the line (no prompt, empty buffer).
-                reader.setDefaultPrompt(null);
+                reader.setPrompt(null);
                 reader.redrawLine();
 
                 // The prompt is whatever was already on the line.
-                String line = reader.readLine(prompt);
-                return line;
-
+                return reader.readLine(prompt);
             } catch (IOException ioe) {
                 // Something went wrong, or we were interrupted (seems only BSD throws this)
                 if (!fromSuspend(ioe)) {
@@ -203,7 +216,7 @@
                     // The interruption seems to be (return from) a ctrl-Z suspension:
                     try {
                         // Must reset JLine and continue (not repeating the prompt)
-                        reader.getTerminal().initializeTerminal();
+                        reader.resetPromptLine (prompt, null, 0);
                         prompt = "";
                     } catch (Exception e) {
                         // Do our best to say what went wrong
@@ -216,46 +229,6 @@
     }
 
     /**
-     * Return the JLine bindings file.
-     *
-     * This handles loading the user's custom key bindings (normally JLine does) so it can fall back
-     * to Jython's (which disable tab completion) when the user's are not available.
-     *
-     * @return an InputStream of the JLine bindings file.
-     */
-    protected static InputStream getBindings(String userHomeSpec, ClassLoader loader) {
-
-        // The key bindings file may be specified explicitly
-        String bindingsFileSpec = System.getProperty("jline.keybindings");
-        File bindingsFile;
-
-        if (bindingsFileSpec != null) {
-            // Bindings file explicitly specified
-            bindingsFile = new File(bindingsFileSpec);
-        } else {
-            // Otherwise try ~/.jlinebindings.properties
-            bindingsFile = new File(userHomeSpec, ".jlinebindings.properties");
-        }
-
-        // See if that file really exists (and can be read)
-        try {
-            if (bindingsFile.isFile()) {
-                try {
-                    return new FileInputStream(bindingsFile);
-                } catch (FileNotFoundException fnfe) {
-                    // Shouldn't really ever happen
-                    fnfe.printStackTrace();
-                }
-            }
-        } catch (SecurityException se) {
-            // continue
-        }
-
-        // User/specific key bindings could not be read: use the ones from the class path or jar.
-        return loader.getResourceAsStream("org/python/util/jline-keybindings.properties");
-    }
-
-    /**
      * Determine if the IOException was likely caused by a SIGSTP (ctrl-z). Seems only applicable to
      * BSD platforms.
      */
diff --git a/src/org/python/util/jline-keybindings.properties b/src/org/python/util/jline-keybindings.properties
deleted file mode 100644
--- a/src/org/python/util/jline-keybindings.properties
+++ /dev/null
@@ -1,64 +0,0 @@
-# Keybinding mapping for JLine. The format is:
-#    [key code]: [logical operation]
-
-# CTRL-B: move to the previous character
-2: PREV_CHAR
-
-# CTRL-G: move to the previous word
-7: PREV_WORD
-
-# CTRL-F: move to the next character
-6: NEXT_CHAR
-
-# CTRL-A: move to the beginning of the line
-1: MOVE_TO_BEG
-
-# CTRL-D: close out the input stream
-4: EXIT
-
-# CTRL-E: move the cursor to the end of the line
-5: MOVE_TO_END
-
-# BACKSPACE, CTRL-H: delete the previous character
-# 8 is the ASCII code for backspace and therefor
-# deleting the previous character
-8: DELETE_PREV_CHAR
-
-## TAB, CTRL-I: signal that console completion should be attempted
-#9: COMPLETE
-# Jython needs a real TAB, disable completion
-9: UNKNOWN
-
-# CTRL-J, CTRL-M: newline
-10: NEWLINE
-
-# CTRL-K: erase the current line
-11: KILL_LINE
-
-# ENTER: newline
-13: NEWLINE
-
-# CTRL-L: clear screen
-12: CLEAR_SCREEN
-
-# CTRL-N: scroll to the next element in the history buffer
-14: NEXT_HISTORY
-
-# CTRL-P: scroll to the previous element in the history buffer
-16: PREV_HISTORY
-
-# CTRL-R: redraw the current line
-18: REDISPLAY
-
-# CTRL-U: delete all the characters before the cursor position
-21: KILL_LINE_PREV
-
-# CTRL-V: paste the contents of the clipboard (useful for Windows terminal)
-22: PASTE
-
-# CTRL-W: delete the word directly before the cursor
-23: DELETE_PREV_WORD
-
-# DELETE, CTRL-?: delete the previous character
-# 127 is the ASCII code for delete
-127: DELETE_PREV_CHAR

-- 
Repository URL: https://hg.python.org/jython


More information about the Jython-checkins mailing list