Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP

Loading…

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

Merged
merged 3 commits into from

3 participants

@minrk
Owner

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
Owner

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

@jdmarch
Collaborator

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
Owner

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

@jdmarch
Collaborator

Yes, that bug is fixed.

@jdmarch
Collaborator

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
Owner

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
Owner

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
Owner

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

@minrk
Owner

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
Owner

@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
@fperez fperez referenced this pull request from a commit
Commit has since been removed from the repository and is no longer available.
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 authored
    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 authored
    Including Terminal pylab, for unsupported backends
    
    Also fix %pylab default in kernel to 'auto' like everywhere else, for uniformity.
This page is out of date. Refresh to see the latest.
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.