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. + + +