From ead71f347ca7c03e6468f6de100a0c05e5d38a14 2010-10-31 03:07:00 From: Fernando Perez Date: 2010-10-31 03:07:00 Subject: [PATCH] Merge branch 'formatters' of http://github.com/rkern/ipython into rkern-formatters --- diff --git a/IPython/core/displayhook.py b/IPython/core/displayhook.py index 89b83d8..764f19f 100644 --- a/IPython/core/displayhook.py +++ b/IPython/core/displayhook.py @@ -20,15 +20,14 @@ Authors: #----------------------------------------------------------------------------- import __builtin__ -from pprint import PrettyPrinter -pformat = PrettyPrinter().pformat from IPython.config.configurable import Configurable from IPython.core import prompts import IPython.utils.generics import IPython.utils.io -from IPython.utils.traitlets import Instance, Int +from IPython.utils.traitlets import Instance, List from IPython.utils.warn import warn +from IPython.core.formatters import DefaultFormatter #----------------------------------------------------------------------------- # Main displayhook class @@ -55,6 +54,16 @@ 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) @@ -109,7 +118,6 @@ class DisplayHook(Configurable): self.output_sep = output_sep self.output_sep2 = output_sep2 self._,self.__,self.___ = '','','' - self.pprint_types = map(type,[(),[],{}]) # these are deliberately global: to_user_ns = {'_':self._,'__':self.__,'___':self.___} @@ -184,39 +192,35 @@ class DisplayHook(Configurable): if self.do_full_cache: IPython.utils.io.Term.cout.write(outprompt) - # TODO: Make this method an extension point. The previous implementation - # has both a result_display hook as well as a result_display generic - # function to customize the repr on a per class basis. We need to rethink - # the hooks mechanism before doing this though. 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 - actual print or write that to a stream. This method may also transform - the result itself, but the default implementation passes the original - through. + actual print or write that to a stream. """ - try: - if self.shell.pprint: - try: - result_repr = pformat(result) - except: - # Work around possible bugs in pformat - result_repr = repr(result) - 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 - # their first line. - result_repr = '\n' + result_repr - else: - result_repr = repr(result) - except TypeError: - # This happens when result.__repr__ doesn't return a string, - # such as when it returns None. - result_repr = '\n' - return result, result_repr - - def write_result_repr(self, result_repr): + result_repr = self.default_formatter(result) + 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 + # their first line. + outprompt = str(self.prompt_out) + if outprompt and not outprompt.endswith('\n'): + # But avoid extraneous empty lines. + result_repr = '\n' + result_repr + + 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 result_repr, extra_formats + + def write_result_repr(self, result_repr, extra_formats): # 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. @@ -271,8 +275,8 @@ class DisplayHook(Configurable): if result is not None and not self.quiet(): self.start_displayhook() self.write_output_prompt() - result, result_repr = self.compute_result_repr(result) - self.write_result_repr(result_repr) + result_repr, extra_formats = self.compute_result_repr(result) + self.write_result_repr(result_repr, extra_formats) self.update_user_ns(result) self.log_output(result) self.finish_displayhook() diff --git a/IPython/core/formatters.py b/IPython/core/formatters.py new file mode 100644 index 0000000..2bf9708 --- /dev/null +++ b/IPython/core/formatters.py @@ -0,0 +1,160 @@ +# -*- 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. + +Authors: + +* Robert Kern +""" + +import abc +from cStringIO import StringIO + +from IPython.config.configurable import Configurable +from IPython.external import pretty +from IPython.utils.traitlets import Bool, Dict, Int, Str + + +class DefaultFormatter(Configurable): + """ The default pretty-printer. + """ + + # The ID of the formatter. + id = Str('default') + + # The kind of data returned. + # This is often, but not always a MIME type. + format = Str('text/plain') + + # Whether to pretty-print or not. + pprint = Bool(True, config=True) + + # Whether to be verbose or not. + verbose = Bool(False, config=True) + + # The maximum width. + max_width = Int(79, config=True) + + # 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): + 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): + 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): + return pretty._deferred_type_pprinters.copy() + + #### FormatterABC interface #### + + def __call__(self, obj): + """ Format the object. + """ + if not self.pprint: + try: + return repr(obj) + except TypeError: + return '' + else: + 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) + printer.pretty(obj) + printer.flush() + return stream.getvalue() + + + #### DefaultFormatter interface #### + + 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 + + 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 + + +class FormatterABC(object): + """ Abstract base class for Formatters. + """ + __metaclass__ = abc.ABCMeta + + # The ID of the formatter. + id = 'abstract' + + # The kind of data returned. + format = 'text/plain' + + @abc.abstractmethod + def __call__(self, obj): + """ Return a JSONable representation of the object. + + If the object cannot be formatted by this formatter, then return None + """ + try: + return repr(obj) + except TypeError: + return None + +FormatterABC.register(DefaultFormatter) diff --git a/IPython/core/interactiveshell.py b/IPython/core/interactiveshell.py index e12af7e..01c5c96 100644 --- a/IPython/core/interactiveshell.py +++ b/IPython/core/interactiveshell.py @@ -475,6 +475,7 @@ class InteractiveShell(Configurable, Magic): def init_displayhook(self): # Initialize displayhook, set in/out prompts and printing system self.displayhook = self.displayhook_class( + config=self.config, shell=self, cache_size=self.cache_size, input_sep = self.separate_in, diff --git a/IPython/core/tests/test_formatters.py b/IPython/core/tests/test_formatters.py new file mode 100644 index 0000000..2a48b5a --- /dev/null +++ b/IPython/core/tests/test_formatters.py @@ -0,0 +1,30 @@ +"""Tests for the Formatters. +""" + +import nose.tools as nt + +from IPython.core.formatters import FormatterABC, DefaultFormatter + +class A(object): + def __repr__(self): + return 'A()' + +class B(A): + def __repr__(self): + return 'B()' + +def foo_printer(obj, pp, cycle): + pp.text('foo') + +def test_pretty(): + f = DefaultFormatter() + f.for_type(A, foo_printer) + nt.assert_equals(f(A()), 'foo') + nt.assert_equals(f(B()), 'foo') + f.pprint = False + nt.assert_equals(f(A()), 'A()') + nt.assert_equals(f(B()), 'B()') + +def test_deferred(): + f = DefaultFormatter() + diff --git a/IPython/external/pretty.py b/IPython/external/pretty.py index 6559fc4..66ef7b7 100644 --- a/IPython/external/pretty.py +++ b/IPython/external/pretty.py @@ -306,10 +306,21 @@ class RepresentationPrinter(PrettyPrinter): verbose mode. """ - def __init__(self, output, verbose=False, max_width=79, newline='\n'): + def __init__(self, output, verbose=False, max_width=79, newline='\n', + singleton_pprinters=None, type_pprinters=None, deferred_pprinters=None): + PrettyPrinter.__init__(self, output, max_width, newline) self.verbose = verbose self.stack = [] + if singleton_pprinters is None: + singleton_pprinters = _singleton_pprinters.copy() + self.singleton_pprinters = singleton_pprinters + if type_pprinters is None: + type_pprinters = _type_pprinters.copy() + self.type_pprinters = type_pprinters + if deferred_pprinters is None: + deferred_pprinters = _deferred_type_pprinters.copy() + self.deferred_pprinters = deferred_pprinters def pretty(self, obj): """Pretty print the given object.""" @@ -322,14 +333,14 @@ class RepresentationPrinter(PrettyPrinter): if hasattr(obj_class, '__pretty__'): return obj_class.__pretty__(obj, self, cycle) try: - printer = _singleton_pprinters[obj_id] + printer = self.singleton_pprinters[obj_id] except (TypeError, KeyError): pass else: return printer(obj, self, cycle) for cls in _get_mro(obj_class): - if cls in _type_pprinters: - return _type_pprinters[cls](obj, self, cycle) + if cls in self.type_pprinters: + return self.type_pprinters[cls](obj, self, cycle) else: printer = self._in_deferred_types(cls) if printer is not None: @@ -351,14 +362,13 @@ class RepresentationPrinter(PrettyPrinter): name = getattr(cls, '__name__', None) key = (mod, name) printer = None - if key in _deferred_type_pprinters: + if key in self.deferred_pprinters: # Move the printer over to the regular registry. - printer = _deferred_type_pprinters.pop(key) - _type_pprinters[cls] = printer + printer = self.deferred_pprinters.pop(key) + self.type_pprinters[cls] = printer return printer - class Printable(object): def output(self, stream, output_width): diff --git a/IPython/frontend/qt/util.py b/IPython/frontend/qt/util.py index b1f18ce..4c4c790 100644 --- a/IPython/frontend/qt/util.py +++ b/IPython/frontend/qt/util.py @@ -1,11 +1,14 @@ """ Defines miscellaneous Qt-related helper classes and functions. """ +# Standard library imports. +import inspect + # System library imports. from PyQt4 import QtCore, QtGui # IPython imports. -from IPython.utils.traitlets import HasTraits +from IPython.utils.traitlets import HasTraits, TraitType #----------------------------------------------------------------------------- # Metaclasses @@ -14,7 +17,6 @@ from IPython.utils.traitlets import HasTraits MetaHasTraits = type(HasTraits) MetaQObject = type(QtCore.QObject) -# You can switch the order of the parents here and it doesn't seem to matter. class MetaQObjectHasTraits(MetaQObject, MetaHasTraits): """ A metaclass that inherits from the metaclasses of HasTraits and QObject. @@ -22,7 +24,24 @@ class MetaQObjectHasTraits(MetaQObject, MetaHasTraits): QObject. Using SuperQObject instead of QObject is highly recommended. See QtKernelManager for an example. """ - pass + def __new__(mcls, name, bases, classdict): + # FIXME: this duplicates the code from MetaHasTraits. + # I don't think a super() call will help me here. + for k,v in classdict.iteritems(): + if isinstance(v, TraitType): + v.name = k + elif inspect.isclass(v): + if issubclass(v, TraitType): + vinst = v() + vinst.name = k + classdict[k] = vinst + cls = MetaQObject.__new__(mcls, name, bases, classdict) + return cls + + def __init__(mcls, name, bases, classdict): + # Note: super() did not work, so we explicitly call these. + MetaQObject.__init__(mcls, name, bases, classdict) + MetaHasTraits.__init__(mcls, name, bases, classdict) #----------------------------------------------------------------------------- # Classes diff --git a/IPython/utils/tests/test_traitlets.py b/IPython/utils/tests/test_traitlets.py index 79992a2..16baab7 100755 --- a/IPython/utils/tests/test_traitlets.py +++ b/IPython/utils/tests/test_traitlets.py @@ -129,6 +129,29 @@ class TestTraitType(TestCase): a = A() self.assertRaises(TraitError, A.tt.error, a, 10) + def test_dynamic_initializer(self): + class A(HasTraits): + x = Int(10) + def _x_default(self): + return 11 + class B(A): + x = Int(20) + class C(A): + def _x_default(self): + return 21 + + a = A() + self.assertEquals(a._trait_values, {}) + self.assertEquals(a.x, 11) + self.assertEquals(a._trait_values, {'x': 11}) + b = B() + self.assertEquals(b._trait_values, {'x': 20}) + self.assertEquals(b.x, 20) + c = C() + self.assertEquals(c._trait_values, {}) + self.assertEquals(c.x, 21) + self.assertEquals(c._trait_values, {'x': 21}) + class TestHasTraitsMeta(TestCase): diff --git a/IPython/utils/traitlets.py b/IPython/utils/traitlets.py index 39c8570..a6bf9f8 100644 --- a/IPython/utils/traitlets.py +++ b/IPython/utils/traitlets.py @@ -248,9 +248,21 @@ class TraitType(object): default values must be delayed until the parent :class:`HasTraits` class has been instantiated. """ - dv = self.get_default_value() - newdv = self._validate(obj, dv) - obj._trait_values[self.name] = newdv + # Check for a deferred initializer defined in the same class as the + # trait declaration or above. + mro = type(obj).mro() + meth_name = '_%s_default' % self.name + for cls in mro[:mro.index(self.this_class)+1]: + if meth_name in cls.__dict__: + break + else: + # We didn't find one. Do static initialization. + dv = self.get_default_value() + newdv = self._validate(obj, dv) + obj._trait_values[self.name] = newdv + return + # Complete the dynamic initialization. + self.dynamic_initializer = cls.__dict__[meth_name] def __get__(self, obj, cls=None): """Get the value of the trait by self.name for the instance. @@ -265,7 +277,19 @@ class TraitType(object): else: try: value = obj._trait_values[self.name] - except: + except KeyError: + # Check for a dynamic initializer. + if hasattr(self, 'dynamic_initializer'): + value = self.dynamic_initializer(obj) + # FIXME: Do we really validate here? + value = self._validate(obj, value) + obj._trait_values[self.name] = value + return value + else: + raise TraitError('Unexpected error in TraitType: ' + 'both default value and dynamic initializer are ' + 'absent.') + except Exception: # HasTraits should call set_default_value to populate # this. So this should never be reached. raise TraitError('Unexpected error in TraitType: ' @@ -294,6 +318,11 @@ class TraitType(object): else: return value + def set_dynamic_initializer(self, method): + """ Set the dynamic initializer method, if any. + """ + self.dynamic_initializer = method + def info(self): return self.info_text diff --git a/IPython/zmq/zmqshell.py b/IPython/zmq/zmqshell.py index b418ec3..51d8501 100644 --- a/IPython/zmq/zmqshell.py +++ b/IPython/zmq/zmqshell.py @@ -65,8 +65,9 @@ class ZMQDisplayHook(DisplayHook): if self.do_full_cache: self.msg['content']['execution_count'] = self.prompt_count - def write_result_repr(self, result_repr): + def write_result_repr(self, result_repr, extra_formats): self.msg['content']['data'] = result_repr + self.msg['content']['extra_formats'] = extra_formats def finish_displayhook(self): """Finish up all displayhook activities.""" diff --git a/docs/source/development/messaging.txt b/docs/source/development/messaging.txt index 230880c..510f371 100644 --- a/docs/source/development/messaging.txt +++ b/docs/source/development/messaging.txt @@ -725,16 +725,35 @@ case, the kernel instantiates as ``sys.displayhook`` an object which has similar behavior, but which instead of printing to stdout, broadcasts these values as ``pyout`` messages for clients to display appropriately. +IPython's displayhook can handle multiple simultaneous formats depending on its +configuration. The default pretty-printed repr text is always given with the +``data`` entry in this message. Any other formats are provided in the +``extra_formats`` list. Frontends are free to display any or all of these +according to its capabilities. ``extra_formats`` list contains 3-tuples of an ID +string, a type string, and the data. The ID is unique to the formatter +implementation that created the data. Frontends will typically ignore the ID +unless if it has requested a particular formatter. The type string tells the +frontend how to interpret the data. It is often, but not always a MIME type. +Frontends should ignore types that it does not understand. The data itself is +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. - 'data' : str, + # 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 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, + + # Any extra formats. + # The tuples are of the form (ID, type, data). + 'extra_formats' : [ + [str, str, object] + ] } Python errors