From 1f7e9d752bea73a8f4e4542f57e846593aa1cacc 2011-10-28 22:24:42
From: Fernando Perez <fperez.net@gmail.com>
Date: 2011-10-28 22:24:42
Subject: [PATCH] Merge pull request #815 from cboos/issue481-qt4-input-hook

Fix #481 using custom qt4 input hook.

Various other cleanups to inputhook libraries.
---

diff --git a/IPython/lib/inputhook.py b/IPython/lib/inputhook.py
index 085a271..7e9b954 100644
--- a/IPython/lib/inputhook.py
+++ b/IPython/lib/inputhook.py
@@ -15,6 +15,7 @@ Inputhook management for GUI event loop integration.
 #-----------------------------------------------------------------------------
 
 import ctypes
+import os
 import sys
 import warnings
 
@@ -31,11 +32,58 @@ GUI_TK = 'tk'
 GUI_OSX = 'osx'
 GUI_GLUT = 'glut'
 GUI_PYGLET = 'pyglet'
+GUI_NONE = 'none' # i.e. disable
 
 #-----------------------------------------------------------------------------
-# Utility classes
+# Utilities
 #-----------------------------------------------------------------------------
 
+def _stdin_ready_posix():
+    """Return True if there's something to read on stdin (posix version)."""
+    infds, outfds, erfds = select.select([sys.stdin],[],[],0)
+    return bool(infds)
+
+def _stdin_ready_nt():
+    """Return True if there's something to read on stdin (nt version)."""
+    return msvcrt.kbhit()
+
+def _stdin_ready_other():
+    """Return True, assuming there's something to read on stdin."""
+    return True #
+
+
+def _ignore_CTRL_C_posix():
+    """Ignore CTRL+C (SIGINT)."""
+    signal.signal(signal.SIGINT, signal.SIG_IGN)
+
+def _allow_CTRL_C_posix():
+    """Take CTRL+C into account (SIGINT)."""
+    signal.signal(signal.SIGINT, signal.default_int_handler)
+
+def _ignore_CTRL_C_other():
+    """Ignore CTRL+C (not implemented)."""
+    pass
+
+def _allow_CTRL_C_other():
+    """Take CTRL+C into account (not implemented)."""
+    pass
+
+if os.name == 'posix':
+    import select
+    import signal
+    stdin_ready = _stdin_ready_posix
+    ignore_CTRL_C = _ignore_CTRL_C_posix
+    allow_CTRL_C = _allow_CTRL_C_posix
+elif os.name == 'nt':
+    import msvcrt
+    stdin_ready = _stdin_ready_nt
+    ignore_CTRL_C = _ignore_CTRL_C_other
+    allow_CTRL_C = _allow_CTRL_C_other
+else:
+    stdin_ready = _stdin_ready_other
+    ignore_CTRL_C = _ignore_CTRL_C_other
+    allow_CTRL_C = _allow_CTRL_C_other
+
 
 #-----------------------------------------------------------------------------
 # Main InputHookManager class
@@ -70,6 +118,11 @@ class InputHookManager(object):
 
     def set_inputhook(self, callback):
         """Set PyOS_InputHook to callback and return the previous one."""
+        # On platforms with 'readline' support, it's all too likely to
+        # have a KeyboardInterrupt signal delivered *even before* an
+        # initial ``try:`` clause in the callback can be executed, so
+        # we need to disable CTRL+C in this situation.
+        ignore_CTRL_C()
         self._callback = callback
         self._callback_pyfunctype = self.PYFUNC(callback)
         pyos_inputhook_ptr = self.get_pyos_inputhook()
@@ -93,6 +146,7 @@ class InputHookManager(object):
         pyos_inputhook_ptr = self.get_pyos_inputhook()
         original = self.get_pyos_inputhook_as_func()
         pyos_inputhook_ptr.value = ctypes.c_void_p(None).value
+        allow_CTRL_C()
         self._reset()
         return original
 
