pytest-notebook by example

Note

This tutorial is a notebook, that can be downloaded to run locally tutorial_intro.ipynb, and was converted to documentation with ipypublish

Python API

The principal component of pytest-notebook is the NBRegressionFixture class, which is an attrs class, whose parameters can be instatiated or set via attibutes.

In:
import importlib_resources
from pytest_notebook import example_nbs
from pytest_notebook.nb_regression import NBRegressionFixture
In:
fixture = NBRegressionFixture(exec_timeout=50)
fixture.diff_color_words = False
fixture
Out:
NBRegressionFixture(exec_notebook=True, exec_cwd=None, exec_allow_errors=False, exec_timeout=50, coverage=False, cov_config=None, cov_source=None, cov_merge=None, post_processors=('coalesce_streams',), process_resources={}, diff_replace=(), diff_ignore=('/cells/*/outputs/*/traceback',), diff_use_color=True, diff_color_words=False, force_regen=False)

The main method is check(), which executes a notebook and compares its initial and final contents.

In:
with importlib_resources.path(example_nbs, "example1.ipynb") as path:
    fixture.check(str(path))
---------------------------------------------------------------------------
NBRegressionError                         Traceback (most recent call last)
<ipython-input-3-23ab8b792066> in <module>
      1 with importlib_resources.path(example_nbs, "example1.ipynb") as path:
----> 2     fixture.check(str(path))

~/GitHub/pytest-notebook/pytest_notebook/nb_regression.py in check(self, path, raise_errors)
    367             raise regen_exc
    368         elif filtered_diff:
