diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 6f4d1e7..de48645 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -4,4 +4,4 @@ commit = True tag = True tag_name = {new_version} -[bumpversion:file:testbook/_version.py] +[bumpversion:file:pyproject.toml] diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 0a09d68..918bd9f 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -9,31 +9,27 @@ on: jobs: build-n-test-n-coverage: name: Build, test and code coverage - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 strategy: fail-fast: false matrix: - python-version: [3.6, 3.7, 3.8, 3.9, "3.10.0-rc.2"] - env: - OS: ubuntu-latest - PYTHON: "3.8" - + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | python -m pip install --upgrade pip - pip install -e .[test] + pip install .[dev] pip install tox-gh-actions - name: Run the tests run: tox - name: Upload coverage to Codecov - uses: codecov/codecov-action@v1 + uses: codecov/codecov-action@v4 with: file: ./coverage.xml flags: unittests diff --git a/.readthedocs.yml b/.readthedocs.yml index 92a1198..96afe15 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -14,9 +14,9 @@ build: image: latest python: - version: 3.7 + version: 3.11 install: - method: pip path: . extra_requirements: - - sphinx + - docs diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4553c6f..f80a54d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -48,13 +48,13 @@ If you are contributing with documentation please jump to [building documentatio 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`): +For the most basic test runs against python 3.11 use this tox subset (callable after `pip install tox`): ```bash -tox -e py36 +tox -e py311 ``` -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. +This will just execute the unittests against python 3.11 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 @@ -62,7 +62,7 @@ For a full test suite of all envs and linting checks simply run tox without any tox ``` -This will require python3.5, python3.6, python3.7, and python 3.8 to be installed. +This will require 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. diff --git a/LICENSE b/LICENSE index 596cc60..98d5d22 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ BSD 3-Clause License -Copyright (c) 2020, nteract +Copyright (c) 2024, nteract All rights reserved. Redistribution and use in source and binary forms, with or without diff --git a/MANIFEST.in b/MANIFEST.in index 7c57d42..34c23ba 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -5,8 +5,6 @@ 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 diff --git a/README.md b/README.md index f0d2e07..2df2ce1 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,9 @@ [![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/) -[![Python 3.9](https://img.shields.io/badge/python-3.9-blue.svg)](https://www.python.org/downloads/release/python-390/) +[![image](https://img.shields.io/pypi/v/testbook.svg)](https://pypi.python.org/pypi/testbook) +[![image](https://img.shields.io/pypi/l/testbook.svg)](https://github.com/astral-sh/testbook/blob/main/LICENSE) +[![image](https://img.shields.io/pypi/pyversions/testbook.svg)](https://pypi.python.org/pypi/testbook) [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/ambv/black) # testbook diff --git a/RELEASING.md b/RELEASING.md index 79db022..a991b5b 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -3,7 +3,7 @@ ## 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` +- Ensure dev requirements are installed `pip install ".[dev]"` ## Push to GitHub @@ -19,6 +19,6 @@ git push upstream && git push upstream --tags ```bash rm -rf dist/* rm -rf build/* -python setup.py bdist_wheel +python -m build twine upload dist/* ``` diff --git a/docs/conf.py b/docs/conf.py index c64df53..f643a14 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -12,6 +12,8 @@ # import os import sys +from importlib.metadata import version as read_version + sys.path.insert(0, os.path.abspath('..')) @@ -19,7 +21,7 @@ # -- Project information ----------------------------------------------------- project = 'testbook' -copyright = '2020, nteract team' +copyright = '2024, nteract team' author = 'nteract team' @@ -49,27 +51,27 @@ # General information about the project. project = 'testbook' -copyright = '2020, nteract team' +copyright = '2024, 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]) +version = '.'.join(read_version(project).split('.')[0:2]) # The full version, including alpha/beta/rc tags. -release = testbook.__version__ +release = read_version(project) # 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 +language = 'en' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. @@ -96,10 +98,10 @@ # html_theme_options = { - "path_to_docs": "docs", - "repository_url": "https://github.com/nteract/testbook", - "repository_branch": "main", - "use_edit_page_button": True, + '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, @@ -107,14 +109,7 @@ # 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" +html_title = 'testbook' # -- Options for HTMLHelp output ------------------------------------------ @@ -142,7 +137,9 @@ # 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')] +latex_documents = [ + (master_doc, 'testbook.tex', 'testbook Documentation', 'nteract team', 'manual') +] # -- Options for manual page output --------------------------------------- @@ -170,4 +167,7 @@ ] # Example configuration for intersphinx: refer to the Python standard library. -intersphinx_mapping = {'https://docs.python.org/': None} +intersphinx_mapping = {'python': ('https://docs.python.org/', None)} + +# Generate heading anchors for h1, h2 and h3. +myst_heading_anchors = 3 diff --git a/docs/index.md b/docs/index.md index da31548..a4572d3 100644 --- a/docs/index.md +++ b/docs/index.md @@ -5,9 +5,9 @@ [![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/) +[![image](https://img.shields.io/pypi/v/testbook.svg)](https://pypi.python.org/pypi/testbook) +[![image](https://img.shields.io/pypi/l/testbook.svg)](https://github.com/astral-sh/testbook/blob/main/LICENSE) +[![image](https://img.shields.io/pypi/pyversions/testbook.svg)](https://pypi.python.org/pypi/testbook) [![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. @@ -41,9 +41,9 @@ def test_func(tb): ## 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) +- [Execute all or some specific cells before unit test](usage/index.md#using-execute-to-control-which-cells-are-executed-before-test) +- [Share kernel context across multiple tests](usage/index.md#share-kernel-context-across-multiple-tests) (using pytest fixtures) +- [Support for patching objects](usage/index.md#support-for-patching-objects) - Inject code into Jupyter notebooks - Works with any unit testing library - unittest, pytest or nose diff --git a/docs/requirements-doc.txt b/docs/requirements-doc.txt deleted file mode 100644 index 7ee0a03..0000000 --- a/docs/requirements-doc.txt +++ /dev/null @@ -1,3 +0,0 @@ -Sphinx>=1.7,<3.0 -sphinx_book_theme==0.0.35 -myst-parser==0.9.1 diff --git a/examples/dataframe-example/dataframe_test.py b/examples/dataframe-example/dataframe_test.py index 66e68b3..4c10181 100644 --- a/examples/dataframe-example/dataframe_test.py +++ b/examples/dataframe-example/dataframe_test.py @@ -1,5 +1,6 @@ from testbook import testbook + @testbook('./dataframe-assertion-example.ipynb') def test_dataframe_manipulation(tb): tb.execute_cell('imports') @@ -15,4 +16,4 @@ def test_dataframe_manipulation(tb): tb.execute_cell('manipulation') # Inject assertion into notebook - tb.inject("assert len(df) == 1") + tb.inject('assert len(df) == 1') diff --git a/examples/requests-example/requests_test.py b/examples/requests-example/requests_test.py index 23951ea..49bd633 100644 --- a/examples/requests-example/requests_test.py +++ b/examples/requests-example/requests_test.py @@ -1,9 +1,10 @@ from 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 = tb.ref('get_details') # get reference to function get_details('https://my-api.com') mock_get.assert_called_with('https://my-api.com') diff --git a/examples/stdout-example/stdout_test.py b/examples/stdout-example/stdout_test.py index edc78bb..95c0d51 100644 --- a/examples/stdout-example/stdout_test.py +++ b/examples/stdout-example/stdout_test.py @@ -1,5 +1,6 @@ from testbook import testbook + @testbook('stdout-assertion-example.ipynb', execute=True) def test_stdout(tb): assert tb.cell_output_text(1) == 'hello world!' diff --git a/pyproject.toml b/pyproject.toml index 8bfffb8..6315b64 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,29 +1,173 @@ -# 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 +[build-system] +requires = [ "setuptools" ] +build-backend = "setuptools.build_meta" + +[project] +name = "testbook" +version = "0.4.2" +description = "A unit testing framework for Jupyter Notebooks" +readme = "README.md" +keywords = [ "jupyter", "notebook", "nteract", "unit-testing" ] +license = { file = "LICENSE" } +maintainers = [ + { name = "Nteract Contributors", email = "nteract@googlegroups.com" }, + { name = "Rohit Sanjay", email = "sanjay.rohit2@gmail.com" }, + { name = "Matthew Seal", email = "mseal007@gmail.com" }, +] +authors = [ + { name = "Nteract Contributors", email = "nteract@googlegroups.com" }, + { name = "Rohit Sanjay", email = "sanjay.rohit2@gmail.com" }, + { name = "Matthew Seal", email = "mseal007@gmail.com" }, +] +requires-python = ">=3.8" + +classifiers = [ + "Intended Audience :: Developers", + "License :: OSI Approved :: BSD License", + "Programming Language :: Python", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Topic :: Software Development :: Testing", + "Topic :: Software Development :: Testing :: Mocking", + "Topic :: Software Development :: Testing :: Unit", +] +dependencies = [ + "nbclient>=0.4", + "nbformat>=5.0.4", +] +optional-dependencies.dev = [ + "ruff", + "bumpversion", + "check-manifest", + "coverage", + "ipykernel", + "ipython", + "ipywidgets", + "pandas", + "pip>=18.1", + "pytest>=4.1", + "pytest-cov>=2.6.1", + "setuptools>=38.6", + "tox", + "build", + "twine>=1.11", + "xmltodict", +] +optional-dependencies.docs = [ + "myst-parser==3.0.1", + "sphinx==7.4.7", + "sphinx-book-theme==1.1.3", +] + +urls.Documentation = "https://testbook.readthedocs.io" +urls.Funding = "https://nteract.io" +urls.Issues = "https://github.com/nteract/testbook/issues" +urls.Repository = "https://github.com/nteract/testbook/" + +[tool.pytest.ini_options] +filterwarnings = "always" +testpaths = [ + "testbook/tests/", +] + +[tool.coverage.run] +branch = false +omit = [ + "testbook/tests/*", +] + +[tool.coverage.report] +exclude_lines = [ + "if self\\.debug:", + "pragma: no cover", + "raise AssertionError", + "raise NotImplementedError", + "if __name__ == '__main__':", +] +ignore_errors = true +omit = [ + "testbook/tests/*", +] + +[tool.ruff] +# Exclude a variety of commonly ignored directories. +exclude = [ + ".bzr", + ".direnv", + ".eggs", + ".git", + ".git-rewrite", + ".hg", + ".ipynb_checkpoints", + ".mypy_cache", + ".nox", + ".pants.d", + ".pyenv", + ".pytest_cache", + ".pytype", + ".ruff_cache", + ".svn", + ".tox", + ".venv", + ".vscode", + "__pypackages__", + "_build", + "buck-out", + "build", + "dist", + "node_modules", + "site-packages", + "venv", + "testbook/__init__.py", +] + +# Same as Black. +line-length = 88 +indent-width = 4 + +# Assume Python 3.8 +target-version = "py38" + +[tool.ruff.lint] +# Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default. +# Unlike Flake8, Ruff doesn't enable pycodestyle warnings (`W`) or +# McCabe complexity (`C901`) by default. +select = ["E4", "E7", "E9", "F"] +ignore = [] + +# Allow fix for all enabled rules (when `--fix`) is provided. +fixable = ["ALL"] +unfixable = [] + +# Allow unused variables when underscore-prefixed. +dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" + +[tool.ruff.format] +quote-style = "single" + +# Like Black, indent with spaces, rather than tabs. +indent-style = "space" + +# Like Black, respect magic trailing commas. +skip-magic-trailing-comma = false + +# Like Black, automatically detect the appropriate line ending. +line-ending = "auto" + +# Enable auto-formatting of code examples in docstrings. Markdown, +# reStructuredText code/literal blocks and doctests are all supported. +# +# This is currently disabled by default, but it is planned for this +# to be opt-out in the future. +docstring-code-format = false + +# Set the line length limit used when formatting code snippets in +# docstrings. +# +# This only has an effect when the `docstring-code-format` setting is +# enabled. +docstring-code-line-length = "dynamic" diff --git a/pytest.ini b/pytest.ini deleted file mode 100644 index 805610b..0000000 --- a/pytest.ini +++ /dev/null @@ -1,3 +0,0 @@ -[pytest] -testpaths = - testbook/tests/ diff --git a/requirements-dev.txt b/requirements-dev.txt deleted file mode 100644 index d3b9874..0000000 --- a/requirements-dev.txt +++ /dev/null @@ -1,18 +0,0 @@ -codecov -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 diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 0f9ff69..0000000 --- a/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -nbformat>=5.0.4 -nbclient>=0.4.0 diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 58ca2c3..0000000 --- a/setup.cfg +++ /dev/null @@ -1,44 +0,0 @@ - -[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 diff --git a/setup.py b/setup.py deleted file mode 100644 index 4b47e06..0000000 --- a/setup.py +++ /dev/null @@ -1,78 +0,0 @@ -#!/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', - ], -) diff --git a/testbook/__init__.py b/testbook/__init__.py index e8f1368..3276cff 100644 --- a/testbook/__init__.py +++ b/testbook/__init__.py @@ -1,2 +1 @@ -from ._version import version as __version__ from .testbook import testbook diff --git a/testbook/_version.py b/testbook/_version.py deleted file mode 100644 index 678d2fe..0000000 --- a/testbook/_version.py +++ /dev/null @@ -1 +0,0 @@ -version = '0.4.2' diff --git a/testbook/client.py b/testbook/client.py index 22cd34b..639ce64 100644 --- a/testbook/client.py +++ b/testbook/client.py @@ -26,7 +26,10 @@ class TestbookNotebookClient(NotebookClient): 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): + 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) @@ -39,7 +42,7 @@ def ref(self, name: str) -> Union[TestbookObjectReference, Any]: # Check if exists self.inject(name, pop=True) try: - self.inject(f"import json; json.dumps({name})", pop=True) + self.inject(f'import json; json.dumps({name})', pop=True) return self.value(name) except Exception: return TestbookObjectReference(self, name) @@ -78,22 +81,22 @@ def _execute_result(cell) -> List: """ return [ - output["data"] - for output in cell["outputs"] - if output["output_type"] == 'execute_result' + 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") + if 'outputs' not in cell: + raise ValueError('cell must be a code cell') text = '' - for output in cell["outputs"]: + 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"] + elif 'data' in output and 'text/plain' in output['data']: + text += output['data']['text/plain'] return text.strip() @@ -109,7 +112,7 @@ def _cell_index(self, tag: Union[int, str]) -> int: for idx, cell in enumerate(self.cells): metadata = cell['metadata'] - if "tags" in metadata and tag in metadata['tags']: + if 'tags' in metadata and tag in metadata['tags']: return idx raise TestbookCellTagNotFoundError("Cell tag '{}' not found".format(tag)) @@ -137,7 +140,9 @@ def execute_cell(self, cell, **kwargs) -> Union[Dict, List[Dict]]: 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)) + raise TestbookRuntimeError( + ce.evalue, ce, self._get_error_class(ce.ename) + ) executed_cells.append(cell) @@ -222,7 +227,9 @@ def inject( lines = dedent(code) elif callable(code): lines = getsource(code) + ( - dedent(self._construct_call_code(code.__name__, args, kwargs)) if run else '' + dedent(self._construct_call_code(code.__name__, args, kwargs)) + if run + else '' ) else: raise TypeError('can only inject function or code block as str') @@ -230,7 +237,7 @@ def inject( inject_idx = len(self.cells) if after is not None and before is not None: - raise ValueError("pass either before or after as kwargs") + 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: @@ -239,7 +246,11 @@ def inject( 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) + 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']) @@ -298,7 +309,7 @@ def value(self, code: str) -> Any: try: outputs = self.inject(inject_code, pop=True).outputs - if outputs[0].output_type == "error": + 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 @@ -330,7 +341,7 @@ def patch(self, target, **kwargs): yield TestbookObjectReference(self, mock_object) - self.inject(f"{patcher}.stop()") + self.inject(f'{patcher}.stop()') @contextmanager def patch_dict(self, in_dict, values=(), clear=False, **kwargs): @@ -353,7 +364,7 @@ def patch_dict(self, in_dict, values=(), clear=False, **kwargs): yield TestbookObjectReference(self, mock_object) - self.inject(f"{patcher}.stop()") + self.inject(f'{patcher}.stop()') @staticmethod def _get_error_class(ename): @@ -366,4 +377,4 @@ def _get_error_class(ename): @staticmethod def _contains_error(result): - return result.get('outputs') and result.get('outputs')[0].output_type == "error" + return result.get('outputs') and result.get('outputs')[0].output_type == 'error' diff --git a/testbook/exceptions.py b/testbook/exceptions.py index 2f7f7aa..39c4970 100644 --- a/testbook/exceptions.py +++ b/testbook/exceptions.py @@ -1,5 +1,6 @@ class TestbookError(Exception): """Generic Testbook exception class""" + __test__ = False diff --git a/testbook/reference.py b/testbook/reference.py index 6c6641d..3a0b0c6 100644 --- a/testbook/reference.py +++ b/testbook/reference.py @@ -2,7 +2,7 @@ TestbookExecuteResultNotFoundError, TestbookAttributeError, TestbookSerializeError, - TestbookRuntimeError + TestbookRuntimeError, ) from .utils import random_varname from .translators import PythonTranslator @@ -15,27 +15,27 @@ def __init__(self, tb, name): @property def _type(self): - return self.tb.value(f"type({self.name}).__name__") + return self.tb.value(f'type({self.name}).__name__') def __repr__(self): - return repr(self.tb.value(f"repr({self.name})")) + 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}") + 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)) + '{lhs} == {rhs}'.format(lhs=self.name, rhs=PythonTranslator.translate(rhs)) ) def __len__(self): - return self.tb.value(f"len({self.name})") + return self.tb.value(f'len({self.name})') def __iter__(self): - iterobjectname = f"___iter_object_{random_varname()}" + iterobjectname = f'___iter_object_{random_varname()}' self.tb.inject(f""" {iterobjectname} = iter({self.name}) """) @@ -43,7 +43,7 @@ def __iter__(self): def __next__(self): try: - return self.tb.value(f"next({self.name})") + return self.tb.value(f'next({self.name})') except TestbookRuntimeError as e: if e.eclass is StopIteration: raise StopIteration @@ -52,7 +52,9 @@ def __next__(self): def __getitem__(self, key): try: - return self.tb.value(f"{self.name}.__getitem__({PythonTranslator.translate(key)})") + return self.tb.value( + f'{self.name}.__getitem__({PythonTranslator.translate(key)})' + ) except TestbookRuntimeError as e: if e.eclass is TypeError: raise TypeError(e.evalue) @@ -63,11 +65,14 @@ def __getitem__(self, key): 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) + 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) @@ -77,7 +82,9 @@ def __setitem__(self, key, value): raise def __contains__(self, item): - return self.tb.value(f"{self.name}.__contains__({PythonTranslator.translate(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) diff --git a/testbook/testbook.py b/testbook/testbook.py index 8fb245b..d70000b 100644 --- a/testbook/testbook.py +++ b/testbook/testbook.py @@ -28,15 +28,23 @@ class testbook: attribute_name = None def __init__( - self, nb, execute=None, timeout=60, kernel_name='python3', allow_errors=False, **kwargs + 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, + 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 + **kwargs, ) self.new = DEFAULT diff --git a/testbook/testbooknode.py b/testbook/testbooknode.py index 22404ec..9a3e2af 100644 --- a/testbook/testbooknode.py +++ b/testbook/testbooknode.py @@ -22,7 +22,7 @@ def output_text(self) -> str: def execute_result(self): """Return data from execute_result outputs""" return [ - output["data"] - for output in self["outputs"] - if output["output_type"] == 'execute_result' + output['data'] + for output in self['outputs'] + if output['output_type'] == 'execute_result' ] diff --git a/testbook/tests/conftest.py b/testbook/tests/conftest.py index ed1b2f8..68fcff0 100644 --- a/testbook/tests/conftest.py +++ b/testbook/tests/conftest.py @@ -32,12 +32,12 @@ def notebook_generator(cells: Optional[List[NotebookNode]] = None) -> NotebookNo 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('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"})], + '', + metadata={'tags': ['dummy-outputs']}, + outputs=[new_output('execute_result', data={'text/plain': 'text'})], ), ] diff --git a/testbook/tests/test_client.py b/testbook/tests/test_client.py index edb1ed9..e57f4d5 100644 --- a/testbook/tests/test_client.py +++ b/testbook/tests/test_client.py @@ -3,7 +3,10 @@ from ..testbook import testbook from ..client import TestbookNotebookClient -from ..exceptions import TestbookCellTagNotFoundError, TestbookExecuteResultNotFoundError +from ..exceptions import ( + TestbookCellTagNotFoundError, + TestbookExecuteResultNotFoundError, +) @pytest.fixture(scope='module') @@ -12,13 +15,13 @@ def notebook(): yield tb -@pytest.mark.parametrize("cell_index_args, expected_result", [(2, 2), ('hello', 1)]) +@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", + '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): @@ -27,7 +30,7 @@ def test_cell_index_raises_error(cell_index_args, expected_error, notebook): @pytest.mark.parametrize( - "var_name, expected_result", + 'var_name, expected_result', [ ('sample_dict', {'foo': 'bar'}), ('sample_list', ['foo', 'bar']), @@ -42,25 +45,25 @@ 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)'), ('')]) +@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, expected_result', [ ( { - "cell_type": "code", - "execution_count": 9, - "metadata": {}, - "outputs": [ + 'cell_type': 'code', + 'execution_count': 9, + 'metadata': {}, + 'outputs': [ { - "name": "stdout", - "output_type": "stream", - "text": "hello world\n" "foo\n" "bar\n", + 'name': 'stdout', + 'output_type': 'stream', + 'text': 'hello world\n' 'foo\n' 'bar\n', } ], }, @@ -70,7 +73,10 @@ def test_value_raises_error(code, notebook): bar """, ), - ({"cell_type": "code", "execution_count": 9, "metadata": {}, "outputs": []}, ""), + ( + {'cell_type': 'code', 'execution_count': 9, 'metadata': {}, 'outputs': []}, + '', + ), ], ) def test_output_text(cell, expected_result): @@ -78,7 +84,7 @@ def test_output_text(cell, expected_result): @pytest.mark.parametrize( - "cell", [{}, {"cell_type": "markdown", "metadata": {}, "source": ["# Hello"]}] + 'cell', [{}, {'cell_type': 'markdown', 'metadata': {}, 'source': ['# Hello']}] ) def test_output_text_raises_error(cell): with pytest.raises(ValueError): @@ -87,16 +93,16 @@ def test_output_text_raises_error(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"}] + 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"}] + 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): @@ -110,4 +116,4 @@ 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") + tb.cell_execute_result('test') diff --git a/testbook/tests/test_datamodel.py b/testbook/tests/test_datamodel.py index 29ecc14..3892f16 100644 --- a/testbook/tests/test_datamodel.py +++ b/testbook/tests/test_datamodel.py @@ -10,13 +10,13 @@ def notebook(): def test_len(notebook): - mylist = notebook.ref("mylist") + mylist = notebook.ref('mylist') assert len(mylist) == 5 def test_iter(notebook): - mylist = notebook.ref("mylist") + mylist = notebook.ref('mylist') expected = [] for x in mylist: @@ -26,7 +26,7 @@ def test_iter(notebook): def test_getitem(notebook): - mylist = notebook.ref("mylist") + mylist = notebook.ref('mylist') mylist.append(6) assert mylist[-1] == 6 @@ -34,14 +34,14 @@ def test_getitem(notebook): def test_getitem_raisesIndexError(notebook): - mylist = notebook.ref("mylist") + mylist = notebook.ref('mylist') with pytest.raises(IndexError): mylist[100] def test_getitem_raisesTypeError(notebook): - mylist = notebook.ref("mylist") + mylist = notebook.ref('mylist') with pytest.raises(TypeError): mylist['hello'] @@ -49,25 +49,25 @@ def test_getitem_raisesTypeError(notebook): def test_setitem(notebook): notebook.inject("mydict = {'key1': 'value1', 'key2': 'value1'}") - mydict = notebook.ref("mydict") + mydict = notebook.ref('mydict') mydict['key3'] = 'value3' assert mydict['key3'] == 'value3' - mylist = notebook.ref("mylist") + mylist = notebook.ref('mylist') mylist[2] = 10 assert mylist[2] == 10 def test_setitem_raisesIndexError(notebook): - mylist = notebook.ref("mylist") + mylist = notebook.ref('mylist') with pytest.raises(IndexError): mylist.__setitem__(10, 100) def test_setitem_raisesTypeError(notebook): - mylist = notebook.ref("mylist") + mylist = notebook.ref('mylist') with pytest.raises(TypeError): mylist.__setitem__('key', 10) @@ -75,7 +75,7 @@ def test_setitem_raisesTypeError(notebook): def test_contains(notebook): notebook.inject("mydict = {'key1': 'value1', 'key2': 'value1'}") - mydict = notebook.ref("mydict") + mydict = notebook.ref('mydict') assert 'key1' in mydict assert 'key2' in mydict diff --git a/testbook/tests/test_execute.py b/testbook/tests/test_execute.py index 6c96f00..db39864 100644 --- a/testbook/tests/test_execute.py +++ b/testbook/tests/test_execute.py @@ -20,9 +20,12 @@ def test_execute_cell(notebook): def test_execute_and_show_pandas_output(notebook): notebook.execute_cell(4) - assert notebook.cell_output_text(4) == """col1 col2 + assert ( + notebook.cell_output_text(4) + == """col1 col2 0 1 3 1 2 4""" + ) def test_execute_cell_tags(notebook): @@ -36,9 +39,9 @@ def test_execute_cell_tags(notebook): def test_execute_cell_raises_error(notebook): with pytest.raises(TestbookRuntimeError): try: - notebook.inject("1/0", pop=True) + notebook.inject('1/0', pop=True) except TestbookRuntimeError as e: - assert e.eclass == ZeroDivisionError + assert e.eclass is ZeroDivisionError raise @@ -61,13 +64,17 @@ def test_testbook_range(): assert tb.code_cells_executed == 4 -@pytest.mark.parametrize("slice_params, expected_result", [(('hello', 'str'), 6), ((2, 5), 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: + with testbook( + 'testbook/tests/resources/inject.ipynb', execute=slice(*slice_params) + ) as tb: assert tb.code_cells_executed == expected_result @@ -80,7 +87,7 @@ def test_testbook_slice_raises_error(): @testbook('testbook/tests/resources/exception.ipynb', execute=True) def test_raise_exception(tb): with pytest.raises(TestbookRuntimeError): - tb.ref("raise_my_exception")() + tb.ref('raise_my_exception')() @testbook('testbook/tests/resources/inject.ipynb') @@ -97,6 +104,6 @@ def foo(x): tb.execute() - foo = tb.ref("foo") + foo = tb.ref('foo') assert foo(2) == 3 diff --git a/testbook/tests/test_inject.py b/testbook/tests/test_inject.py index 3228316..44e43b1 100644 --- a/testbook/tests/test_inject.py +++ b/testbook/tests/test_inject.py @@ -14,7 +14,7 @@ def inject_helper(*args, **kwargs): @pytest.mark.parametrize( - "args, kwargs", + 'args, kwargs', [ (None, None), ([1, 2], None), @@ -32,23 +32,23 @@ def test_inject(args, kwargs, notebook): @pytest.mark.parametrize( - "code_block, expected_text", + 'code_block, expected_text', [ ( - ''' + """ def foo(): print('I ran in the code block') foo() - ''', - "I ran in the code block", + """, + 'I ran in the code block', ), ( - ''' + """ def foo(arg): print(f'You passed {arg}') foo('bar') - ''', - "You passed bar", + """, + 'You passed bar', ), ], ) @@ -65,16 +65,16 @@ def test_inject_raises_exception(notebook): 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_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()" + 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") + 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" + assert notebook.inject('1+1', pop=True).execute_result == [{'text/plain': '2'}] + assert notebook.cells[-1].source != '1+1' diff --git a/testbook/tests/test_patch.py b/testbook/tests/test_patch.py index 6865877..ba5e122 100644 --- a/testbook/tests/test_patch.py +++ b/testbook/tests/test_patch.py @@ -12,7 +12,7 @@ def tb(): class TestPatch: @pytest.mark.parametrize( - "target, func", [("os.listdir", "listdir"), ("os.popen", "get_branch")] + 'target, func', [('os.listdir', 'listdir'), ('os.popen', 'get_branch')] ) def test_patch_basic(self, target, func, tb): with tb.patch(target) as mock_obj: @@ -20,21 +20,22 @@ def test_patch_basic(self, target, func, tb): mock_obj.assert_called_once() @pytest.mark.parametrize( - "target, func", [("os.listdir", "listdir"), ("os.popen", "get_branch")] + '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'] + 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"})], + '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): diff --git a/testbook/tests/test_reference.py b/testbook/tests/test_reference.py index c766aa2..6fac580 100644 --- a/testbook/tests/test_reference.py +++ b/testbook/tests/test_reference.py @@ -11,50 +11,50 @@ def notebook(): def test_create_reference(notebook): - a = notebook.ref("a") - assert repr(a) == "[1, 2, 3]" + 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]" + 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]" + a = notebook.get('a') + assert repr(a) == '[1, 2, 3]' def test_eq_in_notebook(notebook): - a = notebook.ref("a") + 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") + a, b = notebook.ref('a'), notebook.ref('b') assert a == b def test_function_call(notebook): - double = notebook.ref("double") + 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") + double, a = notebook.ref('double'), notebook.ref('a') assert double(a) == [2, 4, 6] def test_reference(notebook): - Foo = notebook.ref("Foo") + 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) == "\"\"" + assert repr(f) == '""' # Valid attribute access assert f.say_hello() diff --git a/testbook/tests/test_testbook.py b/testbook/tests/test_testbook.py index 8247b09..986d5f4 100644 --- a/testbook/tests/test_testbook.py +++ b/testbook/tests/test_testbook.py @@ -13,7 +13,7 @@ def test_testbook_execute_all_cells(tb): @testbook('testbook/tests/resources/inject.ipynb', execute='hello') def test_testbook_class_decorator(tb): - assert tb.inject("say_hello()") + assert tb.inject('say_hello()') @testbook('testbook/tests/resources/inject.ipynb') @@ -27,14 +27,16 @@ def test_testbook_decorator_with_fixture(nb, tmp_path): @testbook('testbook/tests/resources/inject.ipynb', execute=True) -@pytest.mark.parametrize("cell_index_args, expected_result", [(2, 2), ('hello', 1)]) +@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)]) +@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): +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 @@ -46,7 +48,7 @@ def test_testbook_execute_all_cells_context_manager(): def test_testbook_class_decorator_context_manager(): with testbook('testbook/tests/resources/inject.ipynb', execute='hello') as tb: - assert tb.inject("say_hello()") + assert tb.inject('say_hello()') def test_testbook_class_decorator_execute_none_context_manager(): @@ -73,6 +75,6 @@ def test_testbook_with_notebook_node(): def test_function_with_testbook_decorator_returns_value(): @testbook('testbook/tests/resources/inject.ipynb') def test_function(tb): - return "This should be returned" + return 'This should be returned' - assert test_function() == "This should be returned" + assert test_function() == 'This should be returned' diff --git a/testbook/tests/test_translators.py b/testbook/tests/test_translators.py index e21608a..802e53e 100644 --- a/testbook/tests/test_translators.py +++ b/testbook/tests/test_translators.py @@ -1,4 +1,5 @@ """Sourced from https://github.com/nteract/papermill/blob/master/papermill/tests/test_translators.py""" + import pytest from .. import translators @@ -13,19 +14,19 @@ def __repr__(self): @pytest.mark.parametrize( - "test_input,expected", + 'test_input,expected', [ - ("foo", '"foo"'), + ('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\\""}]'), + ({'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'), @@ -43,10 +44,12 @@ 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")]) +@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]" + assert translators.Translator.assign('var1', [1, 2, 3]) == 'var1 = [1, 2, 3]' diff --git a/testbook/translators.py b/testbook/translators.py index 9597a27..3278665 100644 --- a/testbook/translators.py +++ b/testbook/translators.py @@ -1,4 +1,5 @@ """Sourced from https://github.com/nteract/papermill/blob/master/papermill/translators.py""" + import math import sys @@ -46,11 +47,15 @@ def translate_bool(cls, val): @classmethod def translate_dict(cls, val): - raise NotImplementedError('dict type translation not implemented for {}'.format(cls)) + 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)) + raise NotImplementedError( + 'list type translation not implemented for {}'.format(cls) + ) @classmethod def translate(cls, val): @@ -72,7 +77,7 @@ def translate(cls, val): return cls.translate_list(val) elif isinstance(val, tuple): return cls.translate_tuple(val) - elif val.__class__.__name__ == "TestbookObjectReference": + elif val.__class__.__name__ == 'TestbookObjectReference': return val.name # Use this generic translation as a last resort @@ -80,7 +85,9 @@ def translate(cls, val): @classmethod def comment(cls, cmt_str): - raise NotImplementedError('comment translation not implemented for {}'.format(cls)) + raise NotImplementedError( + 'comment translation not implemented for {}'.format(cls) + ) @classmethod def assign(cls, name, str_val): @@ -106,7 +113,10 @@ def translate_bool(cls, val): @classmethod def translate_dict(cls, val): escaped = ', '.join( - ["{}: {}".format(cls.translate_str(k), cls.translate(v)) for k, v in val.items()] + [ + '{}: {}'.format(cls.translate_str(k), cls.translate(v)) + for k, v in val.items() + ] ) return '{{{}}}'.format(escaped) diff --git a/tox.ini b/tox.ini index 8650731..eee5d8e 100644 --- a/tox.ini +++ b/tox.ini @@ -1,20 +1,19 @@ [tox] skipsdist = true -envlist = py{36,37,38,39,310}, flake8, dist, manifest, docs +envlist = py{38,39,310,311,312}, lint, manifest, docs [gh-actions] python = - 3.6: py36 - 3.7: py37 - 3.8: py38, flake8, dist, manifest + 3.8: py38 3.9: py39 3.10: py310 + 3.11: py311, lint, manifest, docs + 3.12: py312 # Linters -[testenv:flake8] +[testenv:lint] skip_install = true -deps = flake8 -commands = flake8 testbook --count --ignore=E203,E731,F811,W503 --max-complexity=23 --max-line-length=104 --show-source --statistics +commands = ruff check # Manifest [testenv:manifest] @@ -27,28 +26,19 @@ commands = check-manifest description = invoke sphinx-build to build the HTML docs skip_install = true deps = - .[sphinx] -extras = docs + .[docs] commands = - sphinx-build -d "{toxworkdir}/docs_doctree" docs "{toxworkdir}/docs_out" --color -W -bhtml {posargs} + sphinx-build -d "{toxworkdir}/docs_doctree" docs "{toxworkdir}/docs_out" --color -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" + python -c "import docs.conf" -# 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 +# Formatter +[testenv:format] +description = apply ruff formatter with desired rules +basepython = python3.11 deps = - black -commands = black . + ruff +commands = ruff format [testenv] # disable Python's hash randomization for tests that stringify dicts, etc @@ -56,14 +46,13 @@ setenv = PYTHONHASHSEED = 0 passenv = * basepython = - py36: python3.6 - py37: python3.7 py38: python3.8 py39: python3.9 py310: python3.10 - flake8: python3.8 - manifest: python3.8 - dist: python3.8 - docs: python3.8 + py311: python3.11 + py312: python3.12 + lint: python3.11 + manifest: python3.11 + docs: python3.11 deps = .[dev] commands = pytest -vv --maxfail=2 --cov=testbook --cov-report=xml -W always {posargs}