Skip to content

Loading…

enable %gui/%pylab magics in the Kernel #905

Merged
merged 3 commits into from

3 participants

@minrk
IPython member

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 auto.

So you can now activate pylab mode after launch, in the QtConsole or Notebook.

@minrk
IPython member

another difference: unlike the Terminal versions, eventloop integration can only be done once and is not reversible.

@jdmarch

In Windows 7, works beautifully with ipython-qtconsole as long as command line switch --pylab is not specified. If it is, qtconsole freezes after banner text, just before first input prompt.

@minrk minrk 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.
d39f0ca
@minrk
IPython member

rebased on master, and that bug should be fixed. Thanks!

@jdmarch

Yes, that bug is fixed.

@jdmarch

If %pylab is invoked twice, there is no RuntimeError traceback, but if --pylab is on the command line, then user does %reset, then %pylab, there is such a traceback in qtconsole, which does not seem useful.

FWIW in the context of this PR: in ipython terminal there is also an unhelpful UserWarning (but at least no traceback) the first time that %pylab is re-invoked (but not subsequent times). This is whether or not the initial invocation was from --pylab or from %pylab.

@fperez
IPython member

Fantastic! Many thanks, @minrk. Tested, works great!

One comment only: when started at the cmd line with --pylab, the qtconsole now shows the pylab message above the banner, where as it should appear below the banner like it does in the terminal.

I should note that before, we were simply swallowing that message, so this is already an improvement as we should show it. It's just that I hadn't realized we were swallowing it, while it's apparent now that it appears misplaced.

Great job though, many thanks!!

@minrk
IPython member

We weren't actually swallowing it before - no message was output at all. We were calling the respective low-level functions separately, rather than pylab_activate() itself, which does setup and the message. I can investigate why stdout messages posted prior to the first execution go above instead of below the banner (you can see the same by adding 'print "hi"' to exec_lines, or even ipython qtconsole -c 'print "hello"'). Perhaps @epatters knows exactly where to look. Should be a simple matter of moving the Cursor.

@minrk
IPython member

Nevermind, I found it, will push in a sec.

@minrk
IPython member

pushed - now print statements prior to first execution (including pylab) should come up after the main banner.

@minrk minrk quiet error messages instead of tracebacks in %pylab/%gui
Including Terminal pylab, for unsupported backends

Also fix %pylab default in kernel to 'auto' like everywhere else, for uniformity.
69f6680
@minrk
IPython member

@jdmarch error messages should be quieter now, no more tracebacks.

The default backend is now 'auto' like everywhere else, so you have to %pylab inline to get the inline backend.

@fperez fperez merged commit 6f94725 into ipython:master
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Oct 20, 2011
  1. @minrk

    enable %gui/%pylab magics in the Kernel

    minrk committed
    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.
Commits on Oct 21, 2011
  1. @minrk
  2. @minrk

    quiet error messages instead of tracebacks in %pylab/%gui

    minrk committed
    Including Terminal pylab, for unsupported backends
    
    Also fix %pylab default in kernel to 'auto' like everywhere else, for uniformity.
View
3 IPython/frontend/qt/console/frontend_widget.py
@@ -485,6 +485,9 @@ def reset(self):
self._control.clear()
self._append_plain_text(self.banner)
+ # update output marker for stdout/stderr, so that startup
+ # messages appear after banner:
+ self._append_before_prompt_pos = self._get_cursor().position()
self._show_interpreter_prompt()
def restart_kernel(self, message, now=False):
View
6 IPython/frontend/terminal/interactiveshell.py
@@ -458,7 +458,11 @@ def enable_pylab(self, gui=None, import_all=True):
# code in an empty namespace, and we update *both* user_ns and
# user_ns_hidden with this information.
ns = {}
- gui = pylab_activate(ns, gui, import_all)
+ try:
+ gui = pylab_activate(ns, gui, import_all)
+ except KeyError:
+ error("Backend %r not supported" % gui)
+ return
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
View
6 IPython/lib/pylabtools.py
@@ -196,7 +196,7 @@ def find_gui_and_backend(gui=None):
import matplotlib
- if gui:
+ if gui and gui != 'auto':
# select backend based on requested gui
backend = backends[gui]
else:
@@ -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].
View
347 IPython/zmq/ipkernel.py
@@ -22,6 +22,7 @@
import time
import traceback
import logging
+
# System library imports.
import zmq
@@ -38,7 +39,7 @@
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 @@ def start(self):
"""
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 @@ def start(self):
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 @@ def _at_shutdown(self):
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,20 @@ def initialize(self, argv=None):
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)
+ gui, backend = pylabtools.find_gui_and_backend(self.pylab)
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
View
78 IPython/zmq/zmqshell.py
@@ -31,6 +31,7 @@
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,78 @@ def magic_edit(self,parameter_s='',last_call=['','']):
}
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
+ try:
+ enable_gui(arg)
+ except Exception as e:
+ # print simple error message, rather than traceback if we can't
+ # hook up the GUI
+ error(str(e))
+
+ 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 = {}
+ try:
+ gui = pylabtools.pylab_activate(ns, gui, import_all, self)
+ except KeyError:
+ error("Backend %r not supported" % gui)
+ return
+ 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
+ try:
+ enable_gui(gui)
+ except Exception as e:
+ # print simple error message, rather than traceback if we can't
+ # hook up the GUI
+ error(str(e))
+ 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
Something went wrong with that request. Please try again.