Skip to content

Instantly share code, notes, and snippets.

@minrk
Last active September 14, 2023 11:23
Show Gist options
  • Star 21 You must be signed in to star a gist
  • Fork 11 You must be signed in to fork a gist
  • Save minrk/2620735 to your computer and use it in GitHub Desktop.
Save minrk/2620735 to your computer and use it in GitHub Desktop.
simple example script for running and testing notebooks.
#!/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
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)
@heplesser
Copy link

Hi Min!

Your script has been very helpful as a starting point for a script evaluating student hand-ins. Unfortunately, when running large numbers of notebooks, Python eventually crashes with "Too many files open". Here is a simple reproducer

from IPython.kernel import KernelManager

n = 1

while True:
    km = KernelManager()
    km.start_kernel(extra_arguments=['--pylab=inline'], stderr=open(os.devnull, 'w'))
    kc = km.client()
    kc.start_channels()

    # do some work here

    kc.stop_channels()
    km.shutdown_kernel()
    del kc
    del km

    print '.',
    if n % 10 == 0:
        print 
    n += 1

which crashes after 24 rounds through the loop:

HEP-Admins-MacBook-Pro:results plesser$ ipython ../work/create_and_close.py 
. . . . . . . . . .
. . . . . . . . . .
. . . .Too many open files (signaler.cpp:330)
.Abort trap

It seems that the kc.stop_channels() and km.shutdown_kernel() and the two dels don't close all connections.

I am using IPython 1.1.0 with Python 2.7.5 (Anaconda) on OSX 10.6.8.

Update: As a work-around, I now create manager and client once in __main__ and then call km.reset_kernel(now=True) at the beginning of test_notebook(). This works fine for at least 175 notebooks.

Best,
Hans

@shoyer
Copy link

shoyer commented Nov 16, 2013

I added comparison of PNG output cells in my fork using pypng: https://gist.github.com/shoyer/7497853

Feel free to merge it in if you like. Your little script has come in very handy for me!

@akimd
Copy link

akimd commented Oct 27, 2014

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.

@darribas
Copy link

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!

@darribas
Copy link

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!

@leriomaggio
Copy link

@darribas (cc/ @minrk)
I'm very interested too in making this script to work with Jupyter.
Any progress so far?

Thanks a lot.
Cheers.

@ociule
Copy link

ociule commented Jul 12, 2018

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