diff --git a/IPython/core/interactiveshell.py b/IPython/core/interactiveshell.py index 7c3dc32..e88c44a 100644 --- a/IPython/core/interactiveshell.py +++ b/IPython/core/interactiveshell.py @@ -62,6 +62,7 @@ from IPython.core.payload import PayloadManager from IPython.core.plugin import PluginManager from IPython.core.prefilter import PrefilterManager, ESC_MAGIC from IPython.core.profiledir import ProfileDir +from IPython.core.pylabtools import pylab_activate from IPython.external.Itpl import ItplNS from IPython.utils import PyColorize from IPython.utils import io @@ -2531,8 +2532,46 @@ class InteractiveShell(SingletonConfigurable, Magic): # Things related to GUI support and pylab #------------------------------------------------------------------------- + def enable_gui(self, gui=None): + raise NotImplementedError('Implement enable_gui in a subclass') + def enable_pylab(self, gui=None, import_all=True): - raise NotImplementedError('Implement enable_pylab in a subclass') + """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 correctly + 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 + + If given, dictates the choice of matplotlib GUI backend to use + (should be one of IPython's supported backends, 'qt', 'osx', 'tk', + 'gtk', 'wx' or 'inline'), otherwise we use the default chosen by + matplotlib (as dictated by the matplotlib build-time options plus the + user's matplotlibrc configuration file). Note that not all backends + make sense in all contexts, for example a terminal ipython can't + display figures inline. + """ + + # 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 = 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 + self.enable_gui(gui) + self.magic_run = self._pylab_magic_run #------------------------------------------------------------------------- # Utilities diff --git a/IPython/core/magic.py b/IPython/core/magic.py index e39d289..bb28845 100644 --- a/IPython/core/magic.py +++ b/IPython/core/magic.py @@ -50,7 +50,7 @@ from IPython.core.profiledir import ProfileDir from IPython.core.macro import Macro from IPython.core import magic_arguments, page from IPython.core.prefilter import ESC_MAGIC -from IPython.lib.pylabtools import mpl_runner +from IPython.core.pylabtools import mpl_runner from IPython.testing.skipdoctest import skip_doctest from IPython.utils import py3compat from IPython.utils.io import file_read, nlprint @@ -3305,24 +3305,30 @@ Defaulting color scheme to 'NoColor'""" This magic replaces IPython's threaded shells that were activated using the (pylab/wthread/etc.) command line flags. GUI toolkits - can now be enabled, disabled and changed at runtime and keyboard + can now be enabled at runtime and keyboard interrupts should work without any problems. The following toolkits - are supported: wxPython, PyQt4, PyGTK, and Tk:: + are supported: wxPython, PyQt4, PyGTK, Tk and Cocoa (OSX):: %gui wx # enable wxPython event loop integration %gui qt4|qt # enable PyQt4 event loop integration %gui gtk # enable PyGTK event loop integration %gui tk # enable Tk event loop integration + %gui OSX # enable Cocoa event loop integration + # (requires %matplotlib 1.1) %gui # disable all 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.lib.inputhook import enable_gui opts, arg = self.parse_options(parameter_s, '') if arg=='': arg = None - return enable_gui(arg) + try: + return self.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 magic_load_ext(self, module_str): """Load an IPython extension by its module name.""" @@ -3416,9 +3422,9 @@ Defaulting color scheme to 'NoColor'""" Parameters ---------- guiname : optional - One of the valid arguments to the %gui magic ('qt', 'wx', 'gtk', 'osx' or - 'tk'). If given, the corresponding Matplotlib backend is used, - otherwise matplotlib's default (which you can override in your + One of the valid arguments to the %gui magic ('qt', 'wx', 'gtk', + 'osx' or 'tk'). If given, the corresponding Matplotlib backend is + used, otherwise matplotlib's default (which you can override in your matplotlib config file) is used. Examples @@ -3449,7 +3455,7 @@ Defaulting color scheme to 'NoColor'""" else: import_all_status = True - self.shell.enable_pylab(s,import_all=import_all_status) + self.shell.enable_pylab(s, import_all=import_all_status) def magic_tb(self, s): """Print the last traceback with the currently active exception mode. diff --git a/IPython/lib/pylabtools.py b/IPython/core/pylabtools.py similarity index 91% rename from IPython/lib/pylabtools.py rename to IPython/core/pylabtools.py index 0b6feb1..b16d67a 100644 --- a/IPython/lib/pylabtools.py +++ b/IPython/core/pylabtools.py @@ -232,7 +232,8 @@ def activate_matplotlib(backend): # For this, we wrap it into a decorator which adds a 'called' flag. pylab.draw_if_interactive = flag_calls(pylab.draw_if_interactive) -def import_pylab(user_ns, backend, import_all=True, shell=None): + +def import_pylab(user_ns, import_all=True): """Import the standard pylab symbols into user_ns.""" # Import numpy as np/pyplot as plt are conventions we're trying to @@ -246,48 +247,62 @@ def import_pylab(user_ns, backend, import_all=True, shell=None): ) exec s in user_ns - if shell is not None: - exec s in shell.user_ns_hidden - # If using our svg payload backend, register the post-execution - # function that will pick up the results for display. This can only be - # done with access to the real shell object. - # - from IPython.zmq.pylab.backend_inline import InlineBackend - - cfg = InlineBackend.instance(config=shell.config) - cfg.shell = shell - if cfg not in shell.configurables: - shell.configurables.append(cfg) - - if backend == backends['inline']: - from IPython.zmq.pylab.backend_inline import flush_figures - from matplotlib import pyplot - shell.register_post_execute(flush_figures) - # load inline_rc - pyplot.rcParams.update(cfg.rc) - - # Add 'figsize' to pyplot and to the user's namespace - user_ns['figsize'] = pyplot.figsize = figsize - shell.user_ns_hidden['figsize'] = figsize - - # Setup the default figure format - fmt = cfg.figure_format - select_figure_format(shell, fmt) - - # The old pastefig function has been replaced by display - from IPython.core.display import display - # Add display and display_png to the user's namespace - user_ns['display'] = display - shell.user_ns_hidden['display'] = display - user_ns['getfigs'] = getfigs - shell.user_ns_hidden['getfigs'] = getfigs - if import_all: s = ("from matplotlib.pylab import *\n" "from numpy import *\n") exec s in user_ns - if shell is not None: - exec s in shell.user_ns_hidden + + +def configure_inline_support(shell, backend, user_ns=None): + """Configure an IPython shell object for matplotlib use. + + Parameters + ---------- + shell : InteractiveShell instance + + backend : matplotlib backend + + user_ns : dict + A namespace where all configured variables will be placed. If not given, + the `user_ns` attribute of the shell object is used. + """ + # If using our svg payload backend, register the post-execution + # function that will pick up the results for display. This can only be + # done with access to the real shell object. + + # Note: if we can't load the inline backend, then there's no point + # continuing (such as in terminal-only shells in environments without + # zeromq available). + try: + from IPython.zmq.pylab.backend_inline import InlineBackend + except ImportError: + return + + user_ns = shell.user_ns if user_ns is None else user_ns + + cfg = InlineBackend.instance(config=shell.config) + cfg.shell = shell + if cfg not in shell.configurables: + shell.configurables.append(cfg) + + if backend == backends['inline']: + from IPython.zmq.pylab.backend_inline import flush_figures + from matplotlib import pyplot + shell.register_post_execute(flush_figures) + # load inline_rc + pyplot.rcParams.update(cfg.rc) + # Add 'figsize' to pyplot and to the user's namespace + user_ns['figsize'] = pyplot.figsize = figsize + + # Setup the default figure format + fmt = cfg.figure_format + select_figure_format(shell, fmt) + + # The old pastefig function has been replaced by display + from IPython.core.display import display + # Add display and getfigs to the user's namespace + user_ns['display'] = display + user_ns['getfigs'] = getfigs def pylab_activate(user_ns, gui=None, import_all=True, shell=None): @@ -313,8 +328,10 @@ def pylab_activate(user_ns, gui=None, import_all=True, shell=None): """ gui, backend = find_gui_and_backend(gui) activate_matplotlib(backend) - import_pylab(user_ns, backend, import_all, shell) - + import_pylab(user_ns, import_all) + if shell is not None: + configure_inline_support(shell, backend, user_ns) + print """ Welcome to pylab, a matplotlib-based Python environment [backend: %s]. For more information, type 'help(pylab)'.""" % backend diff --git a/IPython/lib/tests/test_pylabtools.py b/IPython/core/tests/test_pylabtools.py similarity index 92% rename from IPython/lib/tests/test_pylabtools.py rename to IPython/core/tests/test_pylabtools.py index 02a7dd7..3c12523 100644 --- a/IPython/lib/tests/test_pylabtools.py +++ b/IPython/core/tests/test_pylabtools.py @@ -20,6 +20,7 @@ import matplotlib; matplotlib.use('Agg') import nose.tools as nt from matplotlib import pyplot as plt +import numpy as np # Our own imports from IPython.testing import decorators as dec @@ -52,3 +53,11 @@ def test_figure_to_svg(): plt.draw() svg = pt.print_figure(fig, 'svg')[:100].lower() yield nt.assert_true('doctype svg' in svg) + + +def test_import_pylab(): + ip = get_ipython() + ns = {} + pt.import_pylab(ns, import_all=False) + nt.assert_true('plt' in ns) + nt.assert_equal(ns['np'], np) diff --git a/IPython/frontend/terminal/interactiveshell.py b/IPython/frontend/terminal/interactiveshell.py index 7196ca7..f8cc10d 100644 --- a/IPython/frontend/terminal/interactiveshell.py +++ b/IPython/frontend/terminal/interactiveshell.py @@ -29,8 +29,7 @@ except: from IPython.core.error import TryNext from IPython.core.usage import interactive_usage, default_banner from IPython.core.interactiveshell import InteractiveShell, InteractiveShellABC -from IPython.lib.inputhook import enable_gui -from IPython.lib.pylabtools import pylab_activate +from IPython.core.pylabtools import pylab_activate from IPython.testing.skipdoctest import skip_doctest from IPython.utils import py3compat from IPython.utils.terminal import toggle_set_term_title, set_term_title @@ -171,10 +170,13 @@ class TerminalInteractiveShell(InteractiveShell): help="Enable auto setting the terminal title." ) - def __init__(self, config=None, ipython_dir=None, profile_dir=None, user_ns=None, - user_module=None, custom_exceptions=((),None), - usage=None, banner1=None, banner2=None, - display_banner=None): + # In the terminal, GUI control is done via PyOS_InputHook + from IPython.lib.inputhook import enable_gui + enable_gui = staticmethod(enable_gui) + + def __init__(self, config=None, ipython_dir=None, profile_dir=None, + user_ns=None, user_module=None, custom_exceptions=((),None), + usage=None, banner1=None, banner2=None, display_banner=None): super(TerminalInteractiveShell, self).__init__( config=config, profile_dir=profile_dir, user_ns=user_ns, @@ -517,45 +519,6 @@ class TerminalInteractiveShell(InteractiveShell): return True #------------------------------------------------------------------------- - # Things related to GUI support and pylab - #------------------------------------------------------------------------- - - 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 - - If given, dictates the choice of matplotlib GUI backend to use - (should be one of IPython's supported backends, 'tk', 'qt', 'wx' 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). - """ - # 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 = 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 - # plot updates into account - enable_gui(gui) - self.magic_run = self._pylab_magic_run - - #------------------------------------------------------------------------- # Things related to exiting #------------------------------------------------------------------------- diff --git a/IPython/testing/iptest.py b/IPython/testing/iptest.py index e4fddb6..de180c2 100644 --- a/IPython/testing/iptest.py +++ b/IPython/testing/iptest.py @@ -238,8 +238,8 @@ def make_exclude(): exclusions.append(ipjoin('parallel', 'tests', 'test_mongodb')) if not have['matplotlib']: - exclusions.extend([ipjoin('lib', 'pylabtools'), - ipjoin('lib', 'tests', 'test_pylabtools')]) + exclusions.extend([ipjoin('core', 'pylabtools'), + ipjoin('core', 'tests', 'test_pylabtools')]) if not have['tornado']: exclusions.append(ipjoin('frontend', 'html')) diff --git a/IPython/zmq/eventloops.py b/IPython/zmq/eventloops.py new file mode 100644 index 0000000..d80e011 --- /dev/null +++ b/IPython/zmq/eventloops.py @@ -0,0 +1,206 @@ +# encoding: utf-8 +"""Event loop integration for the ZeroMQ-based kernels. +""" + +#----------------------------------------------------------------------------- +# 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 +#----------------------------------------------------------------------------- + +import sys + +# System library imports. +import zmq + +# Local imports. +from IPython.utils import io + +#------------------------------------------------------------------------------ +# Eventloops for integrating the Kernel into different GUIs +#------------------------------------------------------------------------------ + +def loop_qt4(kernel): + """Start a kernel with PyQt4 event loop integration.""" + + 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) + + +def loop_wx(kernel): + """Start a kernel with wx event loop support.""" + + import wx + from IPython.lib.guisupport import start_event_loop_wx + + doi = kernel.do_one_iteration + # Wx uses milliseconds + poll_interval = int(1000*kernel._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. + kernel.app = IPWxApp(redirect=False) + start_event_loop_wx(kernel.app) + + +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 + + 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() + + 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) + + 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: + # 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, + None : None, +} + + +def enable_gui(gui, kernel=None): + """Enable integration with a given GUI""" + if kernel is None: + from .ipkernel import IPKernelApp + 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 diff --git a/IPython/zmq/ipkernel.py b/IPython/zmq/ipkernel.py index 453de87..d0ec165 100755 --- a/IPython/zmq/ipkernel.py +++ b/IPython/zmq/ipkernel.py @@ -39,12 +39,11 @@ from IPython.utils import py3compat from IPython.utils.jsonutil import json_clean from IPython.lib import pylabtools from IPython.utils.traitlets import ( - Any, List, Instance, Float, Dict, Bool, Unicode, CaselessStrEnum + Any, Instance, Float, Dict, CaselessStrEnum ) from entry_point import base_launch_kernel from kernelapp import KernelApp, kernel_flags, kernel_aliases -from iostream import OutStream from session import Session, Message from zmqshell import ZMQInteractiveShell @@ -212,16 +211,16 @@ class Kernel(Configurable): def _publish_pyin(self, code, parent): """Publish the code request on the pyin stream.""" - pyin_msg = self.session.send(self.iopub_socket, u'pyin',{u'code':code}, parent=parent) + self.session.send(self.iopub_socket, u'pyin', {u'code':code}, + parent=parent) def execute_request(self, ident, parent): - status_msg = self.session.send(self.iopub_socket, - u'status', - {u'execution_state':u'busy'}, - parent=parent - ) - + self.session.send(self.iopub_socket, + u'status', + {u'execution_state':u'busy'}, + parent=parent ) + try: content = parent[u'content'] code = content[u'code'] @@ -331,11 +330,10 @@ class Kernel(Configurable): if reply_msg['content']['status'] == u'error': self._abort_queue() - status_msg = self.session.send(self.iopub_socket, - u'status', - {u'execution_state':u'idle'}, - parent=parent - ) + self.session.send(self.iopub_socket, + u'status', + {u'execution_state':u'idle'}, + parent=parent ) def complete_request(self, ident, parent): txt, matches = self._complete(parent) @@ -375,7 +373,8 @@ class Kernel(Configurable): elif hist_access_type == 'search': pattern = parent['content']['pattern'] - hist = self.shell.history_manager.search(pattern, raw=raw, output=output) + hist = self.shell.history_manager.search(pattern, raw=raw, + output=output) else: hist = [] @@ -396,7 +395,8 @@ class Kernel(Configurable): def shutdown_request(self, ident, parent): self.shell.exit_now = True - self._shutdown_message = self.session.msg(u'shutdown_reply', parent['content'], parent) + self._shutdown_message = self.session.msg(u'shutdown_reply', + parent['content'], parent) sys.exit(0) #--------------------------------------------------------------------------- @@ -427,8 +427,10 @@ class Kernel(Configurable): time.sleep(0.1) def _no_raw_input(self): - """Raise StdinNotImplentedError if active frontend doesn't support stdin.""" - raise StdinNotImplementedError("raw_input was called, but this frontend does not support stdin.") + """Raise StdinNotImplentedError if active frontend doesn't support + stdin.""" + raise StdinNotImplementedError("raw_input was called, but this " + "frontend does not support stdin.") def _raw_input(self, prompt, ident, parent): # Flush output before making the request. @@ -437,7 +439,8 @@ class Kernel(Configurable): # Send the input request. content = json_clean(dict(prompt=prompt)) - msg = self.session.send(self.stdin_socket, u'input_request', content, parent, ident=ident) + self.session.send(self.stdin_socket, u'input_request', content, parent, + ident=ident) # Await a response. while True: @@ -510,189 +513,6 @@ class Kernel(Configurable): # before Python truly shuts down. time.sleep(0.01) - -#------------------------------------------------------------------------------ -# Eventloops for integrating the Kernel into different GUIs -#------------------------------------------------------------------------------ - - -def loop_qt4(kernel): - """Start a kernel with PyQt4 event loop integration.""" - - 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) - - -def loop_wx(kernel): - """Start a kernel with wx event loop support.""" - - import wx - from IPython.lib.guisupport import start_event_loop_wx - - doi = kernel.do_one_iteration - # Wx uses milliseconds - poll_interval = int(1000*kernel._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. - kernel.app = IPWxApp(redirect=False) - start_event_loop_wx(kernel.app) - - -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 - - 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() - - 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) - - 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: - # 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 - - #----------------------------------------------------------------------------- # Aliases and Flags for the IPKernelApp #----------------------------------------------------------------------------- @@ -767,7 +587,8 @@ class IPKernelApp(KernelApp, InteractiveShellApp): # replace pyerr-sending traceback with stdout _showtraceback = shell._showtraceback def print_tb(etype, evalue, stb): - print ("Error initializing pylab, pylab mode will not be active", file=io.stderr) + print ("Error initializing pylab, pylab mode will not " + "be active", file=io.stderr) print (shell.InteractiveTB.stb2text(stb), file=io.stdout) shell._showtraceback = print_tb @@ -790,8 +611,8 @@ class IPKernelApp(KernelApp, InteractiveShellApp): def launch_kernel(*args, **kwargs): """Launches a localhost IPython kernel, binding to the specified ports. - This function simply calls entry_point.base_launch_kernel with the right first - command to start an ipkernel. See base_launch_kernel for arguments. + This function simply calls entry_point.base_launch_kernel with the right + first command to start an ipkernel. See base_launch_kernel for arguments. Returns ------- diff --git a/IPython/zmq/pylab/backend_inline.py b/IPython/zmq/pylab/backend_inline.py index 7bf9b1f..e806588 100644 --- a/IPython/zmq/pylab/backend_inline.py +++ b/IPython/zmq/pylab/backend_inline.py @@ -16,7 +16,7 @@ from matplotlib._pylab_helpers import Gcf # Local imports. from IPython.config.configurable import SingletonConfigurable from IPython.core.displaypub import publish_display_data -from IPython.lib.pylabtools import print_figure, select_figure_format +from IPython.core.pylabtools import print_figure, select_figure_format from IPython.utils.traitlets import Dict, Instance, CaselessStrEnum, CBool from IPython.utils.warn import warn diff --git a/IPython/zmq/zmqshell.py b/IPython/zmq/zmqshell.py index 942ea43..19641a1 100644 --- a/IPython/zmq/zmqshell.py +++ b/IPython/zmq/zmqshell.py @@ -109,6 +109,11 @@ class ZMQInteractiveShell(InteractiveShell): keepkernel_on_exit = None + # Over ZeroMQ, GUI control isn't done with PyOS_InputHook as there is no + # interactive input being read; we provide event loop support in ipkernel + from .eventloops import enable_gui + enable_gui = staticmethod(enable_gui) + def init_environment(self): """Configure the user's environment. @@ -390,79 +395,6 @@ class ZMQInteractiveShell(InteractiveShell): } self.payload_manager.write_payload(payload) - 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 - - # A few magics that are adapted to the specifics of using pexpect and a # remote terminal @@ -567,7 +499,6 @@ class ZMQInteractiveShell(InteractiveShell): except Exception as e: error("Could not start qtconsole: %r" % e) return - def set_next_input(self, text): """Send the specified text to the frontend to be presented at the next @@ -578,4 +509,5 @@ class ZMQInteractiveShell(InteractiveShell): ) self.payload_manager.write_payload(payload) + InteractiveShellABC.register(ZMQInteractiveShell)