@@ -181,33 +235,11 @@ class InputHookManager(object):
             from PyQt4 import QtCore
             app = QtGui.QApplication(sys.argv)
         """
-        from IPython.external.qt_for_kernel import QtCore, QtGui
-
-        if 'pyreadline' in sys.modules:
-            # see IPython GitHub Issue #281 for more info on this issue
-            # Similar intermittent behavior has been reported on OSX,
-            # but not consistently reproducible
-            warnings.warn("""PyReadline's inputhook can conflict with Qt, causing delays
-            in interactive input. If you do see this issue, we recommend using another GUI
-            toolkit if you can, or disable readline with the configuration option
-            'TerminalInteractiveShell.readline_use=False', specified in a config file or
-            at the command-line""",
-            RuntimeWarning)
-        
-        # PyQt4 has had this since 4.3.1.  In version 4.2, PyOS_InputHook
-        # was set when QtCore was imported, but if it ever got removed,
-        # you couldn't reset it.  For earlier versions we can
-        # probably implement a ctypes version.
-        try:
-            QtCore.pyqtRestoreInputHook()
-        except AttributeError:
-            pass
+        from IPython.lib.inputhookqt4 import create_inputhook_qt4
+        app, inputhook_qt4 = create_inputhook_qt4(self, app)
+        self.set_inputhook(inputhook_qt4)
 
         self._current_gui = GUI_QT4
-        if app is None:
-            app = QtCore.QCoreApplication.instance()
-        if app is None:
-            app = QtGui.QApplication([" "])
         app._in_event_loop = True
         self._apps[GUI_QT4] = app
         return app
@@ -416,8 +448,8 @@ def enable_gui(gui=None, app=None):
     Parameters
     ----------
     gui : optional, string or None
-      If None, clears input hook, otherwise it must be one of the recognized
-      GUI names (see ``GUI_*`` constants in module).
+      If None (or 'none'), clears input hook, otherwise it must be one
+      of the recognized GUI names (see ``GUI_*`` constants in module).
 
     app : optional, existing application object.
       For toolkits that have the concept of a global app, you can supply an
@@ -432,6 +464,7 @@ def enable_gui(gui=None, app=None):
     one.
     """
     guis = {None: clear_inputhook,
+            GUI_NONE: clear_inputhook,
             GUI_OSX: lambda app=False: None,
             GUI_TK: enable_tk,
             GUI_GTK: enable_gtk,
diff --git a/IPython/lib/inputhookqt4.py b/IPython/lib/inputhookqt4.py
new file mode 100644
index 0000000..0c0afa8
--- /dev/null
+++ b/IPython/lib/inputhookqt4.py
@@ -0,0 +1,124 @@
+# -*- coding: utf-8 -*-
+"""
+Qt4's inputhook support function
+
+Author: Christian Boos
+"""
+
+#-----------------------------------------------------------------------------
+#  Copyright (C) 2011  The IPython Development Team
+#
+#  Distributed under the terms of the BSD License.  The full license is in
+#  the file COPYING, distributed as part of this software.
+#-----------------------------------------------------------------------------
+
+#-----------------------------------------------------------------------------
+# Imports
+#-----------------------------------------------------------------------------
+
+from IPython.core.interactiveshell import InteractiveShell
+from IPython.external.qt_for_kernel import QtCore, QtGui
+from IPython.lib.inputhook import allow_CTRL_C, ignore_CTRL_C, stdin_ready
+
+#-----------------------------------------------------------------------------
+# Code
+#-----------------------------------------------------------------------------
+
+def create_inputhook_qt4(mgr, app=None):
+    """Create an input hook for running the Qt4 application event loop.
+
+    Parameters
+    ----------
+    mgr : an InputHookManager
+
+    app : Qt Application, optional.
+        Running application to use.  If not given, we probe Qt for an
+        existing application object, and create a new one if none is found.
+
+    Returns
+    -------
+    A pair consisting of a Qt Application (either the one given or the
+    one found or created) and a inputhook.
+
+    Notes
+    -----
+    We use a custom input hook instead of PyQt4's default one, as it
+    interacts better with the readline packages (issue #481).
+
+    The inputhook function works in tandem with a 'pre_prompt_hook'
+    which automatically restores the hook as an inputhook in case the
+    latter has been temporarily disabled after having intercepted a
+    KeyboardInterrupt.
+    """
+
+    if app is None:
+        app = QtCore.QCoreApplication.instance()
+        if app is None:
+            app = QtGui.QApplication([" "])
+
+    # Re-use previously created inputhook if any
+    ip = InteractiveShell.instance()
+    if hasattr(ip, '_inputhook_qt4'):
+        return app, ip._inputhook_qt4
+
+    # Otherwise create the inputhook_qt4/preprompthook_qt4 pair of
+    # hooks (they both share the got_kbdint flag)
+
+    got_kbdint = [False]
+
+    def inputhook_qt4():
+        """PyOS_InputHook python hook for Qt4.
+
+        Process pending Qt events and if there's no pending keyboard
+        input, spend a short slice of time (50ms) running the Qt event
+        loop.
+
+        As a Python ctypes callback can't raise an exception, we catch
+        the KeyboardInterrupt and temporarily deactivate the hook,
+        which will let a *second* CTRL+C be processed normally and go
+        back to a clean prompt line.
+        """
+        try:
+            allow_CTRL_C()
+            app = QtCore.QCoreApplication.instance()
+            if not app: # shouldn't happen, but safer if it happens anyway...
+                return 0
+            app.processEvents(QtCore.QEventLoop.AllEvents, 300)
+            if not stdin_ready():
+                timer = QtCore.QTimer()
+                timer.timeout.connect(app.quit)
+                while not stdin_ready():
+                    timer.start(50)
+                    app.exec_()
+                    timer.stop()
+            ignore_CTRL_C()
+        except KeyboardInterrupt:
+            ignore_CTRL_C()
+            got_kbdint[0] = True
+            print("\nKeyboardInterrupt - qt4 event loop interrupted!"
+                  "\n  * hit CTRL+C again to clear the prompt"
+                  "\n  * use '%gui none' to disable the event loop"
+                  " permanently"
+                  "\n    and '%gui qt4' to re-enable it later")
+            mgr.clear_inputhook()
+        except: # NO exceptions are allowed to escape from a ctypes callback
+            mgr.clear_inputhook()
+            from traceback import print_exc
+            print_exc()
+            print("Got exception from inputhook_qt4, unregistering.")
+        return 0
+
+    def preprompthook_qt4(ishell):
+        """'pre_prompt_hook' used to restore the Qt4 input hook
+
+        (in case the latter was temporarily deactivated after a
+        CTRL+C)
+        """
+        if got_kbdint[0]:
+            mgr.set_inputhook(inputhook_qt4)
+        got_kbdint[0] = False
+
+    ip._inputhook_qt4 = inputhook_qt4
+    ip.set_hook('pre_prompt_hook', preprompthook_qt4)
+
+    return app, inputhook_qt4
diff --git a/IPython/lib/inputhookwx.py b/IPython/lib/inputhookwx.py
index 8e3d25b..4b9bd02 100644
--- a/IPython/lib/inputhookwx.py
+++ b/IPython/lib/inputhookwx.py
@@ -24,26 +24,13 @@ import time
 from timeit import default_timer as clock
 import wx
 
-if os.name == 'posix':
-    import select
-elif sys.platform == 'win32':
-    import msvcrt
+from IPython.lib.inputhook import stdin_ready
+
 
 #-----------------------------------------------------------------------------
 # Code
 #-----------------------------------------------------------------------------
 
-def stdin_ready():
-    if os.name == 'posix':
-        infds, outfds, erfds = select.select([sys.stdin],[],[],0)
-        if infds:
-            return True
-        else:
-            return False
-    elif sys.platform == 'win32':
-        return msvcrt.kbhit()
-
-
 def inputhook_wx1():
     """Run the wx event loop by processing pending events only.