##// END OF EJS Templates
Merge pull request #11848 from Carreau/mime-repr-hook...
Matthias Bussonnier -
r25255:fb277ea0 merge
parent child Browse files
Show More
@@ -0,0 +1,60 b''
1
2 .. _shell_mimerenderer:
3
4
5 Mime Renderer Extensions
6 ========================
7
8 Like it's cousins, Jupyter Notebooks and JupyterLab, Terminal IPython can be
9 thought to render a number of mimetypes in the shell. This can be used to either
10 display inline images if your terminal emulator supports it; or open some
11 display results with external file viewers.
12
13 Registering new mimetype handlers can so far only be done my extensions and
14 requires 4 steps:
15
16 - Define a callable that takes 2 parameters:``data`` and ``metadata``; return
17 value of the callable is so far ignored. This callable is responsible for
18 "displaying" the given mimetype. Which can be sending the right escape
19 sequences and bytes to the current terminal; or open an external program. -
20 - Appending the right mimetype to ``ipython.display_formatter.active_types``
21 for IPython to know it should not ignore those mimetypes.
22 - Enabling the given mimetype: ``ipython.display_formatter.formatters[mime].enabled = True``
23 - Registering above callable with mimetype handler:
24 ``ipython.mime_renderers[mime] = handler``
25
26
27 Here is a complete IPython extension to display images inline and convert math
28 to png, before displaying it inline for iterm2 on macOS ::
29
30
31 from base64 import encodebytes
32 from IPython.lib.latextools import latex_to_png
33
34
35 def mathcat(data, meta):
36 png = latex_to_png(f'$${data}$$'.replace('\displaystyle', '').replace('$$$', '$$'))
37 imcat(png, meta)
38
39 IMAGE_CODE = '\033]1337;File=name=name;inline=true;:{}\a'
40
41 def imcat(image_data, metadata):
42 try:
43 print(IMAGE_CODE.format(encodebytes(image_data).decode()))
44 # bug workaround
45 except:
46 print(IMAGE_CODE.format(image_data))
47
48 def register_mimerenderer(ipython, mime, handler):
49 ipython.display_formatter.active_types.append(mime)
50 ipython.display_formatter.formatters[mime].enabled = True
51 ipython.mime_renderers[mime] = handler
52
53 def load_ipython_extension(ipython):
54 register_mimerenderer(ipython, 'image/png', imcat)
55 register_mimerenderer(ipython, 'image/jpeg', imcat)
56 register_mimerenderer(ipython, 'text/latex', mathcat)
57
58 This example only work for iterm2 on macOS and skip error handling for brevity.
59 One could also invoke an external viewer with ``subprocess.run()`` and a
60 temporary file, which is left as an exercise.
@@ -0,0 +1,22 b''
1 Arbitrary Mimetypes Handing in Terminal
2 =======================================
3
4 When using IPython terminal it is now possible to register function to handle
5 arbitrary mimetypes. While rendering non-text based representation was possible in
6 many jupyter frontend; it was not possible in terminal IPython, as usually
7 terminal are limited to displaying text. As many terminal these days provide
8 escape sequences to display non-text; bringing this loved feature to IPython CLI
9 made a lot of sens. This functionality will not only allow inline images; but
10 allow opening of external program; for example ``mplayer`` to "display" sound
11 files.
12
13 So far only the hooks necessary for this are in place, but no default mime
14 renderers added; so inline images will only be available via extensions. We will
15 progressively enable these features by default in the next few releases, and
16 contribution is welcomed.
17
18 We welcome any feedback on the API. See :ref:`shell_mimerenderer` for more
19 informations.
20
21 This is originally based on work form in :ghpull:`10610` from stephanh42
22 started over two years ago, and still a lot need to be done.
@@ -153,7 +153,7 b' class DisplayHook(Configurable):'
153 # This can be set to True by the write_output_prompt method in a subclass
153 # This can be set to True by the write_output_prompt method in a subclass
154 prompt_end_newline = False
154 prompt_end_newline = False
155
155
156 def write_format_data(self, format_dict, md_dict=None):
156 def write_format_data(self, format_dict, md_dict=None) -> None:
157 """Write the format data dict to the frontend.
157 """Write the format data dict to the frontend.
158
158
159 This default version of this method simply writes the plain text
159 This default version of this method simply writes the plain text
@@ -19,7 +19,7 b' spec.'
19 import sys
19 import sys
20
20
21 from traitlets.config.configurable import Configurable
21 from traitlets.config.configurable import Configurable
22 from traitlets import List
22 from traitlets import List, Dict
23
23
24 # This used to be defined here - it is imported for backwards compatibility
24 # This used to be defined here - it is imported for backwards compatibility
25 from .display import publish_display_data
25 from .display import publish_display_data
@@ -28,6 +28,7 b' from .display import publish_display_data'
28 # Main payload class
28 # Main payload class
29 #-----------------------------------------------------------------------------
29 #-----------------------------------------------------------------------------
30
30
31
31 class DisplayPublisher(Configurable):
32 class DisplayPublisher(Configurable):
32 """A traited class that publishes display data to frontends.
33 """A traited class that publishes display data to frontends.
33
34
@@ -35,6 +36,10 b' class DisplayPublisher(Configurable):'
35 be accessed there.
36 be accessed there.
36 """
37 """
37
38
39 def __init__(self, shell=None, *args, **kwargs):
40 self.shell = shell
41 super().__init__(*args, **kwargs)
42
38 def _validate_data(self, data, metadata=None):
43 def _validate_data(self, data, metadata=None):
39 """Validate the display data.
44 """Validate the display data.
40
45
@@ -53,7 +58,7 b' class DisplayPublisher(Configurable):'
53 raise TypeError('metadata must be a dict, got: %r' % data)
58 raise TypeError('metadata must be a dict, got: %r' % data)
54
59
55 # use * to indicate transient, update are keyword-only
60 # use * to indicate transient, update are keyword-only
56 def publish(self, data, metadata=None, source=None, *, transient=None, update=False, **kwargs):
61 def publish(self, data, metadata=None, source=None, *, transient=None, update=False, **kwargs) -> None:
57 """Publish data and metadata to all frontends.
62 """Publish data and metadata to all frontends.
58
63
59 See the ``display_data`` message in the messaging documentation for
64 See the ``display_data`` message in the messaging documentation for
@@ -98,7 +103,15 b' class DisplayPublisher(Configurable):'
98 rather than creating a new output.
103 rather than creating a new output.
99 """
104 """
100
105
101 # The default is to simply write the plain text data using sys.stdout.
106 handlers = {}
107 if self.shell is not None:
108 handlers = self.shell.mime_renderers
109
110 for mime, handler in handlers.items():
111 if mime in data:
112 handler(data[mime], metadata.get(mime, None))
113 return
114
102 if 'text/plain' in data:
115 if 'text/plain' in data:
103 print(data['text/plain'])
116 print(data['text/plain'])
104
117
@@ -857,7 +857,7 b' class InteractiveShell(SingletonConfigurable):'
857 self.configurables.append(self.display_formatter)
857 self.configurables.append(self.display_formatter)
858
858
859 def init_display_pub(self):
859 def init_display_pub(self):
860 self.display_pub = self.display_pub_class(parent=self)
860 self.display_pub = self.display_pub_class(parent=self, shell=self)
861 self.configurables.append(self.display_pub)
861 self.configurables.append(self.display_pub)
862
862
863 def init_data_pub(self):
863 def init_data_pub(self):
@@ -96,6 +96,8 b' def black_reformat_handler(text_before_cursor):'
96
96
97
97
98 class TerminalInteractiveShell(InteractiveShell):
98 class TerminalInteractiveShell(InteractiveShell):
99 mime_renderers = Dict().tag(config=True)
100
99 space_for_menu = Integer(6, help='Number of line at the bottom of the screen '
101 space_for_menu = Integer(6, help='Number of line at the bottom of the screen '
100 'to reserve for the completion menu'
102 'to reserve for the completion menu'
101 ).tag(config=True)
103 ).tag(config=True)
@@ -551,7 +553,7 b' class TerminalInteractiveShell(InteractiveShell):'
551
553
552 active_eventloop = None
554 active_eventloop = None
553 def enable_gui(self, gui=None):
555 def enable_gui(self, gui=None):
554 if gui:
556 if gui and (gui != 'inline') :
555 self.active_eventloop, self._inputhook =\
557 self.active_eventloop, self._inputhook =\
556 get_inputhook_name_and_func(gui)
558 get_inputhook_name_and_func(gui)
557 else:
559 else:
@@ -89,3 +89,14 b' class RichPromptDisplayHook(DisplayHook):'
89 )
89 )
90 else:
90 else:
91 sys.stdout.write(prompt_txt)
91 sys.stdout.write(prompt_txt)
92
93 def write_format_data(self, format_dict, md_dict=None) -> None:
94 if self.shell.mime_renderers:
95
96 for mime, handler in self.shell.mime_renderers.items():
97 if mime in format_dict:
98 handler(format_dict[mime], None)
99 return
100
101 super().write_format_data(format_dict, md_dict)
102
@@ -12,7 +12,7 b' backends = ['
12 'tk',
12 'tk',
13 'wx',
13 'wx',
14 'pyglet', 'glut',
14 'pyglet', 'glut',
15 'osx',
15 'osx'
16 ]
16 ]
17
17
18 registered = {}
18 registered = {}
@@ -29,6 +29,7 b' Extending and integrating with IPython'
29 extensions/index
29 extensions/index
30 integrating
30 integrating
31 custommagics
31 custommagics
32 shell_mimerenderer
32 inputtransforms
33 inputtransforms
33 callbacks
34 callbacks
34 eventloops
35 eventloops
General Comments 0
You need to be logged in to leave comments. Login now