Skip to content

Commit c5b6b30

Browse files
authored
Adding displayHTML to allow HTML output to the notebook (#122)
* Implementing displayHTML... * Added displayHTML; Small refactoring to ease support for new content types; Added comments. * Added support for lines terminated in carriage-return (\r) to support progress-bar style output; Export NOTEBOOK_BASH_KERNEL_CAPABILITIES environment variable; Fixed and improved comments. * Documented new features in README.rst. * Fixed small breaking mistake for rich content (and manually tested again). * * Added support for Javascript content; * Added option to dynamically update rich content; * Fixing .rst typos. * Fixing .rst typos. * Fixing .rst typos. * Addressing comments in pull request #122.
1 parent 6a7aaf2 commit c5b6b30

File tree

5 files changed

+332
-73
lines changed

5 files changed

+332
-73
lines changed

README.rst

Lines changed: 64 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
... image:: https://mybinder.org/badge_logo.svg
22
:target: https://mybinder.org/v2/gh/takluyver/bash_kernel/master
3-
3+
=========================
44
A Jupyter kernel for bash
5+
=========================
56

7+
Installation
8+
------------
69
This requires IPython 3.
710

8-
To install::
9-
1011
pip install bash_kernel
1112
python -m bash_kernel.install
1213

@@ -19,7 +20,66 @@ To use it, run one of:
1920
jupyter qtconsole --kernel bash
2021
jupyter console --kernel bash
2122
23+
Displaying Rich Content
24+
-----------------------
25+
26+
To use specialized content (images, html, etc) this file defines (in `build_cmds()`) bash functions
27+
that take the contents as standard input. Currently, `display` (images), `displayHTML` (html)
28+
and `displayJS` (javascript) are supported.
29+
30+
Example:
31+
32+
.. code:: shell
33+
34+
cat dog.png | display
35+
echo "<b>Dog</b>, not a cat." | displayHTML
36+
echo "alert('It is known khaleesi\!');" | displayJS
37+
38+
Updating Rich Content Cells
39+
---------------------------
40+
41+
If one is doing something that requires dynamic updates, one can specify a unique display_id,
42+
should be a string name (downstream documentation is not clear on this), and the contents
43+
will be replaced by the new value. Example:
44+
45+
.. code:: shell
46+
47+
display_id="id_${RANDOM}"
48+
((ii=0))
49+
while ((ii < 10)) ; do
50+
echo "<div>${ii}</div>" | displayHTML $display_id
51+
((ii = ii+1))
52+
sleep 1
53+
done
54+
55+
The same works for images or even javascript content.
56+
57+
**Remember to create always a new id** (random ids works perfect) each time the cell is executed, otherwise
58+
it will try to display on an HTML element that no longer exists (they are erased each time a cell is re-run).
59+
60+
Programmatically Generating Rich Content
61+
----------------------------------------
62+
63+
Alternatively one can simply generate the rich content to a file in /tmp (or $TMPDIR)
64+
and then output the corresponding (to the mimetype) context prefix "_TEXT_SAVED_*"
65+
constant. So one can write programs (C++, Go, Rust, etc.) that generates rich content
66+
appropriately, when within a notebook.
67+
68+
The environment variable "NOTEBOOK_BASH_KERNEL_CAPABILITIES" will be set with a comma
69+
separated list of the supported types (currently "image,html,javascript") that a program
70+
can check for.
71+
72+
To output to a particular "display_id", to allow update of content (e.g: dynamically
73+
updating/generating a plot from a command line program), prefix the filename
74+
with "(<display_id>)". E.g: a line to display the contents of /tmp/myHTML.html to
75+
a display id "id_12345" would look like:
76+
77+
bash_kernel: saved html data to: (id_12345) /tmp/myHTML.html
78+
79+
More Information
80+
----------------
81+
2282
For details of how this works, see the Jupyter docs on `wrapper kernels
2383
<http://jupyter-client.readthedocs.org/en/latest/wrapperkernels.html>`_, and
2484
Pexpect's docs on the `replwrap module
25-
<http://pexpect.readthedocs.org/en/latest/api/replwrap.html>`_
85+
<http://pexpect.readthedocs.org/en/latest/api/replwrap.html>`_.

bash_kernel/display.py

Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
"""display.py holds the functions needed to display different types of content.
2+
3+
To use specialized content (images, html, etc) this file defines (in `build_cmds()`) bash functions
4+
that take the contents as standard input. Currently, `display` (images), `displayHTML` (html)
5+
and `displayJS` (javascript) are supported.
6+
7+
Example:
8+
9+
$ cat dog.png | display
10+
$ echo "<b>Dog</b>, not a cat." | displayHTML
11+
$ echo "alert('It is known khaleesi\!');" | displayJS
12+
13+
### Updating rich content cells
14+
15+
If one is doing something that requires dynamic updates, one can specify a display_id,
16+
should be a string name (downstream documentation is not clear on this), and the contents
17+
will be replaced by the new value. Example:
18+
19+
20+
display_id="id_${RANDOM}"
21+
((ii=0))
22+
while ((ii < 10)) ; do
23+
echo "<div>${ii}<script></div>" | displayHTML $display_id
24+
((ii = ii+1))
25+
sleep 1
26+
done
27+
echo
28+
29+
Remember to create a new id each time the cell is executed.javascript. The same
30+
will work for images or even javascript content (execute javascript snippet
31+
without creating new output cells for each execution).
32+
33+
## Programmatically generating rich content
34+
35+
Alternatively one can simply generate the rich content to a file in /tmp (or $TMPDIR)
36+
and then output the corresponding (to the mimetype) context prefix _TEXT_SAVED_*
37+
constant. So one can write programs (C++, Go, Rust, etc.) that generates rich content
38+
appropriately.
39+
40+
The environment variable "NOTEBOOK_BASH_KERNEL_CAPABILITIES" will be set with a comma
41+
separated list of the supported types (currently "image,html,javascript") that a program can check
42+
for.
43+
44+
To output to a particular "display_id", to allow update of content, prefix the filename
45+
with "(<display_id>)". E.g: a line to display the contents of /tmp/myHTML.html to
46+
a display id "id_12345" would look like:
47+
48+
bash_kernel: saved html data to: (id_12345) /tmp/myHTML.html
49+
50+
To add support to new content types: (1) create a constant _TEXT_SAVED_<new_type>; (2) create a function
51+
display_data_for_<new_type>; (3) Create an entry in CONTENT_DATA_PREFIXES. Btw, `$ jupyter-lab --Session.debug=True`
52+
is your friend to debug the format of the content message.
53+
"""
54+
import base64
55+
import imghdr
56+
import json
57+
import os
58+
import re
59+
60+
61+
_TEXT_SAVED_IMAGE = "bash_kernel: saved image data to: "
62+
_TEXT_SAVED_HTML = "bash_kernel: saved html data to: "
63+
_TEXT_SAVED_JAVASCRIPT = "bash_kernel: saved javascript data to: "
64+
65+
def _build_cmd_for_type(display_cmd, line_prefix):
66+
return """
67+
%s () {
68+
display_id="$1"; shift;
69+
TMPFILE=$(mktemp ${TMPDIR-/tmp}/bash_kernel.XXXXXXXXXX)
70+
cat > $TMPFILE
71+
prefix="%s"
72+
if [[ "${display_id}" != "" ]]; then
73+
echo "${prefix}(${display_id}) $TMPFILE" >&2
74+
else
75+
echo "${prefix}$TMPFILE" >&2
76+
fi
77+
}
78+
""" % (display_cmd, line_prefix)
79+
80+
81+
def build_cmds():
82+
commands = []
83+
capabilities = []
84+
for line_prefix, info in CONTENT_DATA_PREFIXES.items():
85+
commands.append(_build_cmd_for_type(info['display_cmd'], line_prefix))
86+
capabilities.append(info['capability'])
87+
capabilities_cmd = 'export NOTEBOOK_BASH_KERNEL_CAPABILITIES="{}"'.format(','.join(capabilities))
88+
commands.append(capabilities_cmd)
89+
return "\n".join(commands)
90+
91+
92+
def _unlink_if_temporary(filename):
93+
tmp_dir = '/tmp'
94+
if 'TMPDIR' in os.environ:
95+
tmp_dir = os.environ['TMPDIR']
96+
if filename.startswith(tmp_dir):
97+
os.unlink(filename)
98+
99+
100+
def display_data_for_image(filename):
101+
with open(filename, 'rb') as f:
102+
image = f.read()
103+
_unlink_if_temporary(filename)
104+
105+
image_type = imghdr.what(None, image)
106+
if image_type is None:
107+
raise ValueError("Not a valid image: %s" % image)
108+
109+
image_data = base64.b64encode(image).decode('ascii')
110+
content = {
111+
'data': {
112+
'image/' + image_type: image_data
113+
},
114+
'metadata': {}
115+
}
116+
return content
117+
118+
119+
def display_data_for_html(filename):
120+
with open(filename, 'rb') as f:
121+
html_data = f.read()
122+
_unlink_if_temporary(filename)
123+
content = {
124+
'data': {
125+
'text/html': html_data.decode('utf-8'),
126+
},
127+
'metadata': {}
128+
}
129+
return content
130+
131+
def display_data_for_js(filename):
132+
"""JavaScript data will all be displayed within the same display_id, to avoid creating different ones for each javascript command."""
133+
with open(filename, 'rb') as f:
134+
html_data = f.read()
135+
_unlink_if_temporary(filename)
136+
content = {
137+
'data': {
138+
'text/javascript': html_data.decode('utf-8'),
139+
},
140+
'metadata': {}
141+
}
142+
return content
143+
144+
def split_lines(text):
145+
"""Split lines on '\n' or '\r', preserving the ending (end-of-line/line-feed or carriage-return)."""
146+
# lines_and_endings will alternate between the line content and a line separator (end-of-line or carriage-return),
147+
# We loop over these putting together again the line contents and one lines+ending, special
148+
# casing when we have '\r\n' (may still be used in DOS/Windows).
149+
lines_and_endings = re.split('([\r\n])', text)
150+
if lines_and_endings[-1] == '':
151+
# re.split will add a spurious empty part in the end, if the text ends in '\r' or '\n'.
152+
lines_and_endings = lines_and_endings[:-1]
153+
num_parts = len(lines_and_endings)
154+
lines = []
155+
ii = 0
156+
while ii < num_parts:
157+
content = lines_and_endings[ii]
158+
ending = '\n'
159+
if ii+1 < num_parts:
160+
ending = lines_and_endings[ii+1]
161+
# Special case old DOS end of line sequence '\r\n':
162+
if ii+3 < num_parts and ending == '\r' and lines_and_endings[ii+2] == '' and lines_and_endings[ii+3] == '\n':
163+
ending = '\n' # Replace by a single end-of-line/line-feed.
164+
ii += 2 # Skip the empty line content between the '\r' and '\n'
165+
lines.append(content+ending)
166+
ii += 2 # Skip to next content+ending parts.
167+
return lines
168+
169+
def extract_contents(output):
170+
"""Returns plain_output string and a list of rich content data."""
171+
output_lines = []
172+
rich_contents = []
173+
for line in split_lines(output):
174+
matched = False
175+
for key, info in CONTENT_DATA_PREFIXES.items():
176+
if line.startswith(key):
177+
filename, display_id = _filename_and_display_id(line[len(key):-1])
178+
content = info['display_data_fn'](filename)
179+
if display_id is not None:
180+
if 'transient' not in content:
181+
content['transient'] = {}
182+
content['transient']['display_id'] = display_id
183+
rich_contents.append(content)
184+
matched = True
185+
break
186+
if not matched:
187+
output_lines.append(line)
188+
189+
plain_output = ''.join(output_lines)
190+
return plain_output, rich_contents
191+
192+
193+
def _filename_and_display_id(line):
194+
"""line will be either "filename" or "(display_id) filename"."""
195+
if line[0] != '(':
196+
return line, None
197+
pos = line.find(')')
198+
if pos == -1:
199+
raise ValueError('Invalid filename/display_id for rich content "{}"'.format(line))
200+
if line[pos+1] == ' ':
201+
filename = line[pos+2:]
202+
else:
203+
filename = line[pos+1:]
204+
return filename, line[1:pos]
205+
206+
207+
# Maps content prefixes to function that display its contents.
208+
CONTENT_DATA_PREFIXES = {
209+
_TEXT_SAVED_IMAGE: {
210+
'display_cmd': 'display',
211+
'display_data_fn': display_data_for_image,
212+
'capability': 'image',
213+
},
214+
_TEXT_SAVED_HTML: {
215+
'display_cmd': 'displayHTML',
216+
'display_data_fn': display_data_for_html,
217+
'capability': 'html',
218+
},
219+
_TEXT_SAVED_JAVASCRIPT: {
220+
'display_cmd': 'displayJS',
221+
'display_data_fn': display_data_for_js,
222+
'capability': 'javascript',
223+
}
224+
}

bash_kernel/images.py

Lines changed: 0 additions & 48 deletions
This file was deleted.

bash_kernel/install.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ def main(argv=None):
3737

3838
prefix_locations.add_argument(
3939
'--user',
40-
help='Install KernelSpec in user homedirectory',
40+
help='Install KernelSpec in user\'s home directory',
4141
action='store_true'
4242
)
4343
prefix_locations.add_argument(

0 commit comments

Comments
 (0)