Repository: ipython/ipython

Issue:
Add repr method for arbitrary mime-types
We have `_repr_foo_` methods for a few common mime-types, but this mapping doesn't extend well to custom mime-types. I propose we add a new magic method that returns the full, formatted mime data, such as:

```python
def _repr_mime_(self):
    """Return mimetype-bundled representation(s) of me"""
    return {
        'data': {
            'application/vnd.tableschema.v1+json': {...},
            'text/html': '<div>...',
        'metadata': {},
    }
```

details: should this override or be in addition to existing methods if found?

cc @rgbkrk 


Discussion:
Thanks for raising this up! It should likely override since it's a full manifest.
Oops, sorry for the dupe of #10058. Copying comment from there: The `_ipython_display_` escape hatch makes this less urgent (and is perhaps why we still haven't done it), but this should still be done nonetheless.
Do any of you want to tackle that for 6.0, or should we bump to wishlist ?
Wishlist is fine, I'm working on other items for the next couple weeks.

Resolving patch:
diff --git a/IPython/core/formatters.py b/IPython/core/formatters.py
--- a/IPython/core/formatters.py
+++ b/IPython/core/formatters.py
@@ -51,12 +51,17 @@ def _active_types_changed(self, change):
                 formatter.enabled = True
             else:
                 formatter.enabled = False
-    
+
     ipython_display_formatter = ForwardDeclaredInstance('FormatterABC')
     @default('ipython_display_formatter')
     def _default_formatter(self):
         return IPythonDisplayFormatter(parent=self)
-    
+
+    mimebundle_formatter = ForwardDeclaredInstance('FormatterABC')
+    @default('mimebundle_formatter')
+    def _default_mime_formatter(self):
+        return MimeBundleFormatter(parent=self)
+
     # A dict of formatter whose keys are format types (MIME types) and whose
     # values are subclasses of BaseFormatter.
     formatters = Dict()
@@ -131,8 +136,13 @@ def format(self, obj, include=None, exclude=None):
         if self.ipython_display_formatter(obj):
             # object handled itself, don't proceed
             return {}, {}
-        
+
+        format_dict, md_dict = self.mimebundle_formatter(obj)
+
         for format_type, formatter in self.formatters.items():
+            if format_type in format_dict:
+                # already got it from mimebundle, don't render again
+                continue
             if include and format_type not in include:
                 continue
             if exclude and format_type in exclude:
@@ -843,7 +853,7 @@ class PDFFormatter(BaseFormatter):
     _return_type = (bytes, str)
 
 class IPythonDisplayFormatter(BaseFormatter):
-    """A Formatter for objects that know how to display themselves.
+    """An escape-hatch 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`
@@ -853,10 +863,16 @@ class IPythonDisplayFormatter(BaseFormatter):
     
     This display formatter has highest priority.
     If it fires, no other display formatter will be called.
+
+    Prior to IPython 6.1, `_ipython_display_` was the only way to display custom mime-types
+    without registering a new Formatter.
+    
+    IPython 6.1 introduces `_repr_mimebundle_` for displaying custom mime-types,
+    so `_ipython_display_` should only be used for objects that require unusual
+    display patterns, such as multiple display calls.
     """
     print_method = ObjectName('_ipython_display_')
     _return_type = (type(None), bool)
-    
 
     @catch_format_error
     def __call__(self, obj):
@@ -877,6 +893,34 @@ def __call__(self, obj):
                 return True
 
 
+class MimeBundleFormatter(BaseFormatter):
+    """A Formatter for arbitrary mime-types.
+
+    Unlike other `_repr_<mimetype>_` methods,
+    `_repr_mimebundle_` should return mime-bundle data,
+    either the mime-keyed `data` dictionary or the tuple `(data, metadata)`.
+    Any mime-type is valid.
+
+    To define the callables that compute the mime-bundle representation of your
+    objects, define a :meth:`_repr_mimebundle_` method or use the :meth:`for_type`
+    or :meth:`for_type_by_name` methods to register functions that handle
+    this.
+
+    .. versionadded:: 6.1
+    """
+    print_method = ObjectName('_repr_mimebundle_')
+    _return_type = dict
+    
+    def _check_return(self, r, obj):
+        r = super(MimeBundleFormatter, self)._check_return(r, obj)
+        # always return (data, metadata):
+        if r is None:
+            return {}, {}
+        if not isinstance(r, tuple):
+            return r, {}
+        return r
+
+
 FormatterABC.register(BaseFormatter)
 FormatterABC.register(PlainTextFormatter)
 FormatterABC.register(HTMLFormatter)
@@ -889,6 +933,7 @@ def __call__(self, obj):
 FormatterABC.register(JSONFormatter)
 FormatterABC.register(JavascriptFormatter)
 FormatterABC.register(IPythonDisplayFormatter)
+FormatterABC.register(MimeBundleFormatter)
 
 
 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
@@ -436,4 +436,55 @@ def _repr_json_(self):
         d = f(JSONString())
     nt.assert_equal(d, {})
     nt.assert_equal(len(w), 1)
-    
\ No newline at end of file
+
+
+def test_repr_mime():
+    class HasReprMime(object):
+        def _repr_mimebundle_(self):
+            return {
+                'application/json+test.v2': {
+                    'x': 'y'
+                }
+            }
+        
+        def _repr_html_(self):
+            return '<b>hi!</b>'
+    
+    f = get_ipython().display_formatter
+    html_f = f.formatters['text/html']
+    save_enabled = html_f.enabled
+    html_f.enabled = True
+    obj = HasReprMime()
+    d, md = f.format(obj)
+    html_f.enabled = save_enabled
+    
+    nt.assert_equal(sorted(d), ['application/json+test.v2', 'text/html', 'text/plain'])
+    nt.assert_equal(md, {})
+
+
+def test_repr_mime_meta():
+    class HasReprMimeMeta(object):
+        def _repr_mimebundle_(self):
+            data = {
+                'image/png': 'base64-image-data',
+            }
+            metadata = {
+                'image/png': {
+                    'width': 5,
+                    'height': 10,
+                }
+            }
+            return (data, metadata)
+    
+    f = get_ipython().display_formatter
+    obj = HasReprMimeMeta()
+    d, md = f.format(obj)
+    nt.assert_equal(sorted(d), ['image/png', 'text/plain'])
+    nt.assert_equal(md, {
+        'image/png': {
+            'width': 5,
+            'height': 10,
+        }
+    })
+    
+