diff --git a/IPython/core/interactiveshell.py b/IPython/core/interactiveshell.py index 18a6570..74847a9 100644 --- a/IPython/core/interactiveshell.py +++ b/IPython/core/interactiveshell.py @@ -162,6 +162,7 @@ class InteractiveShell(Configurable, Magic): object_info_string_level = Enum((0,1,2), default_value=0, config=True) pdb = CBool(False, config=True) + pprint = CBool(True, config=True) profile = Str('', config=True) prompt_in1 = Str('In [\\#]: ', config=True) @@ -212,6 +213,9 @@ class InteractiveShell(Configurable, Magic): plugin_manager = Instance('IPython.core.plugin.PluginManager') payload_manager = Instance('IPython.core.payload.PayloadManager') + # Private interface + _post_execute = set() + def __init__(self, config=None, ipython_dir=None, user_ns=None, user_global_ns=None, custom_exceptions=((), None)): @@ -570,6 +574,13 @@ class InteractiveShell(Configurable, Magic): setattr(self.hooks,name, dp) + def register_post_execute(self, func): + """Register a function for calling after code execution. + """ + if not callable(func): + raise ValueError('argument %s must be callable' % func) + self._post_execute.add(func) + #------------------------------------------------------------------------- # Things related to the "main" module #------------------------------------------------------------------------- @@ -2268,6 +2279,21 @@ class InteractiveShell(Configurable, Magic): outflag = 0 if softspace(sys.stdout, 0): print + + # Execute any registered post-execution functions. Here, any errors + # are reported only minimally and just on the terminal, because the + # main exception channel may be occupied with a user traceback. + # FIXME: we need to think this mechanism a little more carefully. + for func in self._post_execute: + try: + func() + except: + head = '[ ERROR ] Evaluating post_execute function: %s' % func + print >> io.Term.cout, head + print >> io.Term.cout, self._simple_error() + print >> io.Term.cout, 'Removing from post_execute' + self._post_execute.remove(func) + # Flush out code object which has been run (and source) self.code_to_run = None return outflag diff --git a/IPython/frontend/qt/console/ipythonqt.py b/IPython/frontend/qt/console/ipythonqt.py index b6483c1..cae3f0a 100644 --- a/IPython/frontend/qt/console/ipythonqt.py +++ b/IPython/frontend/qt/console/ipythonqt.py @@ -114,10 +114,7 @@ def main(): if args.pure: kernel_manager.start_kernel(ipython=False) elif args.pylab: - if args.rich: - kernel_manager.start_kernel(pylab='payload-svg') - else: - kernel_manager.start_kernel(pylab=args.pylab) + kernel_manager.start_kernel(pylab=args.pylab) else: kernel_manager.start_kernel() kernel_manager.start_channels() @@ -127,7 +124,7 @@ def main(): if args.pure: kind = 'rich' if args.rich else 'plain' widget = FrontendWidget(kind=kind, paging=args.paging) - elif args.rich: + elif args.rich or args.pylab: widget = RichIPythonWidget(paging=args.paging) else: widget = IPythonWidget(paging=args.paging) diff --git a/IPython/lib/pylabtools.py b/IPython/lib/pylabtools.py index ca9442b..b10f702 100644 --- a/IPython/lib/pylabtools.py +++ b/IPython/lib/pylabtools.py @@ -21,6 +21,15 @@ Authors from IPython.utils.decorators import flag_calls +# If user specifies a GUI, that dictates the backend, otherwise we read the +# user's mpl default from the mpl rc structure +backends = {'tk': 'TkAgg', + 'gtk': 'GTKAgg', + 'wx': 'WXAgg', + 'qt': 'Qt4Agg', # qt3 not supported + 'qt4': 'Qt4Agg', + 'payload-svg' : 'module://IPython.zmq.pylab.backend_payload_svg'} + #----------------------------------------------------------------------------- # Main classes and functions #----------------------------------------------------------------------------- @@ -42,24 +51,15 @@ def find_gui_and_backend(gui=None): import matplotlib - # If user specifies a GUI, that dictates the backend, otherwise we read the - # user's mpl default from the mpl rc structure - g2b = {'tk': 'TkAgg', - 'gtk': 'GTKAgg', - 'wx': 'WXAgg', - 'qt': 'Qt4Agg', # qt3 not supported - 'qt4': 'Qt4Agg', - 'payload-svg' : \ - 'module://IPython.zmq.pylab.backend_payload_svg'} - if gui: # select backend based on requested gui - backend = g2b[gui] + backend = backends[gui] else: backend = matplotlib.rcParams['backend'] # In this case, we need to find what the appropriate gui selection call # should be for IPython, so we can activate inputhook accordingly - b2g = dict(zip(g2b.values(),g2b.keys())) + g2b = backends # maps gui names to mpl backend names + b2g = dict(zip(g2b.values(), g2b.keys())) # reverse dict gui = b2g.get(backend, None) return gui, backend @@ -90,7 +90,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, import_all=True): + +def import_pylab(user_ns, backend, import_all=True, shell=None): """Import the standard pylab symbols into user_ns.""" # Import numpy as np/pyplot as plt are conventions we're trying to @@ -103,6 +104,17 @@ def import_pylab(user_ns, import_all=True): "plt = pyplot\n" ) in user_ns + if shell is not None: + # 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. + if backend == backends['payload-svg']: + from IPython.zmq.pylab.backend_payload_svg import flush_svg + shell.register_post_execute(flush_svg) + else: + from IPython.zmq.pylab.backend_payload_svg import paste + user_ns['paste'] = paste + if import_all: exec("from matplotlib.pylab import *\n" "from numpy import *\n") in user_ns @@ -131,7 +143,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) + import_pylab(user_ns, backend) print """ Welcome to pylab, a matplotlib-based Python environment [backend: %s]. diff --git a/IPython/zmq/ipkernel.py b/IPython/zmq/ipkernel.py index d00616c..e34bf84 100755 --- a/IPython/zmq/ipkernel.py +++ b/IPython/zmq/ipkernel.py @@ -192,11 +192,12 @@ class Kernel(Configurable): # FIXME: runlines calls the exception handler itself. shell._reply_content = None + # For now leave this here until we're sure we can stop using it + #shell.runlines(code) + # Experimental: cell mode! Test more before turning into # default and removing the hacks around runlines. shell.run_cell(code) - # For now leave this here until we're sure we can stop using it - #shell.runlines(code) except: status = u'error' # FIXME: this code right now isn't being used yet by default, @@ -210,10 +211,6 @@ class Kernel(Configurable): reply_content.update(shell._showtraceback(etype, evalue, tb_list)) else: status = u'ok' - reply_content[u'payload'] = shell.payload_manager.read_payload() - # Be agressive about clearing the payload because we don't want - # it to sit in memory until the next execute_request comes in. - shell.payload_manager.clear_payload() reply_content[u'status'] = status # Compute the execution counter so clients can display prompts @@ -236,7 +233,15 @@ class Kernel(Configurable): # expressions reply_content[u'user_variables'] = {} reply_content[u'user_expressions'] = {} - + + # Payloads should be retrieved regardless of outcome, so we can both + # recover partial output (that could have been generated early in a + # block, before an error) and clear the payload system always. + reply_content[u'payload'] = shell.payload_manager.read_payload() + # Be agressive about clearing the payload because we don't want + # it to sit in memory until the next execute_request comes in. + shell.payload_manager.clear_payload() + # Send the reply. reply_msg = self.session.msg(u'execute_reply', reply_content, parent) io.raw_print(reply_msg) @@ -571,7 +576,8 @@ given, the GUI backend is matplotlib's, otherwise use one of: \ kernel = make_kernel(namespace, kernel_class, OutStream) if namespace.pylab: - pylabtools.import_pylab(kernel.shell.user_ns) + pylabtools.import_pylab(kernel.shell.user_ns, backend, + shell=kernel.shell) start_kernel(namespace, kernel) diff --git a/IPython/zmq/pylab/backend_payload_svg.py b/IPython/zmq/pylab/backend_payload_svg.py index 3523b49..9504572 100644 --- a/IPython/zmq/pylab/backend_payload_svg.py +++ b/IPython/zmq/pylab/backend_payload_svg.py @@ -3,11 +3,13 @@ #----------------------------------------------------------------------------- # Imports #----------------------------------------------------------------------------- +from __future__ import print_function # Standard library imports from cStringIO import StringIO # System library imports. +import matplotlib from matplotlib.backends.backend_svg import new_figure_manager from matplotlib._pylab_helpers import Gcf @@ -18,17 +20,68 @@ from backend_payload import add_plot_payload # Functions #----------------------------------------------------------------------------- -def show(): - """ Deliver a SVG payload. +def show(close=True): + """Show all figures as SVG payloads sent to the IPython clients. + + Parameters + ---------- + close : bool, optional + If true, a ``plt.close('all')`` call is automatically issued after + sending all the SVG figures. """ for figure_manager in Gcf.get_all_fig_managers(): - # Make the background transparent. - # figure_manager.canvas.figure.patch.set_alpha(0.0) - # Set the background to white instead so it looks good on black. - figure_manager.canvas.figure.set_facecolor('white') - figure_manager.canvas.figure.set_edgecolor('white') - data = svg_from_canvas(figure_manager.canvas) - add_plot_payload('svg', data) + send_svg_canvas(figure_manager.canvas) + if close: + matplotlib.pyplot.close('all') + +# This flag will be reset by draw_if_interactive when called +show._draw_called = False + + +def paste(*figs): + """Paste figures into the console workspace. + + If no arguments are given, all available figures are pasted. If the + argument list contains references to invalid figures, a warning is printed + but the function continues pasting further figures. + + Parameters + ---------- + figs : tuple + A tuple that can contain any mixture of integers and figure objects. + """ + if not figs: + show(close=False) + else: + fig_managers = Gcf.get_all_fig_managers() + fig_index = dict( [(fm.canvas.figure, fm.canvas) for fm in fig_managers] + + [ (fm.canvas.figure.number, fm.canvas) for fm in fig_managers] ) + + for fig in figs: + canvas = fig_index.get(fig) + if canvas is None: + print('Warning: figure %s not available.' % fig) + else: + send_svg_canvas(canvas) + + +def send_svg_canvas(canvas): + """Draw the current canvas and send it as an SVG payload. + """ + # Make the background transparent. + # figure_manager.canvas.figure.patch.set_alpha(0.0) + + # Set the background to white instead so it looks good on black. We store + # the current values to restore them at the end. + fc = canvas.figure.get_facecolor() + ec = canvas.figure.get_edgecolor() + canvas.figure.set_facecolor('white') + canvas.figure.set_edgecolor('white') + try: + add_plot_payload('svg', svg_from_canvas(canvas)) + finally: + canvas.figure.set_facecolor(fc) + canvas.figure.set_edgecolor(ec) def svg_from_canvas(canvas): @@ -37,3 +90,23 @@ def svg_from_canvas(canvas): string_io = StringIO() canvas.print_svg(string_io) return string_io.getvalue() + + +def draw_if_interactive(): + """ + Is called after every pylab drawing command + """ + # We simply flag we were called and otherwise do nothing. At the end of + # the code execution, a separate call to show_close() will act upon this. + show._draw_called = True + + +def flush_svg(): + """Call show, close all open figures, sending all SVG images. + + This is meant to be called automatically and will call show() if, during + prior code execution, there had been any calls to draw_if_interactive. + """ + if show._draw_called: + show(close=True) + show._draw_called = False