--> 369             raise NBRegressionError(diff_string)
    370
    371         return NBRegressionResult(

NBRegressionError:
--- expected
+++ obtained
## replaced /cells/1/execution_count:
-  2
+  1

## modified /cells/2/outputs/0/text:
@@ -1,2 +1,2 @@
 hallo1
-hallo3
+hallo2

To return the results, without raising an exception, use raise_errors=False. This returns a NBRegressionResult instance.

In:
with importlib_resources.path(example_nbs, "example1.ipynb") as path:
    result = fixture.check(str(path), raise_errors=False)

result
Out:
NBRegressionResult(diff_full_length=1,diff_filtered_length=1)
In:
print(result.diff_string)

--- expected
+++ obtained
## replaced /cells/1/execution_count:
-  2
+  1

## modified /cells/2/outputs/0/text:
@@ -1,2 +1,2 @@
 hallo1
-hallo3
+hallo2

The diff of the notebooks is returned as a list of nbdime DiffEntry objects.

In:
result.diff_filtered
Out:
[{'op': 'patch',
  'key': 'cells',
  'diff': [{'op': 'patch',
    'key': 1,
    'diff': [{'op': 'replace', 'key': 'execution_count', 'value': 1}]},
   {'op': 'patch',
    'key': 2,
    'diff': [{'op': 'patch',
      'key': 'outputs',
      'diff': [{'op': 'patch',
        'key': 0,
        'diff': [{'op': 'patch',
          'key': 'text',
          'diff': [{'op': 'patch',
            'key': 1,
            'diff': [{'op': 'addrange', 'key': 5, 'valuelist': '2'},
             {'op': 'removerange', 'key': 5, 'length': 1}]}]}]}]}]}]}]

The notebooks are returned as nbformat.NotebookNode instances.

In:
result.nb_final
Out:
{'cells': [{'cell_type': 'markdown',
   'metadata': {},
   'source': '# Markdown Cell'},
  {'cell_type': 'code',
   'execution_count': 1,
   'metadata': {},
   'outputs': [],
   'source': 'from IPython import display'},
  {'cell_type': 'code',
   'execution_count': 2,
   'metadata': {},
   'outputs': [{'output_type': 'stream',
     'name': 'stdout',
     'text': 'hallo1\nhallo2\n'}],
   'source': "print('hallo1')\nprint('hallo2')"}],
 'metadata': {'kernelspec': {'display_name': 'Python 3',
   'language': 'python',
   'name': 'python3'},
  'language_info': {'name': 'python',
   'version': '3.6.7',
   'mimetype': 'text/x-python',
   'codemirror_mode': {'name': 'ipython', 'version': 3},
   'pygments_lexer': 'ipython3',
   'nbconvert_exporter': 'python',
   'file_extension': '.py'}},
 'nbformat': 4,
 'nbformat_minor': 2}

pytest fixture

See also

pytest_notebook.ipy_magic, for the notebook magic used to run pytest in a Jupyter Notebook.

In:
%load_ext pytest_notebook.ipy_magic

A NBRegressionFixture instance can accessed via the nb_regression() fixture. This instance will be instatiated with parameters dictated by arguments parsed from the pytest command-line and configuration file(s).

Note

pytest-notebook command-line and configuration file parameter names are the same as for NBRegressionFixture, but with the prefix nb_.

The command-line parameter takes precedence over the configuration file one.

In:
%%pytest -v --color=yes --disable-warnings --nb-exec-timeout 50

---
[pytest]
nb_diff_color_words = True
---

import importlib_resources
from pytest_notebook import example_nbs

def test_notebook(nb_regression):
    with importlib_resources.path(example_nbs, "example1.ipynb") as path:
        nb_regression.check(str(path))
============================= test session starts ==============================
platform darwin -- Python 3.6.7, pytest-5.0.1, py-1.8.0, pluggy-0.12.0 -- //anaconda/envs/pytest_nb/bin/python
cachedir: .pytest_cache
rootdir: /private/var/folders/dm/b2qnkb_n3r72slmpxlfmcjvm00lbnd/T/tmppyh8kyts, inifile: pytest.ini
plugins: cov-2.7.1, datadir-1.3.0, regressions-1.0.5, notebook-0.5.1
collecting ... collected 1 item

test_ipycell.py::test_notebook FAILED                                    [100%]

=================================== FAILURES ===================================
________________________________ test_notebook _________________________________

nb_regression = NBRegressionFixture(exec_notebook=True, exec_cwd='/Users/cjs14/GitHub/pytest-notebook/pytest_notebook/example_nbs', ex...place=(), diff_ignore=('/cells/*/outputs/*/traceback',), diff_use_color=True, diff_color_words=True, force_regen=False)

    def test_notebook(nb_regression):
        with importlib_resources.path(example_nbs, "example1.ipynb") as path:
>           nb_regression.check(str(path))
E           pytest_notebook.nb_regression.NBRegressionError: 
E           --- expected
E           +++ obtained
E           ## replaced /cells/1/execution_count:
E           -  2
E           +  1
E           
E           ## modified /cells/2/outputs/0/text:
E           @@ -1,2 +1,2 @@
E           hallo1
E           hallo3hallo2
E           
E           

test_ipycell.py:8: NBRegressionError
===================== 1 failed, 1 warnings in 2.42 seconds =====================

pytest file collection

check() can be run automatically on all notebooks using the pytest collection mechanism. To activate this feature, set --nb-test-files on the command-line, or nb_test_files = True in the configuration file.

In:
notebook1_content = importlib_resources.read_text(example_nbs, "example1_pass.ipynb")
notebook2_content = importlib_resources.read_text(example_nbs, "example1.ipynb")
In:
%%pytest -v --color=yes --disable-warnings --nb-test-files

***
(notebook1_content, "test_notebook1.ipynb")
(notebook2_content, "test_notebook2.ipynb")
***
============================= test session starts ==============================
platform darwin -- Python 3.6.7, pytest-5.0.1, py-1.8.0, pluggy-0.12.0 -- //anaconda/envs/pytest_nb/bin/python
cachedir: .pytest_cache
rootdir: /private/var/folders/dm/b2qnkb_n3r72slmpxlfmcjvm00lbnd/T/tmpvs4w3kxu
plugins: cov-2.7.1, datadir-1.3.0, regressions-1.0.5, notebook-0.5.1
collecting ... collected 2 items

test_notebook1.ipynb::nbregression(test_notebook1) PASSED                [ 50%]
test_notebook2.ipynb::nbregression(test_notebook2) FAILED                [100%]

=================================== FAILURES ===================================
____________________ notebook: nbregression(test_notebook2) ____________________
pytest_notebook.nb_regression.NBRegressionError:
--- expected
+++ obtained
## replaced /cells/1/execution_count:
-  2
+  1

## modified /cells/2/outputs/0/text:
@@ -1,2 +1,2 @@
 hallo1
-hallo3
+hallo2

================ 1 failed, 1 passed, 1 warnings in 3.64 seconds ================
In:
%%pytest -v --color=yes --disable-warnings

---
[pytest]
nb_test_files = True
---

***
(notebook1_content, "test_notebook1.ipynb")
(notebook2_content, "test_notebook2.ipynb")
***
============================= test session starts ==============================
platform darwin -- Python 3.6.7, pytest-5.0.1, py-1.8.0, pluggy-0.12.0 -- //anaconda/envs/pytest_nb/bin/python
cachedir: .pytest_cache
rootdir: /private/var/folders/dm/b2qnkb_n3r72slmpxlfmcjvm00lbnd/T/tmphhv85_77, inifile: pytest.ini
plugins: cov-2.7.1, datadir-1.3.0, regressions-1.0.5, notebook-0.5.1
collecting ... collected 2 items

test_notebook1.ipynb::nbregression(test_notebook1) PASSED                [ 50%]
test_notebook2.ipynb::nbregression(test_notebook2) FAILED                [100%]

=================================== FAILURES ===================================
____________________ notebook: nbregression(test_notebook2) ____________________
pytest_notebook.nb_regression.NBRegressionError:
--- expected
+++ obtained
## replaced /cells/1/execution_count:
-  2
+  1

## modified /cells/2/outputs/0/text:
@@ -1,2 +1,2 @@
 hallo1
-hallo3
+hallo2

================ 1 failed, 1 passed, 1 warnings in 3.76 seconds ================

To restrict the notebook files pytest collects, one or more filename pattern matches can also be set (see fnmatch).

In:
%%pytest -v --color=yes --disable-warnings

---
[pytest]
nb_test_files = True
nb_file_fnmatch = test_*.ipynb tutorial_*.ipynb
---

***
(notebook1_content, "test_notebook1.ipynb")
(notebook2_content, "other_notebook2.ipynb")
***
============================= test session starts ==============================
platform darwin -- Python 3.6.7, pytest-5.0.1, py-1.8.0, pluggy-0.12.0 -- //anaconda/envs/pytest_nb/bin/python
cachedir: .pytest_cache
rootdir: /private/var/folders/dm/b2qnkb_n3r72slmpxlfmcjvm00lbnd/T/tmpd18whmsj, inifile: pytest.ini
plugins: cov-2.7.1, datadir-1.3.0, regressions-1.0.5, notebook-0.5.1
collecting ... collected 1 item

test_notebook1.ipynb::nbregression(test_notebook1) PASSED                [100%]

===================== 1 passed, 1 warnings in 2.31 seconds =====================

Live Logging of Cell Execution

If you wish to view the progress of the notebook execution, you can use the pytest live-logging functionality:

In:
%%pytest -v --color=yes --disable-warnings --nb-test-files --log-cli-level=info

***
(notebook1_content, "test_notebook1.ipynb")
***
============================= test session starts ==============================
platform darwin -- Python 3.6.7, pytest-5.0.1, py-1.8.0, pluggy-0.12.0 -- //anaconda/envs/pytest_nb/bin/python
cachedir: .pytest_cache
rootdir: /private/var/folders/dm/b2qnkb_n3r72slmpxlfmcjvm00lbnd/T/tmpzax121qh
plugins: cov-2.7.1, datadir-1.3.0, regressions-1.0.5, notebook-0.5.1
collecting ... collected 1 item

test_notebook1.ipynb::nbregression(test_notebook1)
-------------------------------- live log call ---------------------------------
INFO     pytest_notebook.execution:execution.py:142 About to execute notebook with 3 cells
INFO     pytest_notebook.execution:execution.py:148 Executing notebook with kernel: python3
INFO     pytest_notebook.execution:execution.py:168 Executing cell 1
INFO     pytest_notebook.execution:execution.py:168 Executing cell 2
PASSED                                                                   [100%]

===================== 1 passed, 1 warnings in 2.40 seconds =====================

Regenerating Notebooks

Failing notebooks can be regenerated by setting --nb-force-regen. This will overwrite failing notebooks with the output from the notebook execution.

Note

Notebooks will not be regenerated if they raise any unexpected exceptions, during execution.

This approach to regeneration mimics pytest-regressions.

In:
%%pytest -v --color=yes --disable-warnings --nb-force-regen

---
[pytest]
nb_test_files = True
nb_force_regen = True
---

***
(notebook1_content, "test_notebook1.ipynb")
(notebook2_content, "test_notebook2.ipynb")
***
============================= test session starts ==============================
platform darwin -- Python 3.6.7, pytest-5.0.1, py-1.8.0, pluggy-0.12.0 -- //anaconda/envs/pytest_nb/bin/python
cachedir: .pytest_cache
NB force regen: True
rootdir: /private/var/folders/dm/b2qnkb_n3r72slmpxlfmcjvm00lbnd/T/tmp7n8y_fg0, inifile: pytest.ini
plugins: cov-2.7.1, datadir-1.3.0, regressions-1.0.5, notebook-0.5.1
collecting ... collected 2 items

test_notebook1.ipynb::nbregression(test_notebook1) PASSED                [ 50%]
test_notebook2.ipynb::nbregression(test_notebook2) FAILED                [100%]

=================================== FAILURES ===================================
____________________ notebook: nbregression(test_notebook2) ____________________
pytest_notebook.nb_regression.NBRegressionError: Files differ and --nb-force-regen set, regenerating file at:
- /private/var/folders/dm/b2qnkb_n3r72slmpxlfmcjvm00lbnd/T/tmp7n8y_fg0/test_notebook2.ipynb
----------------------------- Captured stderr call -----------------------------
Diff before regeneration:

--- expected
+++ obtained
## replaced /cells/1/execution_count:
-  2
+  1

## modified /cells/2/outputs/0/text:
@@ -1,2 +1,2 @@
 hallo1
-hallo3
+hallo2

================ 1 failed, 1 passed, 1 warnings in 3.67 seconds ================

The regenration can be observed, if we run two tests on the same notebook.

In:
%%pytest -v --color=yes --disable-warnings

import os, tempfile
import importlib_resources
import pytest
from pytest_notebook import example_nbs

@pytest.fixture(scope="module")
def notebook():
    tmphandle, tmppath = tempfile.mkstemp(suffix=".ipynb")
    with open(tmppath, "w") as handle:
        handle.write(
            importlib_resources.read_text(example_nbs, "example1.ipynb")
        )
    yield tmppath
    os.remove(tmppath)

def test_notebook1(nb_regression, notebook):
    nb_regression.force_regen = True
    nb_regression.check(notebook)

def test_notebook2(nb_regression, notebook):
    nb_regression.check(notebook)
============================= test session starts ==============================
platform darwin -- Python 3.6.7, pytest-5.0.1, py-1.8.0, pluggy-0.12.0 -- //anaconda/envs/pytest_nb/bin/python
cachedir: .pytest_cache
rootdir: /private/var/folders/dm/b2qnkb_n3r72slmpxlfmcjvm00lbnd/T/tmpeb7sjuqh
plugins: cov-2.7.1, datadir-1.3.0, regressions-1.0.5, notebook-0.5.1
collecting ... collected 2 items

test_ipycell.py::test_notebook1 FAILED                                   [ 50%]
test_ipycell.py::test_notebook2 PASSED                                   [100%]

=================================== FAILURES ===================================
________________________________ test_notebook1 ________________________________

nb_regression = NBRegressionFixture(exec_notebook=True, exec_cwd='/var/folders/dm/b2qnkb_n3r72slmpxlfmcjvm00lbnd/T', exec_allow_errors...place=(), diff_ignore=('/cells/*/outputs/*/traceback',), diff_use_color=True, diff_color_words=False, force_regen=True)
notebook = '/var/folders/dm/b2qnkb_n3r72slmpxlfmcjvm00lbnd/T/tmp64orgem_.ipynb'

    def test_notebook1(nb_regression, notebook):
        nb_regression.force_regen = True
>       nb_regression.check(notebook)
E       pytest_notebook.nb_regression.NBRegressionError: Files differ and --nb-force-regen set, regenerating file at:
E       - /var/folders/dm/b2qnkb_n3r72slmpxlfmcjvm00lbnd/T/tmp64orgem_.ipynb

test_ipycell.py:20: NBRegressionError
----------------------------- Captured stderr call -----------------------------
Diff before regeneration:

--- expected
+++ obtained
## replaced /cells/1/execution_count:
-  2
+  1

## modified /cells/2/outputs/0/text:
@@ -1,2 +1,2 @@
 hallo1
-hallo3
+hallo2

================ 1 failed, 1 passed, 1 warnings in 3.76 seconds ================