Repository: ipython/ipython

Issue:
Allow registration of display function using for types requiring multiple mimetypes.
In https://github.com/quantopian/qgrid, we've implemented a display wrapper that renders pandas DataFrames in an interactive, scrollable format.  The currently implementation works by wrapping a DataFrame in a SlickGrid object that implements `_display_ipython_`.  Internally, our `_display_ipython_` calls both `display_html` and `display_javascript`.  

In https://github.com/quantopian/qgrid/issues/8, @ellisonbg pointed out that it would be nice to be able to register our renderer as the default view for DataFrames.  This isn't possible with our current implementation however, because the existing machinery for these hooks in `IPython.core.formatters.DisplayFormatter` relies on the display function returning data for a specific mimetype.  We could probably refactor our implementation to just use `display_html` by pulling our javascript into a `<script>` block, but it would be nice to have the ability to register the equivalent of an `_ipython_display_` function for an arbitrary type.

Pinging @minrk on this at @ellisonbg's request.



Discussion:
I think this is a good idea, given that the most common usage of
`_ipython_display_` is for JS+HTML and it is a pain to have to dump the JS
into HTMl always.

On Sun, Oct 12, 2014 at 4:18 PM, Scott Sanderson notifications@github.com
wrote:

> In https://github.com/quantopian/qgrid, we've implemented a display
> wrapper that renders pandas DataFrames in an interactive, scrollable
> format. The currently implementation works by wrapping a DataFrame in a
> SlickGrid object that implements _display_ipython_. Internally, our
> _display_ipython_ calls both display_html and display_javascript.
> 
> In quantopian/qgrid#8 https://github.com/quantopian/qgrid/issues/8,
> @ellisonbg https://github.com/ellisonbg pointed out that it would be
> nice to be able to register our renderer as the default view for
> DataFrames. This isn't possible with our current implementation however,
> because the existing machinery for these hooks in
> IPython.core.formatters.DisplayFormatter relies on the display function
> returning data for a specific mimetype. We could probably refactor our
> implementation to just use display_html by pulling our javascript into a
> <script> block, but it would be nice to have the ability to register the
> equivalent of an _ipython_display_ function for an arbitrary type.
> 
> Pinging @minrk https://github.com/minrk on this at @ellisonbg
> https://github.com/ellisonbg's request.
> 
> —
> Reply to this email directly or view it on GitHub
> https://github.com/ipython/ipython/issues/6687.

## 

Brian E. Granger
Cal Poly State University, San Luis Obispo
@ellisonbg on Twitter and GitHub
bgranger@calpoly.edu and ellisonbg@gmail.com

I'm not certain about that. From what I see, you mostly require css in your HTML display hook. This is just a hack around the fact that we don't have (yet) a way when displaying javascript  to actually require the right library to be called (which you achieve to do nicely in your js template) .

including `require-css` would already solve half the problem, should we consider it ? 

For what it's worth, our `_ipython_display_` also uses `display_html` to create a div in which we can place the rendered grid.

I don't see any reason to have display function for all of our other
special methods, but not this one. The idea of `_ipython_display_` is that
we don't have any idea what someone will do in there.

On Tue, Oct 14, 2014 at 11:25 AM, Scott Sanderson notifications@github.com
wrote:

> For what it's worth, our _ipython_display_ also uses display_html to
> create a div in which we can place the rendered grid.
> 
> —
> Reply to this email directly or view it on GitHub
> https://github.com/ipython/ipython/issues/6687#issuecomment-59064088.

## 

Brian E. Granger
Cal Poly State University, San Luis Obispo
@ellisonbg on Twitter and GitHub
bgranger@calpoly.edu and ellisonbg@gmail.com

> For what it's worth, our _ipython_display_ also uses display_html to create a div in which we can place the rendered grid.

But displayed javascript is injected with a div named `element` exactly for this reason. You do not need to create html.

```
%%javascript
$(element).append($('<div>').text('foo'))
```

Does what you expect. Doing it in two stages is unnecessary. From a semantic perspective you shoudl use element  which is **made** for what you are doing. 

> I don't see any reason to have display function for all of our other
> special methods, but not this one. The idea of `_ipython_display_` is that
> we don't have any idea what someone will do in there.

The point of `_repr_html_` is to display something which is pure HTML (say nbconvert), 
`_repr_js_` for js+html, it works perfectly fine. The `display_*` goes completelyagainst the goal of IPython which is that each object should have reprs, and that library should not have to decide what is called. 
`display_*`  are ment to especially circumvent that when you really know what you are doing. 

Library should **never** use `display_*`, like `HTML()` never display HTML, it create an HTML object.

