##// END OF EJS Templates
Backport PR #10048 on branch 5.x...
Kyle Kelley -
Show More
@@ -0,0 +1,359 b''
1 {
2 "cells": [
3 {
4 "cell_type": "markdown",
5 "metadata": {},
6 "source": [
7 "# Updatable Displays\n",
8 "\n",
9 "Note: This feature requires notebook >= 5.0 or JupyterLab, and \n",
10 "\n",
11 "\n",
12 "IPython 6 implements a new API as part of the Jupyter Protocol version 5.1 for easily updating displays.\n",
13 "\n",
14 "When you display something, you can now pass a `display_id` argument to attach an id to that output.\n",
15 "\n",
16 "Any future display with the same ID will also update other displays that had the same ID.\n",
17 "\n",
18 "`display` with a `display_id` will return a `DisplayHandle`\n",
19 "object, which gives you easy access to update the output:"
20 ]
21 },
22 {
23 "cell_type": "code",
24 "execution_count": 10,
25 "metadata": {
26 "collapsed": true
27 },
28 "outputs": [],
29 "source": [
30 "from IPython.display import display, update_display"
31 ]
32 },
33 {
34 "cell_type": "code",
35 "execution_count": 13,
36 "metadata": {},
37 "outputs": [
38 {
39 "data": {
40 "text/plain": [
41 "'z'"
42 ]
43 },
44 "metadata": {},
45 "output_type": "display_data"
46 },
47 {
48 "data": {
49 "text/plain": [
50 "<DisplayHandle display_id=update-me>"
51 ]
52 },
53 "execution_count": 13,
54 "metadata": {},
55 "output_type": "execute_result"
56 }
57 ],
58 "source": [
59 "handle = display('x', display_id='update-me')\n",
60 "handle"
61 ]
62 },
63 {
64 "cell_type": "markdown",
65 "metadata": {},
66 "source": [
67 "When we call `handle.display('y')`, we get a new display of 'y',\n",
68 "but in addition to that, we updated the previous display."
69 ]
70 },
71 {
72 "cell_type": "code",
73 "execution_count": 14,
74 "metadata": {},
75 "outputs": [
76 {
77 "data": {
78 "text/plain": [
79 "'z'"
80 ]
81 },
82 "metadata": {},
83 "output_type": "display_data"
84 }
85 ],
86 "source": [
87 "handle.display('y')"
88 ]
89 },
90 {
91 "cell_type": "markdown",
92 "metadata": {},
93 "source": [
94 "We can also *just* update the existing displays,\n",
95 "without creating a new display:"
96 ]
97 },
98 {
99 "cell_type": "code",
100 "execution_count": 15,
101 "metadata": {
102 "collapsed": true
103 },
104 "outputs": [],
105 "source": [
106 "handle.update('z')"
107 ]
108 },
109 {
110 "cell_type": "markdown",
111 "metadata": {},
112 "source": [
113 "You don't have to generate display_ids yourself,\n",
114 "if you specify `display_id=True`, then a unique ID will be assigned:"
115 ]
116 },
117 {
118 "cell_type": "code",
119 "execution_count": 16,
120 "metadata": {},
121 "outputs": [
122 {
123 "data": {
124 "text/plain": [
125 "'hello'"
126 ]
127 },
128 "metadata": {},
129 "output_type": "display_data"
130 },
131 {
132 "data": {
133 "text/plain": [
134 "<DisplayHandle display_id=07fc47b2ef652ccb70addeee3eb0981a>"
135 ]
136 },
137 "execution_count": 16,
138 "metadata": {},
139 "output_type": "execute_result"
140 }
141 ],
142 "source": [
143 "handle = display(\"hello\", display_id=True)\n",
144 "handle"
145 ]
146 },
147 {
148 "cell_type": "markdown",
149 "metadata": {},
150 "source": [
151 "Calling `handle.display(obj)` is the same as calling `display(obj, handle.display_id)`,\n",
152 "so you don't need to use the handle objects if you don't want to:"
153 ]
154 },
155 {
156 "cell_type": "code",
157 "execution_count": 17,
158 "metadata": {},
159 "outputs": [
160 {
161 "data": {
162 "text/plain": [
163 "'z'"
164 ]
165 },
166 "metadata": {},
167 "output_type": "display_data"
168 }
169 ],
170 "source": [
171 "display('x', display_id='here');"
172 ]
173 },
174 {
175 "cell_type": "code",
176 "execution_count": 18,
177 "metadata": {},
178 "outputs": [
179 {
180 "data": {
181 "text/plain": [
182 "'z'"
183 ]
184 },
185 "metadata": {},
186 "output_type": "display_data"
187 }
188 ],
189 "source": [
190 "display('y', display_id='here');"
191 ]
192 },
193 {
194 "cell_type": "markdown",
195 "metadata": {},
196 "source": [
197 "And just like `display`, there is now `update_display`,\n",
198 "which is what `DisplayHandle.update` calls:"
199 ]
200 },
201 {
202 "cell_type": "code",
203 "execution_count": 19,
204 "metadata": {
205 "collapsed": true
206 },
207 "outputs": [],
208 "source": [
209 "update_display('z', display_id='here')"
210 ]
211 },
212 {
213 "cell_type": "markdown",
214 "metadata": {},
215 "source": [
216 "## More detailed example\n",
217 "\n",
218 "One of the motivating use cases for this is simple progress bars.\n",
219 "\n",
220 "Here is an example ProgressBar using these APIs:"
221 ]
222 },
223 {
224 "cell_type": "code",
225 "execution_count": 35,
226 "metadata": {},
227 "outputs": [
228 {
229 "data": {
230 "text/html": [
231 "<progress\n",
232 " value=10\n",
233 " max=10\n",
234 " style=\"width: 60ex\"/>\n",
235 " 10 / 10\n",
236 " "
237 ],
238 "text/plain": [
239 "[============================================================] 10/10"
240 ]
241 },
242 "metadata": {},
243 "output_type": "display_data"
244 }
245 ],
246 "source": [
247 "import os\n",
248 "from binascii import hexlify\n",
249 "\n",
250 "class ProgressBar(object):\n",
251 " def __init__(self, capacity):\n",
252 " self.progress = 0\n",
253 " self.capacity = capacity\n",
254 " self.html_width = '60ex'\n",
255 " self.text_width = 60\n",
256 " self._display_id = hexlify(os.urandom(8)).decode('ascii')\n",
257 " \n",
258 " def __repr__(self):\n",
259 " fraction = self.progress / self.capacity\n",
260 " filled = '=' * int(fraction * self.text_width)\n",
261 " rest = ' ' * (self.text_width - len(filled))\n",
262 " return '[{}{}] {}/{}'.format(\n",
263 " filled, rest,\n",
264 " self.progress, self.capacity,\n",
265 " )\n",
266 " \n",
267 " def _repr_html_(self):\n",
268 " return \"\"\"<progress\n",
269 " value={progress}\n",
270 " max={capacity}\n",
271 " style=\"width: {width}\"/>\n",
272 " {progress} / {capacity}\n",
273 " \"\"\".format(\n",
274 " progress=self.progress,\n",
275 " capacity=self.capacity,\n",
276 " width=self.html_width,\n",
277 " )\n",
278 " \n",
279 " def display(self):\n",
280 " display(self, display_id=self._display_id)\n",
281 " \n",
282 " def update(self):\n",
283 " update_display(self, display_id=self._display_id)\n",
284 "\n",
285 "bar = ProgressBar(10)\n",
286 "bar.display()"
287 ]
288 },
289 {
290 "cell_type": "markdown",
291 "metadata": {},
292 "source": [
293 "And the ProgressBar has `.display` and `.update` methods:"
294 ]
295 },
296 {
297 "cell_type": "code",
298 "execution_count": 36,
299 "metadata": {},
300 "outputs": [
301 {
302 "data": {
303 "text/html": [
304 "<progress\n",
305 " value=10\n",
306 " max=10\n",
307 " style=\"width: 60ex\"/>\n",
308 " 10 / 10\n",
309 " "
310 ],
311 "text/plain": [
312 "[============================================================] 10/10"
313 ]
314 },
315 "metadata": {},
316 "output_type": "display_data"
317 }
318 ],
319 "source": [
320 "import time\n",
321 "\n",
322 "bar.display()\n",
323 "\n",
324 "for i in range(11):\n",
325 " bar.progress = i\n",
326 " bar.update()\n",
327 " time.sleep(0.25)"
328 ]
329 },
330 {
331 "cell_type": "markdown",
332 "metadata": {},
333 "source": [
334 "We would encourage any updatable-display objects that track their own display_ids to follow-suit with `.display()` and `.update()` or `.update_display()` methods."
335 ]
336 }
337 ],
338 "metadata": {
339 "kernelspec": {
340 "display_name": "Python 3",
341 "language": "python",
342 "name": "python3"
343 },
344 "language_info": {
345 "codemirror_mode": {
346 "name": "ipython",
347 "version": 3
348 },
349 "file_extension": ".py",
350 "mimetype": "text/x-python",
351 "name": "python",
352 "nbconvert_exporter": "python",
353 "pygments_lexer": "ipython3",
354 "version": "3.5.1"
355 }
356 },
357 "nbformat": 4,
358 "nbformat_minor": 1
359 }
@@ -11,6 +11,7 b' try:'
11 except ImportError:
11 except ImportError:
12 from base64 import encodestring as base64_encode
12 from base64 import encodestring as base64_encode
13
13
14 from binascii import b2a_hex
14 import json
15 import json
15 import mimetypes
16 import mimetypes
16 import os
17 import os
@@ -27,7 +28,7 b" __all__ = ['display', 'display_pretty', 'display_html', 'display_markdown',"
27 'display_javascript', 'display_pdf', 'DisplayObject', 'TextDisplayObject',
28 'display_javascript', 'display_pdf', 'DisplayObject', 'TextDisplayObject',
28 'Pretty', 'HTML', 'Markdown', 'Math', 'Latex', 'SVG', 'JSON', 'Javascript',
29 'Pretty', 'HTML', 'Markdown', 'Math', 'Latex', 'SVG', 'JSON', 'Javascript',
29 'Image', 'clear_output', 'set_matplotlib_formats', 'set_matplotlib_close',
30 'Image', 'clear_output', 'set_matplotlib_formats', 'set_matplotlib_close',
30 'publish_display_data']
31 'publish_display_data', 'update_display', 'DisplayHandle']
31
32
32 #-----------------------------------------------------------------------------
33 #-----------------------------------------------------------------------------
33 # utility functions
34 # utility functions
@@ -79,7 +80,8 b' def _display_mimetype(mimetype, objs, raw=False, metadata=None):'
79 # Main functions
80 # Main functions
80 #-----------------------------------------------------------------------------
81 #-----------------------------------------------------------------------------
81
82
82 def publish_display_data(data, metadata=None, source=None):
83 # use * to indicate transient is keyword-only
84 def publish_display_data(data, metadata=None, source=None, *, transient=None, **kwargs):
83 """Publish data and metadata to all frontends.
85 """Publish data and metadata to all frontends.
84
86
85 See the ``display_data`` message in the messaging documentation for
87 See the ``display_data`` message in the messaging documentation for
@@ -114,14 +116,32 b' def publish_display_data(data, metadata=None, source=None):'
114 to specify metadata about particular representations.
116 to specify metadata about particular representations.
115 source : str, deprecated
117 source : str, deprecated
116 Unused.
118 Unused.
119 transient : dict, keyword-only
120 A dictionary of transient data, such as display_id.
117 """
121 """
118 from IPython.core.interactiveshell import InteractiveShell
122 from IPython.core.interactiveshell import InteractiveShell
119 InteractiveShell.instance().display_pub.publish(
123
124 display_pub = InteractiveShell.instance().display_pub
125
126 # only pass transient if supplied,
127 # to avoid errors with older ipykernel.
128 # TODO: We could check for ipykernel version and provide a detailed upgrade message.
129 if transient:
130 kwargs['transient'] = transient
131
132 display_pub.publish(
120 data=data,
133 data=data,
121 metadata=metadata,
134 metadata=metadata,
135 **kwargs
122 )
136 )
123
137
124 def display(*objs, **kwargs):
138
139 def _new_id():
140 """Generate a new random text id with urandom"""
141 return b2a_hex(os.urandom(16)).decode('ascii')
142
143
144 def display(*objs, include=None, exclude=None, metadata=None, transient=None, display_id=None, **kwargs):
125 """Display a Python object in all frontends.
145 """Display a Python object in all frontends.
126
146
127 By default all representations will be computed and sent to the frontends.
147 By default all representations will be computed and sent to the frontends.
@@ -146,11 +166,34 b' def display(*objs, **kwargs):'
146 A dictionary of metadata to associate with the output.
166 A dictionary of metadata to associate with the output.
147 mime-type keys in this dictionary will be associated with the individual
167 mime-type keys in this dictionary will be associated with the individual
148 representation formats, if they exist.
168 representation formats, if they exist.
169 transient : dict, optional
170 A dictionary of transient data to associate with the output.
171 Data in this dict should not be persisted to files (e.g. notebooks).
172 display_id : str, optional
173 Set an id for the display.
174 This id can be used for updating this display area later via update_display.
175 If given as True, generate a new display_id
176 kwargs: additional keyword-args, optional
177 Additional keyword-arguments are passed through to the display publisher.
178
179 Returns
180 -------
181
182 handle: DisplayHandle
183 Returns a handle on updatable displays, if display_id is given.
184 Returns None if no display_id is given (default).
149 """
185 """
150 raw = kwargs.get('raw', False)
186 raw = kwargs.pop('raw', False)
151 include = kwargs.get('include')
187 if transient is None:
152 exclude = kwargs.get('exclude')
188 transient = {}
153 metadata = kwargs.get('metadata')
189 if display_id:
190 if display_id == True:
191 display_id = _new_id()
192 transient['display_id'] = display_id
193 if kwargs.get('update') and 'display_id' not in transient:
194 raise TypeError('display_id required for update_display')
195 if transient:
196 kwargs['transient'] = transient
154
197
155 from IPython.core.interactiveshell import InteractiveShell
198 from IPython.core.interactiveshell import InteractiveShell
156
199
@@ -159,7 +202,7 b' def display(*objs, **kwargs):'
159
202
160 for obj in objs:
203 for obj in objs:
161 if raw:
204 if raw:
162 publish_display_data(data=obj, metadata=metadata)
205 publish_display_data(data=obj, metadata=metadata, **kwargs)
163 else:
206 else:
164 format_dict, md_dict = format(obj, include=include, exclude=exclude)
207 format_dict, md_dict = format(obj, include=include, exclude=exclude)
165 if not format_dict:
208 if not format_dict:
@@ -168,7 +211,69 b' def display(*objs, **kwargs):'
168 if metadata:
211 if metadata:
169 # kwarg-specified metadata gets precedence
212 # kwarg-specified metadata gets precedence
170 _merge(md_dict, metadata)
213 _merge(md_dict, metadata)
171 publish_display_data(data=format_dict, metadata=md_dict)
214 publish_display_data(data=format_dict, metadata=md_dict, **kwargs)
215 if display_id:
216 return DisplayHandle(display_id)
217
218
219 # use * for keyword-only display_id arg
220 def update_display(obj, *, display_id, **kwargs):
221 """Update an existing display by id
222
223 Parameters
224 ----------
225
226 obj:
227 The object with which to update the display
228 display_id: keyword-only
229 The id of the display to update
230 """
231 kwargs['update'] = True
232 display(obj, display_id=display_id, **kwargs)
233
234
235 class DisplayHandle(object):
236 """A handle on an updatable display
237
238 Call .update(obj) to display a new object.
239
240 Call .display(obj) to add a new instance of this display,
241 and update existing instances.
242 """
243
244 def __init__(self, display_id=None):
245 if display_id is None:
246 display_id = _new_id()
247 self.display_id = display_id
248
249 def __repr__(self):
250 return "<%s display_id=%s>" % (self.__class__.__name__, self.display_id)
251
252 def display(self, obj, **kwargs):
253 """Make a new display with my id, updating existing instances.
254
255 Parameters
256 ----------
257
258 obj:
259 object to display
260 **kwargs:
261 additional keyword arguments passed to display
262 """
263 display(obj, display_id=self.display_id, **kwargs)
264
265 def update(self, obj, **kwargs):
266 """Update existing displays with my id
267
268 Parameters
269 ----------
270
271 obj:
272 object to display
273 **kwargs:
274 additional keyword arguments passed to update_display
275 """
276 update_display(obj, display_id=self.display_id, **kwargs)
172
277
173
278
174 def display_pretty(*objs, **kwargs):
279 def display_pretty(*objs, **kwargs):
@@ -53,7 +53,8 b' class DisplayPublisher(Configurable):'
53 if not isinstance(metadata, dict):
53 if not isinstance(metadata, dict):
54 raise TypeError('metadata must be a dict, got: %r' % data)
54 raise TypeError('metadata must be a dict, got: %r' % data)
55
55
56 def publish(self, data, metadata=None, source=None):
56 # use * to indicate transient, update are keyword-only
57 def publish(self, data, metadata=None, source=None, *, transient=None, update=False, **kwargs):
57 """Publish data and metadata to all frontends.
58 """Publish data and metadata to all frontends.
58
59
59 See the ``display_data`` message in the messaging documentation for
60 See the ``display_data`` message in the messaging documentation for
@@ -89,6 +90,13 b' class DisplayPublisher(Configurable):'
89 the data itself.
90 the data itself.
90 source : str, deprecated
91 source : str, deprecated
91 Unused.
92 Unused.
93 transient: dict, keyword-only
94 A dictionary for transient data.
95 Data in this dictionary should not be persisted as part of saving this output.
96 Examples include 'display_id'.
97 update: bool, keyword-only, default: False
98 If True, only update existing outputs with the same display_id,
99 rather than creating a new output.
92 """
100 """
93
101
94 # The default is to simply write the plain text data using sys.stdout.
102 # The default is to simply write the plain text data using sys.stdout.
@@ -5,6 +5,8 b' import json'
5 import os
5 import os
6 import warnings
6 import warnings
7
7
8 from unittest import mock
9
8 import nose.tools as nt
10 import nose.tools as nt
9
11
10 from IPython.core import display
12 from IPython.core import display
@@ -189,3 +191,114 b' def test_video_embedding():'
189 html = v._repr_html_()
191 html = v._repr_html_()
190 nt.assert_in('src="data:video/xyz;base64,YWJj"',html)
192 nt.assert_in('src="data:video/xyz;base64,YWJj"',html)
191
193
194
195 def test_display_id():
196 ip = get_ipython()
197 with mock.patch.object(ip.display_pub, 'publish') as pub:
198 handle = display.display('x')
199 nt.assert_is(handle, None)
200 handle = display.display('y', display_id='secret')
201 nt.assert_is_instance(handle, display.DisplayHandle)
202 handle2 = display.display('z', display_id=True)
203 nt.assert_is_instance(handle2, display.DisplayHandle)
204 nt.assert_not_equal(handle.display_id, handle2.display_id)
205
206 nt.assert_equal(pub.call_count, 3)
207 args, kwargs = pub.call_args_list[0]
208 nt.assert_equal(args, ())
209 nt.assert_equal(kwargs, {
210 'data': {
211 'text/plain': repr('x')
212 },
213 'metadata': {},
214 })
215 args, kwargs = pub.call_args_list[1]
216 nt.assert_equal(args, ())
217 nt.assert_equal(kwargs, {
218 'data': {
219 'text/plain': repr('y')
220 },
221 'metadata': {},
222 'transient': {
223 'display_id': handle.display_id,
224 },
225 })
226 args, kwargs = pub.call_args_list[2]
227 nt.assert_equal(args, ())
228 nt.assert_equal(kwargs, {
229 'data': {
230 'text/plain': repr('z')
231 },
232 'metadata': {},
233 'transient': {
234 'display_id': handle2.display_id,
235 },
236 })
237
238
239 def test_update_display():
240 ip = get_ipython()
241 with mock.patch.object(ip.display_pub, 'publish') as pub:
242 with nt.assert_raises(TypeError):
243 display.update_display('x')
244 display.update_display('x', display_id='1')
245 display.update_display('y', display_id='2')
246 args, kwargs = pub.call_args_list[0]
247 nt.assert_equal(args, ())
248 nt.assert_equal(kwargs, {
249 'data': {
250 'text/plain': repr('x')
251 },
252 'metadata': {},
253 'transient': {
254 'display_id': '1',
255 },
256 'update': True,
257 })
258 args, kwargs = pub.call_args_list[1]
259 nt.assert_equal(args, ())
260 nt.assert_equal(kwargs, {
261 'data': {
262 'text/plain': repr('y')
263 },
264 'metadata': {},
265 'transient': {
266 'display_id': '2',
267 },
268 'update': True,
269 })
270
271
272 def test_display_handle():
273 ip = get_ipython()
274 handle = display.DisplayHandle()
275 nt.assert_is_instance(handle.display_id, str)
276 handle = display.DisplayHandle('my-id')
277 nt.assert_equal(handle.display_id, 'my-id')
278 with mock.patch.object(ip.display_pub, 'publish') as pub:
279 handle.display('x')
280 handle.update('y')
281
282 args, kwargs = pub.call_args_list[0]
283 nt.assert_equal(args, ())
284 nt.assert_equal(kwargs, {
285 'data': {
286 'text/plain': repr('x')
287 },
288 'metadata': {},
289 'transient': {
290 'display_id': handle.display_id,
291 }
292 })
293 args, kwargs = pub.call_args_list[1]
294 nt.assert_equal(args, ())
295 nt.assert_equal(kwargs, {
296 'data': {
297 'text/plain': repr('y')
298 },
299 'metadata': {},
300 'transient': {
301 'display_id': handle.display_id,
302 },
303 'update': True,
304 })
General Comments 0
You need to be logged in to leave comments. Login now