diff --git a/IPython/config/default/ipython_config.py b/IPython/config/default/ipython_config.py index e44d6a1..a753d6f 100644 --- a/IPython/config/default/ipython_config.py +++ b/IPython/config/default/ipython_config.py @@ -81,8 +81,6 @@ c = get_config() # c.InteractiveShell.pdb = False -# c.InteractiveShell.pprint = True - # c.InteractiveShell.prompt_in1 = 'In [\#]: ' # c.InteractiveShell.prompt_in2 = ' .\D.: ' # c.InteractiveShell.prompt_out = 'Out[\#]: ' @@ -129,6 +127,12 @@ c = get_config() # c.InteractiveShell.xmode = 'Context' #----------------------------------------------------------------------------- +# Formatter and display options +#----------------------------------------------------------------------------- + +# c.PlainTextFormatter.pprint = True + +#----------------------------------------------------------------------------- # PrefilterManager options #----------------------------------------------------------------------------- diff --git a/IPython/config/profile/ipython_config_sympy.py b/IPython/config/profile/ipython_config_sympy.py index a32b522..e01308f 100644 --- a/IPython/config/profile/ipython_config_sympy.py +++ b/IPython/config/profile/ipython_config_sympy.py @@ -15,7 +15,15 @@ f, g, h = map(Function, 'fgh') # You have to make sure that attributes that are containers already # exist before using them. Simple assigning a new list will override # all previous values. + if hasattr(c.Global, 'exec_lines'): c.Global.exec_lines.append(lines) else: c.Global.exec_lines = [lines] + +# Load the sympy_printing extension to enable nice printing of sympy expr's. +if hasattr(c.Global, 'extensions'): + c.Global.extensions.append('sympy_printing') +else: + c.Global.extensions = ['sympy_printing'] + diff --git a/IPython/core/display.py b/IPython/core/display.py new file mode 100644 index 0000000..57375c2 --- /dev/null +++ b/IPython/core/display.py @@ -0,0 +1,122 @@ +# -*- coding: utf-8 -*- +"""Top-level display functions for displaying object in different formats. + +Authors: + +* Brian Granger +""" + +#----------------------------------------------------------------------------- +# Copyright (C) 2008-2010 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 +#----------------------------------------------------------------------------- + +#----------------------------------------------------------------------------- +# Main functions +#----------------------------------------------------------------------------- + +def display(*objs, **kwargs): + """Display a Python object in all frontends. + + By default all representations will be computed and sent to the frontends. + Frontends can decide which representation is used and how. + + Parameters + ---------- + objs : tuple of objects + The Python objects to display. + include : list or tuple, optional + A list of format type strings (MIME types) to include in the + format data dict. If this is set *only* the format types included + in this list will be computed. + exclude : list or tuple, optional + A list of format type string (MIME types) to exclue in the format + data dict. If this is set all format types will be computed, + except for those included in this argument. + """ + include = kwargs.get('include') + exclude = kwargs.get('exclude') + + from IPython.core.interactiveshell import InteractiveShell + inst = InteractiveShell.instance() + format = inst.display_formatter.format + publish = inst.display_pub.publish + + for obj in objs: + format_dict = format(obj, include=include, exclude=exclude) + publish('IPython.core.display.display', format_dict) + + +def display_pretty(*objs): + """Display the pretty (default) representation of an object. + + Parameters + ---------- + objs : tuple of objects + The Python objects to display. + """ + display(*objs, include=['text/plain']) + + +def display_html(*objs): + """Display the HTML representation of an object. + + Parameters + ---------- + objs : tuple of objects + The Python objects to display. + """ + display(*objs, include=['text/plain','text/html']) + + +def display_svg(*objs): + """Display the SVG representation of an object. + + Parameters + ---------- + objs : tuple of objects + The Python objects to display. + """ + display(*objs, include=['text/plain','image/svg+xml']) + + +def display_png(*objs): + """Display the PNG representation of an object. + + Parameters + ---------- + objs : tuple of objects + The Python objects to display. + """ + display(*objs, include=['text/plain','image/png']) + + +def display_latex(*objs): + """Display the LaTeX representation of an object. + + Parameters + ---------- + objs : tuple of objects + The Python objects to display. + """ + display(*objs, include=['text/plain','text/latex']) + + +def display_json(*objs): + """Display the JSON representation of an object. + + Parameters + ---------- + objs : tuple of objects + The Python objects to display. + """ + display(*objs, include=['text/plain','application/json']) + + + diff --git a/IPython/core/displayhook.py b/IPython/core/displayhook.py index 9324ce6..c5d3116 100644 --- a/IPython/core/displayhook.py +++ b/IPython/core/displayhook.py @@ -1,10 +1,13 @@ # -*- coding: utf-8 -*- """Displayhook for IPython. +This defines a callable class that IPython uses for `sys.displayhook`. + Authors: * Fernando Perez * Brian Granger +* Robert Kern """ #----------------------------------------------------------------------------- @@ -27,7 +30,6 @@ import IPython.utils.generics import IPython.utils.io from IPython.utils.traitlets import Instance, List from IPython.utils.warn import warn -from IPython.core.formatters import DefaultFormatter #----------------------------------------------------------------------------- # Main displayhook class @@ -54,19 +56,6 @@ class DisplayHook(Configurable): shell = Instance('IPython.core.interactiveshell.InteractiveShellABC') - # The default formatter. - default_formatter = Instance('IPython.core.formatters.FormatterABC') - def _default_formatter_default(self): - # FIXME: backwards compatibility for the InteractiveShell.pprint option? - return DefaultFormatter(config=self.config) - - # Any additional FormatterABC instances we use. - # FIXME: currently unused. - extra_formatters = List(config=True) - - # Each call to the In[] prompt raises it by 1, even the first. - #prompt_count = Int(0) - def __init__(self, shell=None, cache_size=1000, colors='NoColor', input_sep='\n', output_sep='\n', output_sep2='', @@ -185,36 +174,63 @@ class DisplayHook(Configurable): pass def write_output_prompt(self): - """Write the output prompt.""" + """Write the output prompt. + + The default implementation simply writes the prompt to + ``io.Term.cout``. + """ # Use write, not print which adds an extra space. IPython.utils.io.Term.cout.write(self.output_sep) outprompt = str(self.prompt_out) if self.do_full_cache: IPython.utils.io.Term.cout.write(outprompt) - def compute_result_repr(self, result): - """Compute and return the repr of the object to be displayed. - - This method only compute the string form of the repr and should NOT - actually print or write that to a stream. + def compute_format_data(self, result): + """Compute format data of the object to be displayed. + + The format data is a generalization of the :func:`repr` of an object. + In the default implementation the format data is a :class:`dict` of + key value pair where the keys are valid MIME types and the values + are JSON'able data structure containing the raw data for that MIME + type. It is up to frontends to determine pick a MIME to to use and + display that data in an appropriate manner. + + This method only computes the format data for the object and should + NOT actually print or write that to a stream. + + Parameters + ---------- + result : object + The Python object passed to the display hook, whose format will be + computed. + + Returns + ------- + format_data : dict + A :class:`dict` whose keys are valid MIME types and values are + JSON'able raw data for that MIME type. It is recommended that + all return values of this should always include the "text/plain" + MIME type representation of the object. """ - result_repr = self.default_formatter(result) - extra_formats = [] - for f in self.extra_formatters: - try: - data = f(result) - except Exception: - # FIXME: log the exception. - continue - if data is not None: - extra_formats.append((f.id, f.format, data)) + return self.shell.display_formatter.format(result) - return result_repr, extra_formats + def write_format_data(self, format_dict): + """Write the format data dict to the frontend. - def write_result_repr(self, result_repr, extra_formats): + This default version of this method simply writes the plain text + representation of the object to ``io.Term.cout``. Subclasses should + override this method to send the entire `format_dict` to the + frontends. + + Parameters + ---------- + format_dict : dict + The format dict for the object passed to `sys.displayhook`. + """ # We want to print because we want to always make sure we have a # newline, even if all the prompt separators are ''. This is the # standard IPython behavior. + result_repr = format_dict['text/plain'] if '\n' in result_repr: # So that multi-line strings line up with the left column of # the screen, instead of having the output prompt mess up @@ -278,8 +294,8 @@ class DisplayHook(Configurable): if result is not None and not self.quiet(): self.start_displayhook() self.write_output_prompt() - result_repr, extra_formats = self.compute_result_repr(result) - self.write_result_repr(result_repr, extra_formats) + format_dict = self.compute_format_data(result) + self.write_format_data(format_dict) self.update_user_ns(result) self.log_output(result) self.finish_displayhook() @@ -300,5 +316,6 @@ class DisplayHook(Configurable): if '_' not in __builtin__.__dict__: self.shell.user_ns.update({'_':None,'__':None, '___':None}) import gc - gc.collect() # xxx needed? + # TODO: Is this really needed? + gc.collect() diff --git a/IPython/core/displaypub.py b/IPython/core/displaypub.py new file mode 100644 index 0000000..44b6a3e --- /dev/null +++ b/IPython/core/displaypub.py @@ -0,0 +1,145 @@ +"""An interface for publishing rich data to frontends. + +There are two components of the display system: + +* Display formatters, which take a Python object and compute the + representation of the object in various formats (text, HTML, SVg, etc.). +* The display publisher that is used to send the representation data to the + various frontends. + +This module defines the logic display publishing. The display publisher uses +the ``display_data`` message type that is defined in the IPython messaging +spec. + +Authors: + +* Brian Granger +""" + +#----------------------------------------------------------------------------- +# Copyright (C) 2008-2010 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 +#----------------------------------------------------------------------------- + +from __future__ import print_function + +from IPython.config.configurable import Configurable + +#----------------------------------------------------------------------------- +# Main payload class +#----------------------------------------------------------------------------- + +class DisplayPublisher(Configurable): + """A traited class that publishes display data to frontends. + + Instances of this class are created by the main IPython object and should + be accessed there. + """ + + def _validate_data(self, source, data, metadata=None): + """Validate the display data. + + Parameters + ---------- + source : str + The fully dotted name of the callable that created the data, like + :func:`foo.bar.my_formatter`. + data : dict + The formata data dictionary. + metadata : dict + Any metadata for the data. + """ + + if not isinstance(source, str): + raise TypeError('source must be a str, got: %r' % source) + if not isinstance(data, dict): + raise TypeError('data must be a dict, got: %r' % data) + if metadata is not None: + if not isinstance(metadata, dict): + raise TypeError('metadata must be a dict, got: %r' % data) + + def publish(self, source, data, metadata=None): + """Publish data and metadata to all frontends. + + See the ``display_data`` message in the messaging documentation for + more details about this message type. + + The following MIME types are currently implemented: + + * text/plain + * text/html + * text/latex + * application/json + * image/png + * immage/svg+xml + + Parameters + ---------- + source : str + A string that give the function or method that created the data, + such as 'IPython.core.page'. + data : dict + A dictionary having keys that are valid MIME types (like + 'text/plain' or 'image/svg+xml') and values that are the data for + that MIME type. The data itself must be a JSON'able data + structure. Minimally all data should have the 'text/plain' data, + which can be displayed by all frontends. If more than the plain + text is given, it is up to the frontend to decide which + representation to use. + metadata : dict + A dictionary for metadata related to the data. This can contain + arbitrary key, value pairs that frontends can use to interpret + the data. + """ + from IPython.utils import io + # The default is to simply write the plain text data using io.Term. + if data.has_key('text/plain'): + print(data['text/plain'], file=io.Term.cout) + + +def publish_display_data(self, source, data, metadata=None): + """Publish data and metadata to all frontends. + + See the ``display_data`` message in the messaging documentation for + more details about this message type. + + The following MIME types are currently implemented: + + * text/plain + * text/html + * text/latex + * application/json + * image/png + * immage/svg+xml + + Parameters + ---------- + source : str + A string that give the function or method that created the data, + such as 'IPython.core.page'. + data : dict + A dictionary having keys that are valid MIME types (like + 'text/plain' or 'image/svg+xml') and values that are the data for + that MIME type. The data itself must be a JSON'able data + structure. Minimally all data should have the 'text/plain' data, + which can be displayed by all frontends. If more than the plain + text is given, it is up to the frontend to decide which + representation to use. + metadata : dict + A dictionary for metadata related to the data. This can contain + arbitrary key, value pairs that frontends can use to interpret + the data. + """ + from IPython.core.interactiveshell import InteractiveShell + InteractiveShell.instance().display_pub.publish( + source, + data, + metadata + ) + diff --git a/IPython/core/formatters.py b/IPython/core/formatters.py index be13192..f28336f 100644 --- a/IPython/core/formatters.py +++ b/IPython/core/formatters.py @@ -1,36 +1,11 @@ # -*- coding: utf-8 -*- -"""Displayhook formatters. - -The DefaultFormatter is always present and may be configured from the -ipython_config.py file. For example, to add a pretty-printer for a numpy.dtype -object:: - - def dtype_pprinter(obj, p, cycle): - if cycle: - return p.text('dtype(...)') - if hasattr(obj, 'fields'): - if obj.fields is None: - p.text(repr(obj)) - else: - p.begin_group(7, 'dtype([') - for i, field in enumerate(obj.descr): - if i > 0: - p.text(',') - p.breakable() - p.pretty(field) - p.end_group(7, '])') - - c.DefaultFormatter.deferred_pprinters = { - ('numpy', 'dtype'): dtype_pprinter, - } - -The deferred_pprinters dictionary is the preferred way to configure these -pretty-printers. This allows you to define the pretty-printer without needing to -import the type itself. The dictionary maps (modulename, typename) pairs to -a function. - -See the `IPython.external.pretty` documentation for how to write -pretty-printer functions. +"""Display formatters. + + +Authors: + +* Robert Kern +* Brian Granger """ #----------------------------------------------------------------------------- # Copyright (c) 2010, IPython Development Team. @@ -40,9 +15,14 @@ pretty-printer functions. # The full license is in the file COPYING.txt, distributed with this software. #----------------------------------------------------------------------------- +#----------------------------------------------------------------------------- +# Imports +#----------------------------------------------------------------------------- + # Stdlib imports import abc -from cStringIO import StringIO +# We must use StringIO, as cStringIO doesn't handle unicode properly. +from StringIO import StringIO # Our own imports from IPython.config.configurable import Configurable @@ -51,19 +31,315 @@ from IPython.utils.traitlets import Bool, Dict, Int, Str #----------------------------------------------------------------------------- -# Classes and functions +# The main DisplayFormatter class +#----------------------------------------------------------------------------- + + +class DisplayFormatter(Configurable): + + # When set to true only the default plain text formatter will be used. + plain_text_only = Bool(False, config=True) + + # A dict of formatter whose keys are format types (MIME types) and whose + # values are subclasses of BaseFormatter. + formatters = Dict(config=True) + def _formatters_default(self): + """Activate the default formatters.""" + formatter_classes = [ + PlainTextFormatter, + HTMLFormatter, + SVGFormatter, + PNGFormatter, + LatexFormatter, + JSONFormatter + ] + d = {} + for cls in formatter_classes: + f = cls(config=self.config) + d[f.format_type] = f + return d + + def format(self, obj, include=None, exclude=None): + """Return a format data dict for an object. + + By default all format types will be computed. + + The following MIME types are currently implemented: + + * text/plain + * text/html + * text/latex + * application/json + * image/png + * immage/svg+xml + + Parameters + ---------- + obj : object + The Python object whose format data will be computed. + include : list or tuple, optional + A list of format type strings (MIME types) to include in the + format data dict. If this is set *only* the format types included + in this list will be computed. + exclude : list or tuple, optional + A list of format type string (MIME types) to exclue in the format + data dict. If this is set all format types will be computed, + except for those included in this argument. + + Returns + ------- + format_dict : dict + A dictionary of key/value pairs, one or each format that was + generated for the object. The keys are the format types, which + will usually be MIME type strings and the values and JSON'able + data structure containing the raw data for the representation in + that format. + """ + format_dict = {} + + # If plain text only is active + if self.plain_text_only: + formatter = self.formatters['text/plain'] + try: + data = formatter(obj) + except: + # FIXME: log the exception + raise + if data is not None: + format_dict['text/plain'] = data + return format_dict + + for format_type, formatter in self.formatters.items(): + if include is not None: + if format_type not in include: + continue + if exclude is not None: + if format_type in exclude: + continue + try: + data = formatter(obj) + except: + # FIXME: log the exception + raise + if data is not None: + format_dict[format_type] = data + return format_dict + + @property + def format_types(self): + """Return the format types (MIME types) of the active formatters.""" + return self.formatters.keys() + + +#----------------------------------------------------------------------------- +# Formatters for specific format types (text, html, svg, etc.) #----------------------------------------------------------------------------- -class DefaultFormatter(Configurable): - """ The default pretty-printer. + +class FormatterABC(object): + """ Abstract base class for Formatters. + + A formatter is a callable class that is responsible for computing the + raw format data for a particular format type (MIME type). For example, + an HTML formatter would have a format type of `text/html` and would return + the HTML representation of the object when called. """ + __metaclass__ = abc.ABCMeta + + # The format type of the data returned, usually a MIME type. + format_type = 'text/plain' - # The ID of the formatter. - id = Str('default') + # Is the formatter enabled... + enabled = True - # The kind of data returned. - # This is often, but not always a MIME type. - format = Str('text/plain') + @abc.abstractmethod + def __call__(self, obj): + """Return a JSON'able representation of the object. + + If the object cannot be formatted by this formatter, then return None + """ + try: + return repr(obj) + except TypeError: + return None + + +class BaseFormatter(Configurable): + """A base formatter class that is configurable. + + This formatter should usually be used as the base class of all formatters. + It is a traited :class:`Configurable` class and includes an extensible + API for users to determine how their objects are formatted. The following + logic is used to find a function to format an given object. + + 1. The object is introspected to see if it has a method with the name + :attr:`print_method`. If is does, that object is passed to that method + for formatting. + 2. If no print method is found, three internal dictionaries are consulted + to find print method: :attr:`singleton_printers`, :attr:`type_printers` + and :attr:`deferred_printers`. + + Users should use these dictionaries to register functions that will be + used to compute the format data for their objects (if those objects don't + have the special print methods). The easiest way of using these + dictionaries is through the :meth:`for_type` and :meth:`for_type_by_name` + methods. + + If no function/callable is found to compute the format data, ``None`` is + returned and this format type is not used. + """ + + format_type = Str('text/plain') + + enabled = Bool(True, config=True) + + print_method = Str('__repr__') + + # The singleton printers. + # Maps the IDs of the builtin singleton objects to the format functions. + singleton_printers = Dict(config=True) + def _singleton_printers_default(self): + return {} + + # The type-specific printers. + # Map type objects to the format functions. + type_printers = Dict(config=True) + def _type_printers_default(self): + return {} + + # The deferred-import type-specific printers. + # Map (modulename, classname) pairs to the format functions. + deferred_printers = Dict(config=True) + def _deferred_printers_default(self): + return {} + + def __call__(self, obj): + """Compute the format for an object.""" + if self.enabled: + obj_id = id(obj) + try: + obj_class = getattr(obj, '__class__', None) or type(obj) + if hasattr(obj_class, self.print_method): + printer = getattr(obj_class, self.print_method) + return printer(obj) + try: + printer = self.singleton_printers[obj_id] + except (TypeError, KeyError): + pass + else: + return printer(obj) + for cls in pretty._get_mro(obj_class): + if cls in self.type_printers: + return self.type_printers[cls](obj) + else: + printer = self._in_deferred_types(cls) + if printer is not None: + return printer(obj) + return None + except Exception: + pass + else: + return None + + def for_type(self, typ, func): + """Add a format function for a given type. + + Parameters + ----------- + typ : class + The class of the object that will be formatted using `func`. + func : callable + The callable that will be called to compute the format data. The + call signature of this function is simple, it must take the + object to be formatted and return the raw data for the given + format. Subclasses may use a different call signature for the + `func` argument. + """ + oldfunc = self.type_printers.get(typ, None) + if func is not None: + # To support easy restoration of old printers, we need to ignore + # Nones. + self.type_printers[typ] = func + return oldfunc + + def for_type_by_name(self, type_module, type_name, func): + """Add a format function for a type specified by the full dotted + module and name of the type, rather than the type of the object. + + Parameters + ---------- + type_module : str + The full dotted name of the module the type is defined in, like + ``numpy``. + type_name : str + The name of the type (the class name), like ``dtype`` + func : callable + The callable that will be called to compute the format data. The + call signature of this function is simple, it must take the + object to be formatted and return the raw data for the given + format. Subclasses may use a different call signature for the + `func` argument. + """ + key = (type_module, type_name) + oldfunc = self.deferred_printers.get(key, None) + if func is not None: + # To support easy restoration of old printers, we need to ignore + # Nones. + self.deferred_printers[key] = func + return oldfunc + + def _in_deferred_types(self, cls): + """ + Check if the given class is specified in the deferred type registry. + + Returns the printer from the registry if it exists, and None if the + class is not in the registry. Successful matches will be moved to the + regular type registry for future use. + """ + mod = getattr(cls, '__module__', None) + name = getattr(cls, '__name__', None) + key = (mod, name) + printer = None + if key in self.deferred_printers: + # Move the printer over to the regular registry. + printer = self.deferred_printers.pop(key) + self.type_printers[cls] = printer + return printer + + +class PlainTextFormatter(BaseFormatter): + """The default pretty-printer. + + This uses :mod:`IPython.external.pretty` to compute the format data of + the object. If the object cannot be pretty printed, :func:`repr` is used. + See the documentation of :mod:`IPython.external.pretty` for details on + how to write pretty printers. Here is a simple example:: + + def dtype_pprinter(obj, p, cycle): + if cycle: + return p.text('dtype(...)') + if hasattr(obj, 'fields'): + if obj.fields is None: + p.text(repr(obj)) + else: + p.begin_group(7, 'dtype([') + for i, field in enumerate(obj.descr): + if i > 0: + p.text(',') + p.breakable() + p.pretty(field) + p.end_group(7, '])') + """ + + # The format type of data returned. + format_type = Str('text/plain') + + # This subclass ignores this attribute as it always need to return + # something. + enabled = Bool(True, config=False) + + # Look for a __pretty__ methods to use for pretty printing. + print_method = Str('__pretty__') # Whether to pretty-print or not. pprint = Bool(True, config=True) @@ -77,93 +353,152 @@ class DefaultFormatter(Configurable): # The newline character. newline = Str('\n', config=True) - # The singleton prettyprinters. - # Maps the IDs of the builtin singleton objects to the format functions. - singleton_pprinters = Dict(config=True) - def _singleton_pprinters_default(self): + # Use the default pretty printers from IPython.external.pretty. + def _singleton_printers_default(self): return pretty._singleton_pprinters.copy() - # The type-specific prettyprinters. - # Map type objects to the format functions. - type_pprinters = Dict(config=True) - def _type_pprinters_default(self): + def _type_printers_default(self): return pretty._type_pprinters.copy() - # The deferred-import type-specific prettyprinters. - # Map (modulename, classname) pairs to the format functions. - deferred_pprinters = Dict(config=True) - def _deferred_pprinters_default(self): + def _deferred_printers_default(self): return pretty._deferred_type_pprinters.copy() #### FormatterABC interface #### def __call__(self, obj): - """ Format the object. - """ + """Compute the pretty representation of the object.""" if not self.pprint: try: return repr(obj) except TypeError: return '' else: + # This uses use StringIO, as cStringIO doesn't handle unicode. stream = StringIO() printer = pretty.RepresentationPrinter(stream, self.verbose, self.max_width, self.newline, - singleton_pprinters=self.singleton_pprinters, - type_pprinters=self.type_pprinters, - deferred_pprinters=self.deferred_pprinters) + singleton_pprinters=self.singleton_printers, + type_pprinters=self.type_printers, + deferred_pprinters=self.deferred_printers) printer.pretty(obj) printer.flush() return stream.getvalue() - #### DefaultFormatter interface #### +class HTMLFormatter(BaseFormatter): + """An HTML formatter. - def for_type(self, typ, func): - """ - Add a pretty printer for a given type. - """ - oldfunc = self.type_pprinters.get(typ, None) - if func is not None: - # To support easy restoration of old pprinters, we need to ignore - # Nones. - self.type_pprinters[typ] = func - return oldfunc + To define the callables that compute the HTML representation of your + objects, define a :meth:`__html__` method or use the :meth:`for_type` + or :meth:`for_type_by_name` methods to register functions that handle + this. + """ + format_type = Str('text/html') - def for_type_by_name(self, type_module, type_name, func): - """ - Add a pretty printer for a type specified by the module and name of - a type rather than the type object itself. - """ - key = (type_module, type_name) - oldfunc = self.deferred_pprinters.get(key, None) - if func is not None: - # To support easy restoration of old pprinters, we need to ignore - # Nones. - self.deferred_pprinters[key] = func - return oldfunc + print_method = Str('__html__') -class FormatterABC(object): - """ Abstract base class for Formatters. +class SVGFormatter(BaseFormatter): + """An SVG formatter. + + To define the callables that compute the SVG representation of your + objects, define a :meth:`__svg__` method or use the :meth:`for_type` + or :meth:`for_type_by_name` methods to register functions that handle + this. """ - __metaclass__ = abc.ABCMeta + format_type = Str('image/svg+xml') - # The ID of the formatter. - id = 'abstract' + print_method = Str('__svg__') - # The kind of data returned. - format = 'text/plain' - @abc.abstractmethod - def __call__(self, obj): - """ Return a JSONable representation of the object. +class PNGFormatter(BaseFormatter): + """A PNG formatter. - If the object cannot be formatted by this formatter, then return None - """ - try: - return repr(obj) - except TypeError: - return None + To define the callables that compute the PNG representation of your + objects, define a :meth:`__png__` method or use the :meth:`for_type` + or :meth:`for_type_by_name` methods to register functions that handle + this. The raw data should be the base64 encoded raw png data. + """ + format_type = Str('image/png') + + print_method = Str('__png__') + + +class LatexFormatter(BaseFormatter): + """A LaTeX formatter. + + To define the callables that compute the LaTeX representation of your + objects, define a :meth:`__latex__` method or use the :meth:`for_type` + or :meth:`for_type_by_name` methods to register functions that handle + this. + """ + format_type = Str('text/latex') + + print_method = Str('__latex__') + + +class JSONFormatter(BaseFormatter): + """A JSON string formatter. + + To define the callables that compute the JSON string representation of + your objects, define a :meth:`__json__` method or use the :meth:`for_type` + or :meth:`for_type_by_name` methods to register functions that handle + this. + """ + format_type = Str('application/json') + + print_method = Str('__json__') + + +FormatterABC.register(BaseFormatter) +FormatterABC.register(PlainTextFormatter) +FormatterABC.register(HTMLFormatter) +FormatterABC.register(SVGFormatter) +FormatterABC.register(PNGFormatter) +FormatterABC.register(LatexFormatter) +FormatterABC.register(JSONFormatter) + + +def format_display_data(obj, include=None, exclude=None): + """Return a format data dict for an object. + + By default all format types will be computed. + + The following MIME types are currently implemented: + + * text/plain + * text/html + * text/latex + * application/json + * image/png + * immage/svg+xml + + Parameters + ---------- + obj : object + The Python object whose format data will be computed. + + Returns + ------- + format_dict : dict + A dictionary of key/value pairs, one or each format that was + generated for the object. The keys are the format types, which + will usually be MIME type strings and the values and JSON'able + data structure containing the raw data for the representation in + that format. + include : list or tuple, optional + A list of format type strings (MIME types) to include in the + format data dict. If this is set *only* the format types included + in this list will be computed. + exclude : list or tuple, optional + A list of format type string (MIME types) to exclue in the format + data dict. If this is set all format types will be computed, + except for those included in this argument. + """ + from IPython.core.interactiveshell import InteractiveShell -FormatterABC.register(DefaultFormatter) + InteractiveShell.instance().display_formatter.format( + obj, + include, + exclude + ) diff --git a/IPython/core/hooks.py b/IPython/core/hooks.py index 5ea055b..712342f 100644 --- a/IPython/core/hooks.py +++ b/IPython/core/hooks.py @@ -156,33 +156,6 @@ class CommandChainDispatcher: return iter(self.chain) -def result_display(self,arg): - """ Default display hook. - - Called for displaying the result to the user. - """ - - if self.pprint: - try: - out = pformat(arg) - except: - # Work around possible bugs in pformat - out = repr(arg) - if '\n' in out: - # So that multi-line strings line up with the left column of - # the screen, instead of having the output prompt mess up - # their first line. - IPython.utils.io.Term.cout.write('\n') - print >>IPython.utils.io.Term.cout, out - else: - # By default, the interactive prompt uses repr() to display results, - # so we should honor this. Users who'd rather use a different - # mechanism can easily override this hook. - print >>IPython.utils.io.Term.cout, repr(arg) - # the default display hook doesn't manipulate the value to put in history - return None - - def input_prefilter(self,line): """ Default input prefilter diff --git a/IPython/core/interactiveshell.py b/IPython/core/interactiveshell.py index 52753b3..16cf17e 100644 --- a/IPython/core/interactiveshell.py +++ b/IPython/core/interactiveshell.py @@ -41,9 +41,11 @@ from IPython.core.builtin_trap import BuiltinTrap from IPython.core.compilerop import CachingCompiler from IPython.core.display_trap import DisplayTrap from IPython.core.displayhook import DisplayHook +from IPython.core.displaypub import DisplayPublisher from IPython.core.error import TryNext, UsageError from IPython.core.extensions import ExtensionManager from IPython.core.fakemodule import FakeModule, init_fakemod_dict +from IPython.core.formatters import DisplayFormatter from IPython.core.history import HistoryManager from IPython.core.inputsplitter import IPythonInputSplitter from IPython.core.logger import Logger @@ -149,7 +151,10 @@ class InteractiveShell(Configurable, Magic): default_value=get_default_colors(), config=True) debug = CBool(False, config=True) deep_reload = CBool(False, config=True) + display_formatter = Instance(DisplayFormatter) displayhook_class = Type(DisplayHook) + display_pub_class = Type(DisplayPublisher) + exit_now = CBool(False) # Monotonically increasing execution counter execution_count = Int(1) @@ -167,7 +172,6 @@ class InteractiveShell(Configurable, Magic): config=True) pdb = CBool(False, config=True) - pprint = CBool(True, config=True) profile = Str('', config=True) prompt_in1 = Str('In [\\#]: ', config=True) prompt_in2 = Str(' .\\D.: ', config=True) @@ -284,6 +288,8 @@ class InteractiveShell(Configurable, Magic): self.init_io() self.init_traceback_handlers(custom_exceptions) self.init_prompts() + self.init_display_formatter() + self.init_display_pub() self.init_displayhook() self.init_reload_doctest() self.init_magics() @@ -481,6 +487,12 @@ class InteractiveShell(Configurable, Magic): # will initialize that object and all prompt related information. pass + def init_display_formatter(self): + self.display_formatter = DisplayFormatter(config=self.config) + + def init_display_pub(self): + self.display_pub = self.display_pub_class(config=self.config) + def init_displayhook(self): # Initialize displayhook, set in/out prompts and printing system self.displayhook = self.displayhook_class( diff --git a/IPython/core/magic.py b/IPython/core/magic.py index 531c9ed..12df58c 100644 --- a/IPython/core/magic.py +++ b/IPython/core/magic.py @@ -2424,12 +2424,12 @@ Defaulting color scheme to 'NoColor'""" else: shell.inspector.set_active_scheme('NoColor') - def magic_Pprint(self, parameter_s=''): + def magic_pprint(self, parameter_s=''): """Toggle pretty printing on/off.""" - - self.shell.pprint = 1 - self.shell.pprint + ptformatter = self.shell.display_formatter.formatters['text/plain'] + ptformatter.pprint = bool(1 - ptformatter.pprint) print 'Pretty printing has been turned', \ - ['OFF','ON'][self.shell.pprint] + ['OFF','ON'][ptformatter.pprint] def magic_Exit(self, parameter_s=''): """Exit IPython.""" @@ -3163,6 +3163,8 @@ Defaulting color scheme to 'NoColor'""" shell = self.shell oc = shell.displayhook meta = shell.meta + disp_formatter = self.shell.display_formatter + ptformatter = disp_formatter.formatters['text/plain'] # dstore is a data store kept in the instance metadata bag to track any # changes we make, so we can undo them later. dstore = meta.setdefault('doctest_mode',Struct()) @@ -3170,12 +3172,13 @@ Defaulting color scheme to 'NoColor'""" # save a few values we'll need to recover later mode = save_dstore('mode',False) - save_dstore('rc_pprint',shell.pprint) + save_dstore('rc_pprint',ptformatter.pprint) save_dstore('xmode',shell.InteractiveTB.mode) save_dstore('rc_separate_out',shell.separate_out) save_dstore('rc_separate_out2',shell.separate_out2) save_dstore('rc_prompts_pad_left',shell.prompts_pad_left) save_dstore('rc_separate_in',shell.separate_in) + save_dstore('rc_plain_text_only',disp_formatter.plain_text_only) if mode == False: # turn on @@ -3191,7 +3194,8 @@ Defaulting color scheme to 'NoColor'""" oc.prompt1.pad_left = oc.prompt2.pad_left = \ oc.prompt_out.pad_left = False - shell.pprint = False + ptformatter.pprint = False + disp_formatter.plain_text_only = True shell.magic_xmode('Plain') else: @@ -3208,7 +3212,8 @@ Defaulting color scheme to 'NoColor'""" oc.prompt1.pad_left = oc.prompt2.pad_left = \ oc.prompt_out.pad_left = dstore.rc_prompts_pad_left - shell.pprint = dstore.rc_pprint + ptformatter.pprint = dstore.rc_pprint + disp_formatter.plain_text_only = dstore.rc_plain_text_only shell.magic_xmode(dstore.xmode) diff --git a/IPython/core/usage.py b/IPython/core/usage.py index 011fe85..77aa3e2 100644 --- a/IPython/core/usage.py +++ b/IPython/core/usage.py @@ -441,6 +441,31 @@ We have provided as magics ``%less`` to page files (aliased to ``%more``), most common commands you'd want to call in your subshell and that would cause problems if invoked via ``!cmd``, but you need to be aware of this limitation. +Display +======= + +The IPython console can now display objects in a variety of formats, including +HTML, PNG and SVG. This is accomplished using the display functions in +``IPython.core.display``:: + + In [4]: from IPython.core.display import display, display_html + + In [5]: from IPython.core.display import display_png, display_svg + +Python objects can simply be passed to these functions and the appropriate +representations will be displayed in the console as long as the objects know +how to compute those representations. The easiest way of teaching objects how +to format themselves in various representations is to define special methods +such as: ``__html``, ``__svg__`` and ``__png__``. IPython's display formatters +can also be given custom formatter functions for various types:: + + In [6]: ip = get_ipython() + + In [7]: html_formatter = ip.display_formatter.formatters['text/html'] + + In [8]: html_formatter.for_type(Foo, foo_to_html) + +For further details, see ``IPython.core.formatters``. Inline matplotlib graphics ========================== @@ -448,10 +473,14 @@ Inline matplotlib graphics The IPython console is capable of displaying matplotlib figures inline, in SVG format. If started with the ``--pylab inline`` flag, then all figures are rendered inline automatically. If started with ``--pylab`` or ``--pylab ``, then a GUI backend will be used, but the ``pastefig()`` function is -added to the global and ``plt`` namespaces. You can paste any figure that is -currently open in a window with this function; type ``pastefig?`` for -additional details.""" +backend>``, then a GUI backend will be used, but IPython's ``display()`` and +``getfigs()`` functions can be used to view plots inline:: + + In [9]: display(*getfigs()) # display all figures inline + + In[10]: display(*getfigs(1,2)) # display figures 1 and 2 inline +""" + quick_guide = """\ ? -> Introduction and overview of IPython's features. diff --git a/IPython/extensions/sympy_printing.py b/IPython/extensions/sympy_printing.py new file mode 100644 index 0000000..191fa6c --- /dev/null +++ b/IPython/extensions/sympy_printing.py @@ -0,0 +1,65 @@ +"""A print function that pretty prints sympy Basic objects. + +Authors: +* Brian Granger +""" +#----------------------------------------------------------------------------- +# Copyright (C) 2008-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 +#----------------------------------------------------------------------------- + +from IPython.lib.latextools import latex_to_png + +from sympy import pretty, latex + +#----------------------------------------------------------------------------- +# Definitions of magic functions for use with IPython +#----------------------------------------------------------------------------- + +def print_basic_unicode(o, p, cycle): + """A function to pretty print sympy Basic objects.""" + if cycle: + return p.text('Basic(...)') + out = pretty(o, use_unicode=True) + if '\n' in out: + p.text(u'\n') + p.text(out) + + +def print_png(o): + """A funciton to display sympy expression using LaTex -> PNG.""" + s = latex(o, mode='inline') + # mathtext does not understand certain latex flags, so we try to replace + # them with suitable subs. + s = s.replace('\\operatorname','') + s = s.replace('\\overline', '\\bar') + png = latex_to_png(s, encode=True) + return png + +_loaded = False + + +def load_ipython_extension(ip): + """Load the extension in IPython.""" + global _loaded + if not _loaded: + plaintext_formatter = ip.display_formatter.formatters['text/plain'] + plaintext_formatter.for_type_by_name( + 'sympy.core.basic', 'Basic', print_basic_unicode + ) + plaintext_formatter.for_type_by_name( + 'sympy.matrices.matrices', 'Matrix', print_basic_unicode + ) + + png_formatter = ip.display_formatter.formatters['image/png'] + png_formatter.for_type_by_name( + 'sympy.core.basic', 'Basic', print_png + ) + _loaded = True + diff --git a/IPython/frontend/qt/console/frontend_widget.py b/IPython/frontend/qt/console/frontend_widget.py index f170639..f0ae16d 100644 --- a/IPython/frontend/qt/console/frontend_widget.py +++ b/IPython/frontend/qt/console/frontend_widget.py @@ -352,7 +352,7 @@ class FrontendWidget(HistoryConsoleWidget, BaseFrontendMixin): """ Handle display hook output. """ if not self._hidden and self._is_from_this_session(msg): - self._append_plain_text(msg['content']['data'] + '\n') + self._append_plain_text(msg['content']['data']['text/plain'] + '\n') def _handle_stream(self, msg): """ Handle stdout, stderr, and stdin. diff --git a/IPython/frontend/qt/console/ipython_widget.py b/IPython/frontend/qt/console/ipython_widget.py index f879131..7b88990 100644 --- a/IPython/frontend/qt/console/ipython_widget.py +++ b/IPython/frontend/qt/console/ipython_widget.py @@ -179,9 +179,39 @@ class IPythonWidget(FrontendWidget): if not self._hidden and self._is_from_this_session(msg): content = msg['content'] prompt_number = content['execution_count'] - self._append_plain_text(self.output_sep) - self._append_html(self._make_out_prompt(prompt_number)) - self._append_plain_text(content['data']+self.output_sep2) + data = content['data'] + if data.has_key('text/html'): + self._append_plain_text(self.output_sep) + self._append_html(self._make_out_prompt(prompt_number)) + html = data['text/html'] + self._append_plain_text('\n') + self._append_html(html + self.output_sep2) + elif data.has_key('text/plain'): + self._append_plain_text(self.output_sep) + self._append_html(self._make_out_prompt(prompt_number)) + text = data['text/plain'] + self._append_plain_text(text + self.output_sep2) + + def _handle_display_data(self, msg): + """ The base handler for the ``display_data`` message. + """ + # For now, we don't display data from other frontends, but we + # eventually will as this allows all frontends to monitor the display + # data. But we need to figure out how to handle this in the GUI. + if not self._hidden and self._is_from_this_session(msg): + source = msg['content']['source'] + data = msg['content']['data'] + metadata = msg['content']['metadata'] + # In the regular IPythonWidget, we simply print the plain text + # representation. + if data.has_key('text/html'): + html = data['text/html'] + self._append_html(html) + elif data.has_key('text/plain'): + text = data['text/plain'] + self._append_plain_text(text) + # This newline seems to be needed for text and html output. + self._append_plain_text(u'\n') def _started_channels(self): """ Reimplemented to make a history request. @@ -444,7 +474,7 @@ class IPythonWidget(FrontendWidget): else: self._page(item['text'], html=False) - #------ Trait change handlers --------------------------------------------- + #------ Trait change handlers -------------------------------------------- def _style_sheet_changed(self): """ Set the style sheets of the underlying widgets. @@ -464,4 +494,4 @@ class IPythonWidget(FrontendWidget): self._highlighter.set_style(self.syntax_style) else: self._highlighter.set_style_sheet(self.style_sheet) - + diff --git a/IPython/frontend/qt/console/rich_ipython_widget.py b/IPython/frontend/qt/console/rich_ipython_widget.py index 98d0cb1..9657f8d 100644 --- a/IPython/frontend/qt/console/rich_ipython_widget.py +++ b/IPython/frontend/qt/console/rich_ipython_widget.py @@ -1,6 +1,7 @@ # System library imports import os import re +from base64 import decodestring from PyQt4 import QtCore, QtGui # Local imports @@ -56,7 +57,60 @@ class RichIPythonWidget(IPythonWidget): menu.addAction('Save SVG As...', lambda: save_svg(svg, self._control)) return menu - + + #--------------------------------------------------------------------------- + # 'BaseFrontendMixin' abstract interface + #--------------------------------------------------------------------------- + + def _handle_pyout(self, msg): + """ Overridden to handle rich data types, like SVG. + """ + if not self._hidden and self._is_from_this_session(msg): + content = msg['content'] + prompt_number = content['execution_count'] + data = content['data'] + if data.has_key('image/svg+xml'): + self._append_plain_text(self.output_sep) + self._append_html(self._make_out_prompt(prompt_number)) + # TODO: try/except this call. + self._append_svg(data['image/svg+xml']) + self._append_html(self.output_sep2) + elif data.has_key('image/png'): + self._append_plain_text(self.output_sep) + self._append_html(self._make_out_prompt(prompt_number)) + # This helps the output to look nice. + self._append_plain_text('\n') + # TODO: try/except these calls + png = decodestring(data['image/png']) + self._append_png(png) + self._append_html(self.output_sep2) + else: + # Default back to the plain text representation. + return super(RichIPythonWidget, self)._handle_pyout(msg) + + def _handle_display_data(self, msg): + """ Overridden to handle rich data types, like SVG. + """ + if not self._hidden and self._is_from_this_session(msg): + source = msg['content']['source'] + data = msg['content']['data'] + metadata = msg['content']['metadata'] + # Try to use the svg or html representations. + # FIXME: Is this the right ordering of things to try? + if data.has_key('image/svg+xml'): + svg = data['image/svg+xml'] + # TODO: try/except this call. + self._append_svg(svg) + elif data.has_key('image/png'): + # TODO: try/except these calls + # PNG data is base64 encoded as it passes over the network + # in a JSON structure so we decode it. + png = decodestring(data['image/png']) + self._append_png(png) + else: + # Default back to the plain text representation. + return super(RichIPythonWidget, self)._handle_display_data(msg) + #--------------------------------------------------------------------------- # 'FrontendWidget' protected interface #--------------------------------------------------------------------------- @@ -64,21 +118,12 @@ class RichIPythonWidget(IPythonWidget): def _process_execute_payload(self, item): """ Reimplemented to handle matplotlib plot payloads. """ + # TODO: remove this as all plot data is coming back through the + # display_data message type. if item['source'] == self._payload_source_plot: if item['format'] == 'svg': svg = item['data'] - try: - image = svg_to_image(svg) - except ValueError: - self._append_plain_text('Received invalid plot data.') - else: - format = self._add_image(image) - self._name_to_svg[str(format.name())] = svg - format.setProperty(self._svg_text_format_property, svg) - cursor = self._get_end_cursor() - cursor.insertBlock() - cursor.insertImage(format) - cursor.insertBlock() + self._append_svg(svg) return True else: # Add other plot formats here! @@ -90,6 +135,37 @@ class RichIPythonWidget(IPythonWidget): # 'RichIPythonWidget' protected interface #--------------------------------------------------------------------------- + def _append_svg(self, svg): + """ Append raw svg data to the widget. + """ + try: + image = svg_to_image(svg) + except ValueError: + self._append_plain_text('Received invalid plot data.') + else: + format = self._add_image(image) + self._name_to_svg[str(format.name())] = svg + format.setProperty(self._svg_text_format_property, svg) + cursor = self._get_end_cursor() + cursor.insertBlock() + cursor.insertImage(format) + cursor.insertBlock() + + def _append_png(self, png): + """ Append raw svg data to the widget. + """ + try: + image = QtGui.QImage() + image.loadFromData(png, 'PNG') + except ValueError: + self._append_plain_text('Received invalid plot data.') + else: + format = self._add_image(image) + cursor = self._get_end_cursor() + cursor.insertBlock() + cursor.insertImage(format) + cursor.insertBlock() + def _add_image(self, image): """ Adds the specified QImage to the document and returns a QTextImageFormat that references it. @@ -192,4 +268,4 @@ class RichIPythonWidget(IPythonWidget): else: return 'Unrecognized image format' - + diff --git a/IPython/frontend/qt/kernelmanager.py b/IPython/frontend/qt/kernelmanager.py index 98df284..79b8edb 100644 --- a/IPython/frontend/qt/kernelmanager.py +++ b/IPython/frontend/qt/kernelmanager.py @@ -101,6 +101,9 @@ class QtSubSocketChannel(SocketChannelQObject, SubSocketChannel): # Emitted when a message of type 'pyerr' is received. pyerr_received = QtCore.pyqtSignal(object) + # Emitted when a message of type 'display_data' is received + display_data_received = QtCore.pyqtSignal(object) + # Emitted when a crash report message is received from the kernel's # last-resort sys.excepthook. crash_received = QtCore.pyqtSignal(object) @@ -117,7 +120,6 @@ class QtSubSocketChannel(SocketChannelQObject, SubSocketChannel): """ # Emit the generic signal. self.message_received.emit(msg) - # Emit signals for specialized message types. msg_type = msg['msg_type'] signal = getattr(self, msg_type + '_received', None) diff --git a/IPython/frontend/terminal/ipapp.py b/IPython/frontend/terminal/ipapp.py index 874a63f..1e51e43 100755 --- a/IPython/frontend/terminal/ipapp.py +++ b/IPython/frontend/terminal/ipapp.py @@ -188,10 +188,10 @@ class IPAppConfigLoader(BaseAppConfigLoader): action='store_false', dest='InteractiveShell.pdb', help="Disable auto calling the pdb debugger after every exception.") paa('--pprint', - action='store_true', dest='InteractiveShell.pprint', + action='store_true', dest='PlainTextFormatter.pprint', help="Enable auto pretty printing of results.") paa('--no-pprint', - action='store_false', dest='InteractiveShell.pprint', + action='store_false', dest='PlainTextFormatter.pprint', help="Disable auto auto pretty printing of results.") paa('--prompt-in1','-pi1', type=str, dest='InteractiveShell.prompt_in1', @@ -443,7 +443,7 @@ class IPythonApp(Application): if hasattr(config.Global, 'classic'): if config.Global.classic: config.InteractiveShell.cache_size = 0 - config.InteractiveShell.pprint = 0 + config.PlainTextFormatter.pprint = 0 config.InteractiveShell.prompt_in1 = '>>> ' config.InteractiveShell.prompt_in2 = '... ' config.InteractiveShell.prompt_out = '' diff --git a/IPython/kernel/client.py b/IPython/kernel/client.py index 3178a0b..3eb24be 100644 --- a/IPython/kernel/client.py +++ b/IPython/kernel/client.py @@ -88,7 +88,7 @@ def _result_list_printer(obj, p, cycle): # ResultList is a list subclass and will use the default pretty printer. # This overrides that to use the __repr__ of ResultList. ip = get_ipython() -ip.displayhook.default_formatter.for_type_by_name( +ip.display_formatter.formatters['text/plain'].for_type_by_name( 'IPython.kernel.multiengineclient', 'ResultList', _result_list_printer ) diff --git a/IPython/lib/latextools.py b/IPython/lib/latextools.py new file mode 100644 index 0000000..3d97975 --- /dev/null +++ b/IPython/lib/latextools.py @@ -0,0 +1,62 @@ +# -*- coding: utf-8 -*- +"""Tools for handling LaTeX. + +Authors: + +* Brian Granger +""" +#----------------------------------------------------------------------------- +# Copyright (c) 2010, IPython Development Team. +# +# Distributed under the terms of the Modified BSD License. +# +# The full license is in the file COPYING.txt, distributed with this software. +#----------------------------------------------------------------------------- + +#----------------------------------------------------------------------------- +# Imports +#----------------------------------------------------------------------------- + +from StringIO import StringIO +from base64 import encodestring + +#----------------------------------------------------------------------------- +# Tools +#----------------------------------------------------------------------------- + + +def latex_to_png(s, encode=True): + """Render a LaTeX string to PNG using matplotlib.mathtext. + + Parameters + ---------- + s : str + The raw string containing valid inline LaTeX. + encode : bool, optional + Should the PNG data bebase64 encoded to make it JSON'able. + """ + from matplotlib import mathtext + + mt = mathtext.MathTextParser('bitmap') + f = StringIO() + mt.to_png(f, s, fontsize=12) + bin_data = f.getvalue() + if encode: + bin_data = encodestring(bin_data) + return bin_data + +_data_uri_template_png = """%s""" + +def latex_to_html(s, alt='image'): + """Render LaTeX to HTML with embedded PNG data using data URIs. + + Parameters + ---------- + s : str + The raw string containing valid inline LateX. + alt : str + The alt text to use for the HTML. + """ + base64_data = latex_to_png(s, encode=True) + return _data_uri_template_png % (base64_data, alt) + diff --git a/IPython/lib/pylabtools.py b/IPython/lib/pylabtools.py index 5a13a19..c26c06d 100644 --- a/IPython/lib/pylabtools.py +++ b/IPython/lib/pylabtools.py @@ -19,6 +19,8 @@ Authors # Imports #----------------------------------------------------------------------------- +from cStringIO import StringIO + from IPython.utils.decorators import flag_calls # If user specifies a GUI, that dictates the backend, otherwise we read the @@ -31,7 +33,107 @@ backends = {'tk': 'TkAgg', 'inline' : 'module://IPython.zmq.pylab.backend_inline'} #----------------------------------------------------------------------------- -# Main classes and functions +# Matplotlib utilities +#----------------------------------------------------------------------------- + + +def getfigs(*fig_nums): + """Get a list of matplotlib figures by figure numbers. + + If no arguments are given, all available figures are returned. 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 of ints giving the figure numbers of the figures to return. + """ + from matplotlib._pylab_helpers import Gcf + if not fig_nums: + fig_managers = Gcf.get_all_fig_managers() + return [fm.canvas.figure for fm in fig_managers] + else: + figs = [] + for num in fig_nums: + f = Gcf.figs.get(num) + if f is None: + print('Warning: figure %s not available.' % num) + figs.append(f.canvas.figure) + return figs + + +def figsize(sizex, sizey): + """Set the default figure size to be [sizex, sizey]. + + This is just an easy to remember, convenience wrapper that sets:: + + matplotlib.rcParams['figure.figsize'] = [sizex, sizey] + """ + import matplotlib + matplotlib.rcParams['figure.figsize'] = [sizex, sizey] + + +def figure_to_svg(fig): + """Convert a figure to svg for inline display.""" + fc = fig.get_facecolor() + ec = fig.get_edgecolor() + fig.set_facecolor('white') + fig.set_edgecolor('white') + try: + string_io = StringIO() + fig.canvas.print_figure(string_io, format='svg') + svg = string_io.getvalue() + finally: + fig.set_facecolor(fc) + fig.set_edgecolor(ec) + return svg + + +# We need a little factory function here to create the closure where +# safe_execfile can live. +def mpl_runner(safe_execfile): + """Factory to return a matplotlib-enabled runner for %run. + + Parameters + ---------- + safe_execfile : function + This must be a function with the same interface as the + :meth:`safe_execfile` method of IPython. + + Returns + ------- + A function suitable for use as the ``runner`` argument of the %run magic + function. + """ + + def mpl_execfile(fname,*where,**kw): + """matplotlib-aware wrapper around safe_execfile. + + Its interface is identical to that of the :func:`execfile` builtin. + + This is ultimately a call to execfile(), but wrapped in safeties to + properly handle interactive rendering.""" + + import matplotlib + import matplotlib.pylab as pylab + + #print '*** Matplotlib runner ***' # dbg + # turn off rendering until end of script + is_interactive = matplotlib.rcParams['interactive'] + matplotlib.interactive(False) + safe_execfile(fname,*where,**kw) + matplotlib.interactive(is_interactive) + # make rendering call now, if the user tried to do it + if pylab.draw_if_interactive.called: + pylab.draw() + pylab.draw_if_interactive.called = False + + return mpl_execfile + + +#----------------------------------------------------------------------------- +# Code for initializing matplotlib and importing pylab #----------------------------------------------------------------------------- @@ -111,7 +213,7 @@ def import_pylab(user_ns, backend, import_all=True, shell=None): # function that will pick up the results for display. This can only be # done with access to the real shell object. if backend == backends['inline']: - from IPython.zmq.pylab.backend_inline import flush_svg, figsize + from IPython.zmq.pylab.backend_inline import flush_svg from matplotlib import pyplot shell.register_post_execute(flush_svg) # The typical default figure size is too large for inline use. We @@ -120,11 +222,22 @@ def import_pylab(user_ns, backend, import_all=True, shell=None): # Add 'figsize' to pyplot and to the user's namespace user_ns['figsize'] = pyplot.figsize = figsize shell.user_ns_hidden['figsize'] = figsize - else: - from IPython.zmq.pylab.backend_inline import pastefig - from matplotlib import pyplot - # Add 'paste' to pyplot and to the user's namespace - user_ns['pastefig'] = pyplot.pastefig = pastefig + + # The old pastefig function has been replaced by display + # Always add this svg formatter so display works. + from IPython.zmq.pylab.backend_inline import figure_to_svg + from IPython.core.display import display, display_svg + svg_formatter = shell.display_formatter.formatters['image/svg+xml'] + svg_formatter.for_type_by_name( + 'matplotlib.figure','Figure',figure_to_svg + ) + # Add display and display_png to the user's namespace + user_ns['display'] = display + shell.user_ns_hidden['display'] = display + user_ns['display_svg'] = display_svg + shell.user_ns_hidden['display_svg'] = display_svg + user_ns['getfigs'] = getfigs + shell.user_ns_hidden['getfigs'] = getfigs if import_all: s = ("from matplotlib.pylab import *\n" @@ -165,44 +278,3 @@ For more information, type 'help(pylab)'.""" % backend return gui -# We need a little factory function here to create the closure where -# safe_execfile can live. -def mpl_runner(safe_execfile): - """Factory to return a matplotlib-enabled runner for %run. - - Parameters - ---------- - safe_execfile : function - This must be a function with the same interface as the - :meth:`safe_execfile` method of IPython. - - Returns - ------- - A function suitable for use as the ``runner`` argument of the %run magic - function. - """ - - def mpl_execfile(fname,*where,**kw): - """matplotlib-aware wrapper around safe_execfile. - - Its interface is identical to that of the :func:`execfile` builtin. - - This is ultimately a call to execfile(), but wrapped in safeties to - properly handle interactive rendering.""" - - import matplotlib - import matplotlib.pylab as pylab - - #print '*** Matplotlib runner ***' # dbg - # turn off rendering until end of script - is_interactive = matplotlib.rcParams['interactive'] - matplotlib.interactive(False) - safe_execfile(fname,*where,**kw) - matplotlib.interactive(is_interactive) - # make rendering call now, if the user tried to do it - if pylab.draw_if_interactive.called: - pylab.draw() - pylab.draw_if_interactive.called = False - - return mpl_execfile - diff --git a/IPython/utils/generics.py b/IPython/utils/generics.py index 4fcc32b..a482d49 100644 --- a/IPython/utils/generics.py +++ b/IPython/utils/generics.py @@ -2,15 +2,6 @@ """Generic functions for extending IPython. See http://cheeseshop.python.org/pypi/simplegeneric. - -Here is an example from IPython.utils.text:: - - def print_lsstring(arg): - "Prettier (non-repr-like) and more informative printer for LSString" - print "LSString (.p, .n, .l, .s available). Value:" - print arg - - print_lsstring = result_display.when_type(LSString)(print_lsstring) """ #----------------------------------------------------------------------------- diff --git a/IPython/utils/ipstruct.py b/IPython/utils/ipstruct.py index b5fe613..e9898b1 100644 --- a/IPython/utils/ipstruct.py +++ b/IPython/utils/ipstruct.py @@ -388,8 +388,6 @@ class Struct(dict): inv_conflict_solve_user[func] = inv_conflict_solve_user[name] del inv_conflict_solve_user[name] conflict_solve.update(self.__dict_invert(inv_conflict_solve_user)) - #print 'merge. conflict_solve: '; pprint(conflict_solve) # dbg - #print '*'*50,'in merger. conflict_solver:'; pprint(conflict_solve) for key in data_dict: if key not in self: self[key] = data_dict[key] diff --git a/IPython/zmq/ipkernel.py b/IPython/zmq/ipkernel.py index 011608f..018515c 100755 --- a/IPython/zmq/ipkernel.py +++ b/IPython/zmq/ipkernel.py @@ -91,6 +91,8 @@ class Kernel(Configurable): self.shell = ZMQInteractiveShell.instance() self.shell.displayhook.session = self.session self.shell.displayhook.pub_socket = self.pub_socket + self.shell.display_pub.session = self.session + self.shell.display_pub.pub_socket = self.pub_socket # TMP - hack while developing self.shell._reply_content = None @@ -194,6 +196,7 @@ class Kernel(Configurable): # Set the parent message of the display hook and out streams. shell.displayhook.set_parent(parent) + shell.display_pub.set_parent(parent) sys.stdout.set_parent(parent) sys.stderr.set_parent(parent) diff --git a/IPython/zmq/pylab/backend_inline.py b/IPython/zmq/pylab/backend_inline.py index f844d4c..8a42357 100644 --- a/IPython/zmq/pylab/backend_inline.py +++ b/IPython/zmq/pylab/backend_inline.py @@ -6,99 +6,39 @@ 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 # Local imports. -from backend_payload import add_plot_payload +from IPython.core.displaypub import publish_display_data +from IPython.lib.pylabtools import figure_to_svg #----------------------------------------------------------------------------- # Functions #----------------------------------------------------------------------------- -def show(close=True): +def show(close=False): """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. + sending all the SVG figures. If this is set, the figures will entirely + removed from the internal list of figures. """ for figure_manager in Gcf.get_all_fig_managers(): - send_svg_canvas(figure_manager.canvas) + send_svg_figure(figure_manager.canvas.figure) if close: matplotlib.pyplot.close('all') + # This flag will be reset by draw_if_interactive when called show._draw_called = False -def figsize(sizex, sizey): - """Set the default figure size to be [sizex, sizey]. - - This is just an easy to remember, convenience wrapper that sets:: - - matplotlib.rcParams['figure.figsize'] = [sizex, sizey] - """ - matplotlib.rcParams['figure.figsize'] = [sizex, sizey] - - -def pastefig(*figs): - """Paste one or more 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. - """ - # 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): - """ Return a string containing the SVG representation of a FigureCanvasSvg. - """ - string_io = StringIO() - canvas.print_figure(string_io, format='svg') - return string_io.getvalue() - - def draw_if_interactive(): """ Is called after every pylab drawing command @@ -115,5 +55,19 @@ def flush_svg(): prior code execution, there had been any calls to draw_if_interactive. """ if show._draw_called: - show(close=True) + # Show is called with the default close=False here, otherwise, the + # Figure will be closed and not available for future plotting. + show() show._draw_called = False + + +def send_svg_figure(fig): + """Draw the current figure and send it as an SVG payload. + """ + svg = figure_to_svg(fig) + publish_display_data( + 'IPython.zmq.pylab.backend_inline.send_svg_figure', + 'Matplotlib Plot', + {'image/svg+xml' : svg} + ) + diff --git a/IPython/zmq/pylab/backend_payload.py b/IPython/zmq/pylab/backend_payload.py deleted file mode 100644 index 74d4de6..0000000 --- a/IPython/zmq/pylab/backend_payload.py +++ /dev/null @@ -1,26 +0,0 @@ -""" Provides basic funtionality for payload backends. -""" - -# Local imports. -from IPython.core.interactiveshell import InteractiveShell - - -def add_plot_payload(format, data, metadata={}): - """ Add a plot payload to the current execution reply. - - Parameters: - ----------- - format : str - Identifies the format of the plot data. - - data : str - The raw plot data. - - metadata : dict, optional [default empty] - Allows for specification of additional information about the plot data. - """ - payload = dict( - source='IPython.zmq.pylab.backend_payload.add_plot_payload', - format=format, data=data, metadata=metadata - ) - InteractiveShell.instance().payload_manager.write_payload(payload) diff --git a/IPython/zmq/zmqshell.py b/IPython/zmq/zmqshell.py index 1eda3d9..d5071c9 100644 --- a/IPython/zmq/zmqshell.py +++ b/IPython/zmq/zmqshell.py @@ -26,6 +26,7 @@ from IPython.core.interactiveshell import ( ) from IPython.core import page from IPython.core.displayhook import DisplayHook +from IPython.core.displaypub import DisplayPublisher from IPython.core.macro import Macro from IPython.core.payloadpage import install_payload_page from IPython.utils import io @@ -48,6 +49,7 @@ install_payload_page() #----------------------------------------------------------------------------- class ZMQDisplayHook(DisplayHook): + """A displayhook subclass that publishes data using ZeroMQ.""" session = Instance(Session) pub_socket = Instance('zmq.Socket') @@ -65,9 +67,8 @@ class ZMQDisplayHook(DisplayHook): if self.do_full_cache: self.msg['content']['execution_count'] = self.prompt_count - def write_result_repr(self, result_repr, extra_formats): - self.msg['content']['data'] = result_repr - self.msg['content']['extra_formats'] = extra_formats + def write_format_data(self, format_dict): + self.msg['content']['data'] = format_dict def finish_displayhook(self): """Finish up all displayhook activities.""" @@ -75,10 +76,37 @@ class ZMQDisplayHook(DisplayHook): self.msg = None +class ZMQDisplayPublisher(DisplayPublisher): + """A display publisher that publishes data using a ZeroMQ PUB socket.""" + + session = Instance(Session) + pub_socket = Instance('zmq.Socket') + parent_header = Dict({}) + + def set_parent(self, parent): + """Set the parent for outbound messages.""" + self.parent_header = extract_header(parent) + + def publish(self, source, data, metadata=None): + if metadata is None: + metadata = {} + self._validate_data(source, data, metadata) + content = {} + content['source'] = source + content['data'] = data + content['metadata'] = metadata + self.session.send( + self.pub_socket, u'display_data', content, + parent=self.parent_header + ) + + class ZMQInteractiveShell(InteractiveShell): """A subclass of InteractiveShell for ZMQ.""" displayhook_class = Type(ZMQDisplayHook) + display_pub_class = Type(ZMQDisplayPublisher) + keepkernel_on_exit = None def init_environment(self): @@ -178,6 +206,8 @@ class ZMQInteractiveShell(InteractiveShell): # Shorthands shell = self.shell + disp_formatter = self.shell.display_formatter + ptformatter = disp_formatter.formatters['text/plain'] # dstore is a data store kept in the instance metadata bag to track any # changes we make, so we can undo them later. dstore = shell.meta.setdefault('doctest_mode', Struct()) @@ -185,16 +215,19 @@ class ZMQInteractiveShell(InteractiveShell): # save a few values we'll need to recover later mode = save_dstore('mode', False) - save_dstore('rc_pprint', shell.pprint) + save_dstore('rc_pprint', ptformatter.pprint) + save_dstore('rc_plain_text_only',disp_formatter.plain_text_only) save_dstore('xmode', shell.InteractiveTB.mode) if mode == False: # turn on - shell.pprint = False + ptformatter.pprint = False + disp_formatter.plain_text_only = True shell.magic_xmode('Plain') else: # turn off - shell.pprint = dstore.rc_pprint + ptformatter.pprint = dstore.rc_pprint + disp_formatter.plain_text_only = dstore.rc_plain_text_only shell.magic_xmode(dstore.xmode) # Store new mode and inform on console diff --git a/docs/source/development/messaging.txt b/docs/source/development/messaging.txt index 510f371..ef48379 100644 --- a/docs/source/development/messaging.txt +++ b/docs/source/development/messaging.txt @@ -689,10 +689,10 @@ Message type: ``stream``:: content = { # The name of the stream is one of 'stdin', 'stdout', 'stderr' - 'name' : str, + 'name' : str, - # The data is an arbitrary string to be written to that stream - 'data' : str, + # The data is an arbitrary string to be written to that stream + 'data' : str, } When a kernel receives a raw_input call, it should also broadcast it on the pub @@ -700,6 +700,39 @@ socket with the names 'stdin' and 'stdin_reply'. This will allow other clients to monitor/display kernel interactions and possibly replay them to their user or otherwise expose them. +Display Data +------------ + +This type of message is used to bring back data that should be diplayed (text, +html, svg, etc.) in the frontends. This data is published to all frontends. +Each message can have multiple representations of the data; it is up to the +frontend to decide which to use and how. A single message should contain all +possible representations of the same information. Each representation should +be a JSON'able data structure, and should be a valid MIME type. + +Some questions remain about this design: + +* Do we use this message type for pyout/displayhook? Probably not, because + the displayhook also has to handle the Out prompt display. On the other hand + we could put that information into the metadata secion. + +Message type: ``display_data``:: + + content = { + + # Who create the data + 'source' : str, + + # The data dict contains key/value pairs, where the kids are MIME + # types and the values are the raw data of the representation in that + # format. The data dict must minimally contain the ``text/plain`` + # MIME type which is used as a backup representation. + 'data' : dict, + + # Any metadata that describes the data + 'metadata' : dict + } + Python inputs ------------- @@ -708,8 +741,7 @@ These messages are the re-broadcast of the ``execute_request``. Message type: ``pyin``:: content = { - # Source code to be executed, one or more lines - 'code' : str + 'code' : str # Source code to be executed, one or more lines } Python outputs @@ -740,20 +772,18 @@ any JSON object and depends on the format. It is often, but not always a string. Message type: ``pyout``:: content = { - # The data is typically the repr() of the object. It should be displayed - # as monospaced text. - 'data' : str, - + # The counter for this execution is also provided so that clients can # display it, since IPython automatically creates variables called _N # (for prompt N). 'execution_count' : int, + + # The data dict contains key/value pairs, where the kids are MIME + # types and the values are the raw data of the representation in that + # format. The data dict must minimally contain the ``text/plain`` + # MIME type which is used as a backup representation. + 'data' : dict, - # Any extra formats. - # The tuples are of the form (ID, type, data). - 'extra_formats' : [ - [str, str, object] - ] } Python errors diff --git a/setupbase.py b/setupbase.py index 835e87a..946f573 100644 --- a/setupbase.py +++ b/setupbase.py @@ -213,13 +213,7 @@ def find_data_files(): data_files = [ (manpagebase, manpages), (pjoin(docdirbase, 'extensions'), igridhelpfiles), ] + manual_files + example_files - - ## import pprint # dbg - ## print('*'*80) - ## print('data files') - ## pprint.pprint(data_files) - ## print('*'*80) - + return data_files