Yes, given the flexibility of JS and HTML you can always use only JS or
only HTML to get both JS and HTML onto the page. But there are many
situations where actually writing HTML DOM elements in JS is horrible or
where dumping JS into <script> tags is horrible.

Using `_repr_js_` for JS+HTML is fine for simple things, but for complex
HTML that is generated by templating libraries in Python (look at how
folium works), you really want the ability to put pure HTML on the page
using `_repr_html_` and then add interactivity with JS using `_repr_js_`.
mpld3 is another example usage case like this. I have written a couple of
display things like this myself and those would be greatly improved by
adding display function registration for them.

If we don't add this, what is the recommended way of triggering automatic
display logic on third party object that have complex, multi-mime type
display logic (beyond just HTML+JS)?

On Tue, Oct 14, 2014 at 11:50 AM, Matthias Bussonnier <
notifications@github.com> wrote:

> For what it's worth, our _ipython_display_ also uses display_html to
> create a div in which we can place the rendered grid.
> 
> But displayed javascript is injected with a div named element exactly for
> this reason. You do not need to create html.
> 
> %%javascript
> $(element).append($('<div>').text('foo'))
> 
> Does what you expect. Doing it in two stages is unnecessary. From a
> semantic perspective you shoudl use element which is _made_ for what you
> are doing.
> 
> I don't see any reason to have display function for all of our other
> special methods, but not this one. The idea of _ipython_display_ is that
> we don't have any idea what someone will do in there.
> 
> The point of _repr_html_ is to display something which is pure HTML (say
> nbconvert),
> _repr_js_ for js+html, it works perfectly fine. The display_\* goes
> completelyagainst the goal of IPython which is that each object should have
> reprs, and that library should not have to decide what is called.
> display_\* are ment to especially circumvent that when you really know
> what you are doing.
> 
> Library should _never_ use display_*, like HTML() never display HTML, it
> create an HTML object.
> 
> —
> Reply to this email directly or view it on GitHub
> https://github.com/ipython/ipython/issues/6687#issuecomment-59068372.

## 

Brian E. Granger
Cal Poly State University, San Luis Obispo
@ellisonbg on Twitter and GitHub
bgranger@calpoly.edu and ellisonbg@gmail.com

@ellisonbg @Carreau any resolution on this?  If it's not likely to happen any time soon, I'll probably go with a single-mimetype solution for https://github.com/quantopian/qgrid/issues/8.

We could ensure that js display is called with access to the rest of the mimebundle. Then it could do what it wanted with the HTML. Alternatively, we could treat js output as non-exclusive, so that when we see js to display, we still put the HTML on the page if there is any.  I can investigate that once nbformat 4 is merged.


Resolving patch:
diff --git a/IPython/core/display.py b/IPython/core/display.py
--- a/IPython/core/display.py
+++ b/IPython/core/display.py
@@ -164,20 +164,13 @@ def display(*objs, **kwargs):
         format = InteractiveShell.instance().display_formatter.format
 
     for obj in objs:
-
-        # If _ipython_display_ is defined, use that to display this object.
-        display_method = _safe_get_formatter_method(obj, '_ipython_display_')
-        if display_method is not None:
-            try:
-                display_method(**kwargs)
-            except NotImplementedError:
-                pass
-            else:
-                continue
         if raw:
             publish_display_data(data=obj, metadata=metadata)
         else:
             format_dict, md_dict = format(obj, include=include, exclude=exclude)
+            if not format_dict:
+                # nothing to display (e.g. _ipython_display_ took over)
+                continue
             if metadata:
                 # kwarg-specified metadata gets precedence
                 _merge(md_dict, metadata)
diff --git a/IPython/core/displayhook.py b/IPython/core/displayhook.py
--- a/IPython/core/displayhook.py
+++ b/IPython/core/displayhook.py
@@ -221,20 +221,13 @@ def __call__(self, result=None):
         """
         self.check_for_underscore()
         if result is not None and not self.quiet():
-            # If _ipython_display_ is defined, use that to display this object.
-            display_method = _safe_get_formatter_method(result, '_ipython_display_')
-            if display_method is not None:
-                try:
-                    return display_method()
-                except NotImplementedError:
-                    pass
-            
             self.start_displayhook()
             self.write_output_prompt()
             format_dict, md_dict = self.compute_format_data(result)
-            self.write_format_data(format_dict, md_dict)
             self.update_user_ns(result)
-            self.log_output(format_dict)
+            if format_dict:
+                self.write_format_data(format_dict, md_dict)
+                self.log_output(format_dict)
             self.finish_displayhook()
 
     def cull_cache(self):
diff --git a/IPython/core/formatters.py b/IPython/core/formatters.py
--- a/IPython/core/formatters.py
+++ b/IPython/core/formatters.py
@@ -14,7 +14,6 @@
 import inspect
 import sys
 import traceback
-import types
 import warnings
 
 from IPython.external.decorator import decorator
@@ -24,6 +23,7 @@
 from IPython.lib import pretty
 from IPython.utils.traitlets import (
     Bool, Dict, Integer, Unicode, CUnicode, ObjectName, List,
+    ForwardDeclaredInstance,
 )
 from IPython.utils.py3compat import (
     unicode_to_str, with_metaclass, PY3, string_types, unicode_type,
@@ -89,6 +89,10 @@ def _active_types_changed(self, name, old, new):
             else:
                 formatter.enabled = False
     
+    ipython_display_formatter = ForwardDeclaredInstance('FormatterABC')
+    def _ipython_display_formatter_default(self):
+        return IPythonDisplayFormatter(parent=self)
+    
     # A dict of formatter whose keys are format types (MIME types) and whose
     # values are subclasses of BaseFormatter.
     formatters = Dict()
@@ -158,7 +162,11 @@ def format(self, obj, include=None, exclude=None):
         """
         format_dict = {}
         md_dict = {}
