From cbfdf70d36f0ff2d5ebbedb61163f2d8859c487e 2019-08-11 20:54:41
From: Matthias Bussonnier <mbussonnier@ucmerced.edu>
Date: 2019-08-11 20:54:41
Subject: [PATCH] Provide hooks for arbitrary mimetypes handling.

This should allow at some point inline images, an math in the terminal.

---

diff --git a/IPython/core/displayhook.py b/IPython/core/displayhook.py
index 2128c82..29d41dd 100644
--- a/IPython/core/displayhook.py
+++ b/IPython/core/displayhook.py
@@ -153,7 +153,7 @@ class DisplayHook(Configurable):
     # This can be set to True by the write_output_prompt method in a subclass
     prompt_end_newline = False
 
-    def write_format_data(self, format_dict, md_dict=None):
+    def write_format_data(self, format_dict, md_dict=None) -> None:
         """Write the format data dict to the frontend.
 
         This default version of this method simply writes the plain text
diff --git a/IPython/core/displaypub.py b/IPython/core/displaypub.py
index 9625da2..d769692 100644
--- a/IPython/core/displaypub.py
+++ b/IPython/core/displaypub.py
@@ -19,7 +19,7 @@ spec.
 import sys
 
 from traitlets.config.configurable import Configurable
-from traitlets import List
+from traitlets import List, Dict
 
 # This used to be defined here - it is imported for backwards compatibility
 from .display import publish_display_data
@@ -28,6 +28,7 @@ from .display import publish_display_data
 # Main payload class
 #-----------------------------------------------------------------------------
 
+
 class DisplayPublisher(Configurable):
     """A traited class that publishes display data to frontends.
 
@@ -35,6 +36,10 @@ class DisplayPublisher(Configurable):
     be accessed there.
     """
 
+    def __init__(self, shell=None, *args, **kwargs):
+        self.shell = shell
+        super().__init__(*args, **kwargs)
+
     def _validate_data(self, data, metadata=None):
         """Validate the display data.
 
@@ -53,7 +58,7 @@ class DisplayPublisher(Configurable):
                 raise TypeError('metadata must be a dict, got: %r' % data)
 
     # use * to indicate transient, update are keyword-only
-    def publish(self, data, metadata=None, source=None, *, transient=None, update=False, **kwargs):
+    def publish(self, data, metadata=None, source=None, *, transient=None, update=False, **kwargs) -> None:
         """Publish data and metadata to all frontends.
 
         See the ``display_data`` message in the messaging documentation for
@@ -98,7 +103,15 @@ class DisplayPublisher(Configurable):
             rather than creating a new output.
         """
 
-        # The default is to simply write the plain text data using sys.stdout.
+        handlers = {}
+        if self.shell is not None:
+            handlers = self.shell.mime_renderers
+
+        for mime, handler in handlers.items():
+            if mime in data:
+                handler(data[mime], metadata.get(mime, None))
+                return
+
         if 'text/plain' in data:
             print(data['text/plain'])
 
diff --git a/IPython/core/interactiveshell.py b/IPython/core/interactiveshell.py
index 6dbf307..cc7ff1d 100644
--- a/IPython/core/interactiveshell.py
+++ b/IPython/core/interactiveshell.py
@@ -863,7 +863,7 @@ class InteractiveShell(SingletonConfigurable):
         self.configurables.append(self.display_formatter)
 
     def init_display_pub(self):
-        self.display_pub = self.display_pub_class(parent=self)
+        self.display_pub = self.display_pub_class(parent=self, shell=self)
         self.configurables.append(self.display_pub)
 
     def init_data_pub(self):
diff --git a/IPython/terminal/interactiveshell.py b/IPython/terminal/interactiveshell.py
index be738f8..262c574 100644
--- a/IPython/terminal/interactiveshell.py
+++ b/IPython/terminal/interactiveshell.py
@@ -88,6 +88,8 @@ else:
 _use_simple_prompt = ('IPY_TEST_SIMPLE_PROMPT' in os.environ) or (not _is_tty)
 
 class TerminalInteractiveShell(InteractiveShell):
+    mime_renderers = Dict().tag(config=True)
+
     space_for_menu = Integer(6, help='Number of line at the bottom of the screen '
                                      'to reserve for the completion menu'
                             ).tag(config=True)
diff --git a/IPython/terminal/prompts.py b/IPython/terminal/prompts.py
index 1a7563b..db1b751 100644
--- a/IPython/terminal/prompts.py
+++ b/IPython/terminal/prompts.py
@@ -89,3 +89,14 @@ class RichPromptDisplayHook(DisplayHook):
                 )
             else:
                 sys.stdout.write(prompt_txt)
+
+    def write_format_data(self, format_dict, md_dict=None) -> None:
+        if self.shell.mime_renderers:
+
+            for mime, handler in self.shell.mime_renderers.items():
+                if mime in format_dict:
+                    handler(format_dict[mime], None)
+                    return
+                
+        super().write_format_data(format_dict, md_dict)
+
diff --git a/docs/source/whatsnew/pr/mimerenderer.rst b/docs/source/whatsnew/pr/mimerenderer.rst
new file mode 100644
index 0000000..44aeb9d
--- /dev/null
+++ b/docs/source/whatsnew/pr/mimerenderer.rst
@@ -0,0 +1,55 @@
+Arbitrary Mimetypes Handing in Terminal
+=======================================
+
+When using IPython terminal it is now possible to register function to handle
+arbitrary mimetypes (``TerminalInteractiveShell.mime_renderers`` ``Dict``
+configurable). While rendering non-text based representation was possible in
+many jupyter frontend; it was not possible in terminal IPython, as usually
+terminal are limited to displaying text. As many terminal these days provide
+escape sequences to display non-text; bringing this loved feature to IPython CLI
+made a lot of sens. This functionality will not only allow inline images; but
+allow opening of external program; for example ``fmplayer`` to "display" sound
+files.
+
+Here is a complete IPython tension to display images inline and convert math to
+png, before displaying it inline ::
+
+
+    from base64 import encodebytes
+    from IPython.lib.latextools import latex_to_png
+
+
+    def mathcat(data, meta):
+        png = latex_to_png(f'$${data}$$'.replace('\displaystyle', '').replace('$$$', '$$'))
+        imcat(png, meta)
+
+    IMAGE_CODE = '\033]1337;File=name=name;inline=true;:{}\a'
+
+    def imcat(image_data, metadata):
+        try:
+            print(IMAGE_CODE.format(encodebytes(image_data).decode()))
+        # bug workaround
+        except:
+            print(IMAGE_CODE.format(image_data))
+
+    def register_mimerenderer(ipython, mime, handler):
+        ipython.display_formatter.active_types.append(mime)
+        ipython.display_formatter.formatters[mime].enabled = True
+        ipython.mime_renderers[mime] = handler
+
+    def load_ipython_extension(ipython):
+        register_mimerenderer(ipython, 'image/png', imcat)
+        register_mimerenderer(ipython, 'image/jpeg', imcat)
+        register_mimerenderer(ipython, 'text/latex', mathcat)
+
+This example only work for iterm2 on mac os and skip error handling for brevity.
+One could also invoke an external viewer with ``subporcess.run()`` and a
+tempfile, which is left as an exercise.
+
+So far only the hooks necessary for this are in place, but no default mime
+renderers added; so inline images will only be available via extensions. We will
+progressively enable these features by default in the next few releases, and
+contribution is welcomed.
+
+
+