pax_global_header00006660000000000000000000000064140557614220014520gustar00rootroot0000000000000052 comment=f6692b41e761addd65497df229b1e75532bdc9c6 testbook-0.4.2/000077500000000000000000000000001405576142200133555ustar00rootroot00000000000000testbook-0.4.2/.bumpversion.cfg000066400000000000000000000002011405576142200164560ustar00rootroot00000000000000[bumpversion] current_version = 0.4.2 commit = True tag = True tag_name = {new_version} [bumpversion:file:testbook/_version.py] testbook-0.4.2/.github/000077500000000000000000000000001405576142200147155ustar00rootroot00000000000000testbook-0.4.2/.github/workflows/000077500000000000000000000000001405576142200167525ustar00rootroot00000000000000testbook-0.4.2/.github/workflows/main.yml000066400000000000000000000017271405576142200204300ustar00rootroot00000000000000name: CI on: push: branches: "*" pull_request: branches: "*" jobs: build-n-test-n-coverage: name: Build, test and code coverage runs-on: ubuntu-latest strategy: fail-fast: false matrix: python-version: [3.6, 3.7, 3.8] env: OS: ubuntu-latest PYTHON: "3.8" steps: - name: Checkout uses: actions/checkout@v2 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | python -m pip install --upgrade pip pip install -e .[test] pip install tox-gh-actions - name: Run the tests run: tox - name: Upload coverage to Codecov uses: codecov/codecov-action@v1 with: file: ./coverage.xml flags: unittests name: codecov-umbrella fail_ci_if_error: false testbook-0.4.2/.gitignore000066400000000000000000000034511405576142200153500ustar00rootroot00000000000000# Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ pip-wheel-metadata/ share/python-wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .nox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover *.py,cover .hypothesis/ .pytest_cache/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py db.sqlite3 db.sqlite3-journal # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder target/ # Jupyter Notebook .ipynb_checkpoints # IPython profile_default/ ipython_config.py # pyenv .python-version # pipenv # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. # However, in case of collaboration, if having platform-specific dependencies or dependencies # having no cross-platform support, pipenv may install dependencies that don't work, or not # install all needed dependencies. #Pipfile.lock # PEP 582; used by e.g. github.com/David-OConnor/pyflow __pypackages__/ # Celery stuff celerybeat-schedule celerybeat.pid # SageMath parsed files *.sage.py # Environments .env .venv env/ venv/ ENV/ env.bak/ venv.bak/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ .dmypy.json dmypy.json # Pyre type checker .pyre/ # Editor configs .vscode/ .idea/ testbook-0.4.2/.readthedocs.yml000066400000000000000000000005241405576142200164440ustar00rootroot00000000000000# .readthedocs.yml # Read the Docs configuration file # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details # Required version: 2 sphinx: configuration: docs/conf.py formats: all build: image: latest python: version: 3.7 install: - method: pip path: . extra_requirements: - sphinx testbook-0.4.2/CODE_OF_CONDUCT.md000066400000000000000000000001751405576142200161570ustar00rootroot00000000000000# Code of Conduct Please read our entire [Code of Conduct](https://github.com/nteract/nteract/blob/main/CODE_OF_CONDUCT.md) testbook-0.4.2/CONTRIBUTING.md000066400000000000000000000115441405576142200156130ustar00rootroot00000000000000# So You Want to Contribute to testbook! We welcome all contributions to testbook both large and small. We encourage you to join our community. ## Our Community Values We are an open and friendly community. Everybody is welcome. We encourage friendly discussions and respect for all. There are no exceptions. All contributions are equally important. Documentation, answering questions, and fixing bugs are equally as valuable as adding new features. Please read our entire code of conduct [here](https://github.com/nteract/nteract/blob/master/CODE_OF_CONDUCT.md). Also, check out the for the [Python](https://github.com/nteract/nteract/blob/master/CODE_OF_CONDUCT.md) code of conduct. ## Setting up Your Development Environment Following these instructions should give you an efficient path to opening your first pull-request. ### Cloning the testbook Repository Fork the repository to your local Github account. Clone this repository to your local development machine. ```bash git clone https://github.com//testbook cd testbook ``` ### Install an Editable Version Use you favorite editor environment, here's an example for venv: ```bash python3 -m venv dev source dev/bin/activate ``` Install testbook using: ```bash pip install -e '.[dev]' ``` _Note: When you are finished you can use `source deactivate` to go back to your base environment._ ### Running Tests Locally If you are contributing with documentation please jump to [building documentation.](#Building-Documentation) We need to install the development package before we can run the tests. If anything is confusing below, always resort to the relevant documentation. For the most basic test runs against python 3.6 use this tox subset (callable after `pip install tox`): ```bash tox -e py36 ``` This will just execute the unittests against python 3.6 in a new virtual env. The first run will take longer to setup the virtualenv, but will be fast after that point. For a full test suite of all envs and linting checks simply run tox without any arguments ```bash tox ``` This will require python3.5, python3.6, python3.7, and python 3.8 to be installed. Alternavitely pytest can be used if you have an environment already setup which works or has custom packages not present in the tox build. ```bash pytest ``` Now there should be a working and editable installation of testbook to start making your own contributions. ### Building Documentation The documentation is built using the [Sphinx](http://www.sphinx-doc.org/en/master/) engine. To contribute, edit the [RestructuredText (`.rst`)](https://en.wikipedia.org/wiki/ReStructuredText) files in the docs directory to make changes and additions. Once you are done editing, to generate the documentation, use tox and the following command from the root directory of the repository: ```bash tox -e docs ``` This will generate `.html` files in the `/.tox/docs_out/` directory. Once you are satisfied, feel free to jump to the next section. ## So You're Ready to Pull Request The general workflow for this will be: 1. Run local tests 2. Pushed changes to your forked repository 3. Open pull request to main repository ### Run Tests Locally ```bash tox ``` Note that the manifest test reads the `MANIFEST.in` file and explicitly specify the files to include in the source distribution. You can read more about how this works [here](https://docs.python.org/3/distutils/sourcedist.html). ### Push Changes to Forked Repo Your commits should be pushed to the forked repository. To verify this type ```bash git remote -v ``` and ensure the remotes point to your GitHub. Don't work on the master branch! 1. Commit changes to local repository: ```bash git checkout -b my-feature git add git commit ``` 2. Push changes to your remote repository: ```bash git push -u origin my-feature ``` ### Create Pull Request Follow [these](https://help.github.com/articles/creating-a-pull-request-from-a-fork/) instrucutions to create a pull request from a forked repository. If you are submitting a bug-fix for a specific issue make sure to reference the issue in the pull request. There are good references to the [Git documentation](https://git-scm.com/doc) and [Git workflows](https://docs.scipy.org/doc/numpy/dev/gitwash/development_workflow.html) for more information if any of this is unfamiliar. _Note: You might want to set a reference to the main repository to fetch/merge from there instead of your forked repository. You can do that using:_ ```bash git remote add upstream https://github.com/nteract/testbook ``` It's possible you will have conflicts between your repository and master. Here, `master` is meant to be synchronized with the `upstream` repository. GitHub has some good [documentation](https://help.github.com/articles/resolving-a-merge-conflict-using-the-command-line/) on merging pull requests from the command line. Happy hacking on testbook! testbook-0.4.2/LICENSE000066400000000000000000000027531405576142200143710ustar00rootroot00000000000000BSD 3-Clause License Copyright (c) 2020, nteract All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. testbook-0.4.2/MANIFEST.in000066400000000000000000000011521405576142200151120ustar00rootroot00000000000000recursive-include testbook *.py recursive-include testbook *.ipynb recursive-include testbook *.json recursive-include testbook *.yaml recursive-include testbook *.keep recursive-include testbook *.txt include setup.py include requirements*.txt include tox.ini include pytest.ini include README.md include LICENSE include MANIFEST.in include *.md include *.toml include *.yml include .bumpversion.cfg # Documentation graft docs # exclude build files prune docs/_build # exclude sample notebooks for binder prune binder # Test env prune .tox # Build files prune azure-pipelines.yml # Exclude examples prune examples testbook-0.4.2/README.md000066400000000000000000000047341405576142200146440ustar00rootroot00000000000000[![Build Status](https://github.com/nteract/testbook/workflows/CI/badge.svg)](https://github.com/nteract/testbook/actions) [![image](https://codecov.io/github/nteract/testbook/coverage.svg?branch=master)](https://codecov.io/github/nteract/testbook?branch=master) [![Documentation Status](https://readthedocs.org/projects/testbook/badge/?version=latest)](https://testbook.readthedocs.io/en/latest/?badge=latest) [![PyPI](https://img.shields.io/pypi/v/testbook.svg)](https://pypi.org/project/testbook/) [![Python 3.6](https://img.shields.io/badge/python-3.6-blue.svg)](https://www.python.org/downloads/release/python-360/) [![Python 3.7](https://img.shields.io/badge/python-3.7-blue.svg)](https://www.python.org/downloads/release/python-370/) [![Python 3.8](https://img.shields.io/badge/python-3.8-blue.svg)](https://www.python.org/downloads/release/python-380/) [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/ambv/black) # testbook **testbook** is a unit testing framework extension for testing code in Jupyter Notebooks. Previous attempts at unit testing notebooks involved writing the tests in the notebook itself. However, testbook will allow for unit tests to be run against notebooks in separate test files, hence treating .ipynb files as .py files. testbook helps you set up **conventional unit tests for your Jupyter Notebooks**. Here is an example of a unit test written using testbook Consider the following code cell in a Jupyter Notebook: ```python def func(a, b): return a + b ``` You would write a unit test using `testbook` in a Python file as follows: ```python from testbook import testbook @testbook('/path/to/notebook.ipynb', execute=True) def test_func(tb): func = tb.get("func") assert func(1, 2) == 3 ``` ## Installing `testbook` ```{code-block} bash pip install testbook ``` NOTE: This does not install any kernels for running your notebooks. You'll need to install in the same way you do for running the notebooks normally. Usually this is done with `pip install ipykernel` Alternatively if you want all the same dev dependencies and the ipython kernel you can install these dependencies with: ```{code-block} bash pip install testbook[dev] ``` ## Documentation See [readthedocs](https://testbook.readthedocs.io/en/latest/) for more in-depth details. ## Development Guide Read [CONTRIBUTING.md](./CONTRIBUTING.md) for guidelines on how to setup a local development environment and make code changes back to testbook. testbook-0.4.2/RELEASING.md000066400000000000000000000007121405576142200152100ustar00rootroot00000000000000# Releasing ## Prerequisites - First check that the CHANGELOG is up to date for the next release version - Ensure dev requirements are installed `pip install -r requirements-dev.txt` ## Push to GitHub Change from patch to minor or major for appropriate version updates. ```bash bumpversion patch git push upstream && git push upstream --tags ``` ## Push to PyPI ```bash rm -rf dist/* rm -rf build/* python setup.py bdist_wheel twine upload dist/* ``` testbook-0.4.2/docs/000077500000000000000000000000001405576142200143055ustar00rootroot00000000000000testbook-0.4.2/docs/Makefile000066400000000000000000000011721405576142200157460ustar00rootroot00000000000000# Minimal makefile for Sphinx documentation # # You can set these variables from the command line, and also # from the environment for the first two. SPHINXOPTS ?= SPHINXBUILD ?= sphinx-build SOURCEDIR = . BUILDDIR = _build # Put it first so that "make" without argument is like "make help". help: @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) .PHONY: help Makefile # Catch-all target: route all unknown targets to Sphinx using the new # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). %: Makefile @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) testbook-0.4.2/docs/UPDATE.md000066400000000000000000000001541405576142200156110ustar00rootroot00000000000000TODO: Figure out make options needed for non-api changes ``` sphinx-apidoc -f -o reference ../testbook ``` testbook-0.4.2/docs/changelog.md000066400000000000000000000026641405576142200165660ustar00rootroot00000000000000# Changelog ## 0.4.2 - Documentation and CoC updates to improve developer access (Thank you PyLadies Vancouver!) - The `text/plain` media type is now visible when calling `notebook.cell_output_text(idx)` ## 0.4.1 - check for errors when `allow_errors` is true ## 0.4.0 - Testbook now returns actual object for JSON serializable objects instead of reference objects. Please note that this may break tests written with prior versions. ## 0.3.0 - Implemented container methods -- __len__ -- __iter__ -- __next__ -- __getitem__ -- __setitem__ -- __contains__ - Fixed testbook to work with ipykernel 5.5 ## 0.2.6 - Fixed Python underscore (`_`) issue ## 0.2.5 - Fixed testbook decorator. ## 0.2.4 - Add `cell_execute_result` to `TestbookNotebookClient` - Use testbook decorator with pytest fixture and marker ## 0.2.3 - Accept notebook node as argument to testbook - Added support for specifying kernel with `kernel_name` kwarg ## 0.2.2 - Added support for passing notebook as file-like object or path as str ## 0.2.1 - Added support for `allow_errors` ## 0.2.0 - Changed to new package name `testbook` - Supports for patch and patch_dict - Slices now supported for execute patterns - Raises TestbookRuntimeError for all exceptions that occur during cell execution ## 0.1.3 - Added warning about package name change ## 0.1.2 - Updated docs link in setup.py ## 0.1.1 - Unpin dependencies ## 0.1.0 - Initial release with basic features testbook-0.4.2/docs/conf.py000066400000000000000000000126121405576142200156060ustar00rootroot00000000000000# Configuration file for the Sphinx documentation builder. # # This file only contains a selection of the most common options. For a full # list see the documentation: # https://www.sphinx-doc.org/en/master/usage/configuration.html # -- Path setup -------------------------------------------------------------- # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # import os import sys sys.path.insert(0, os.path.abspath('..')) # -- Project information ----------------------------------------------------- project = 'testbook' copyright = '2020, nteract team' author = 'nteract team' # -- General configuration --------------------------------------------------- # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ 'sphinx.ext.autodoc', 'sphinx.ext.intersphinx', 'sphinx.ext.mathjax', 'sphinx.ext.napoleon', 'myst_parser', ] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: # source_suffix = ['.rst', '.md'] # The master toctree document. master_doc = 'index' # General information about the project. project = 'testbook' copyright = '2020, nteract team' author = 'nteract team' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # import testbook # The short X.Y version. version = '.'.join(testbook.__version__.split('.')[0:2]) # The full version, including alpha/beta/rc tags. release = testbook.__version__ # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line foexitr these cases. language = None # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This patterns also effect to html_static_path and html_extra_path exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store', 'UPDATE.md'] # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # If true, `todo` and `todoList` produce output, else they produce nothing. todo_include_todos = False # -- Options for HTML output ---------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # html_theme = 'sphinx_book_theme' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. # html_theme_options = { "path_to_docs": "docs", "repository_url": "https://github.com/nteract/testbook", "repository_branch": "main", "use_edit_page_button": True, } # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". # html_static_path = ['_static'] # Custom sidebar templates, must be a dictionary that maps document names # to template names. # # This is required for the alabaster theme # refs: http://alabaster.readthedocs.io/en/latest/installation.html#sidebars html_sidebars = {'**': ['about.html', 'navigation.html', 'relations.html', 'searchbox.html']} html_title = "testbook" # -- Options for HTMLHelp output ------------------------------------------ # Output file base name for HTML help builder. htmlhelp_basename = 'testbookdoc' # -- Options for LaTeX output --------------------------------------------- latex_elements = { # The paper size ('letterpaper' or 'a4paper'). # # 'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). # # 'pointsize': '10pt', # Additional stuff for the LaTeX preamble. # # 'preamble': '', # Latex figure (float) alignment # # 'figure_align': 'htbp', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [(master_doc, 'testbook.tex', 'testbook Documentation', 'nteract team', 'manual')] # -- Options for manual page output --------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [(master_doc, 'testbook', 'testbook Documentation', [author], 1)] # -- Options for Texinfo output ------------------------------------------- # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ ( master_doc, 'testbook', 'testbook Documentation', author, 'testbook', 'One line description of project.', 'Miscellaneous', ) ] # Example configuration for intersphinx: refer to the Python standard library. intersphinx_mapping = {'https://docs.python.org/': None} testbook-0.4.2/docs/examples/000077500000000000000000000000001405576142200161235ustar00rootroot00000000000000testbook-0.4.2/docs/examples/index.md000066400000000000000000000026401405576142200175560ustar00rootroot00000000000000# Examples Here are some common testing patterns where testbook can help. ## Mocking requests library **Notebook:** ![mock-requests-library](https://imgur.com/GM1YExq.png) **Test:** ```python from testbook import testbook @testbook('/path/to/notebook.ipynb', execute=True) def test_get_details(tb): with tb.patch('requests.get') as mock_get: get_details = tb.get('get_details') # get reference to function get_details('https://my-api.com') mock_get.assert_called_with('https://my-api.com') ``` ## Asserting dataframe manipulations **Notebook:** ![dataframe-manip](https://imgur.com/g1DrVn2.png) **Test:** ```python from testbook import testbook @testbook('/path/to/notebook.ipynb') def test_dataframe_manipulation(tb): tb.execute_cell('imports') # Inject a dataframe with code tb.inject( """ df = pandas.DataFrame([[1, None, 3], [4, 5, 6]], columns=['a', 'b', 'c'], dtype='float') """ ) # Perform manipulation tb.execute_cell('manipulation') # Inject assertion into notebook tb.inject("assert len(df) == 1") ``` ## Asserting STDOUT of a cell **Notebook:** ![dataframe-manip](https://imgur.com/cgtvkph.png) **Test:** ```python from testbook import testbook @testbook('stdout.ipynb', execute=True) def test_stdout(tb): assert tb.cell_output_text(1) == 'hello world!' assert 'The current time is' in tb.cell_output_text(2) ``` testbook-0.4.2/docs/getting-started/000077500000000000000000000000001405576142200174125ustar00rootroot00000000000000testbook-0.4.2/docs/getting-started/index.md000066400000000000000000000030501405576142200210410ustar00rootroot00000000000000# Installation and Getting Started `testbook` is a unit testing framework for testing code in Jupyter Notebooks. ## Installing `testbook` Using a virtual environment or system Python: ```{code-block} bash pip install testbook ``` Using Anaconda: ```{code-block} bash conda install testbook ``` ## What is a Jupyter Notebook? [An introduction to Jupyter](https://jupyter-notebook-beginner-guide.readthedocs.io/en/latest/what_is_jupyter.html) ## Installing and Launching Jupyter Notebook [How to install Jupyter](https://jupyterlab.readthedocs.io/en/stable/getting_started/installation.html) ```{code-block} bash jupyter lab ``` ## Create your first test Create a new notebook: To do add image Write the following code into the first cell of a Jupyter Notebook: ```{code-block} python def foo(x): return x + 1 ``` Save this Notebook as `notebook.ipynb`. Create a new `.py` file. In this new file, write the following unit test: ```{code-block} python from testbook import testbook @testbook('notebook.ipynb', execute=True) def test_foo(tb): foo = tb.get("foo") assert foo(2) == 3 ``` That's it! You can now execute the test. ## General workflow when using testbook to write a unit test 1. Use `testbook.testbook` as a decorator or context manager to specify the path to the Jupyter Notebook. Passing `execute=True` will execute all the cells, and passing `execute=['cell-tag-1', 'cell-tag-2']` will only execute specific cells identified by cell tags. 2. Obtain references to objects under test using the `.get` method. 3. Write the test! testbook-0.4.2/docs/index.md000066400000000000000000000053261405576142200157440ustar00rootroot00000000000000# Welcome to testbook [![Github-CI][github-badge]][github-link] [![Github-CI][github-ci]][github-ci-link] [![Coverage Status][codecov-badge]][codecov-link] [![Documentation Status][rtd-badge]][rtd-link] [![PyPI][pypi-badge]][pypi-link] [![Python 3.6](https://img.shields.io/badge/python-3.6-blue.svg)](https://www.python.org/downloads/release/python-360/) [![Python 3.7](https://img.shields.io/badge/python-3.7-blue.svg)](https://www.python.org/downloads/release/python-370/) [![Python 3.8](https://img.shields.io/badge/python-3.8-blue.svg)](https://www.python.org/downloads/release/python-380/) [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/ambv/black) **testbook** is a unit testing framework for testing code in Jupyter Notebooks. Previous attempts at unit testing notebooks involved writing the tests in the notebook itself. However, testbook will allow for unit tests to be run against notebooks in separate test files, hence treating `.ipynb` files as `.py` files. Here is an example of a unit test written using testbook Consider the following code cell in a Jupyter Notebook: ```{code-block} python def func(a, b): return a + b ``` You would write a unit test using `testbook` in a Python file as follows: ```python from testbook import testbook @testbook('/path/to/notebook.ipynb', execute=True) def test_func(tb): func = tb.get("func") assert func(1, 2) == 3 ``` --- ## Features - Write conventional unit tests for Jupyter Notebooks - [Execute all or some specific cells before unit test](usage/index.html#using-execute-to-control-which-cells-are-executed-before-test) - [Share kernel context across multiple tests](usage/index.html#share-kernel-context-across-multiple-tests) (using pytest fixtures) - [Support for patching objects](usage/index.html#support-for-patching-objects) - Inject code into Jupyter notebooks - Works with any unit testing library - unittest, pytest or nose ## Documentation ```{toctree} :maxdepth: 3 getting-started/index.md usage/index.md examples/index.md reference/index.rst changelog.md ``` [github-ci]: https://github.com/nteract/testbook/workflows/CI/badge.svg [github-ci-link]: https://github.com/nteract/testbook/actions [github-link]: https://github.com/nteract/testbook [rtd-badge]: https://readthedocs.org/projects/testbook/badge/?version=latest [rtd-link]: https://testbook.readthedocs.io/en/latest/?badge=latest [codecov-badge]: https://codecov.io/gh/nteract/testbook/branch/master/graph/badge.svg [codecov-link]: https://codecov.io/gh/nteract/testbook [github-badge]: https://img.shields.io/github/stars/nteract/testbook?label=github [pypi-badge]: https://img.shields.io/pypi/v/testbook.svg [pypi-link]: https://pypi.org/project/testbook/ testbook-0.4.2/docs/make.bat000066400000000000000000000014331405576142200157130ustar00rootroot00000000000000@ECHO OFF pushd %~dp0 REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) set SOURCEDIR=. set BUILDDIR=_build if "%1" == "" goto help %SPHINXBUILD% >NUL 2>NUL if errorlevel 9009 ( echo. echo.The 'sphinx-build' command was not found. Make sure you have Sphinx echo.installed, then set the SPHINXBUILD environment variable to point echo.to the full path of the 'sphinx-build' executable. Alternatively you echo.may add the Sphinx directory to PATH. echo. echo.If you don't have Sphinx installed, grab it from echo.http://sphinx-doc.org/ exit /b 1 ) %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% goto end :help %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% :end popd testbook-0.4.2/docs/reference/000077500000000000000000000000001405576142200162435ustar00rootroot00000000000000testbook-0.4.2/docs/reference/index.rst000066400000000000000000000006161405576142200201070ustar00rootroot00000000000000Reference ========= This part of the documentation lists the full API reference of all public classes and functions. testbook.client module ---------------------- .. automodule:: testbook.client :members: :undoc-members: :show-inheritance: testbook.exceptions module -------------------------- .. automodule:: testbook.exceptions :members: :undoc-members: :show-inheritance: testbook-0.4.2/docs/requirements-doc.txt000066400000000000000000000000761405576142200203370ustar00rootroot00000000000000Sphinx>=1.7,<3.0 sphinx_book_theme==0.0.35 myst-parser==0.9.1 testbook-0.4.2/docs/usage/000077500000000000000000000000001405576142200154115ustar00rootroot00000000000000testbook-0.4.2/docs/usage/index.md000066400000000000000000000124131405576142200170430ustar00rootroot00000000000000# Usage The motivation behind creating testbook was to be able to write conventional unit tests for Jupyter Notebooks. ## How it works Testbook achieves conventional unit tests to be written by setting up references to variables/functions/classes in the Jupyter Notebook. All interactions with these reference objects are internally "pushed down" into the kernel, which is where it gets executed. ## Set up Jupyter Notebook under test ### Decorator and context manager pattern These patterns are interchangeable in most cases. If there are nested decorators on your unit test function, consider using the context manager pattern instead. - Decorator pattern ```{code-block} python from testbook import testbook @testbook('/path/to/notebook.ipynb', execute=True) def test_func(tb): func = tb.get("func") assert func(1, 2) == 3 ``` - Context manager pattern ```{code-block} python from testbook import testbook def test_func(): with testbook('/path/to/notebook.ipynb', execute=True) as tb: func = tb.get("func") assert func(1, 2) == 3 ``` ### Using `execute` to control which cells are executed before test You may also choose to execute all or some cells: - Pass `execute=True` to execute the entire notebook before the test. In this case, it might be better to set up a [module scoped pytest fixture](#share-kernel-context-across-multiple-tests). - Pass `execute=['cell1', 'cell2']` or `execute='cell1'` to only execute the specified cell(s) before the test. - Pass `execute=slice('start-cell', 'end-cell')` or `execute=range(2, 10)` to execute all cells in the specified range. ## Obtain references to objects present in notebook ### Testing functions in Jupyter Notebook Consider the following code cell in a Jupyter Notebook: ```{code-block} python def foo(name): return f"You passed {name}!" my_list = ['list', 'from', 'notebook'] ``` Reference objects to functions can be called with, - explicit JSON serializable values (like `dict`, `list`, `int`, `float`, `str`, `bool`, etc) - other reference objects ```{code-block} python @testbook.testbook('/path/to/notebook.ipynb', execute=True) def test_foo(tb): foo = tb.get("foo") # passing in explicitly assert foo(['spam', 'eggs']) == "You passed ['spam', 'eggs']!" # passing in reference object as arg my_list = tb.get("my_list") assert foo(my_list) == "You passed ['list', 'from', 'notebook']!" ``` ### Testing function/class returning a non-serializable value Consider the following code cell in a Jupyter Notebook: ```{code-block} python class Foo: def __init__(self): self.name = name def say_hello(self): return f"Hello {self.name}!" ``` When `Foo` is instantiated from the test, the return value will be a reference object which stores a reference to the non-serializable `Foo` object. ```{code-block} python @testbook.testbook('/path/to/notebook.ipynb', execute=True) def test_say_hello(tb): Foo = tb.get("Foo") bar = Foo("bar") assert bar.say_hello() == "Hello bar!" ``` ## Share kernel context across multiple tests If your use case requires you to execute many cells (or all cells) of a Jupyter Notebook, before a test can be executed, then it would make sense to share the kernel context with multiple tests. It can be done by setting up a [module or package scoped pytest fixture][fixture]. Consider the code cells below, ```{code-block} python def foo(a, b): return a + b ``` ```{code-block} python def bar(a): return [x*2 for x in a] ``` The unit tests can be written as follows, ```{code-block} python import pytest from testbook import testbook @pytest.fixture(scope='module') def tb(): with testbook('/path/to/notebook.ipynb', execute=True) as tb: yield tb def test_foo(tb): foo = tb.get("foo") assert foo(1, 2) == 3 def test_bar(tb): bar = tb.get("bar") tb.inject(""" data = [1, 2, 3] """) data = tb.get("data") assert bar(data) == [2, 4, 6] ``` ```{warning} Note that since the kernel is being shared in case of module scoped fixtures, you might run into weird state issues. Please keep in mind that changes made to an object in one test will reflect in other tests too. This will likely be fixed in future versions of testbook. ``` ## Support for patching objects Use the `patch` and `patch_dict` contextmanager to patch out objects during unit test. Learn more about how to use `patch` [here](https://docs.python.org/3/library/unittest.mock.html#unittest.mock.patch). **Example usage of `patch`:** ```{code-block} python def foo(): bar() ``` ```{code-block} python @testbook('/path/to/notebook.ipynb', execute=True) def test_method(tb): with tb.patch('__main__.bar') as mock_bar: foo = tb.get("foo") foo() mock_bar.assert_called_once() ``` **Example usage of `patch_dict`:** ```{code-block} python my_dict = {'hello': 'world'} ``` ```{code-block} python @testbook('/path/to/notebook.ipynb', execute=True) def test_my_dict(tb): with tb.patch('__main__.my_dict', {'hello' : 'new world'}) as mock_my_dict: my_dict = tb.get("my_dict") assert my_dict == {'hello' : 'new world'} ``` [fixture]: https://docs.pytest.org/en/stable/fixture.html#scope-sharing-a-fixture-instance-across-tests-in-a-class-module-or-session testbook-0.4.2/examples/000077500000000000000000000000001405576142200151735ustar00rootroot00000000000000testbook-0.4.2/examples/dataframe-example/000077500000000000000000000000001405576142200205505ustar00rootroot00000000000000testbook-0.4.2/examples/dataframe-example/dataframe-assertion-example.ipynb000066400000000000000000000045621405576142200272040ustar00rootroot00000000000000{ "cells": [ { "cell_type": "code", "execution_count": 1, "id": "6786d1ad", "metadata": { "tags": [ "imports" ] }, "outputs": [], "source": [ "import pandas as pd" ] }, { "cell_type": "code", "execution_count": 2, "id": "48caab90", "metadata": {}, "outputs": [], "source": [ "df = pd.DataFrame([[1, 2, 3], [4, None, 6]], columns = ['a', 'b', 'c'], dtype='float')" ] }, { "cell_type": "code", "execution_count": 3, "id": "03137c43", "metadata": { "tags": [ "manipulation" ] }, "outputs": [], "source": [ "df = df.dropna()" ] }, { "cell_type": "code", "execution_count": 4, "id": "f28853da", "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
abc
01.02.03.0
\n", "
" ], "text/plain": [ " a b c\n", "0 1.0 2.0 3.0" ] }, "execution_count": 4, "metadata": {}, "output_type": "execute_result" } ], "source": [ "df" ] } ], "metadata": { "celltoolbar": "Tags", "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.7.6" } }, "nbformat": 4, "nbformat_minor": 5 } testbook-0.4.2/examples/dataframe-example/dataframe_test.py000066400000000000000000000007151405576142200241100ustar00rootroot00000000000000from testbook import testbook @testbook('./dataframe-assertion-example.ipynb') def test_dataframe_manipulation(tb): tb.execute_cell('imports') # Inject a dataframe with code tb.inject( """ df = pd.DataFrame([[1, None, 3], [4, 5, 6]], columns=['a', 'b', 'c'], dtype='float') """ ) # Perform manipulation tb.execute_cell('manipulation') # Inject assertion into notebook tb.inject("assert len(df) == 1") testbook-0.4.2/examples/requests-example/000077500000000000000000000000001405576142200204775ustar00rootroot00000000000000testbook-0.4.2/examples/requests-example/requests-example.ipynb000066400000000000000000000014051405576142200250460ustar00rootroot00000000000000{ "cells": [ { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import requests" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "def get_details(url):\n", " return requests.get(url).content" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.7.6" } }, "nbformat": 4, "nbformat_minor": 4 } testbook-0.4.2/examples/requests-example/requests_test.py000066400000000000000000000005061405576142200237640ustar00rootroot00000000000000from testbook import testbook @testbook('./requests-test.ipynb', execute=True) def test_get_details(tb): with tb.patch('requests.get') as mock_get: get_details = tb.ref('get_details') # get reference to function get_details('https://my-api.com') mock_get.assert_called_with('https://my-api.com') testbook-0.4.2/examples/stdout-example/000077500000000000000000000000001405576142200201465ustar00rootroot00000000000000testbook-0.4.2/examples/stdout-example/stdout-assertion-example.ipynb000066400000000000000000000017501405576142200261740ustar00rootroot00000000000000{ "cells": [ { "cell_type": "code", "execution_count": null, "id": "bce1bee1", "metadata": {}, "outputs": [], "source": [ "from datetime import datetime" ] }, { "cell_type": "code", "execution_count": null, "id": "b906f510", "metadata": {}, "outputs": [], "source": [ "print(\"hello world!\")" ] }, { "cell_type": "code", "execution_count": null, "id": "c9fbf668", "metadata": {}, "outputs": [], "source": [ "print(f\"The current time is {datetime.now().strftime('%H:%M:%S')}\")" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.7.6" } }, "nbformat": 4, "nbformat_minor": 5 } testbook-0.4.2/examples/stdout-example/stdout_test.py000066400000000000000000000003361405576142200231030ustar00rootroot00000000000000from testbook import testbook @testbook('stdout-assertion-example.ipynb', execute=True) def test_stdout(tb): assert tb.cell_output_text(1) == 'hello world!' assert 'The current time is' in tb.cell_output_text(2) testbook-0.4.2/pyproject.toml000066400000000000000000000011431405576142200162700ustar00rootroot00000000000000# Example configuration for Black. # NOTE: you have to use single-quoted strings in TOML for regular expressions. # It's the equivalent of r-strings in Python. Multiline strings are treated as # verbose regular expressions by Black. Use [ ] to denote a significant space # character. [tool.black] line-length = 100 include = '\.pyi?$' exclude = ''' /( \.git | \.hg | \.mypy_cache | \.tox | \.venv | _build | buck-out | build | dist # The following are specific to Black, you probably don't want those. | blib2to3 | tests/data | profiling )/ ''' skip-string-normalization = true testbook-0.4.2/pytest.ini000066400000000000000000000000511405576142200154020ustar00rootroot00000000000000[pytest] testpaths = testbook/tests/ testbook-0.4.2/requirements-dev.txt000066400000000000000000000003331405576142200174140ustar00rootroot00000000000000codecov coverage ipython ipykernel ipywidgets pandas pytest>=4.1 pytest-cov>=2.6.1 check-manifest flake8 tox bumpversion xmltodict black; python_version >= '3.6' pip>=18.1 wheel>=0.31.0 setuptools>=38.6.0 twine>=1.11.0 testbook-0.4.2/requirements.txt000066400000000000000000000000401405576142200166330ustar00rootroot00000000000000nbformat>=5.0.4 nbclient>=0.4.0 testbook-0.4.2/setup.cfg000066400000000000000000000015541405576142200152030ustar00rootroot00000000000000 [flake8] # References: # https://flake8.readthedocs.io/en/latest/user/configuration.html # https://flake8.readthedocs.io/en/latest/user/error-codes.html # Note: there cannot be spaces after comma's here exclude = __init__.py ignore = # Extra space in brackets E20, # Multiple spaces around "," E231,E241, # Comments E26, # Import formatting E4, # Comparing types instead of isinstance E721, # Assigning lambda expression E731 max-line-length = 120 [bdist_wheel] universal=0 [coverage:run] branch = False omit = testbook/tests/* testbook/_version.py [coverage:report] exclude_lines = if self\.debug: pragma: no cover raise AssertionError raise NotImplementedError if __name__ == .__main__.: ignore_errors = True omit = testbook/tests/*,testbook/_version.py [tool:pytest] filterwarnings = always testbook-0.4.2/setup.py000066400000000000000000000050221405576142200150660ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- """" setup.py See: https://packaging.python.org/tutorials/packaging-projects/ https://packaging.python.org/en/latest/distributing.html https://github.com/pypa/sampleproject """ import os from setuptools import setup local_path = os.path.dirname(__file__) # Fix for tox which manipulates execution pathing if not local_path: local_path = '.' here = os.path.abspath(local_path) def version(): with open(here + '/testbook/_version.py', 'r') as ver: for line in ver.readlines(): if line.startswith('version ='): return line.split(' = ')[-1].strip()[1:-1] raise ValueError('No version found in testbook/version.py') def read(fname): with open(fname, 'r') as fhandle: return fhandle.read() def read_reqs(fname): req_path = os.path.join(here, fname) return [req.strip() for req in read(req_path).splitlines() if req.strip()] long_description = read(os.path.join(os.path.dirname(__file__), "README.md")) requirements = read(os.path.join(os.path.dirname(__file__), "requirements.txt")) dev_reqs = read_reqs(os.path.join(os.path.dirname(__file__), 'requirements-dev.txt')) doc_reqs = read_reqs(os.path.join(os.path.dirname(__file__), 'docs/requirements-doc.txt')) extras_require = {"test": dev_reqs, "dev": dev_reqs, "sphinx": doc_reqs} setup( name='testbook', version=version(), description='A unit testing framework for Jupyter Notebooks', author='nteract contributors', author_email='nteract@googlegroups.com', license='BSD', # Note that this is a string of words separated by whitespace, not a list. keywords='jupyter mapreduce nteract pipeline notebook', long_description=long_description, long_description_content_type='text/markdown', url='https://github.com/nteract/testbook', packages=['testbook'], python_requires='>=3.6', install_requires=requirements, extras_require=extras_require, project_urls={ 'Documentation': 'https://testbook.readthedocs.io', 'Funding': 'https://nteract.io', 'Source': 'https://github.com/nteract/testbook/', 'Tracker': 'https://github.com/nteract/testbook/issues', }, classifiers=[ 'Intended Audience :: Developers', 'License :: OSI Approved :: BSD License', 'Programming Language :: Python', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', ], ) testbook-0.4.2/testbook/000077500000000000000000000000001405576142200152075ustar00rootroot00000000000000testbook-0.4.2/testbook/__init__.py000066400000000000000000000001141405576142200173140ustar00rootroot00000000000000from ._version import version as __version__ from .testbook import testbook testbook-0.4.2/testbook/_version.py000066400000000000000000000000221405576142200173770ustar00rootroot00000000000000version = '0.4.2' testbook-0.4.2/testbook/client.py000066400000000000000000000264231405576142200170460ustar00rootroot00000000000000from contextlib import contextmanager from inspect import getsource from textwrap import dedent from typing import Any, Dict, List, Optional, Union from nbclient import NotebookClient from nbclient.exceptions import CellExecutionError from nbformat.v4 import new_code_cell from .exceptions import ( TestbookCellTagNotFoundError, TestbookExecuteResultNotFoundError, TestbookSerializeError, TestbookRuntimeError, TestbookError, ) from .reference import TestbookObjectReference from .testbooknode import TestbookNode from .translators import PythonTranslator from .utils import random_varname, all_subclasses class TestbookNotebookClient(NotebookClient): __test__ = False def __init__(self, nb, km=None, **kw): # Fix the ipykernel 5.5 issue where execute requests after errors are aborted ea = kw.get('extra_arguments', []) if not any(arg.startswith('--Kernel.stop_on_error_timeout') for arg in self.extra_arguments): ea.append('--Kernel.stop_on_error_timeout=0') kw['extra_arguments'] = ea super().__init__(nb, km=km, **kw) def ref(self, name: str) -> Union[TestbookObjectReference, Any]: """ Return a reference to an object in the kernel """ # Check if exists self.inject(name, pop=True) try: self.inject(f"import json; json.dumps({name})", pop=True) return self.value(name) except Exception: return TestbookObjectReference(self, name) def get(self, item): return self.ref(item) def __getitem__(self, item): return self.ref(item) @staticmethod def _construct_call_code( func_name: str, args: Optional[List] = None, kwargs: Optional[Dict] = None ) -> str: return """ {func_name}(*{args_list}, **{kwargs_dict}) """.format( func_name=func_name, args_list=PythonTranslator.translate(args) if args else [], kwargs_dict=PythonTranslator.translate(kwargs) if kwargs else {}, ) @property def cells(self): return self.nb.cells @staticmethod def _execute_result(cell) -> List: """ Return data from execute_result outputs """ return [ output["data"] for output in cell["outputs"] if output["output_type"] == 'execute_result' ] @staticmethod def _output_text(cell) -> str: if "outputs" not in cell: raise ValueError("cell must be a code cell") text = '' for output in cell["outputs"]: if 'text' in output: text += output['text'] elif "data" in output and "text/plain" in output["data"]: text += output["data"]["text/plain"] return text.strip() def _cell_index(self, tag: Union[int, str]) -> int: """ Get cell index from the cell tag """ if isinstance(tag, int): return tag elif not isinstance(tag, str): raise TypeError('expected tag as str') for idx, cell in enumerate(self.cells): metadata = cell['metadata'] if "tags" in metadata and tag in metadata['tags']: return idx raise TestbookCellTagNotFoundError("Cell tag '{}' not found".format(tag)) def execute_cell(self, cell, **kwargs) -> Union[Dict, List[Dict]]: """ Executes a cell or list of cells """ if isinstance(cell, slice): start, stop = self._cell_index(cell.start), self._cell_index(cell.stop) if cell.step is not None: raise TestbookError('testbook does not support step argument') cell = range(start, stop + 1) elif isinstance(cell, str) or isinstance(cell, int): cell = [cell] cell_indexes = cell if all(isinstance(x, str) for x in cell): cell_indexes = [self._cell_index(tag) for tag in cell] executed_cells = [] for idx in cell_indexes: try: cell = super().execute_cell(self.nb['cells'][idx], idx, **kwargs) except CellExecutionError as ce: raise TestbookRuntimeError(ce.evalue, ce, self._get_error_class(ce.ename)) executed_cells.append(cell) return executed_cells[0] if len(executed_cells) == 1 else executed_cells def execute(self) -> None: """ Executes all cells """ for index, cell in enumerate(self.nb.cells): super().execute_cell(cell, index) def cell_output_text(self, cell) -> str: """ Return cell text output """ cell_index = self._cell_index(cell) return self._output_text(self.nb['cells'][cell_index]) def cell_execute_result(self, cell: Union[int, str]) -> List[Dict[str, Any]]: """Return the execute results of cell at a given index or with a given tag. Each result is expressed with a dictionary for which the key is the mimetype of the data. A same result can have different representation corresponding to different mimetype. Parameters ---------- cell : int or str The index or tag to look for Returns ------- List[Dict[str, Any]] The execute results Raises ------ IndexError If index is invalid TestbookCellTagNotFoundError If tag is not found """ cell_index = self._cell_index(cell) return self._execute_result(self.nb['cells'][cell_index]) def inject( self, code: str, args: List = None, kwargs: Dict = None, run: bool = True, before: Optional[Union[str, int]] = None, after: Optional[Union[str, int]] = None, pop: bool = False, ) -> TestbookNode: """Injects and executes given code block Parameters ---------- code : str Code or function to be injected args : iterable, optional tuple of arguments to be passed to the function kwargs : dict, optional dict of keyword arguments to be passed to the function run : bool, optional Control immediate execution after injection (default is True) before, after : int, str, optional Inject code before or after cell pop : bool Pop cell after execution (default is False) Returns ------- TestbookNode Injected cell """ if isinstance(code, str): lines = dedent(code) elif callable(code): lines = getsource(code) + ( dedent(self._construct_call_code(code.__name__, args, kwargs)) if run else '' ) else: raise TypeError('can only inject function or code block as str') inject_idx = len(self.cells) if after is not None and before is not None: raise ValueError("pass either before or after as kwargs") elif before is not None: inject_idx = self._cell_index(before) elif after is not None: inject_idx = self._cell_index(after) + 1 code_cell = new_code_cell(lines) self.cells.insert(inject_idx, code_cell) cell = TestbookNode(self.execute_cell(inject_idx)) if run else TestbookNode(code_cell) if self._contains_error(cell): eclass = self._get_error_class(cell.get('outputs')[0]['ename']) evalue = cell.get('outputs')[0]['evalue'] raise TestbookRuntimeError(evalue, evalue, eclass) if run and pop: self.cells.pop(inject_idx) return cell def value(self, code: str) -> Any: """ Execute given code in the kernel and return JSON serializeable result. If the result is not JSON serializeable, it raises `TestbookAttributeError`. This error object will also contain an attribute called `save_varname` which can be used to create a reference object with :meth:`ref`. Parameters ---------- code: str This can be any executable code that returns a value. It can be used the return the value of an object, or the output of a function call. Returns ------- The output of the executed code Raises ------ TestbookSerializeError """ result = self.inject(code, pop=True) if not self._execute_result(result): raise TestbookExecuteResultNotFoundError( 'code provided does not produce execute_result' ) save_varname = random_varname() inject_code = f""" import json from IPython import get_ipython from IPython.display import JSON {save_varname} = get_ipython().last_execution_result.result json.dumps({save_varname}) JSON({{"value" : {save_varname}}}) """ try: outputs = self.inject(inject_code, pop=True).outputs if outputs[0].output_type == "error": # will receive error when `allow_errors` is set to True raise TestbookRuntimeError( outputs[0].evalue, outputs[0].traceback, outputs[0].ename ) return outputs[0].data['application/json']['value'] except TestbookRuntimeError: e = TestbookSerializeError('could not JSON serialize output') e.save_varname = save_varname raise e @contextmanager def patch(self, target, **kwargs): """Used as contextmanager to patch objects in the kernel""" mock_object = f'_mock_{random_varname()}' patcher = f'_patcher_{random_varname()}' self.inject( f""" from unittest.mock import patch {patcher} = patch( {PythonTranslator.translate(target)}, **{PythonTranslator.translate(kwargs)} ) {mock_object} = {patcher}.start() """ ) yield TestbookObjectReference(self, mock_object) self.inject(f"{patcher}.stop()") @contextmanager def patch_dict(self, in_dict, values=(), clear=False, **kwargs): """Used as contextmanager to patch dictionaries in the kernel""" mock_object = f'_mock_{random_varname()}' patcher = f'_patcher_{random_varname()}' self.inject( f""" from unittest.mock import patch {patcher} = patch.dict( {PythonTranslator.translate(in_dict)}, {PythonTranslator.translate(values)}, {PythonTranslator.translate(clear)}, **{PythonTranslator.translate(kwargs)} ) {mock_object} = {patcher}.start() """ ) yield TestbookObjectReference(self, mock_object) self.inject(f"{patcher}.stop()") @staticmethod def _get_error_class(ename): eclass = None for klass in all_subclasses(Exception): if klass.__name__ == ename: eclass = klass break return eclass @staticmethod def _contains_error(result): return result.get('outputs') and result.get('outputs')[0].output_type == "error" testbook-0.4.2/testbook/exceptions.py000066400000000000000000000016341405576142200177460ustar00rootroot00000000000000class TestbookError(Exception): """Generic Testbook exception class""" __test__ = False class TestbookCellTagNotFoundError(TestbookError): """Raised when cell tag is not declared in notebook""" pass class TestbookSerializeError(TestbookError): """Raised when output cannot be JSON serialized""" pass class TestbookExecuteResultNotFoundError(TestbookError): """Raised when there is no execute_result""" pass class TestbookAttributeError(AttributeError): __test__ = False class TestbookRuntimeError(RuntimeError): __test__ = False def __init__(self, evalue, traceback, eclass=None): super().__init__(evalue) self.evalue = evalue self.traceback = traceback self.eclass = eclass def __str__(self): # pragma: no cover return str(self.traceback) def __repr__(self): # pragma: no cover return str(self.traceback) testbook-0.4.2/testbook/reference.py000066400000000000000000000057011405576142200175220ustar00rootroot00000000000000from .exceptions import ( TestbookExecuteResultNotFoundError, TestbookAttributeError, TestbookSerializeError, TestbookRuntimeError ) from .utils import random_varname from .translators import PythonTranslator class TestbookObjectReference: def __init__(self, tb, name): self.tb = tb self.name: str = name @property def _type(self): return self.tb.value(f"type({self.name}).__name__") def __repr__(self): return repr(self.tb.value(f"repr({self.name})")) def __getattr__(self, name): if self.tb.value(f"hasattr({self.name}, '{name}')"): return TestbookObjectReference(self.tb, f"{self.name}.{name}") raise TestbookAttributeError(f"'{self._type}' object has no attribute {name}") def __eq__(self, rhs): return self.tb.value( "{lhs} == {rhs}".format(lhs=self.name, rhs=PythonTranslator.translate(rhs)) ) def __len__(self): return self.tb.value(f"len({self.name})") def __iter__(self): iterobjectname = f"___iter_object_{random_varname()}" self.tb.inject(f""" {iterobjectname} = iter({self.name}) """) return TestbookObjectReference(self.tb, iterobjectname) def __next__(self): try: return self.tb.value(f"next({self.name})") except TestbookRuntimeError as e: if e.eclass is StopIteration: raise StopIteration else: raise def __getitem__(self, key): try: return self.tb.value(f"{self.name}.__getitem__({PythonTranslator.translate(key)})") except TestbookRuntimeError as e: if e.eclass is TypeError: raise TypeError(e.evalue) elif e.eclass is IndexError: raise IndexError(e.evalue) else: raise def __setitem__(self, key, value): try: return self.tb.inject("{name}[{key}] = {value}".format( name=self.name, key=PythonTranslator.translate(key), value=PythonTranslator.translate(value) ), pop=True) except TestbookRuntimeError as e: if e.eclass is TypeError: raise TypeError(e.evalue) elif e.eclass is IndexError: raise IndexError(e.evalue) else: raise def __contains__(self, item): return self.tb.value(f"{self.name}.__contains__({PythonTranslator.translate(item)})") def __call__(self, *args, **kwargs): code = self.tb._construct_call_code(self.name, args, kwargs) try: return self.tb.value(code) except TestbookExecuteResultNotFoundError: # No return value from function call pass except TestbookSerializeError as e: return TestbookObjectReference(self.tb, e.save_varname) def resolve(self): return self.tb.value(self.name) testbook-0.4.2/testbook/testbook.py000066400000000000000000000036711405576142200174220ustar00rootroot00000000000000import functools from unittest.mock import DEFAULT import nbformat from .client import TestbookNotebookClient class testbook: """`testbook` acts as function decorator or a context manager. When the function/with statement exits the kernels started when entering the function/with statement will be terminated. If `testbook` is used as a decorator, the `TestbookNotebookClient` will be passed as first argument to the decorated function. """ # Developer notes: # # To trick pytest, we mimic the API of unittest.mock.patch in testbook. # Notably, the following elements are added: # * attribute_name, Class attribute (see below) # * new, Instance attribute (see __init__) # * patchings, wrapper attributes (see __call__) attribute_name = None def __init__( self, nb, execute=None, timeout=60, kernel_name='python3', allow_errors=False, **kwargs ): self.execute = execute self.client = TestbookNotebookClient( nbformat.read(nb, as_version=4) if not isinstance(nb, nbformat.NotebookNode) else nb, timeout=timeout, allow_errors=allow_errors, kernel_name=kernel_name, **kwargs ) self.new = DEFAULT def _prepare(self): if self.execute is True: self.client.execute() elif self.execute not in [None, False]: self.client.execute_cell(self.execute) def __enter__(self): with self.client.setup_kernel(cleanup_kc=False): self._prepare() return self.client def __exit__(self, *args): self.client._cleanup_kernel() def __call__(self, func): @functools.wraps(func) def wrapper(*args, **kwargs): # pragma: no cover with self.client.setup_kernel(): self._prepare() func(self.client, *args, **kwargs) wrapper.patchings = [self] return wrapper testbook-0.4.2/testbook/testbooknode.py000066400000000000000000000012271405576142200202630ustar00rootroot00000000000000from nbformat import NotebookNode class TestbookNode(NotebookNode): """ Extends `NotebookNode` to perform assertions """ def __init__(self, *args, **kw): super().__init__(*args, **kw) @property def output_text(self): text = '' for output in self['outputs']: if 'text' in output: text += output['text'] return text.strip() @property def execute_result(self): """Return data from execute_result outputs""" return [ output["data"] for output in self["outputs"] if output["output_type"] == 'execute_result' ] testbook-0.4.2/testbook/tests/000077500000000000000000000000001405576142200163515ustar00rootroot00000000000000testbook-0.4.2/testbook/tests/__init__.py000066400000000000000000000000001405576142200204500ustar00rootroot00000000000000testbook-0.4.2/testbook/tests/conftest.py000066400000000000000000000027371405576142200205610ustar00rootroot00000000000000from typing import List, Optional import pytest from jupyter_client import kernelspec from nbformat.notebooknode import NotebookNode from nbformat.v4 import new_notebook, new_code_cell, new_output @pytest.fixture def notebook_factory(): """Pytest fixture to generate a valid notebook.""" def notebook_generator(cells: Optional[List[NotebookNode]] = None) -> NotebookNode: """Generate an executable notebook. The notebook cells are the one passed as arguments or the hard-coded cells if no cells is provided. """ metadata = {} for name in kernelspec.find_kernel_specs(): ks = kernelspec.get_kernel_spec(name) metadata = { 'kernelspec': { 'name': name, 'language': ks.language, 'display_name': ks.display_name, } } break if cells is not None: all_cells = cells else: # Default cells all_cells = [ new_code_cell('a = 2', metadata={"tags": []}), new_code_cell('b=22\nb', metadata={"tags": ["test"]}), new_code_cell( "", metadata={"tags": ["dummy-outputs"]}, outputs=[new_output('execute_result', data={"text/plain": "text"})], ), ] return new_notebook(metadata=metadata, cells=all_cells) return notebook_generator testbook-0.4.2/testbook/tests/resources/000077500000000000000000000000001405576142200203635ustar00rootroot00000000000000testbook-0.4.2/testbook/tests/resources/datamodel.ipynb000066400000000000000000000013751405576142200233660ustar00rootroot00000000000000{ "cells": [ { "cell_type": "code", "execution_count": 1, "id": "political-plaintiff", "metadata": {}, "outputs": [], "source": [ "mylist = [1, 2, 3, 4, 5]" ] }, { "cell_type": "code", "execution_count": null, "id": "postal-contemporary", "metadata": {}, "outputs": [], "source": [] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.8.6" } }, "nbformat": 4, "nbformat_minor": 5 } testbook-0.4.2/testbook/tests/resources/exception.ipynb000066400000000000000000000014411405576142200234240ustar00rootroot00000000000000{ "cells": [ { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "def raise_my_exception():\n", " class MyException(Exception):\n", " pass\n", " raise MyException" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.7.7" } }, "nbformat": 4, "nbformat_minor": 4 } testbook-0.4.2/testbook/tests/resources/foo.ipynb000066400000000000000000000030061405576142200222100ustar00rootroot00000000000000{ "cells": [ { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "1 + 1" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "tags": [ "test1" ] }, "outputs": [], "source": [ "print('hello world')\n", "\n", "print([1, 2, 3])" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "tags": [ "prepare_foo" ] }, "outputs": [], "source": [ "def foo():\n", " print('foo')" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "tags": [ "execute_foo" ] }, "outputs": [], "source": [ "foo()" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import pandas as pd\n", "d = {'col1': [1, 2], 'col2': [3, 4]}\n", "df = pd.DataFrame(data=d)\n", "df" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [] } ], "metadata": { "celltoolbar": "Tags", "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.7.7" } }, "nbformat": 4, "nbformat_minor": 2 } testbook-0.4.2/testbook/tests/resources/inject.ipynb000066400000000000000000000037671405576142200227170ustar00rootroot00000000000000{ "cells": [ { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "foo = 'Hello'\n", "bar = 'World'" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "tags": [ "hello" ] }, "outputs": [], "source": [ "def say_hello():\n", " print(\"Hello there\")" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "tags": [ "bye" ] }, "outputs": [], "source": [ "def say_bye():\n", " print(\"Bye\")" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "tags": [ "dict" ] }, "outputs": [], "source": [ "sample_dict = {'foo' : 'bar'}" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "tags": [ "list" ] }, "outputs": [], "source": [ "sample_list = ['foo', 'bar']" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "tags": [ "int" ] }, "outputs": [], "source": [ "sample_int = 42" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "tags": [ "str" ] }, "outputs": [], "source": [ "sample_str = 'hello world'" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "sample_list + [1]" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [] } ], "metadata": { "celltoolbar": "Tags", "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.8.1" } }, "nbformat": 4, "nbformat_minor": 2 } testbook-0.4.2/testbook/tests/resources/patch.ipynb000066400000000000000000000024161405576142200225300ustar00rootroot00000000000000{ "cells": [ { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import os\n", "import subprocess" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "def listdir():\n", " return os.listdir()" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "def get_branch():\n", " return os.popen('git rev-parse --abbrev-ref HEAD').read().strip()" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "def env():\n", " return os.environ" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "foo = {}" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.7.7" } }, "nbformat": 4, "nbformat_minor": 4 } testbook-0.4.2/testbook/tests/resources/reference.ipynb000066400000000000000000000026271405576142200233730ustar00rootroot00000000000000{ "cells": [ { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [], "source": [ "a = [1, 2, 3]" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "b = [1, 2, 3]" ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [], "source": [ "def double(ll):\n", " return [_*2 for _ in ll]" ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [], "source": [ "class Foo:\n", " def __init__(self, name):\n", " self.name = name\n", " \n", " def __repr__(self):\n", " return f\"\"\n", " \n", " def say_hello(self):\n", " return f\"Hello {self.name}!\"" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [] } ], "metadata": { "celltoolbar": "Tags", "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.7.7" } }, "nbformat": 4, "nbformat_minor": 2 } testbook-0.4.2/testbook/tests/test_client.py000066400000000000000000000070251405576142200212440ustar00rootroot00000000000000import pytest from textwrap import dedent from ..testbook import testbook from ..client import TestbookNotebookClient from ..exceptions import TestbookCellTagNotFoundError, TestbookExecuteResultNotFoundError @pytest.fixture(scope='module') def notebook(): with testbook('testbook/tests/resources/inject.ipynb', execute=True) as tb: yield tb @pytest.mark.parametrize("cell_index_args, expected_result", [(2, 2), ('hello', 1)]) def test_cell_index(cell_index_args, expected_result, notebook): assert notebook._cell_index(cell_index_args) == expected_result @pytest.mark.parametrize( "cell_index_args, expected_error", [([1, 2, 3], TypeError), ('non-existent-tag', TestbookCellTagNotFoundError)], ) def test_cell_index_raises_error(cell_index_args, expected_error, notebook): with pytest.raises(expected_error): notebook._cell_index(cell_index_args) @pytest.mark.parametrize( "var_name, expected_result", [ ('sample_dict', {'foo': 'bar'}), ('sample_list', ['foo', 'bar']), ('sample_list + ["hello world"]', ['foo', 'bar', 'hello world']), ('sample_int', 42), ('sample_int * 2', 84), ('sample_str', 'hello world'), ('sample_str + " foo"', 'hello world foo'), ], ) def test_value(var_name, expected_result, notebook): assert notebook.value(var_name) == expected_result @pytest.mark.parametrize("code", [('sample_int *= 2'), ('print(sample_int)'), ('')]) def test_value_raises_error(code, notebook): with pytest.raises(TestbookExecuteResultNotFoundError): notebook.value(code) @pytest.mark.parametrize( "cell, expected_result", [ ( { "cell_type": "code", "execution_count": 9, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": "hello world\n" "foo\n" "bar\n", } ], }, """ hello world foo bar """, ), ({"cell_type": "code", "execution_count": 9, "metadata": {}, "outputs": []}, ""), ], ) def test_output_text(cell, expected_result): assert TestbookNotebookClient._output_text(cell) == dedent(expected_result).strip() @pytest.mark.parametrize( "cell", [{}, {"cell_type": "markdown", "metadata": {}, "source": ["# Hello"]}] ) def test_output_text_raises_error(cell): with pytest.raises(ValueError): assert TestbookNotebookClient._output_text(cell) def test_cell_execute_result_index(notebook_factory): nb = notebook_factory() with testbook(nb, execute="test") as tb: assert tb.cell_execute_result(1) == [{"text/plain": "22"}] assert tb.cell_execute_result(2) == [{"text/plain": "text"}] def test_cell_execute_result_tag(notebook_factory): nb = notebook_factory() with testbook(nb, execute="test") as tb: assert tb.cell_execute_result("test") == [{"text/plain": "22"}] assert tb.cell_execute_result("dummy-outputs") == [{"text/plain": "text"}] def test_cell_execute_result_indexerror(notebook_factory): nb = notebook_factory([]) with testbook(nb) as tb: with pytest.raises(IndexError): tb.cell_execute_result(1) def test_cell_execute_result_tagnotfound(notebook_factory): nb = notebook_factory([]) with testbook(nb) as tb: with pytest.raises(TestbookCellTagNotFoundError): tb.cell_execute_result("test") testbook-0.4.2/testbook/tests/test_datamodel.py000066400000000000000000000033331405576142200217160ustar00rootroot00000000000000import pytest from ..testbook import testbook @pytest.fixture(scope='module') def notebook(): with testbook('testbook/tests/resources/datamodel.ipynb', execute=True) as tb: yield tb def test_len(notebook): mylist = notebook.ref("mylist") assert len(mylist) == 5 def test_iter(notebook): mylist = notebook.ref("mylist") expected = [] for x in mylist: expected.append(x) assert mylist == expected def test_getitem(notebook): mylist = notebook.ref("mylist") mylist.append(6) assert mylist[-1] == 6 assert mylist.__getitem__(-1) == 6 def test_getitem_raisesIndexError(notebook): mylist = notebook.ref("mylist") with pytest.raises(IndexError): mylist[100] def test_getitem_raisesTypeError(notebook): mylist = notebook.ref("mylist") with pytest.raises(TypeError): mylist['hello'] def test_setitem(notebook): notebook.inject("mydict = {'key1': 'value1', 'key2': 'value1'}") mydict = notebook.ref("mydict") mydict['key3'] = 'value3' assert mydict['key3'] == 'value3' mylist = notebook.ref("mylist") mylist[2] = 10 assert mylist[2] == 10 def test_setitem_raisesIndexError(notebook): mylist = notebook.ref("mylist") with pytest.raises(IndexError): mylist.__setitem__(10, 100) def test_setitem_raisesTypeError(notebook): mylist = notebook.ref("mylist") with pytest.raises(TypeError): mylist.__setitem__('key', 10) def test_contains(notebook): notebook.inject("mydict = {'key1': 'value1', 'key2': 'value1'}") mydict = notebook.ref("mydict") assert 'key1' in mydict assert 'key2' in mydict assert mydict.__contains__('key1') assert mydict.__contains__('key2') testbook-0.4.2/testbook/tests/test_execute.py000066400000000000000000000055141405576142200214310ustar00rootroot00000000000000import pytest from ..testbook import testbook from ..exceptions import TestbookRuntimeError, TestbookError @pytest.fixture(scope='module') def notebook(): with testbook('testbook/tests/resources/foo.ipynb', execute=True) as tb: yield tb def test_execute_cell(notebook): notebook.execute_cell(1) assert notebook.cell_output_text(1) == 'hello world\n[1, 2, 3]' notebook.execute_cell([2, 3]) assert notebook.cell_output_text(3) == 'foo' def test_execute_and_show_pandas_output(notebook): notebook.execute_cell(4) assert notebook.cell_output_text(4) == """col1 col2 0 1 3 1 2 4""" def test_execute_cell_tags(notebook): notebook.execute_cell('test1') assert notebook.cell_output_text('test1') == 'hello world\n[1, 2, 3]' notebook.execute_cell(['prepare_foo', 'execute_foo']) assert notebook.cell_output_text('execute_foo') == 'foo' def test_execute_cell_raises_error(notebook): with pytest.raises(TestbookRuntimeError): try: notebook.inject("1/0", pop=True) except TestbookRuntimeError as e: assert e.eclass == ZeroDivisionError raise def test_testbook_with_execute(notebook): notebook.execute_cell('execute_foo') assert notebook.cell_output_text('execute_foo') == 'foo' def test_testbook_with_execute_context_manager(notebook): notebook.execute_cell('execute_foo') assert notebook.cell_output_text('execute_foo') == 'foo' def test_testbook_range(): with testbook('testbook/tests/resources/inject.ipynb') as tb: tb.execute_cell(range(4)) assert tb.code_cells_executed == 4 with testbook('testbook/tests/resources/inject.ipynb', execute=range(4)) as tb: assert tb.code_cells_executed == 4 @pytest.mark.parametrize("slice_params, expected_result", [(('hello', 'str'), 6), ((2, 5), 4)]) def test_testbook_slice(slice_params, expected_result): with testbook('testbook/tests/resources/inject.ipynb') as tb: tb.execute_cell(slice(*slice_params)) assert tb.code_cells_executed == expected_result with testbook('testbook/tests/resources/inject.ipynb', execute=slice(*slice_params)) as tb: assert tb.code_cells_executed == expected_result def test_testbook_slice_raises_error(): with pytest.raises(TestbookError): with testbook('testbook/tests/resources/inject.ipynb', execute=slice(3, 1, -1)): pass @testbook('testbook/tests/resources/exception.ipynb', execute=True) def test_raise_exception(tb): with pytest.raises(TestbookRuntimeError): tb.ref("raise_my_exception")() @testbook('testbook/tests/resources/inject.ipynb') def test_underscore(tb): tb.inject( """ _ = 20 def foo(x): return x + 1 """, run=False, ) tb.execute() foo = tb.ref("foo") assert foo(2) == 3 testbook-0.4.2/testbook/tests/test_inject.py000066400000000000000000000041471405576142200212440ustar00rootroot00000000000000import pytest from ..testbook import testbook @pytest.fixture(scope='module') def notebook(): with testbook('testbook/tests/resources/inject.ipynb', execute=True) as tb: yield tb def inject_helper(*args, **kwargs): pass @pytest.mark.parametrize( "args, kwargs", [ (None, None), ([1, 2], None), ((1, 2), None), ((True, False), None), (['a', 'b'], None), ([1.1, float('nan'), float('inf'), float('-inf')], None), ([{'key1': 'value1'}, {'key2': 'value2'}], None), ((1, 2, False), {'key2': 'value2'}), ((None, None, False), {'key2': 'value2'}), ], ) def test_inject(args, kwargs, notebook): assert notebook.inject(inject_helper, args=args, kwargs=kwargs, pop=True) @pytest.mark.parametrize( "code_block, expected_text", [ ( ''' def foo(): print('I ran in the code block') foo() ''', "I ran in the code block", ), ( ''' def foo(arg): print(f'You passed {arg}') foo('bar') ''', "You passed bar", ), ], ) def test_inject_code_block(code_block, expected_text, notebook): assert notebook.inject(code_block, pop=True).output_text == expected_text def test_inject_raises_exception(notebook): values = [3, {'key': 'value'}, ['a', 'b', 'c'], (1, 2, 3), {1, 2, 3}] for value in values: with pytest.raises(TypeError): notebook.inject(value) def test_inject_before_after(notebook): notebook.inject("say_hello()", run=False, after="hello") assert notebook.cells[notebook._cell_index("hello") + 1].source == "say_hello()" notebook.inject("say_bye()", before="hello") assert notebook.cells[notebook._cell_index("hello") - 1].source == "say_bye()" with pytest.raises(ValueError): notebook.inject("say_hello()", before="hello", after="bye") def test_inject_pop(notebook): assert notebook.inject("1+1", pop=True).execute_result == [{'text/plain': '2'}] assert notebook.cells[-1].source != "1+1" testbook-0.4.2/testbook/tests/test_patch.py000066400000000000000000000024771405576142200210730ustar00rootroot00000000000000from ..testbook import testbook from ..exceptions import TestbookRuntimeError import pytest @pytest.fixture(scope='module') def tb(): with testbook('testbook/tests/resources/patch.ipynb', execute=True) as tb: yield tb class TestPatch: @pytest.mark.parametrize( "target, func", [("os.listdir", "listdir"), ("os.popen", "get_branch")] ) def test_patch_basic(self, target, func, tb): with tb.patch(target) as mock_obj: tb.ref(func)() mock_obj.assert_called_once() @pytest.mark.parametrize( "target, func", [("os.listdir", "listdir"), ("os.popen", "get_branch")] ) def test_patch_raises_error(self, target, func, tb): with pytest.raises(TestbookRuntimeError), tb.patch(target) as mock_obj: mock_obj.assert_called_once() def test_patch_return_value(self, tb): with tb.patch("os.listdir", return_value=['file1', 'file2']) as mock_listdir: assert tb.ref("listdir")() == ['file1', 'file2'] mock_listdir.assert_called_once() class TestPatchDict: @pytest.mark.parametrize( "in_dict, values", [("os.environ", {"PATH": "/usr/bin"})], ) def test_patch_dict(self, in_dict, values, tb): with tb.patch_dict(in_dict, values, clear=True): assert tb.ref(in_dict) == values testbook-0.4.2/testbook/tests/test_reference.py000066400000000000000000000030761405576142200217260ustar00rootroot00000000000000import pytest from ..testbook import testbook from ..exceptions import TestbookAttributeError, TestbookSerializeError @pytest.fixture(scope='module') def notebook(): with testbook('testbook/tests/resources/reference.ipynb', execute=True) as tb: yield tb def test_create_reference(notebook): a = notebook.ref("a") assert repr(a) == "[1, 2, 3]" def test_create_reference_getitem(notebook): a = notebook["a"] assert repr(a) == "[1, 2, 3]" def test_create_reference_get(notebook): a = notebook.get("a") assert repr(a) == "[1, 2, 3]" def test_eq_in_notebook(notebook): a = notebook.ref("a") a.append(4) assert a == [1, 2, 3, 4] def test_eq_in_notebook_ref(notebook): a, b = notebook.ref("a"), notebook.ref("b") assert a == b def test_function_call(notebook): double = notebook.ref("double") assert double([1, 2, 3]) == [2, 4, 6] def test_function_call_with_ref_object(notebook): double, a = notebook.ref("double"), notebook.ref("a") assert double(a) == [2, 4, 6] def test_reference(notebook): Foo = notebook.ref("Foo") # Check that when a non-serializeable object is returned, it returns # a reference to that object instead f = Foo('bar') assert repr(f) == "\"\"" # Valid attribute access assert f.say_hello() # Invalid attribute access with pytest.raises(TestbookAttributeError): f.does_not_exist assert f.say_hello() == 'Hello bar!' # non JSON-serializeable output with pytest.raises(TestbookSerializeError): f.resolve() testbook-0.4.2/testbook/tests/test_testbook.py000066400000000000000000000043571405576142200216250ustar00rootroot00000000000000import nbformat import pytest from ..testbook import testbook @testbook('testbook/tests/resources/inject.ipynb', execute=True) def test_testbook_execute_all_cells(tb): for cell in tb.cells[:-1]: assert cell.execution_count @testbook('testbook/tests/resources/inject.ipynb', execute='hello') def test_testbook_class_decorator(tb): assert tb.inject("say_hello()") @testbook('testbook/tests/resources/inject.ipynb') def test_testbook_class_decorator_execute_none(tb): assert tb.code_cells_executed == 0 @testbook('testbook/tests/resources/inject.ipynb', execute=True) def test_testbook_decorator_with_fixture(nb, tmp_path): assert True # Check that the decorator accept to be passed along side a fixture @testbook('testbook/tests/resources/inject.ipynb', execute=True) @pytest.mark.parametrize("cell_index_args, expected_result", [(2, 2), ('hello', 1)]) def test_testbook_decorator_with_markers(nb, cell_index_args, expected_result): assert nb._cell_index(cell_index_args) == expected_result @pytest.mark.parametrize("cell_index_args, expected_result", [(2, 2), ('hello', 1)]) @testbook('testbook/tests/resources/inject.ipynb', execute=True) def test_testbook_decorator_with_markers_order_does_not_matter(nb, cell_index_args, expected_result): assert nb._cell_index(cell_index_args) == expected_result def test_testbook_execute_all_cells_context_manager(): with testbook('testbook/tests/resources/inject.ipynb', execute=True) as tb: for cell in tb.cells[:-1]: assert cell.execution_count def test_testbook_class_decorator_context_manager(): with testbook('testbook/tests/resources/inject.ipynb', execute='hello') as tb: assert tb.inject("say_hello()") def test_testbook_class_decorator_execute_none_context_manager(): with testbook('testbook/tests/resources/inject.ipynb') as tb: assert tb.code_cells_executed == 0 def test_testbook_with_file_object(): f = open('testbook/tests/resources/inject.ipynb') with testbook(f) as tb: assert tb.code_cells_executed == 0 f.close() def test_testbook_with_notebook_node(): nb = nbformat.read('testbook/tests/resources/inject.ipynb', as_version=4) with testbook(nb) as tb: assert tb.code_cells_executed == 0 testbook-0.4.2/testbook/tests/test_translators.py000066400000000000000000000032701405576142200223400ustar00rootroot00000000000000"""Sourced from https://github.com/nteract/papermill/blob/master/papermill/tests/test_translators.py""" import pytest from .. import translators class Foo: def __init__(self, val): self.val = val def __repr__(self): return "".format(val=self.val) @pytest.mark.parametrize( "test_input,expected", [ ("foo", '"foo"'), ('{"foo": "bar"}', '"{\\"foo\\": \\"bar\\"}"'), ({"foo": "bar"}, '{"foo": "bar"}'), ({"foo": '"bar"'}, '{"foo": "\\"bar\\""}'), ({"foo": ["bar"]}, '{"foo": ["bar"]}'), ({"foo": {"bar": "baz"}}, '{"foo": {"bar": "baz"}}'), ({"foo": {"bar": '"baz"'}}, '{"foo": {"bar": "\\"baz\\""}}'), (["foo"], '["foo"]'), (["foo", '"bar"'], '["foo", "\\"bar\\""]'), ([{"foo": "bar"}], '[{"foo": "bar"}]'), ([{"foo": '"bar"'}], '[{"foo": "\\"bar\\""}]'), (12345, '12345'), (-54321, '-54321'), (1.2345, '1.2345'), (-5432.1, '-5432.1'), (float('nan'), "float('nan')"), (float('-inf'), "float('-inf')"), (float('inf'), "float('inf')"), (True, 'True'), (False, 'False'), (None, 'None'), (Foo('bar'), '''""'''), ], ) def test_translate_type_python(test_input, expected): assert translators.PythonTranslator.translate(test_input) == expected @pytest.mark.parametrize("test_input,expected", [(3.14, "3.14"), (False, "false"), (True, "true")]) def test_translate_float(test_input, expected): assert translators.Translator.translate(test_input) == expected def test_translate_assign(): assert translators.Translator.assign('var1', [1, 2, 3]) == "var1 = [1, 2, 3]" testbook-0.4.2/testbook/translators.py000066400000000000000000000074161405576142200201450ustar00rootroot00000000000000"""Sourced from https://github.com/nteract/papermill/blob/master/papermill/translators.py""" import math import sys class Translator(object): @classmethod def translate_raw_str(cls, val): """Reusable by most interpreters""" return '{}'.format(val) @classmethod def translate_escaped_str(cls, str_val): """Reusable by most interpreters""" if isinstance(str_val, str): str_val = str_val.encode('unicode_escape') if sys.version_info >= (3, 0): str_val = str_val.decode('utf-8') str_val = str_val.replace('"', r'\"') return '"{}"'.format(str_val) @classmethod def translate_str(cls, val): """Default behavior for translation""" return cls.translate_escaped_str(val) @classmethod def translate_none(cls, val): """Default behavior for translation""" return cls.translate_raw_str(val) @classmethod def translate_int(cls, val): """Default behavior for translation""" return cls.translate_raw_str(val) @classmethod def translate_float(cls, val): """Default behavior for translation""" return cls.translate_raw_str(val) @classmethod def translate_bool(cls, val): """Default behavior for translation""" return 'true' if val else 'false' @classmethod def translate_dict(cls, val): raise NotImplementedError('dict type translation not implemented for {}'.format(cls)) @classmethod def translate_list(cls, val): raise NotImplementedError('list type translation not implemented for {}'.format(cls)) @classmethod def translate(cls, val): """Translate each of the standard json/yaml types to appropriate objects.""" if val is None: return cls.translate_none(val) elif isinstance(val, str): return cls.translate_str(val) # Needs to be before integer checks elif isinstance(val, bool): return cls.translate_bool(val) elif isinstance(val, int): return cls.translate_int(val) elif isinstance(val, float): return cls.translate_float(val) elif isinstance(val, dict): return cls.translate_dict(val) elif isinstance(val, list): return cls.translate_list(val) elif isinstance(val, tuple): return cls.translate_tuple(val) elif val.__class__.__name__ == "TestbookObjectReference": return val.name # Use this generic translation as a last resort return cls.translate_escaped_str(val) @classmethod def comment(cls, cmt_str): raise NotImplementedError('comment translation not implemented for {}'.format(cls)) @classmethod def assign(cls, name, str_val): return '{} = {}'.format(name, str_val) class PythonTranslator(Translator): @classmethod def translate_float(cls, val): if math.isfinite(val): return cls.translate_raw_str(val) elif math.isnan(val): return "float('nan')" elif val < 0: return "float('-inf')" else: return "float('inf')" @classmethod def translate_bool(cls, val): return cls.translate_raw_str(val) @classmethod def translate_dict(cls, val): escaped = ', '.join( ["{}: {}".format(cls.translate_str(k), cls.translate(v)) for k, v in val.items()] ) return '{{{}}}'.format(escaped) @classmethod def translate_list(cls, val): escaped = ', '.join([cls.translate(v) for v in val]) return '[{}]'.format(escaped) @classmethod def translate_tuple(cls, val): escaped = ', '.join([cls.translate(v) for v in val]) + ', ' return '({})'.format(escaped) testbook-0.4.2/testbook/utils.py000066400000000000000000000015101405576142200167160ustar00rootroot00000000000000import random import string def random_varname(length=10): """ Creates a random variable name as string of a given length. This is used in testbook to generate temporary variables within the notebook. Parameters ---------- length (int) Returns: -------- random variable name as string of given length """ return ''.join(random.choice(string.ascii_lowercase) for _ in range(length)) def all_subclasses(klass): """ This is a function that returns a generator. Inspects subclasses associated with a given class in a recursive manner and yields them, such that subclasses of subclasses will be yielded. Parameters: ----------- klass """ for subklass in klass.__subclasses__(): yield subklass yield from all_subclasses(subklass) testbook-0.4.2/tox.ini000066400000000000000000000032661405576142200146770ustar00rootroot00000000000000[tox] skipsdist = true envlist = py{36,37,38}, flake8, dist, manifest, docs [gh-actions] python = 3.6: py36 3.7: py37 3.8: py38, flake8, dist, manifest # Linters [testenv:flake8] skip_install = true deps = flake8 commands = flake8 testbook --count --ignore=E203,E731,F811,W503 --max-complexity=23 --max-line-length=104 --show-source --statistics # Manifest [testenv:manifest] skip_install = true deps = check-manifest commands = check-manifest # Docs [testenv:docs] description = invoke sphinx-build to build the HTML docs skip_install = true deps = .[sphinx] extras = docs commands = sphinx-build -d "{toxworkdir}/docs_doctree" docs "{toxworkdir}/docs_out" --color -W -bhtml {posargs} python -c 'import pathlib; print("documentation available under file://\{0\}".format(pathlib.Path(r"{toxworkdir}") / "docs_out" / "index.html"))' python "{toxinidir}/docs/conf.py" # Distro [testenv:dist] skip_install = true # Have to use /bin/bash or the `*` will cause that argument to get quoted by the tox command line... commands = python setup.py bdist_wheel --dist-dir={distdir} /bin/bash -c 'python -m pip install -U --force-reinstall {distdir}/testbook*.whl' # Black [testenv:black] description = apply black linter with desired rules basepython = python3.6 deps = black commands = black . [testenv] # disable Python's hash randomization for tests that stringify dicts, etc setenv = PYTHONHASHSEED = 0 passenv = * basepython = py36: python3.6 py37: python3.7 py38: python3.8 flake8: python3.8 manifest: python3.8 dist: python3.8 docs: python3.8 deps = .[dev] commands = pytest -vv --maxfail=2 --cov=testbook --cov-report=xml -W always {posargs}