Fix #481 using custom qt4 input hook #815

Merged
merged 15 commits into from Oct 28, 2011
View
89 IPython/lib/inputhook.py
@@ -15,6 +15,7 @@
#-----------------------------------------------------------------------------
import ctypes
+import os
import sys
import warnings
@@ -31,11 +32,58 @@
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 @@ def get_pyos_inputhook_as_func(self):
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 @@ def clear_inputhook(self, app=None):
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 @@ def enable_qt4(self, app=None):
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,
View
124 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
View
17 IPython/lib/inputhookwx.py
@@ -24,26 +24,13 @@
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.