From d39f0ca4a396ec164c838d8c593d78103a1fdb13 2011-10-20 20:21:50 From: MinRK Date: 2011-10-20 20:21:50 Subject: [PATCH] enable %gui/%pylab magics in the Kernel This isn't as significant as it looks, as it's principally a big dedent in zmq.ipkernel. All the single-method Kernel subclasses were just dedented, and are used as functions. This lets them be plugged into the existing kernel's event loop. The enable_pylab and magic_gui methods in zmqshell only differ from the originals in the source of the enable_gui function, and the change of the default pylab backend to inline, from matplotlib autodetect. --- diff --git a/IPython/lib/pylabtools.py b/IPython/lib/pylabtools.py index 9db8b93..ccafe44 100644 --- a/IPython/lib/pylabtools.py +++ b/IPython/lib/pylabtools.py @@ -289,7 +289,7 @@ def import_pylab(user_ns, backend, import_all=True, shell=None): exec s in shell.user_ns_hidden -def pylab_activate(user_ns, gui=None, import_all=True): +def pylab_activate(user_ns, gui=None, import_all=True, shell=None): """Activate pylab mode in the user's namespace. Loads and initializes numpy, matplotlib and friends for interactive use. @@ -312,7 +312,7 @@ def pylab_activate(user_ns, gui=None, import_all=True): """ gui, backend = find_gui_and_backend(gui) activate_matplotlib(backend) - import_pylab(user_ns, backend, import_all) + import_pylab(user_ns, backend, import_all, shell) print """ Welcome to pylab, a matplotlib-based Python environment [backend: %s]. diff --git a/IPython/zmq/ipkernel.py b/IPython/zmq/ipkernel.py index 94f1b9f..6977d4a 100755 --- a/IPython/zmq/ipkernel.py +++ b/IPython/zmq/ipkernel.py @@ -22,6 +22,7 @@ import sys import time import traceback import logging + # System library imports. import zmq @@ -38,7 +39,7 @@ from IPython.utils import py3compat from IPython.utils.jsonutil import json_clean from IPython.lib import pylabtools from IPython.utils.traitlets import ( - List, Instance, Float, Dict, Bool, Int, Unicode, CaselessStrEnum + Any, List, Instance, Float, Dict, Bool, Int, Unicode, CaselessStrEnum ) from entry_point import base_launch_kernel @@ -58,6 +59,9 @@ class Kernel(Configurable): # Kernel interface #--------------------------------------------------------------------------- + # attribute to override with a GUI + eventloop = Any(None) + shell = Instance('IPython.core.interactiveshell.InteractiveShellABC') session = Instance(Session) shell_socket = Instance('zmq.Socket') @@ -164,7 +168,8 @@ class Kernel(Configurable): """ poller = zmq.Poller() poller.register(self.shell_socket, zmq.POLLIN) - while True: + # loop while self.eventloop has not been overridden + while self.eventloop is None: try: # scale by extra factor of 10, because there is no # reason for this to be anything less than ~ 0.1s @@ -181,6 +186,13 @@ class Kernel(Configurable): except KeyboardInterrupt: # Ctrl-C shouldn't crash the kernel io.raw_print("KeyboardInterrupt caught in kernel") + if self.eventloop is not None: + try: + self.eventloop(self) + except KeyboardInterrupt: + # Ctrl-C shouldn't crash the kernel + io.raw_print("KeyboardInterrupt caught in kernel") + def record_ports(self, ports): """Record the ports that this kernel is using. @@ -496,174 +508,186 @@ class Kernel(Configurable): time.sleep(0.01) -class QtKernel(Kernel): - """A Kernel subclass with Qt support.""" +#------------------------------------------------------------------------------ +# Eventloops for integrating the Kernel into different GUIs +#------------------------------------------------------------------------------ - def start(self): - """Start a kernel with QtPy4 event loop integration.""" - from IPython.external.qt_for_kernel import QtCore - from IPython.lib.guisupport import get_app_qt4, start_event_loop_qt4 +def loop_qt4(kernel): + """Start a kernel with PyQt4 event loop integration.""" - self.app = get_app_qt4([" "]) - self.app.setQuitOnLastWindowClosed(False) - self.timer = QtCore.QTimer() - self.timer.timeout.connect(self.do_one_iteration) - # Units for the timer are in milliseconds - self.timer.start(1000*self._poll_interval) - start_event_loop_qt4(self.app) + from IPython.external.qt_for_kernel import QtCore + from IPython.lib.guisupport import get_app_qt4, start_event_loop_qt4 + kernel.app = get_app_qt4([" "]) + kernel.app.setQuitOnLastWindowClosed(False) + kernel.timer = QtCore.QTimer() + kernel.timer.timeout.connect(kernel.do_one_iteration) + # Units for the timer are in milliseconds + kernel.timer.start(1000*kernel._poll_interval) + start_event_loop_qt4(kernel.app) -class WxKernel(Kernel): - """A Kernel subclass with Wx support.""" - def start(self): - """Start a kernel with wx event loop support.""" - - import wx - from IPython.lib.guisupport import start_event_loop_wx - - doi = self.do_one_iteration - # Wx uses milliseconds - poll_interval = int(1000*self._poll_interval) - - # We have to put the wx.Timer in a wx.Frame for it to fire properly. - # We make the Frame hidden when we create it in the main app below. - class TimerFrame(wx.Frame): - def __init__(self, func): - wx.Frame.__init__(self, None, -1) - self.timer = wx.Timer(self) - # Units for the timer are in milliseconds - self.timer.Start(poll_interval) - self.Bind(wx.EVT_TIMER, self.on_timer) - self.func = func - - def on_timer(self, event): - self.func() - - # We need a custom wx.App to create our Frame subclass that has the - # wx.Timer to drive the ZMQ event loop. - class IPWxApp(wx.App): - def OnInit(self): - self.frame = TimerFrame(doi) - self.frame.Show(False) - return True - - # The redirect=False here makes sure that wx doesn't replace - # sys.stdout/stderr with its own classes. - self.app = IPWxApp(redirect=False) - start_event_loop_wx(self.app) - - -class TkKernel(Kernel): - """A Kernel subclass with Tk support.""" +def loop_wx(kernel): + """Start a kernel with wx event loop support.""" - def start(self): - """Start a Tk enabled event loop.""" + import wx + from IPython.lib.guisupport import start_event_loop_wx - import Tkinter - doi = self.do_one_iteration - # Tk uses milliseconds - poll_interval = int(1000*self._poll_interval) - # For Tkinter, we create a Tk object and call its withdraw method. - class Timer(object): - def __init__(self, func): - self.app = Tkinter.Tk() - self.app.withdraw() - self.func = func + doi = kernel.do_one_iteration + # Wx uses milliseconds + poll_interval = int(1000*kernel._poll_interval) - def on_timer(self): - self.func() - self.app.after(poll_interval, self.on_timer) + # We have to put the wx.Timer in a wx.Frame for it to fire properly. + # We make the Frame hidden when we create it in the main app below. + class TimerFrame(wx.Frame): + def __init__(self, func): + wx.Frame.__init__(self, None, -1) + self.timer = wx.Timer(self) + # Units for the timer are in milliseconds + self.timer.Start(poll_interval) + self.Bind(wx.EVT_TIMER, self.on_timer) + self.func = func - def start(self): - self.on_timer() # Call it once to get things going. - self.app.mainloop() + def on_timer(self, event): + self.func() - self.timer = Timer(doi) - self.timer.start() + # We need a custom wx.App to create our Frame subclass that has the + # wx.Timer to drive the ZMQ event loop. + class IPWxApp(wx.App): + def OnInit(self): + self.frame = TimerFrame(doi) + self.frame.Show(False) + return True + # The redirect=False here makes sure that wx doesn't replace + # sys.stdout/stderr with its own classes. + kernel.app = IPWxApp(redirect=False) + start_event_loop_wx(kernel.app) -class GTKKernel(Kernel): - """A Kernel subclass with GTK support.""" - def start(self): - """Start the kernel, coordinating with the GTK event loop""" - from .gui.gtkembed import GTKEmbed +def loop_tk(kernel): + """Start a kernel with the Tk event loop.""" + + import Tkinter + doi = kernel.do_one_iteration + # Tk uses milliseconds + poll_interval = int(1000*kernel._poll_interval) + # For Tkinter, we create a Tk object and call its withdraw method. + class Timer(object): + def __init__(self, func): + self.app = Tkinter.Tk() + self.app.withdraw() + self.func = func - gtk_kernel = GTKEmbed(self) - gtk_kernel.start() + def on_timer(self): + self.func() + self.app.after(poll_interval, self.on_timer) + def start(self): + self.on_timer() # Call it once to get things going. + self.app.mainloop() -class OSXKernel(TkKernel): - """A Kernel subclass with Cocoa support via the matplotlib OSX backend.""" + kernel.timer = Timer(doi) + kernel.timer.start() + + +def loop_gtk(kernel): + """Start the kernel, coordinating with the GTK event loop""" + from .gui.gtkembed import GTKEmbed + + gtk_kernel = GTKEmbed(kernel) + gtk_kernel.start() + + +def loop_cocoa(kernel): + """Start the kernel, coordinating with the Cocoa CFRunLoop event loop + via the matplotlib MacOSX backend. + """ + import matplotlib + if matplotlib.__version__ < '1.1.0': + kernel.log.warn( + "MacOSX backend in matplotlib %s doesn't have a Timer, " + "falling back on Tk for CFRunLoop integration. Note that " + "even this won't work if Tk is linked against X11 instead of " + "Cocoa (e.g. EPD). To use the MacOSX backend in the kernel, " + "you must use matplotlib >= 1.1.0, or a native libtk." + ) + return loop_tk(kernel) - def start(self): - """Start the kernel, coordinating with the Cocoa CFRunLoop event loop - via the matplotlib MacOSX backend. - """ - import matplotlib - if matplotlib.__version__ < '1.1.0': - self.log.warn( - "MacOSX backend in matplotlib %s doesn't have a Timer, " - "falling back on Tk for CFRunLoop integration. Note that " - "even this won't work if Tk is linked against X11 instead of " - "Cocoa (e.g. EPD). To use the MacOSX backend in the kernel, " - "you must use matplotlib >= 1.1.0, or a native libtk." - ) - return TkKernel.start(self) - - from matplotlib.backends.backend_macosx import TimerMac, show - - # scale interval for sec->ms - poll_interval = int(1000*self._poll_interval) - - real_excepthook = sys.excepthook - def handle_int(etype, value, tb): - """don't let KeyboardInterrupts look like crashes""" - if etype is KeyboardInterrupt: - io.raw_print("KeyboardInterrupt caught in CFRunLoop") - else: - real_excepthook(etype, value, tb) - - # add doi() as a Timer to the CFRunLoop - def doi(): - # restore excepthook during IPython code - sys.excepthook = real_excepthook - self.do_one_iteration() - # and back: - sys.excepthook = handle_int - - t = TimerMac(poll_interval) - t.add_callback(doi) - t.start() - - # but still need a Poller for when there are no active windows, - # during which time mainloop() returns immediately - poller = zmq.Poller() - poller.register(self.shell_socket, zmq.POLLIN) - - while True: + from matplotlib.backends.backend_macosx import TimerMac, show + + # scale interval for sec->ms + poll_interval = int(1000*kernel._poll_interval) + + real_excepthook = sys.excepthook + def handle_int(etype, value, tb): + """don't let KeyboardInterrupts look like crashes""" + if etype is KeyboardInterrupt: + io.raw_print("KeyboardInterrupt caught in CFRunLoop") + else: + real_excepthook(etype, value, tb) + + # add doi() as a Timer to the CFRunLoop + def doi(): + # restore excepthook during IPython code + sys.excepthook = real_excepthook + kernel.do_one_iteration() + # and back: + sys.excepthook = handle_int + + t = TimerMac(poll_interval) + t.add_callback(doi) + t.start() + + # but still need a Poller for when there are no active windows, + # during which time mainloop() returns immediately + poller = zmq.Poller() + poller.register(kernel.shell_socket, zmq.POLLIN) + + while True: + try: + # double nested try/except, to properly catch KeyboardInterrupt + # due to pyzmq Issue #130 try: - # double nested try/except, to properly catch KeyboardInterrupt - # due to pyzmq Issue #130 - try: - # don't let interrupts during mainloop invoke crash_handler: - sys.excepthook = handle_int - show.mainloop() - sys.excepthook = real_excepthook - # use poller if mainloop returned (no windows) - # scale by extra factor of 10, since it's a real poll - poller.poll(10*poll_interval) - self.do_one_iteration() - except: - raise - except KeyboardInterrupt: - # Ctrl-C shouldn't crash the kernel - io.raw_print("KeyboardInterrupt caught in kernel") - finally: - # ensure excepthook is restored + # don't let interrupts during mainloop invoke crash_handler: + sys.excepthook = handle_int + show.mainloop() sys.excepthook = real_excepthook + # use poller if mainloop returned (no windows) + # scale by extra factor of 10, since it's a real poll + poller.poll(10*poll_interval) + kernel.do_one_iteration() + except: + raise + except KeyboardInterrupt: + # Ctrl-C shouldn't crash the kernel + io.raw_print("KeyboardInterrupt caught in kernel") + finally: + # ensure excepthook is restored + sys.excepthook = real_excepthook + +# mapping of keys to loop functions +loop_map = { + 'qt' : loop_qt4, + 'qt4': loop_qt4, + 'inline': None, + 'osx': loop_cocoa, + 'wx' : loop_wx, + 'tk' : loop_tk, + 'gtk': loop_gtk, +} + +def enable_gui(gui, kernel=None): + """Enable integration with a give GUI""" + if kernel is None: + kernel = IPKernelApp.instance().kernel + if gui not in loop_map: + raise ValueError("GUI %r not supported" % gui) + loop = loop_map[gui] + if kernel.eventloop is not None and kernel.eventloop is not loop: + raise RuntimeError("Cannot activate multiple GUI eventloops") + kernel.eventloop = loop #----------------------------------------------------------------------------- @@ -715,37 +739,21 @@ class IPKernelApp(KernelApp, InteractiveShellApp): def init_kernel(self): kernel_factory = Kernel - kernel_map = { - 'qt' : QtKernel, - 'qt4': QtKernel, - 'inline': Kernel, - 'osx': OSXKernel, - 'wx' : WxKernel, - 'tk' : TkKernel, - 'gtk': GTKKernel, - } - if self.pylab: key = None if self.pylab == 'auto' else self.pylab gui, backend = pylabtools.find_gui_and_backend(key) - kernel_factory = kernel_map.get(gui) - if kernel_factory is None: - raise ValueError('GUI is not supported: %r' % gui) - pylabtools.activate_matplotlib(backend) kernel = kernel_factory(config=self.config, session=self.session, shell_socket=self.shell_socket, iopub_socket=self.iopub_socket, stdin_socket=self.stdin_socket, - log=self.log + log=self.log, ) self.kernel = kernel kernel.record_ports(self.ports) if self.pylab: - import_all = self.pylab_import_all - pylabtools.import_pylab(kernel.shell.user_ns, backend, import_all, - shell=kernel.shell) + kernel.shell.enable_pylab(gui, import_all=self.pylab_import_all) def init_shell(self): self.shell = self.kernel.shell diff --git a/IPython/zmq/zmqshell.py b/IPython/zmq/zmqshell.py index 822ff5c..cb8f843 100644 --- a/IPython/zmq/zmqshell.py +++ b/IPython/zmq/zmqshell.py @@ -31,6 +31,7 @@ from IPython.core.displaypub import DisplayPublisher from IPython.core.macro import Macro from IPython.core.magic import MacroToEdit from IPython.core.payloadpage import install_payload_page +from IPython.lib import pylabtools from IPython.lib.kernel import ( get_connection_file, get_connection_info, connect_qtconsole ) @@ -389,13 +390,65 @@ class ZMQInteractiveShell(InteractiveShell): } self.payload_manager.write_payload(payload) - def magic_gui(self, *args, **kwargs): - raise NotImplementedError( - 'Kernel GUI support is not implemented yet, except for --pylab.') + def magic_gui(self, parameter_s=''): + """Enable or disable IPython GUI event loop integration. + + %gui [GUINAME] + + This magic replaces IPython's threaded shells that were activated + using the (pylab/wthread/etc.) command line flags. GUI toolkits + can now be enabled at runtime and keyboard + interrupts should work without any problems. The following toolkits + are supported: wxPython, PyQt4, PyGTK, Cocoa, and Tk:: + + %gui wx # enable wxPython event loop integration + %gui qt4|qt # enable PyQt4 event loop integration + %gui gtk # enable PyGTK event loop integration + %gui OSX # enable Cocoa event loop integration (requires matplotlib 1.1) + %gui tk # enable Tk event loop integration + + WARNING: after any of these has been called you can simply create + an application object, but DO NOT start the event loop yourself, as + we have already handled that. + """ + from IPython.zmq.ipkernel import enable_gui + opts, arg = self.parse_options(parameter_s, '') + if arg=='': arg = None + return enable_gui(arg) + + def enable_pylab(self, gui=None, import_all=True): + """Activate pylab support at runtime. + + This turns on support for matplotlib, preloads into the interactive + namespace all of numpy and pylab, and configures IPython to correcdtly + interact with the GUI event loop. The GUI backend to be used can be + optionally selected with the optional :param:`gui` argument. + + Parameters + ---------- + gui : optional, string [default: inline] + + If given, dictates the choice of matplotlib GUI backend to use + (should be one of IPython's supported backends, 'inline', 'qt', 'osx', + 'tk', or 'gtk'), otherwise we use the default chosen by matplotlib + (as dictated by the matplotlib build-time options plus the user's + matplotlibrc configuration file). + """ + from IPython.zmq.ipkernel import enable_gui + # We want to prevent the loading of pylab to pollute the user's + # namespace as shown by the %who* magics, so we execute the activation + # code in an empty namespace, and we update *both* user_ns and + # user_ns_hidden with this information. + ns = {} + # override default to inline, from auto-detect + gui = pylabtools.pylab_activate(ns, gui or 'inline', import_all, self) + self.user_ns.update(ns) + self.user_ns_hidden.update(ns) + # Now we must activate the gui pylab wants to use, and fix %run to take + # plot updates into account + enable_gui(gui) + self.magic_run = self._pylab_magic_run - def magic_pylab(self, *args, **kwargs): - raise NotImplementedError( - 'pylab support must be enabled in command line options.') # A few magics that are adapted to the specifics of using pexpect and a # remote terminal