Doctest Post Run Cell

A common use for notebooks is to “test an idea”. Designing pathways to mature informal notebook to formal testing tools like doctest. This notebook implements the ability to doctest when a cell is run.

import doctest, traitlets, IPython, contextlib, ast, textwrap

A full featured interactive doctest tool will be able to access any modifications to the interactive shell. For example magics must work.

    @contextlib.contextmanager
    def wrapped_compiler(shell):
        """`wrapped_compiler` replaces the `doctest` compiler with the interactive shell."""
        def compiler(input, filename, symbol, *args, **kwargs):
            nonlocal shell
            return shell.compile(ast.Interactive(body=shell.transform_ast(shell.compile.ast_parse(shell.transform_cell(textwrap.indent(input, " " * 4)))).body), filename, "single",)
        yield setattr(doctest, "compile", compiler)
        try: doctest.compile = compile
        except: ...

run_docstring_examples implements doctest machinery to test code with the interactive python shell.

    def run_docstring_examples(str, shell=IPython.get_ipython(), verbose=False, compileflags=None):
        runner = doctest.DocTestRunner(verbose=verbose, optionflags=doctest.ELLIPSIS)
        with wrapped_compiler(shell):
            for test in doctest.DocTestFinder(verbose).find(str, name=shell.user_module.__name__):
                test.globs = shell.user_ns
                runner.run(test, compileflags=compileflags, clear_globs=False)
        return runner

    def run(result): return run_docstring_examples(result.info.raw_cell, IPython.get_ipython())
    def unload_ipython_extension(shell):
        try: shell.events.unregister('post_run_cell', run)
        except ValueError: ...
    def load_ipython_extension(shell): unload_ipython_extension(shell), shell.events.register('post_run_cell', run)
    __name__ == '__main__' and load_ipython_extension(get_ipython())
Written on October 4, 2019