-
+        
+        if self.ipython_display_formatter(obj):
+            # object handled itself, don't proceed
+            return {}, {}
+        
         for format_type, formatter in self.formatters.items():
             if include and format_type not in include:
                 continue
@@ -831,6 +839,40 @@ class PDFFormatter(BaseFormatter):
 
     _return_type = (bytes, unicode_type)
 
+class IPythonDisplayFormatter(BaseFormatter):
+    """A Formatter for objects that know how to display themselves.
+    
+    To define the callables that compute the representation of your
+    objects, define a :meth:`_ipython_display_` method or use the :meth:`for_type`
+    or :meth:`for_type_by_name` methods to register functions that handle
+    this. Unlike mime-type displays, this method should not return anything,
+    instead calling any appropriate display methods itself.
+    
+    This display formatter has highest priority.
+    If it fires, no other display formatter will be called.
+    """
+    print_method = ObjectName('_ipython_display_')
+    _return_type = (type(None), bool)
+    
+
+    @warn_format_error
+    def __call__(self, obj):
+        """Compute the format for an object."""
+        if self.enabled:
+            # lookup registered printer
+            try:
+                printer = self.lookup(obj)
+            except KeyError:
+                pass
+            else:
+                printer(obj)
+                return True
+            # Finally look for special method names
+            method = _safe_get_formatter_method(obj, self.print_method)
+            if method is not None:
+                method()
+                return True
+
 
 FormatterABC.register(BaseFormatter)
 FormatterABC.register(PlainTextFormatter)
@@ -843,6 +885,7 @@ class PDFFormatter(BaseFormatter):
 FormatterABC.register(LatexFormatter)
 FormatterABC.register(JSONFormatter)
 FormatterABC.register(JavascriptFormatter)
+FormatterABC.register(IPythonDisplayFormatter)
 
 
 def format_display_data(obj, include=None, exclude=None):


Test patch:
diff --git a/IPython/core/tests/test_formatters.py b/IPython/core/tests/test_formatters.py
--- a/IPython/core/tests/test_formatters.py
+++ b/IPython/core/tests/test_formatters.py
@@ -10,7 +10,8 @@
 
 from IPython.config import Config
 from IPython.core.formatters import (
-    PlainTextFormatter, HTMLFormatter, PDFFormatter, _mod_name_key
+    PlainTextFormatter, HTMLFormatter, PDFFormatter, _mod_name_key,
+    DisplayFormatter,
 )
 from IPython.utils.io import capture_output
 
@@ -387,3 +388,33 @@ def test_pretty_max_seq_length():
     text = f(list(range(1024)))
     lines = text.splitlines()
     nt.assert_equal(len(lines), 1024)
+
+
+def test_ipython_display_formatter():
+    """Objects with _ipython_display_ defined bypass other formatters"""
+    f = get_ipython().display_formatter
+    catcher = []
+    class SelfDisplaying(object):
+        def _ipython_display_(self):
+            catcher.append(self)
+
+    class NotSelfDisplaying(object):
+        def __repr__(self):
+            return "NotSelfDisplaying"
+        
+        def _ipython_display_(self):
+            raise NotImplementedError
+    
+    yes = SelfDisplaying()
+    no = NotSelfDisplaying()
+    
+    d, md = f.format(no)
+    nt.assert_equal(d, {'text/plain': repr(no)})
+    nt.assert_equal(md, {})
+    nt.assert_equal(catcher, [])
+    
+    d, md = f.format(yes)
+    nt.assert_equal(d, {})
+    nt.assert_equal(md, {})
+    nt.assert_equal(catcher, [yes])
+