In [1]:
print("typical output")
There was no simple way to make code in one cell to write output to another cell. Now there is!
This feature is so new that we need to patch display
until nteract depends on an IPython release which includes ipython/ipykernel#204 and ipython/ipython#10048. So as a temporary workaround, we will define display_with_id
and its signature and behavior will be provided with display
in future versions of IPython.
You do not need to understand any of the code in the next cell, just execute it so you can get to the fun stuff.
In [2]:
# a temporary patch of display for demonstration purposes
ip = get_ipython()
def display_with_id(obj, display_id=None, update=False):
iopub = ip.kernel.iopub_socket
session = ip.kernel.session
data, md = ip.display_formatter.format(obj)
transient = {'display_id': display_id}
content = {'data': data, 'metadata': md, 'transient': transient}
if display_id is None: content.pop('transient') # make display_id option
msg_type = 'update_display_data' if update else 'display_data'
session.send(iopub, msg_type, content, parent=ip.parent_header)
display = display_with_id
In [3]:
display('initial display', 'some_destination')
Ok, so far, nothing earth shattering. But what happens if you call display with the same display_id
again?
In [4]:
display('spoiler alert: output updated in both', 'some_destination')
Fantastic! We have a way of mirroring output in multiple places. But what if you only want update the previously named displays, without creating a new one? Just call display
with update=True
, like this:
In [5]:
display('no output here, update above', 'some_destination', update=True)
Though we have been working with text so far, this also works for the all other output types. Let's make an HTML-based progress bar!
In [6]:
import os
from binascii import hexlify
class ProgressBar(object):
def __init__(self, capacity):
self._display_id = hexlify(os.urandom(8)).decode('ascii')
self.capacity = capacity
self.progress = 0
def _repr_html_(self):
return "<progress style='width:100%' max='{}' value='{}'></progress>".format(self.capacity, self.progress)
def display(self):
display_with_id(self, display_id=self._display_id)
def update(self):
display_with_id(self, display_id=self._display_id, update=True)
bar = ProgressBar(100)
bar.display()
The progress bar is drawn and it starts off at 0
. Fill it up half way and call its update
method to get a redraw.
In [7]:
bar.progress = 50
bar.update()
Now go half-way again
In [8]:
bar.progress = 75
bar.update()
Our original bar is kind of far away now, let's get another view of it below.
In [9]:
bar.display()
This is good, but it would be awesome to have a progress bar that would automatically update whenever its progress was modified - that would be truly progressive. We subclass ProgressBar
and now we make progress
into a Python property, which will allow us to set it and get it like an attribute, but do that using methods. In particular, whenever we assign a new value to progress
, we also call update
.
In [10]:
class AutoupdatingProgressBar(ProgressBar):
@property
def progress(self):
return self._progress
@progress.setter
def progress(self, value):
self._progress = value
self.update()
In [11]:
better_bar = AutoupdatingProgressBar(100)
better_bar.display()
In [12]:
better_bar.progress = 40
Much better. No more pesky update
calls. Let's make a little animation that Zeno would be proud of:
In [13]:
import time
better_bar.progress = 0
for _ in range(10):
time.sleep(.5)
better_bar.progress += (better_bar.capacity - better_bar.progress) / 2
You might have noticed that each ProgressBar
autogenerates a random display_id
which is handy if you want to have several of them.
In [14]:
num_bars = 5
bars = [AutoupdatingProgressBar(100) for _ in range(num_bars)]
for b in bars:
b.display()
In [15]:
import random
for x in range(40):
time.sleep(.1)
idx = random.randrange(num_bars)
bars[idx].progress += random.randint(-2, 10)
In [16]:
for b in bars:
b.display()