Last active
September 14, 2023 11:23
-
-
Save minrk/2620735 to your computer and use it in GitHub Desktop.
simple example script for running and testing notebooks.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python | |
""" | |
simple example script for running and testing notebooks. | |
Usage: `ipnbdoctest.py foo.ipynb [bar.ipynb [...]]` | |
Each cell is submitted to the kernel, and the outputs are compared with those stored in the notebook. | |
""" | |
# License: Public Domain, but credit is nice (Min RK). | |
import os,sys,time | |
import base64 | |
import re | |
from collections import defaultdict | |
from Queue import Empty | |
try: | |
from IPython.kernel import KernelManager | |
except ImportError: | |
from IPython.zmq.blockingkernelmanager import BlockingKernelManager as KernelManager | |
from IPython.nbformat.current import reads, NotebookNode | |
def compare_png(a64, b64): | |
"""compare two b64 PNGs (incomplete)""" | |
try: | |
import Image | |
except ImportError: | |
pass | |
adata = base64.decodestring(a64) | |
bdata = base64.decodestring(b64) | |
return True | |
def sanitize(s): | |
"""sanitize a string for comparison. | |
fix universal newlines, strip trailing newlines, and normalize likely random values (memory addresses and UUIDs) | |
""" | |
if not isinstance(s, basestring): | |
return s | |
# normalize newline: | |
s = s.replace('\r\n', '\n') | |
# ignore trailing newlines (but not space) | |
s = s.rstrip('\n') | |
# normalize hex addresses: | |
s = re.sub(r'0x[a-f0-9]+', '0xFFFFFFFF', s) | |
# normalize UUIDs: | |
s = re.sub(r'[a-f0-9]{8}(\-[a-f0-9]{4}){3}\-[a-f0-9]{12}', 'U-U-I-D', s) | |
return s | |
def consolidate_outputs(outputs): | |
"""consolidate outputs into a summary dict (incomplete)""" | |
data = defaultdict(list) | |
data['stdout'] = '' | |
data['stderr'] = '' | |
for out in outputs: | |
if out.type == 'stream': | |
data[out.stream] += out.text | |
elif out.type == 'pyerr': | |
data['pyerr'] = dict(ename=out.ename, evalue=out.evalue) | |
else: | |
for key in ('png', 'svg', 'latex', 'html', 'javascript', 'text', 'jpeg',): | |
if key in out: | |
data[key].append(out[key]) | |
return data | |
def compare_outputs(test, ref, skip_compare=('png', 'traceback', 'latex', 'prompt_number')): | |
for key in ref: | |
if key not in test: | |
print "missing key: %s != %s" % (test.keys(), ref.keys()) | |
return False | |
elif key not in skip_compare and sanitize(test[key]) != sanitize(ref[key]): | |
print "mismatch %s:" % key | |
print test[key] | |
print ' != ' | |
print ref[key] | |
return False | |
return True | |
def run_cell(shell, iopub, cell): | |
# print cell.input | |
shell.execute(cell.input) | |
# wait for finish, maximum 20s | |
shell.get_msg(timeout=20) | |
outs = [] | |
while True: | |
try: | |
msg = iopub.get_msg(timeout=0.2) | |
except Empty: | |
break | |
msg_type = msg['msg_type'] | |
if msg_type in ('status', 'pyin'): | |
continue | |
elif msg_type == 'clear_output': | |
outs = [] | |
continue | |
content = msg['content'] | |
# print msg_type, content | |
out = NotebookNode(output_type=msg_type) | |
if msg_type == 'stream': | |
out.stream = content['name'] | |
out.text = content['data'] | |
elif msg_type in ('display_data', 'pyout'): | |
out['metadata'] = content['metadata'] | |
for mime, data in content['data'].iteritems(): | |
attr = mime.split('/')[-1].lower() | |
# this gets most right, but fix svg+html, plain | |
attr = attr.replace('+xml', '').replace('plain', 'text') | |
setattr(out, attr, data) | |
if msg_type == 'pyout': | |
out.prompt_number = content['execution_count'] | |
elif msg_type == 'pyerr': | |
out.ename = content['ename'] | |
out.evalue = content['evalue'] | |
out.traceback = content['traceback'] | |
else: | |
print "unhandled iopub msg:", msg_type | |
outs.append(out) | |
return outs | |
def test_notebook(nb): | |
km = KernelManager() | |
km.start_kernel(extra_arguments=['--pylab=inline'], stderr=open(os.devnull, 'w')) | |
try: | |
kc = km.client() | |
kc.start_channels() | |
iopub = kc.iopub_channel | |
except AttributeError: | |
# IPython 0.13 | |
kc = km | |
kc.start_channels() | |
iopub = kc.sub_channel | |
shell = kc.shell_channel | |
# run %pylab inline, because some notebooks assume this | |
# even though they shouldn't | |
shell.execute("pass") | |
shell.get_msg() | |
while True: | |
try: | |
iopub.get_msg(timeout=1) | |
except Empty: | |
break | |
successes = 0 | |
failures = 0 | |
errors = 0 | |
for ws in nb.worksheets: | |
for cell in ws.cells: | |
if cell.cell_type != 'code': | |
continue | |
try: | |
outs = run_cell(shell, iopub, cell) | |
except Exception as e: | |
print "failed to run cell:", repr(e) | |
print cell.input | |
errors += 1 | |
continue | |
failed = False | |
for out, ref in zip(outs, cell.outputs): | |
if not compare_outputs(out, ref): | |
failed = True | |
if failed: | |
failures += 1 | |
else: | |
successes += 1 | |
sys.stdout.write('.') | |
print "tested notebook %s" % nb.metadata.name | |
print " %3i cells successfully replicated" % successes | |
if failures: | |
print " %3i cells mismatched output" % failures | |
if errors: | |
print " %3i cells failed to complete" % errors | |
kc.stop_channels() | |
km.shutdown_kernel() | |
del km | |
if __name__ == '__main__': | |
for ipynb in sys.argv[1:]: | |
print "testing %s" % ipynb | |
with open(ipynb) as f: | |
nb = reads(f.read(), 'json') | |
test_notebook(nb) |
Thank you very much for this, it's fantastic and incredibly useful. I've just tried to run it on an IPython 4.0 install and get the following warning:
[~] $ ipnbdoctest.py
/home/dani/anaconda/lib/python2.7/site-packages/IPython/kernel/__init__.py:13: ShimWarning: The `IPython.kernel` package has been deprecated. You should import from ipykernel or jupyter_client instead.
"You should import from ipykernel or jupyter_client instead.", ShimWarning)
/home/dani/anaconda/lib/python2.7/site-packages/IPython/nbformat.py:13: ShimWarning: The `IPython.nbformat` package has been deprecated. You should import from nbformat instead.
"You should import from nbformat instead.", ShimWarning)
/home/dani/anaconda/lib/python2.7/site-packages/nbformat/current.py:19: UserWarning: nbformat.current is deprecated.
- use nbformat for read/write/validate public API
- use nbformat.vX directly to composing notebooks of a particular version
""")
[~] $
At this point it still works but I assume it'll be phased out at some point, would it be too difficult to update it? Alternatively, has this been replaced by something else in core IPython (or Jupyter)? Thanks very much again!
Actually, I'll take that back, the script doesn't seem to work with modern IPython/Jupyter any more:
In [1]: run ./ipnbdoctest.py check_gds_stack.ipynb test.ipynb
/home/dani/anaconda/envs/gds_exp/lib/python2.7/site-packages/IPython/kernel/__init__.py:13: ShimWarning: The `IPython.kernel` package has been deprecated. You should import from ipykernel or jupyter_client instead.
"You should import from ipykernel or jupyter_client instead.", ShimWarning)
/home/dani/anaconda/envs/gds_exp/lib/python2.7/site-packages/IPython/nbformat.py:13: ShimWarning: The `IPython.nbformat` package has been deprecated. You should import from nbformat instead.
"You should import from nbformat instead.", ShimWarning)
/home/dani/anaconda/envs/gds_exp/lib/python2.7/site-packages/nbformat/current.py:19: UserWarning: nbformat.current is deprecated.
- use nbformat for read/write/validate public API
- use nbformat.vX directly to composing notebooks of a particular version
""")
testing check_gds_stack.ipynb
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
/media/dani/baul/AAA/Documents/teaching/u-lvl/2015/geocomp/GIT/gds15/content/infrastructure/ipnbdoctest.py in <module>()
234 print "testing %s" % args.input_ipynb
235 nb = reads(f.read(), 'json')
--> 236 test_notebook(nb)
237 with io.open(args.output_ipynb, 'w', encoding='utf8') as f:
238 write(nb, f, 'json')
/media/dani/baul/AAA/Documents/teaching/u-lvl/2015/geocomp/GIT/gds15/content/infrastructure/ipnbdoctest.py in test_notebook(nb)
149 # run %pylab inline, because some notebooks assume this
150 # even though they shouldn't
--> 151 shell.execute("pass")
152 shell.get_msg()
153 while True:
AttributeError: 'ZMQSocketChannel' object has no attribute 'execute'
In [2]:
@minrk Any idea what could fix it? Thanks in advance!
Here's a version that works on py3.6 with jupyter 5:
https://gist.github.com/ociule/3ebbaf13f44eeca1966399d92609b499
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
I've left in https://gist.github.com/akimd/f60b2f26ea70c72db175 a version which displays the diff of the mismatches, instead of just displaying the expected result, and the effective one. At least in my cases, this is much more readable.