├── tests ├── __init__.py ├── data │ ├── __init__.py │ ├── t.py │ ├── t.ipynb │ ├── notebook_with_local_import.ipynb │ ├── empty_notebook.ipynb │ ├── invalid_import_in_doctest.ipynb │ ├── default_magic.ipynb │ ├── non_default_magic.ipynb │ ├── env_var.ipynb │ ├── notebook_for_testing_copy.md │ ├── clean_notebook.ipynb │ ├── clean_notebook_with_trailing_semicolon.ipynb │ ├── clean_notebook_with_multiline.ipynb │ ├── notebook_with_cell_after_def.ipynb │ ├── transformed_magics.ipynb │ ├── databricks_notebook.ipynb │ ├── percent_format.ipynb │ ├── notebook_with_trailing_semicolon.ipynb │ ├── comment_after_trailing_semicolon.ipynb │ ├── all_magic_cell.ipynb │ ├── commented_out_magic.ipynb │ ├── notebook_with_other_magics.ipynb │ ├── notebook_with_separated_imports.ipynb │ ├── markdown_then_imports.ipynb │ ├── notebook_with_separated_imports_other.ipynb │ ├── notebook_for_testing.md │ ├── notebook_for_autoflake.ipynb │ ├── simple_imports.ipynb │ ├── starting_with_comment.ipynb │ ├── notebook_for_testing_copy.ipynb │ ├── footer.ipynb │ ├── notebook_starting_with_md.ipynb │ ├── notebook_for_testing.ipynb │ └── notebook_with_indented_magics.ipynb ├── tools │ ├── __init__.py │ ├── test_pydocstyle.py │ ├── test_mdformat.py │ ├── test_blacken_docs.py │ ├── test_mypy_works.py │ ├── test_yapf.py │ ├── test_autopep8.py │ ├── test_doctest.py │ ├── test_pylint_works.py │ ├── test_pyupgrade.py │ ├── test_autoflake.py │ ├── test_ruff_works.py │ └── test_flake8_works.py ├── invalid_data │ ├── __init__.py │ ├── foobarqux.py │ ├── mymod │ │ ├── __init__.py │ │ ├── __main__.py │ │ └── mysubmod.py │ ├── octave_notebook.md │ ├── non_python_notebook.ipynb │ ├── assignment_to_literal.ipynb │ ├── invalid_syntax.ipynb │ ├── automagic.ipynb │ └── tracker.md ├── print_6174.py ├── test_version.py ├── local_script.py ├── remove_comments.py ├── test_file_not_found.py ├── remove_all.py ├── test_deprecated.py ├── test_non_python_notebook.py ├── test_hash_collision.py ├── test_get_notebooks.py ├── test_configs_work.py ├── test_find_root.py ├── test_transformed_magics.py ├── test_map_python_line_to_nb_lines.py ├── test_local_script.py ├── test_running_from_different_dir.py ├── test_skip_bad_cells.py ├── test_nbqa_diff.py ├── test_pyproject_toml.py ├── test_skip_celltags.py ├── test_include_exclude.py ├── test_nbqa_shell.py ├── test_return_code.py ├── test_ipython_magics.py └── conftest.py ├── nbqa ├── config │ ├── __init__.py │ └── config.py ├── __init__.py ├── text.py ├── optional.py ├── notebook_info.py ├── find_root.py ├── output_parser.py ├── path_utils.py ├── save_markdown_source.py └── cmdline.py ├── setup.py ├── docs ├── readme.md ├── _templates │ └── autosummary │ │ ├── method.rst │ │ ├── function.rst │ │ ├── attribute.rst │ │ ├── class.rst │ │ └── module.rst ├── requirements-docs.txt ├── clean_build_artifacts.py ├── index.rst ├── Makefile ├── make.bat ├── known-limitations.rst ├── tutorial.rst ├── pre-commit.rst ├── examples.rst ├── contributing.rst ├── conf.py └── configuration.rst ├── MANIFEST.in ├── .gitignore ├── requirements-dev.txt ├── .github ├── workflows │ ├── slash_dispatch.yml │ ├── docs.yml │ ├── tox.yml │ ├── dispatch_pre-commit.yml │ └── publish_to_pypi.yml └── FUNDING.yml ├── tox.ini ├── .readthedocs.yaml ├── pyproject.toml ├── LICENSE ├── LICENSES └── BLACK_LICENSE ├── setup.cfg ├── .pre-commit-config.yaml └── .pre-commit-hooks.yaml /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/data/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/tools/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/data/t.py: -------------------------------------------------------------------------------- 1 | class A: ... 2 | -------------------------------------------------------------------------------- /tests/invalid_data/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/invalid_data/foobarqux.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/invalid_data/mymod/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/invalid_data/mymod/__main__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/invalid_data/mymod/mysubmod.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /nbqa/config/__init__.py: -------------------------------------------------------------------------------- 1 | """Configuration files.""" 2 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup() 4 | -------------------------------------------------------------------------------- /docs/readme.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | 3 | ```{include} ../README.md 4 | 5 | ``` 6 | -------------------------------------------------------------------------------- /nbqa/__init__.py: -------------------------------------------------------------------------------- 1 | """Attributes used by docs / packaging.""" 2 | 3 | __version__ = "1.9.1" 4 | -------------------------------------------------------------------------------- /docs/_templates/autosummary/method.rst: -------------------------------------------------------------------------------- 1 | {{ name | escape | underline }} 2 | 3 | .. automethod:: {{ fullname }} 4 | -------------------------------------------------------------------------------- /docs/requirements-docs.txt: -------------------------------------------------------------------------------- 1 | myst-parser 2 | Sphinx>=3.2.0 3 | sphinx-copybutton>=0.3.0 4 | sphinx-rtd-theme>=0.5.0 5 | -------------------------------------------------------------------------------- /docs/_templates/autosummary/function.rst: -------------------------------------------------------------------------------- 1 | {{ name | escape | underline }} 2 | 3 | .. autofunction:: {{ fullname }} 4 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-exclude tests * 2 | recursive-exclude docs * 3 | recursive-exclude * __pycache__ 4 | recursive-exclude * *.py[co] 5 | -------------------------------------------------------------------------------- /docs/_templates/autosummary/attribute.rst: -------------------------------------------------------------------------------- 1 | {{ name | escape | underline }} 2 | 3 | .. autoattribute:: {{ fullname }} 4 | :noindex: 5 | -------------------------------------------------------------------------------- /nbqa/text.py: -------------------------------------------------------------------------------- 1 | """Formatting options when printing user-facing text.""" 2 | 3 | RED = "\033[31m" 4 | GREEN = "\033[32m" 5 | BOLD = "\033[1m" 6 | RESET = "\x1b[0m" 7 | -------------------------------------------------------------------------------- /nbqa/optional.py: -------------------------------------------------------------------------------- 1 | """Import importlib.metadata, using backport if on an old version of Python.""" 2 | 3 | from importlib import import_module 4 | 5 | metadata = import_module("importlib.metadata") 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | .ipynb_checkpoints 3 | .venv 4 | test-output.xml 5 | .coverage 6 | coverage.xml 7 | !.coveragerc 8 | htmlcov/ 9 | junit 10 | *.egg* 11 | docs/_build 12 | dist 13 | .tox 14 | build/ 15 | docs/api 16 | .idea 17 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | autoflake 2 | autopep8 3 | black 4 | blacken-docs 5 | coverage[toml] 6 | flake8 7 | isort>=5.4.2 8 | jupytext 9 | mdformat 10 | mypy 11 | pre-commit 12 | pre-commit-hooks 13 | pydocstyle 14 | pylint 15 | pyrefly 16 | pytest 17 | pytest-cov 18 | pytest-randomly 19 | pyupgrade 20 | ruff 21 | yapf 22 | -------------------------------------------------------------------------------- /tests/print_6174.py: -------------------------------------------------------------------------------- 1 | """Silly little module which produces unparsable output.""" 2 | 3 | import argparse 4 | 5 | if __name__ == "__main__": 6 | parser = argparse.ArgumentParser() 7 | parser.add_argument("files", nargs="*") 8 | args = parser.parse_args() 9 | for file in args.files: 10 | print(f"{file}:6174:0 some silly warning") 11 | -------------------------------------------------------------------------------- /tests/test_version.py: -------------------------------------------------------------------------------- 1 | """Check you can run :code:`nbqa --version`.""" 2 | 3 | import subprocess 4 | 5 | from nbqa import __version__ 6 | 7 | 8 | def test_version() -> None: 9 | """Check you can run :code:`nbqa --version`.""" 10 | output = subprocess.run(["nbqa", "--version"], capture_output=True, text=True) 11 | assert output.stdout.strip() == f"nbqa {__version__}" 12 | assert output.returncode == 0 13 | -------------------------------------------------------------------------------- /.github/workflows/slash_dispatch.yml: -------------------------------------------------------------------------------- 1 | name: Slash Command Dispatch 2 | on: 3 | issue_comment: 4 | types: [created] 5 | jobs: 6 | slashCommandDispatch: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: Slash Command Dispatch 10 | uses: peter-evans/slash-command-dispatch@v2 11 | with: 12 | token: ${{ secrets.ACTION_TRIGGER_TOKEN }} 13 | issue-type: pull-request 14 | commands: | 15 | pre-commit-run 16 | -------------------------------------------------------------------------------- /docs/clean_build_artifacts.py: -------------------------------------------------------------------------------- 1 | """Cross platform way to call 'rm -rf docs/_build/ docs/api/'""" 2 | 3 | from pathlib import Path 4 | from shutil import rmtree 5 | 6 | 7 | def delete_artifacts() -> None: 8 | """Delete API and _build directories""" 9 | current_dir = Path(__file__).parent 10 | rmtree(current_dir / "api", ignore_errors=True) 11 | rmtree(current_dir / "_build", ignore_errors=True) 12 | 13 | 14 | if __name__ == "__main__": 15 | delete_artifacts() 16 | -------------------------------------------------------------------------------- /tests/local_script.py: -------------------------------------------------------------------------------- 1 | """Local module with subcommand.""" 2 | 3 | import argparse 4 | import sys 5 | from typing import Optional, Sequence 6 | 7 | 8 | def main(argv: Optional[Sequence[str]] = None) -> int: 9 | """Print word (subcommand), ignore paths""" 10 | parser = argparse.ArgumentParser() 11 | parser.add_argument("word", nargs=1) 12 | parser.add_argument("paths", nargs="*") 13 | args = parser.parse_args(argv) 14 | print(args.word) 15 | return 0 16 | 17 | 18 | if __name__ == "__main__": 19 | sys.exit(main()) 20 | -------------------------------------------------------------------------------- /tests/remove_comments.py: -------------------------------------------------------------------------------- 1 | """ 2 | Silly little module which removes comments. 3 | 4 | This is just so we can check what happens when running nbqa on a tool which causes a failure. 5 | """ 6 | 7 | import argparse 8 | from pathlib import Path 9 | 10 | if __name__ == "__main__": 11 | parser = argparse.ArgumentParser() 12 | parser.add_argument("path") 13 | args, _ = parser.parse_known_args() 14 | file_ = Path(args.path).read_text(encoding="utf-8") 15 | file_ = file_.replace("#", "") 16 | Path(args.path).write_text(file_, encoding="utf-8") 17 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. image:: https://github.com/nbQA-dev/nbQA-demo/raw/master/assets/logo.png 2 | :width: 400 3 | 4 | nbQA 5 | ==== 6 | 7 | .. toctree:: 8 | :maxdepth: 2 9 | :caption: USER DOCS: 10 | 11 | readme 12 | tutorial 13 | examples 14 | configuration 15 | pre-commit 16 | known-limitations 17 | history 18 | 19 | 20 | .. toctree:: 21 | :maxdepth: 2 22 | :caption: DEVELOPER DOCS: 23 | 24 | contributing 25 | 26 | Indices and tables 27 | ================== 28 | * :ref:`genindex` 29 | * :ref:`modindex` 30 | * :ref:`search` 31 | -------------------------------------------------------------------------------- /tests/test_file_not_found.py: -------------------------------------------------------------------------------- 1 | """Check what happens when running on non-existent file/directory.""" 2 | 3 | from typing import TYPE_CHECKING 4 | 5 | from nbqa.__main__ import main 6 | 7 | if TYPE_CHECKING: 8 | from _pytest.capture import CaptureFixture 9 | 10 | 11 | def test_file_not_found(capsys: "CaptureFixture") -> None: 12 | """Check useful error message is raised if file or directory doesn't exist.""" 13 | msg = "No such file or directory: i_dont_exist.ipynb" 14 | 15 | main(["isort", "i_dont_exist.ipynb", "--profile=black"]) 16 | _, err = capsys.readouterr() 17 | assert msg in err 18 | -------------------------------------------------------------------------------- /tests/data/t.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "from t import A" 10 | ] 11 | } 12 | ], 13 | "metadata": { 14 | "language_info": { 15 | "codemirror_mode": { 16 | "name": "ipython", 17 | "version": 3 18 | }, 19 | "file_extension": ".py", 20 | "mimetype": "text/x-python", 21 | "name": "python", 22 | "nbconvert_exporter": "python", 23 | "pygments_lexer": "ipython3", 24 | "version": 3 25 | } 26 | }, 27 | "nbformat": 4, 28 | "nbformat_minor": 2 29 | } 30 | -------------------------------------------------------------------------------- /tests/remove_all.py: -------------------------------------------------------------------------------- 1 | """ 2 | Silly little module which removes cell content except for first lne. 3 | 4 | This is just so we can check what happens when running nbqa on a tool which causes a failure. 5 | """ 6 | 7 | import argparse 8 | from pathlib import Path 9 | 10 | if __name__ == "__main__": 11 | parser = argparse.ArgumentParser() 12 | parser.add_argument("path") 13 | args, _ = parser.parse_known_args() 14 | file_ = Path(args.path).read_text(encoding="utf-8") 15 | newlines = [line + "\n" for line in file_.splitlines() if line.startswith("#")] 16 | Path(args.path).write_text("\n".join(newlines), encoding="utf-8") 17 | -------------------------------------------------------------------------------- /tests/data/notebook_with_local_import.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "from nbqa import *" 10 | ] 11 | } 12 | ], 13 | "metadata": { 14 | "language_info": { 15 | "codemirror_mode": { 16 | "name": "ipython", 17 | "version": 3 18 | }, 19 | "file_extension": ".py", 20 | "mimetype": "text/x-python", 21 | "name": "python", 22 | "nbconvert_exporter": "python", 23 | "pygments_lexer": "ipython3", 24 | "version": 3 25 | } 26 | }, 27 | "nbformat": 4, 28 | "nbformat_minor": 2 29 | } 30 | -------------------------------------------------------------------------------- /tests/invalid_data/octave_notebook.md: -------------------------------------------------------------------------------- 1 | --- 2 | jupyter: 3 | kernelspec: 4 | display_name: Octave 5 | language: octave 6 | name: octave 7 | --- 8 | 9 | A markdown cell 10 | 11 | ```octave 12 | 1 + 1 13 | ``` 14 | 15 | ```octave 16 | % a code cell with comments 17 | 2 + 2 18 | ``` 19 | 20 | ```octave 21 | % a simple plot 22 | x = -10:0.1:10; 23 | plot (x, sin (x)); 24 | ``` 25 | 26 | ```octave 27 | %plot -w 800 28 | % a simple plot with a magic instruction 29 | x = -10:0.1:10; 30 | plot (x, sin (x)); 31 | ``` 32 | 33 | And to finish with, a Python cell 34 | 35 | ```python 36 | a = 1 37 | ``` 38 | 39 | ```python 40 | a + 1 41 | ``` 42 | -------------------------------------------------------------------------------- /tests/test_deprecated.py: -------------------------------------------------------------------------------- 1 | """Test deprecations.""" 2 | 3 | import os 4 | from typing import TYPE_CHECKING 5 | 6 | from nbqa.__main__ import main 7 | 8 | if TYPE_CHECKING: 9 | from _pytest.capture import CaptureFixture 10 | 11 | 12 | def test_deprecated(capsys: "CaptureFixture") -> None: 13 | """Test deprecation errors.""" 14 | path = os.path.join("tests", "data", "clean_notebook.ipynb") 15 | main(["flake8", path, "--nbqa-skip-bad-cells"]) 16 | _, err = capsys.readouterr() 17 | assert err == ( 18 | "Flag --nbqa-skip-bad-cells was deprecated in 0.13.0\n" 19 | "Cells with invalid syntax are now skipped by default\n" 20 | ) 21 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = pyrefly, py{38,39,310,311}, docs, docs-links 3 | 4 | [testenv:docs] 5 | deps = -rdocs/requirements-docs.txt 6 | commands = 7 | {envpython} -m sphinx -b html docs docs/_build/html 8 | 9 | [testenv:clean-docs] 10 | skip_install = true 11 | deps = 12 | commands = python {toxinidir}/docs/clean_build_artifacts.py 13 | 14 | [testenv] 15 | deps = 16 | -rrequirements-dev.txt 17 | commands = 18 | coverage erase 19 | coverage run -m pytest {posargs:tests -vv -W error} 20 | coverage xml 21 | coverage report --fail-under 100 --show-missing 22 | 23 | [testenv:pyrefly] 24 | deps = 25 | -rrequirements-dev.txt 26 | commands = 27 | pyrefly check nbqa 28 | -------------------------------------------------------------------------------- /tests/data/empty_notebook.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "assert 1 + 1 == 2" 8 | ] 9 | } 10 | ], 11 | "metadata": { 12 | "kernelspec": { 13 | "display_name": "Python 3", 14 | "language": "python", 15 | "name": "python3" 16 | }, 17 | "language_info": { 18 | "codemirror_mode": { 19 | "name": "ipython", 20 | "version": 3 21 | }, 22 | "file_extension": ".py", 23 | "mimetype": "text/x-python", 24 | "name": "python", 25 | "nbconvert_exporter": "python", 26 | "pygments_lexer": "ipython3", 27 | "version": "3.7.7" 28 | } 29 | }, 30 | "nbformat": 4, 31 | "nbformat_minor": 4 32 | } 33 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yaml 2 | # Read the Docs configuration file 3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 4 | 5 | # Required 6 | version: 2 7 | 8 | # Set the version of Python and other tools you might need 9 | build: 10 | os: ubuntu-22.04 11 | tools: 12 | python: "3.11" 13 | 14 | # Build documentation in the docs/ directory with Sphinx 15 | sphinx: 16 | configuration: docs/conf.py 17 | 18 | # We recommend specifying your dependencies to enable reproducible builds: 19 | # https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html 20 | python: 21 | install: 22 | - requirements: docs/requirements-docs.txt 23 | - method: pip 24 | path: . 25 | -------------------------------------------------------------------------------- /tests/invalid_data/non_python_notebook.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": { 7 | "_execution_state": "idle", 8 | "_uuid": "051d70d956493feee0c6d64651c6a088724dca2a" 9 | }, 10 | "outputs": [], 11 | "source": [ 12 | "library(tidyverse) " 13 | ] 14 | } 15 | ], 16 | "metadata": { 17 | "kernelspec": { 18 | "display_name": "R", 19 | "language": "R", 20 | "name": "ir" 21 | }, 22 | "language_info": { 23 | "codemirror_mode": "r", 24 | "file_extension": ".r", 25 | "mimetype": "text/x-r-source", 26 | "name": "R", 27 | "pygments_lexer": "r", 28 | "version": "4.0.5" 29 | } 30 | }, 31 | "nbformat": 4, 32 | "nbformat_minor": 4 33 | } 34 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: MarcoGorelli # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /tests/data/invalid_import_in_doctest.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "import thisdoesnotexist\n", 10 | "\n", 11 | "def f():\n", 12 | " \"\"\"\n", 13 | " >>> import thisdoesnotexist\n", 14 | " \"\"\"" 15 | ] 16 | } 17 | ], 18 | "metadata": { 19 | "language_info": { 20 | "codemirror_mode": { 21 | "name": "ipython", 22 | "version": 3 23 | }, 24 | "file_extension": ".py", 25 | "mimetype": "text/x-python", 26 | "name": "python", 27 | "nbconvert_exporter": "python", 28 | "pygments_lexer": "ipython3", 29 | "version": 3 30 | } 31 | }, 32 | "nbformat": 4, 33 | "nbformat_minor": 2 34 | } 35 | -------------------------------------------------------------------------------- /tests/data/default_magic.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "%%timeit\n", 10 | "\n", 11 | "a = 2 " 12 | ] 13 | } 14 | ], 15 | "metadata": { 16 | "kernelspec": { 17 | "display_name": "Python 3", 18 | "language": "python", 19 | "name": "python3" 20 | }, 21 | "language_info": { 22 | "codemirror_mode": { 23 | "name": "ipython", 24 | "version": 3 25 | }, 26 | "file_extension": ".py", 27 | "mimetype": "text/x-python", 28 | "name": "python", 29 | "nbconvert_exporter": "python", 30 | "pygments_lexer": "ipython3", 31 | "version": "3.6.4" 32 | } 33 | }, 34 | "nbformat": 4, 35 | "nbformat_minor": 1 36 | } 37 | -------------------------------------------------------------------------------- /tests/data/non_default_magic.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "%%javascript\n", 10 | "\n", 11 | "a = 2 " 12 | ] 13 | } 14 | ], 15 | "metadata": { 16 | "kernelspec": { 17 | "display_name": "Python 3", 18 | "language": "python", 19 | "name": "python3" 20 | }, 21 | "language_info": { 22 | "codemirror_mode": { 23 | "name": "ipython", 24 | "version": 3 25 | }, 26 | "file_extension": ".py", 27 | "mimetype": "text/x-python", 28 | "name": "python", 29 | "nbconvert_exporter": "python", 30 | "pygments_lexer": "ipython3", 31 | "version": "3.6.4" 32 | } 33 | }, 34 | "nbformat": 4, 35 | "nbformat_minor": 1 36 | } 37 | -------------------------------------------------------------------------------- /tests/invalid_data/assignment_to_literal.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "1 = [1,2,3]" 10 | ] 11 | } 12 | ], 13 | "metadata": { 14 | "anaconda-cloud": {}, 15 | "kernelspec": { 16 | "display_name": "Python 3", 17 | "language": "python", 18 | "name": "python3" 19 | }, 20 | "language_info": { 21 | "codemirror_mode": { 22 | "name": "ipython", 23 | "version": 3 24 | }, 25 | "file_extension": ".py", 26 | "mimetype": "text/x-python", 27 | "name": "python", 28 | "nbconvert_exporter": "python", 29 | "pygments_lexer": "ipython3", 30 | "version": "3.8.0-final" 31 | } 32 | }, 33 | "nbformat": 4, 34 | "nbformat_minor": 4 35 | } 36 | -------------------------------------------------------------------------------- /tests/test_non_python_notebook.py: -------------------------------------------------------------------------------- 1 | """Test non-Python notebook.""" 2 | 3 | import os 4 | from typing import TYPE_CHECKING 5 | 6 | from nbqa.__main__ import main 7 | 8 | if TYPE_CHECKING: 9 | from _pytest.capture import CaptureFixture 10 | 11 | 12 | def test_non_python_notebook(capsys: "CaptureFixture") -> None: 13 | """ 14 | Should ignore non-Python notebook. 15 | 16 | Parameters 17 | ---------- 18 | capsys 19 | Pytest fixture to capture stdout and stderr. 20 | """ 21 | path = os.path.join("tests", "invalid_data", "non_python_notebook.ipynb") 22 | result = main(["black", path, "--nbqa-diff"]) 23 | out, err = capsys.readouterr() 24 | assert out == "" 25 | assert err == "No valid Python notebooks found in given path(s)\n" 26 | assert result == 0 27 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = python -msphinx 7 | SPHINXPROJ = nbqa 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | API_TOCTREE_DIR = api 11 | 12 | # Put it first so that "make" without argument is like "make help". 13 | help: 14 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 15 | 16 | .PHONY: help Makefile 17 | 18 | clean: 19 | rm -rf $(BUILDDIR)/* 20 | rm -rf $(API_TOCTREE_DIR)/* 21 | 22 | # Catch-all target: route all unknown targets to Sphinx using the new 23 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 24 | %: Makefile 25 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 26 | -------------------------------------------------------------------------------- /tests/test_hash_collision.py: -------------------------------------------------------------------------------- 1 | """Check what happens if cell separator appears in notebook.""" 2 | 3 | import os 4 | 5 | import pytest 6 | from _pytest.capture import CaptureFixture 7 | from _pytest.monkeypatch import MonkeyPatch 8 | 9 | from nbqa.__main__ import main 10 | 11 | 12 | @pytest.mark.skip(reason="too slow - TODO how to re-enable / speedup?") 13 | def test_hash_collision(monkeypatch: MonkeyPatch, capsys: CaptureFixture) -> None: 14 | """Check hash collision error message.""" 15 | path = os.path.join("tests", "data", "notebook_for_testing.ipynb") 16 | monkeypatch.setattr("nbqa.save_code_source.CODE_SEPARATOR", "pprint\n") 17 | main(["flake8", path]) 18 | _, err = capsys.readouterr() 19 | assert ( 20 | "Extremely rare hash collision occurred - please re-run nbQA to fix this" in err 21 | ) 22 | -------------------------------------------------------------------------------- /tests/data/env_var.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": { 7 | "tags": [ 8 | "skip-flake8" 9 | ] 10 | }, 11 | "outputs": [], 12 | "source": [ 13 | "var = %env var\n", 14 | "foo = %foo" 15 | ] 16 | } 17 | ], 18 | "metadata": { 19 | "anaconda-cloud": {}, 20 | "kernelspec": { 21 | "display_name": "Python 3", 22 | "language": "python", 23 | "name": "python3" 24 | }, 25 | "language_info": { 26 | "codemirror_mode": { 27 | "name": "ipython", 28 | "version": 3 29 | }, 30 | "file_extension": ".py", 31 | "mimetype": "text/x-python", 32 | "name": "python", 33 | "nbconvert_exporter": "python", 34 | "pygments_lexer": "ipython3", 35 | "version": "3.8.5" 36 | } 37 | }, 38 | "nbformat": 4, 39 | "nbformat_minor": 4 40 | } 41 | -------------------------------------------------------------------------------- /tests/data/notebook_for_testing_copy.md: -------------------------------------------------------------------------------- 1 | --- 2 | jupyter: 3 | jupytext: 4 | text_representation: 5 | extension: .md 6 | format_name: markdown 7 | format_version: "1.3" 8 | jupytext_version: 1.14.1 9 | kernelspec: 10 | display_name: Python 3 11 | language: python 12 | name: python3 13 | --- 14 | 15 | ```python 16 | import os 17 | 18 | import glob 19 | 20 | import nbqa 21 | ``` 22 | 23 | # Some markdown cell containing \n 24 | 25 | ```python 26 | %%time 27 | def hello(name: str = "world\n"): 28 | """ 29 | Greet user. 30 | 31 | Examples 32 | -------- 33 | >>> hello() 34 | 'hello world\\n' 35 | >>> hello("goodbye") 36 | 'hello goodby' 37 | """ 38 | if True: 39 | %time # indented magic! 40 | return f'hello {name}' 41 | 42 | 43 | hello(3) 44 | ``` 45 | 46 | ```python 47 | 48 | ``` 49 | -------------------------------------------------------------------------------- /tests/data/clean_notebook.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "assert 1 + 1 == 2" 10 | ] 11 | }, 12 | { 13 | "cell_type": "code", 14 | "execution_count": null, 15 | "metadata": {}, 16 | "outputs": [], 17 | "source": [] 18 | } 19 | ], 20 | "metadata": { 21 | "kernelspec": { 22 | "display_name": "Python 3", 23 | "language": "python", 24 | "name": "python3" 25 | }, 26 | "language_info": { 27 | "codemirror_mode": { 28 | "name": "ipython", 29 | "version": 3 30 | }, 31 | "file_extension": ".py", 32 | "mimetype": "text/x-python", 33 | "name": "python", 34 | "nbconvert_exporter": "python", 35 | "pygments_lexer": "ipython3", 36 | "version": "3.7.7" 37 | } 38 | }, 39 | "nbformat": 4, 40 | "nbformat_minor": 4 41 | } 42 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=python -msphinx 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | set SPHINXPROJ=nbqa 13 | 14 | if "%1" == "" goto help 15 | 16 | %SPHINXBUILD% >NUL 2>NUL 17 | if errorlevel 9009 ( 18 | echo. 19 | echo.The Sphinx module was not found. Make sure you have Sphinx installed, 20 | echo.then set the SPHINXBUILD environment variable to point to the full 21 | echo.path of the 'sphinx-build' executable. Alternatively you may add the 22 | echo.Sphinx directory to PATH. 23 | echo. 24 | echo.If you don't have Sphinx installed, grab it from 25 | echo.http://sphinx-doc.org/ 26 | exit /b 1 27 | ) 28 | 29 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 30 | goto end 31 | 32 | :help 33 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 34 | 35 | :end 36 | popd 37 | -------------------------------------------------------------------------------- /tests/data/clean_notebook_with_trailing_semicolon.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "assert 1 + 1 == 2;" 10 | ] 11 | }, 12 | { 13 | "cell_type": "code", 14 | "execution_count": null, 15 | "metadata": {}, 16 | "outputs": [], 17 | "source": [] 18 | } 19 | ], 20 | "metadata": { 21 | "kernelspec": { 22 | "display_name": "Python 3", 23 | "language": "python", 24 | "name": "python3" 25 | }, 26 | "language_info": { 27 | "codemirror_mode": { 28 | "name": "ipython", 29 | "version": 3 30 | }, 31 | "file_extension": ".py", 32 | "mimetype": "text/x-python", 33 | "name": "python", 34 | "nbconvert_exporter": "python", 35 | "pygments_lexer": "ipython3", 36 | "version": "3.7.7" 37 | } 38 | }, 39 | "nbformat": 4, 40 | "nbformat_minor": 4 41 | } 42 | -------------------------------------------------------------------------------- /tests/data/clean_notebook_with_multiline.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "assert 1 + 1 == 2; assert 1 + 1 == 2;" 10 | ] 11 | }, 12 | { 13 | "cell_type": "code", 14 | "execution_count": null, 15 | "metadata": {}, 16 | "outputs": [], 17 | "source": [] 18 | } 19 | ], 20 | "metadata": { 21 | "kernelspec": { 22 | "display_name": "Python 3", 23 | "language": "python", 24 | "name": "python3" 25 | }, 26 | "language_info": { 27 | "codemirror_mode": { 28 | "name": "ipython", 29 | "version": 3 30 | }, 31 | "file_extension": ".py", 32 | "mimetype": "text/x-python", 33 | "name": "python", 34 | "nbconvert_exporter": "python", 35 | "pygments_lexer": "ipython3", 36 | "version": "3.7.7" 37 | } 38 | }, 39 | "nbformat": 4, 40 | "nbformat_minor": 4 41 | } 42 | -------------------------------------------------------------------------------- /nbqa/notebook_info.py: -------------------------------------------------------------------------------- 1 | """Store information about the code cells for processing.""" 2 | 3 | from typing import Mapping, NamedTuple, Sequence, Set 4 | 5 | from nbqa.handle_magics import MagicHandler 6 | 7 | 8 | class NotebookInfo(NamedTuple): 9 | """ 10 | Store information about notebook cells used for processing. 11 | 12 | Attributes 13 | ---------- 14 | cell_mapping 15 | Mapping from Python line numbers to Jupyter notebooks cells. 16 | trailing_semicolons 17 | Cell numbers where there were originally trailing semicolons. 18 | temporary_lines 19 | Mapping from cell number to all the magics substituted in those cell. 20 | code_cells_to_ignore 21 | List of code cell to ignore when modifying the source notebook. 22 | """ 23 | 24 | cell_mappings: Mapping[int, str] 25 | trailing_semicolons: Set[int] 26 | temporary_lines: Mapping[int, Sequence[MagicHandler]] 27 | code_cells_to_ignore: Set[int] 28 | -------------------------------------------------------------------------------- /tests/invalid_data/invalid_syntax.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "%%time\n", 10 | "if True\n", 11 | " print('definitely invalid')" 12 | ] 13 | }, 14 | { 15 | "cell_type": "code", 16 | "execution_count": null, 17 | "metadata": {}, 18 | "outputs": [], 19 | "source": [] 20 | } 21 | ], 22 | "metadata": { 23 | "anaconda-cloud": {}, 24 | "kernelspec": { 25 | "display_name": "Python 3", 26 | "language": "python", 27 | "name": "python3" 28 | }, 29 | "language_info": { 30 | "codemirror_mode": { 31 | "name": "ipython", 32 | "version": 3 33 | }, 34 | "file_extension": ".py", 35 | "mimetype": "text/x-python", 36 | "name": "python", 37 | "nbconvert_exporter": "python", 38 | "pygments_lexer": "ipython3", 39 | "version": "3.7.6" 40 | } 41 | }, 42 | "nbformat": 4, 43 | "nbformat_minor": 4 44 | } 45 | -------------------------------------------------------------------------------- /tests/data/notebook_with_cell_after_def.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "def add(num1, num2):\n", 10 | " return num1 + num2" 11 | ] 12 | }, 13 | { 14 | "cell_type": "code", 15 | "execution_count": null, 16 | "metadata": {}, 17 | "outputs": [], 18 | "source": [ 19 | "add(2, 3)" 20 | ] 21 | }, 22 | { 23 | "cell_type": "code", 24 | "execution_count": null, 25 | "metadata": {}, 26 | "outputs": [], 27 | "source": [] 28 | } 29 | ], 30 | "metadata": { 31 | "language_info": { 32 | "codemirror_mode": { 33 | "name": "ipython", 34 | "version": 3 35 | }, 36 | "file_extension": ".py", 37 | "mimetype": "text/x-python", 38 | "name": "python", 39 | "nbconvert_exporter": "python", 40 | "pygments_lexer": "ipython3", 41 | "version": 3 42 | } 43 | }, 44 | "nbformat": 4, 45 | "nbformat_minor": 2 46 | } 47 | -------------------------------------------------------------------------------- /tests/invalid_data/automagic.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "if True:\n", 10 | " print('definitely valid')" 11 | ] 12 | }, 13 | { 14 | "cell_type": "code", 15 | "execution_count": null, 16 | "metadata": {}, 17 | "outputs": [], 18 | "source": [ 19 | "pip install pandas" 20 | ] 21 | } 22 | ], 23 | "metadata": { 24 | "anaconda-cloud": {}, 25 | "kernelspec": { 26 | "display_name": "Python 3", 27 | "language": "python", 28 | "name": "python3" 29 | }, 30 | "language_info": { 31 | "codemirror_mode": { 32 | "name": "ipython", 33 | "version": 3 34 | }, 35 | "file_extension": ".py", 36 | "mimetype": "text/x-python", 37 | "name": "python", 38 | "nbconvert_exporter": "python", 39 | "pygments_lexer": "ipython3", 40 | "version": "3.7.6" 41 | } 42 | }, 43 | "nbformat": 4, 44 | "nbformat_minor": 4 45 | } 46 | -------------------------------------------------------------------------------- /tests/data/transformed_magics.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "%time foo()\n", 10 | "2+2\n", 11 | "get_ipython().run_cell_magic(\"time\", \"\", \"foo()\\\\n\")" 12 | ] 13 | }, 14 | { 15 | "cell_type": "code", 16 | "execution_count": null, 17 | "metadata": {}, 18 | "outputs": [], 19 | "source": [ 20 | "2+2" 21 | ] 22 | } 23 | ], 24 | "metadata": { 25 | "kernelspec": { 26 | "display_name": "Python 3", 27 | "language": "python", 28 | "name": "python3" 29 | }, 30 | "language_info": { 31 | "codemirror_mode": { 32 | "name": "ipython", 33 | "version": 3 34 | }, 35 | "file_extension": ".py", 36 | "mimetype": "text/x-python", 37 | "name": "python", 38 | "nbconvert_exporter": "python", 39 | "pygments_lexer": "ipython3", 40 | "version": "3.7.7" 41 | } 42 | }, 43 | "nbformat": 4, 44 | "nbformat_minor": 4 45 | } 46 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.interrogate] 2 | fail-under = 100 3 | ignore-init-method = true 4 | 5 | [tool.isort] 6 | profile = "black" 7 | 8 | [tool.coverage.run] 9 | source = ["nbqa"] 10 | branch = true 11 | 12 | [tool.coverage.report] 13 | exclude_lines = [ 14 | "pragma: nocover", 15 | "raise NotImplementedError", 16 | "if __name__ == .__main__.:", 17 | "if TYPE_CHECKING:", 18 | "raise AssertionError", 19 | ] 20 | ignore_errors = true 21 | omit = [ 22 | "tests/*", 23 | ] 24 | 25 | [tool.pylint.messages_control] 26 | disable = [ 27 | "C0103", # snake case for methods (necessary for subclassing ast stuff) 28 | "C0301", # line too long (checked by flake8 already) 29 | "R0801", # Unfortunately I do need some duplication, e.g. with imports 30 | "R0903", # Too few public methods 31 | "W1510", # I need to run subprocess.run without check because it's OK if it fails 32 | "E0401", # Unable to import '_pytest.capture' (this is just for typing anyway) 33 | "E0611", # No name 'TypedDict' in module 'typing' 34 | ] 35 | -------------------------------------------------------------------------------- /tests/data/databricks_notebook.ipynb: -------------------------------------------------------------------------------- 1 | {"cells": [{"cell_type": "code", "source": [""], "metadata": {"application/vnd.databricks.v1+cell": {"title": "", "showTitle": false, "inputWidgets": {}, "nuid": "c278d99c-c25e-4005-b7b0-5126de2b8a80"}}, "outputs": [{"output_type": "display_data", "metadata": {"application/vnd.databricks.v1+output": {"type": "ipynbError", "data": "", "errorSummary": "", "arguments": {}}}, "data": {"text/html": [""]}}], "execution_count": 0}, {"cell_type": "code", "source": ["set(())"]}], "metadata": {"application/vnd.databricks.v1+notebook": {"notebookName": "live_sku_EDA_databricks", "dashboards": [], "language": "python", "widgets": {}, "notebookOrigID": 121207}}, "nbformat": 4, "nbformat_minor": 0} 2 | -------------------------------------------------------------------------------- /tests/data/percent_format.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "%%time\n", 10 | "\n", 11 | "print(\n", 12 | " \"%s\"\n", 13 | " % 'foo'\n", 14 | ")" 15 | ] 16 | }, 17 | { 18 | "cell_type": "code", 19 | "execution_count": null, 20 | "metadata": {}, 21 | "outputs": [], 22 | "source": [ 23 | "a = b??" 24 | ] 25 | } 26 | ], 27 | "metadata": { 28 | "kernelspec": { 29 | "display_name": "Python 3", 30 | "language": "python", 31 | "name": "python3" 32 | }, 33 | "language_info": { 34 | "codemirror_mode": { 35 | "name": "ipython", 36 | "version": 3 37 | }, 38 | "file_extension": ".py", 39 | "mimetype": "text/x-python", 40 | "name": "python", 41 | "nbconvert_exporter": "python", 42 | "pygments_lexer": "ipython3", 43 | "version": "3.7.7" 44 | } 45 | }, 46 | "nbformat": 4, 47 | "nbformat_minor": 4 48 | } 49 | -------------------------------------------------------------------------------- /tests/test_get_notebooks.py: -------------------------------------------------------------------------------- 1 | """Check function which lists notebooks in directory.""" 2 | 3 | import shutil 4 | from pathlib import Path 5 | from typing import TYPE_CHECKING 6 | 7 | import pytest 8 | 9 | from nbqa.__main__ import _get_notebooks 10 | 11 | if TYPE_CHECKING: 12 | from py._path.local import LocalPath 13 | 14 | CLEAN_NOTEBOOK = Path("tests") / "data/clean_notebook.ipynb" 15 | 16 | 17 | @pytest.mark.parametrize("dir_", [".git", "venv", "_build"]) 18 | def test_get_notebooks(tmpdir: "LocalPath", dir_: str) -> None: 19 | """ 20 | Check that unwanted directories are excluded. 21 | 22 | Parameters 23 | ---------- 24 | tmpdir 25 | Pytest fixture, gives us a temporary directory. 26 | dir_ 27 | Directory where we expected notebooks to be ignored. 28 | """ 29 | Path(tmpdir / f"{dir_}/tests/data").mkdir(parents=True) 30 | shutil.copy(str(CLEAN_NOTEBOOK), str(tmpdir / dir_ / CLEAN_NOTEBOOK)) 31 | result = list(_get_notebooks(tmpdir)) 32 | assert not result 33 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: docs 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: [master] 7 | 8 | jobs: 9 | docs: 10 | name: "Running: ${{ matrix.tox-env-name }}" 11 | strategy: 12 | matrix: 13 | tox-env-name: ["docs"] 14 | 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v4 18 | - uses: actions/setup-python@v4 19 | with: 20 | python-version: 3.12 # Same as RTD 21 | - name: Cache multiple paths 22 | uses: actions/cache@v4 23 | with: 24 | path: ~/.cache/pip 25 | key: docs 26 | - name: install-tox 27 | run: python -m pip install --upgrade tox virtualenv setuptools pip 28 | - name: run ${{ matrix.tox-env-name }} 29 | run: tox -e ${{ matrix.tox-env-name }} 30 | - name: Upload docs artifact 31 | if: ${{ matrix.tox-env-name }} == "docs" 32 | uses: actions/upload-artifact@v4 33 | with: 34 | name: nbqa-docs 35 | path: docs/_build/html 36 | -------------------------------------------------------------------------------- /nbqa/config/config.py: -------------------------------------------------------------------------------- 1 | """Module responsible for storing and handling nbqa configuration.""" 2 | 3 | from typing import TYPE_CHECKING, Callable, Optional, Sequence, Union 4 | 5 | ConfigParser = Callable[[str], Union[str, bool, Sequence[str]]] 6 | 7 | if TYPE_CHECKING: 8 | from typing import TypedDict 9 | else: 10 | TypedDict = dict 11 | 12 | 13 | class Configs(TypedDict): 14 | """nbQA-specific configs.""" 15 | 16 | addopts: Sequence[str] 17 | diff: bool 18 | exclude: Optional[str] 19 | files: Optional[str] 20 | process_cells: Sequence[str] 21 | dont_skip_bad_cells: bool 22 | skip_celltags: Sequence[str] 23 | md: bool 24 | shell: bool 25 | 26 | 27 | def get_default_config() -> Configs: 28 | """Get defaults.""" 29 | return Configs( 30 | addopts=[], 31 | diff=False, 32 | exclude=None, 33 | files=None, 34 | process_cells=[], 35 | dont_skip_bad_cells=False, 36 | skip_celltags=[], 37 | md=False, 38 | shell=False, 39 | ) 40 | -------------------------------------------------------------------------------- /tests/data/notebook_with_trailing_semicolon.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "import glob;\n", 10 | "\n", 11 | "import nbqa;" 12 | ] 13 | }, 14 | { 15 | "cell_type": "code", 16 | "execution_count": null, 17 | "metadata": {}, 18 | "outputs": [], 19 | "source": [ 20 | "def func(a, b):\n", 21 | " pass;\n", 22 | " " 23 | ] 24 | } 25 | ], 26 | "metadata": { 27 | "anaconda-cloud": {}, 28 | "kernelspec": { 29 | "display_name": "Python 3", 30 | "language": "python", 31 | "name": "python3" 32 | }, 33 | "language_info": { 34 | "codemirror_mode": { 35 | "name": "ipython", 36 | "version": 3 37 | }, 38 | "file_extension": ".py", 39 | "mimetype": "text/x-python", 40 | "name": "python", 41 | "nbconvert_exporter": "python", 42 | "pygments_lexer": "ipython3", 43 | "version": "3.7.7" 44 | } 45 | }, 46 | "nbformat": 4, 47 | "nbformat_minor": 4 48 | } 49 | -------------------------------------------------------------------------------- /tests/test_configs_work.py: -------------------------------------------------------------------------------- 1 | """Check local config files are picked up by nbqa.""" 2 | 3 | from pathlib import Path 4 | from textwrap import dedent 5 | from typing import TYPE_CHECKING 6 | 7 | from nbqa.__main__ import main 8 | 9 | if TYPE_CHECKING: 10 | from _pytest.capture import CaptureFixture 11 | 12 | 13 | def test_configs_work(capsys: "CaptureFixture") -> None: 14 | """ 15 | Check a flake8 cfg file is picked up by nbqa. 16 | 17 | Parameters 18 | ---------- 19 | capsys 20 | Pytest fixture to capture stdout and stderr. 21 | """ 22 | Path(".flake8").write_text( 23 | dedent( 24 | """\ 25 | [flake8] 26 | ignore=F401 27 | select=E303 28 | quiet=1 29 | """ 30 | ), 31 | encoding="utf-8", 32 | ) 33 | 34 | main(["flake8", "tests", "--ignore", "E302"]) 35 | 36 | Path(".flake8").unlink() 37 | 38 | # check out and err 39 | out, _ = capsys.readouterr() 40 | expected_out = "" 41 | assert out == expected_out 42 | -------------------------------------------------------------------------------- /tests/test_find_root.py: -------------------------------------------------------------------------------- 1 | """Check project root is round correctly.""" 2 | 3 | from pathlib import Path 4 | from typing import Sequence 5 | 6 | import pytest 7 | 8 | from nbqa.find_root import find_project_root 9 | 10 | 11 | @pytest.mark.parametrize( 12 | "src", 13 | [ 14 | (Path.cwd(),), 15 | (Path.cwd() / "tests", Path.cwd() / "tests/data"), 16 | ], 17 | ) 18 | def test_find_project_root(src: Sequence[str]) -> None: 19 | """ 20 | Check project root is found correctly. 21 | 22 | Parameters 23 | ---------- 24 | src 25 | Source paths. 26 | """ 27 | result = find_project_root(src, root_dirs=(".does.not.exist",)) 28 | expected = Path.cwd() 29 | assert result == expected 30 | 31 | 32 | def test_find_project_root_no_root() -> None: 33 | """Check root of filesystem is returned if no root file exists.""" 34 | result = find_project_root( 35 | (Path.cwd() / "tests",), (".this.does.not.exist",), (".nor.does.this",) 36 | ) 37 | expected = Path("/").resolve() 38 | assert result == expected 39 | -------------------------------------------------------------------------------- /.github/workflows/tox.yml: -------------------------------------------------------------------------------- 1 | name: tox 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: [main] 7 | 8 | jobs: 9 | tox: 10 | strategy: 11 | matrix: 12 | python-version: ["3.9", "3.12", "3.13"] 13 | os: [ubuntu-latest, windows-latest] 14 | 15 | runs-on: ${{ matrix.os }} 16 | steps: 17 | - uses: actions/checkout@v4 18 | - uses: actions/setup-python@v4 19 | with: 20 | python-version: ${{ matrix.python-version }} 21 | - name: Cache multiple paths 22 | uses: actions/cache@v4 23 | with: 24 | path: | 25 | ~/.cache/pip 26 | $RUNNER_TOOL_CACHE/Python/* 27 | ~\AppData\Local\pip\Cache 28 | key: ${{ runner.os }}-build-${{ matrix.python-version }} 29 | - name: install-reqs 30 | run: | 31 | python -m pip install --upgrade tox virtualenv setuptools pip 32 | python -m pip install -U -r requirements-dev.txt 33 | python -m pip install -e . 34 | - name: run-tests 35 | run: pytest tests --cov=100 36 | -------------------------------------------------------------------------------- /tests/data/comment_after_trailing_semicolon.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "import glob;\n", 10 | "\n", 11 | "import nbqa;\n", 12 | "# this is a comment" 13 | ] 14 | }, 15 | { 16 | "cell_type": "code", 17 | "execution_count": null, 18 | "metadata": {}, 19 | "outputs": [], 20 | "source": [ 21 | "def func(a, b):\n", 22 | " pass;\n", 23 | " " 24 | ] 25 | } 26 | ], 27 | "metadata": { 28 | "anaconda-cloud": {}, 29 | "kernelspec": { 30 | "display_name": "Python 3", 31 | "language": "python", 32 | "name": "python3" 33 | }, 34 | "language_info": { 35 | "codemirror_mode": { 36 | "name": "ipython", 37 | "version": 3 38 | }, 39 | "file_extension": ".py", 40 | "mimetype": "text/x-python", 41 | "name": "python", 42 | "nbconvert_exporter": "python", 43 | "pygments_lexer": "ipython3", 44 | "version": "3.7.7" 45 | } 46 | }, 47 | "nbformat": 4, 48 | "nbformat_minor": 4 49 | } 50 | -------------------------------------------------------------------------------- /tests/test_transformed_magics.py: -------------------------------------------------------------------------------- 1 | """Test the skip_celltags option.""" 2 | 3 | import os 4 | from typing import TYPE_CHECKING 5 | 6 | from nbqa.__main__ import main 7 | 8 | if TYPE_CHECKING: 9 | from _pytest.capture import CaptureFixture 10 | 11 | 12 | def test_transformed_magics(capsys: "CaptureFixture") -> None: 13 | """ 14 | Should ignore cells with transformed magics. 15 | 16 | Parameters 17 | ---------- 18 | capsys 19 | Pytest fixture to capture stdout and stderr. 20 | """ 21 | path = os.path.join("tests", "data", "transformed_magics.ipynb") 22 | main(["black", path, "--nbqa-diff"]) 23 | out, _ = capsys.readouterr() 24 | expected_out = ( 25 | "\x1b[1mCell 2\x1b[0m\n" 26 | "------\n" 27 | f"\x1b[1;37m--- {path}\n" 28 | f"\x1b[0m\x1b[1;37m+++ {path}\n" 29 | "\x1b[0m\x1b[36m@@ -1 +1 @@\n" 30 | "\x1b[0m\x1b[31m-2+2\n" 31 | "\x1b[0m\x1b[32m+2 + 2\n" 32 | "\x1b[0m\n" 33 | "To apply these changes, remove the `--nbqa-diff` flag\n" 34 | ) 35 | assert out == expected_out 36 | -------------------------------------------------------------------------------- /tests/data/all_magic_cell.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": { 7 | "tags": [ 8 | "skip-flake8" 9 | ] 10 | }, 11 | "outputs": [], 12 | "source": [ 13 | "%load_ext nb_black\n", 14 | "\n", 15 | "%matplotlib inline\n" 16 | ] 17 | }, 18 | { 19 | "cell_type": "code", 20 | "execution_count": null, 21 | "metadata": {}, 22 | "outputs": [], 23 | "source": [ 24 | "import sys\n", 25 | "\n", 26 | "\n", 27 | "sys.version" 28 | ] 29 | } 30 | ], 31 | "metadata": { 32 | "anaconda-cloud": {}, 33 | "kernelspec": { 34 | "display_name": "Python 3", 35 | "language": "python", 36 | "name": "python3" 37 | }, 38 | "language_info": { 39 | "codemirror_mode": { 40 | "name": "ipython", 41 | "version": 3 42 | }, 43 | "file_extension": ".py", 44 | "mimetype": "text/x-python", 45 | "name": "python", 46 | "nbconvert_exporter": "python", 47 | "pygments_lexer": "ipython3", 48 | "version": "3.8.5" 49 | } 50 | }, 51 | "nbformat": 4, 52 | "nbformat_minor": 4 53 | } 54 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020, Marco Gorelli 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /LICENSES/BLACK_LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Łukasz Langa 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /tests/data/commented_out_magic.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "[1, 2,\n", 10 | "3, 4]" 11 | ] 12 | }, 13 | { 14 | "cell_type": "code", 15 | "execution_count": null, 16 | "metadata": {}, 17 | "outputs": [], 18 | "source": [ 19 | "# %%capture\n", 20 | "\n", 21 | "p1 = 1\n", 22 | "!!ls" 23 | ] 24 | }, 25 | { 26 | "cell_type": "code", 27 | "execution_count": null, 28 | "metadata": {}, 29 | "outputs": [], 30 | "source": [] 31 | } 32 | ], 33 | "metadata": { 34 | "anaconda-cloud": {}, 35 | "kernelspec": { 36 | "display_name": "Python 3", 37 | "language": "python", 38 | "name": "python3" 39 | }, 40 | "language_info": { 41 | "codemirror_mode": { 42 | "name": "ipython", 43 | "version": 3 44 | }, 45 | "file_extension": ".py", 46 | "mimetype": "text/x-python", 47 | "name": "python", 48 | "nbconvert_exporter": "python", 49 | "pygments_lexer": "ipython3", 50 | "version": "3.8.5-final" 51 | } 52 | }, 53 | "nbformat": 4, 54 | "nbformat_minor": 4 55 | } 56 | -------------------------------------------------------------------------------- /tests/tools/test_pydocstyle.py: -------------------------------------------------------------------------------- 1 | """Check that running :code:`pydocstyle` works.""" 2 | 3 | import os 4 | from typing import TYPE_CHECKING 5 | 6 | from nbqa.__main__ import main 7 | 8 | if TYPE_CHECKING: 9 | from _pytest.capture import CaptureFixture 10 | 11 | 12 | def test_pydocstyle_works(capsys: "CaptureFixture") -> None: 13 | """ 14 | Check pydocstyle works. 15 | 16 | Parameters 17 | ---------- 18 | capsys 19 | Pytest fixture to capture stdout and stderr. 20 | """ 21 | path = os.path.join("tests", "data", "notebook_for_testing.ipynb") 22 | main(["pydocstyle", path]) 23 | 24 | # check out and err 25 | out, err = capsys.readouterr() 26 | expected_out = ( 27 | f"{path}:cell_1:0 at module level:\n" 28 | " D100: Missing docstring in public module\n" 29 | f"{path}:cell_2:3 in public function `hello`:\n" 30 | " D202: No blank lines allowed after function docstring (found 1)\n" 31 | f"{path}:cell_2:3 in public function `hello`:\n" 32 | ' D301: Use r""" if any backslashes in a docstring\n' 33 | ) 34 | assert out.replace("\r\n", "\n") == expected_out 35 | assert err == "" 36 | -------------------------------------------------------------------------------- /tests/data/notebook_with_other_magics.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "%%custommagic\n", 10 | "\n", 11 | "import os" 12 | ] 13 | }, 14 | { 15 | "cell_type": "code", 16 | "execution_count": null, 17 | "metadata": {}, 18 | "outputs": [], 19 | "source": [ 20 | "%%anothercustommagic\n", 21 | "\n", 22 | "import glob" 23 | ] 24 | }, 25 | { 26 | "cell_type": "code", 27 | "execution_count": null, 28 | "metadata": {}, 29 | "outputs": [], 30 | "source": [] 31 | } 32 | ], 33 | "metadata": { 34 | "anaconda-cloud": {}, 35 | "kernelspec": { 36 | "display_name": "Python 3", 37 | "language": "python", 38 | "name": "python3" 39 | }, 40 | "language_info": { 41 | "codemirror_mode": { 42 | "name": "ipython", 43 | "version": 3 44 | }, 45 | "file_extension": ".py", 46 | "mimetype": "text/x-python", 47 | "name": "python", 48 | "nbconvert_exporter": "python", 49 | "pygments_lexer": "ipython3", 50 | "version": "3.7.7" 51 | } 52 | }, 53 | "nbformat": 4, 54 | "nbformat_minor": 4 55 | } 56 | -------------------------------------------------------------------------------- /tests/data/notebook_with_separated_imports.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "import os\n", 10 | "\n", 11 | "os.listdir()" 12 | ] 13 | }, 14 | { 15 | "cell_type": "code", 16 | "execution_count": null, 17 | "metadata": {}, 18 | "outputs": [], 19 | "source": [ 20 | "import numpy\n", 21 | "\n", 22 | "numpy.random.randn(1)" 23 | ] 24 | }, 25 | { 26 | "cell_type": "code", 27 | "execution_count": null, 28 | "metadata": {}, 29 | "outputs": [], 30 | "source": [] 31 | } 32 | ], 33 | "metadata": { 34 | "anaconda-cloud": {}, 35 | "kernelspec": { 36 | "display_name": "Python 3", 37 | "language": "python", 38 | "name": "python3" 39 | }, 40 | "language_info": { 41 | "codemirror_mode": { 42 | "name": "ipython", 43 | "version": 3 44 | }, 45 | "file_extension": ".py", 46 | "mimetype": "text/x-python", 47 | "name": "python", 48 | "nbconvert_exporter": "python", 49 | "pygments_lexer": "ipython3", 50 | "version": "3.7.7" 51 | } 52 | }, 53 | "nbformat": 4, 54 | "nbformat_minor": 4 55 | } 56 | -------------------------------------------------------------------------------- /tests/data/markdown_then_imports.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "hello world" 8 | ] 9 | }, 10 | { 11 | "cell_type": "code", 12 | "execution_count": null, 13 | "metadata": {}, 14 | "outputs": [], 15 | "source": [ 16 | "import os" 17 | ] 18 | }, 19 | { 20 | "cell_type": "code", 21 | "execution_count": null, 22 | "metadata": {}, 23 | "outputs": [], 24 | "source": [ 25 | "import sys" 26 | ] 27 | } 28 | ], 29 | "metadata": { 30 | "kernelspec": { 31 | "display_name": "Python 3 (ipykernel)", 32 | "language": "python", 33 | "name": "python3" 34 | }, 35 | "language_info": { 36 | "codemirror_mode": { 37 | "name": "ipython", 38 | "version": 3 39 | }, 40 | "file_extension": ".py", 41 | "mimetype": "text/x-python", 42 | "name": "python", 43 | "nbconvert_exporter": "python", 44 | "pygments_lexer": "ipython3", 45 | "version": "3.10.6" 46 | }, 47 | "vscode": { 48 | "interpreter": { 49 | "hash": "864e9cd7dbf32e150cc52d563f4c3a35053498b9e9d82e11d8edd2e505b61bc9" 50 | } 51 | } 52 | }, 53 | "nbformat": 4, 54 | "nbformat_minor": 4 55 | } 56 | -------------------------------------------------------------------------------- /tests/data/notebook_with_separated_imports_other.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "import os" 10 | ] 11 | }, 12 | { 13 | "cell_type": "code", 14 | "execution_count": null, 15 | "metadata": {}, 16 | "outputs": [], 17 | "source": [ 18 | "import os\n", 19 | "\n", 20 | "# This is a comment on the second import\n", 21 | "import numpy" 22 | ] 23 | }, 24 | { 25 | "cell_type": "code", 26 | "execution_count": null, 27 | "metadata": {}, 28 | "outputs": [], 29 | "source": [] 30 | } 31 | ], 32 | "metadata": { 33 | "anaconda-cloud": {}, 34 | "kernelspec": { 35 | "display_name": "Python 3", 36 | "language": "python", 37 | "name": "python3" 38 | }, 39 | "language_info": { 40 | "codemirror_mode": { 41 | "name": "ipython", 42 | "version": 3 43 | }, 44 | "file_extension": ".py", 45 | "mimetype": "text/x-python", 46 | "name": "python", 47 | "nbconvert_exporter": "python", 48 | "pygments_lexer": "ipython3", 49 | "version": "3.7.7" 50 | } 51 | }, 52 | "nbformat": 4, 53 | "nbformat_minor": 4 54 | } 55 | -------------------------------------------------------------------------------- /tests/invalid_data/tracker.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 'Tracker issue' 3 | about: *Internal template for PyMC documentation team only* 4 | labels: tracker id 5 | 6 | --- 7 | 8 | File: 9 | Reviewers: 10 | 11 | > The sections below may still be pending. If so, the issue is still available, it simply doesn't 12 | > have specific guidance yet. Please refer to [this overview of updates](https://github.com/pymc-devs/pymc-examples/wiki/Notebook-updates-overview) 13 | 14 | ## Known changes needed 15 | 16 | Changes listed in this section should all be done at some point in order to get this 17 | notebook to a "Best Practices" state. However, these are probably not enough! 18 | Make sure to thoroughly review the notebook and search for other updates. 19 | 20 | ### General updates 21 | 22 | - 23 | 24 | ### ArviZ related 25 | 26 | - 27 | 28 | ## Changes for discussion 29 | 30 | Changes listed in this section are up for discussion, these are ideas on how to improve 31 | the notebook but may not have a clear implementation, or fix some know issue only partially. 32 | 33 | ### General updates 34 | 35 | - 36 | 37 | ### ArviZ related 38 | 39 | - 40 | 41 | ## Notes 42 | 43 | ### Exotic dependencies 44 | 45 | ### Computing requirements 46 | -------------------------------------------------------------------------------- /docs/_templates/autosummary/class.rst: -------------------------------------------------------------------------------- 1 | {{ objname | escape | underline}} 2 | 3 | .. currentmodule:: {{ module }} 4 | 5 | .. autoclass:: {{ objname }} 6 | 7 | {% block attributes_summary %} 8 | {% if attributes %} 9 | 10 | .. rubric:: Attributes Summary 11 | 12 | .. autosummary:: 13 | {% for item in attributes %} 14 | ~{{ item }} 15 | {%- endfor %} 16 | 17 | {% endif %} 18 | {% endblock %} 19 | 20 | {% block methods_summary %} 21 | {% if methods %} 22 | 23 | {% if '__init__' in methods %} 24 | {% set caught_result = methods.remove('__init__') %} 25 | {% endif %} 26 | 27 | .. rubric:: Methods Summary 28 | 29 | .. autosummary:: 30 | :toctree: {{ objname }}/methods 31 | :nosignatures: 32 | 33 | {% for item in methods %} 34 | ~{{ name }}.{{ item }} 35 | {%- endfor %} 36 | 37 | {% endif %} 38 | {% endblock %} 39 | 40 | {% block methods_documentation %} 41 | {% if methods %} 42 | 43 | .. rubric:: Methods Documentation 44 | 45 | {% for item in methods %} 46 | .. automethod:: {{ name }}.{{ item }} 47 | :noindex: 48 | {%- endfor %} 49 | 50 | {% endif %} 51 | {% endblock %} 52 | -------------------------------------------------------------------------------- /docs/known-limitations.rst: -------------------------------------------------------------------------------- 1 | ================= 2 | Known limitations 3 | ================= 4 | 5 | By default, ``nbQA`` will skip cells with invalid syntax. 6 | If you choose to process cells with invalid syntax via the ``--nbqa-dont-skip-bad-cells`` flag (see :ref:`configuration`), 7 | then the following will still not be processed: 8 | 9 | - cells with multi-line magics; 10 | - automagics (ideas for how to detect them statically are welcome!); 11 | - cells with code which ``IPython`` would transform magics into (e.g. ``get_ipython().system('ls')``). 12 | 13 | Because ``nbQA`` converts the code cells in Jupyter notebooks to temporary Python files for linting, certain flags like ``flake8``'s 14 | ``--per-file-ignores`` don't work perfectly. 15 | The temporary Python files will not match the specified file patterns and ignored error codes will still 16 | surface (`GH issue `_). 17 | nbqa-generated temporary files will contain the string ``nbqa_ipynb``, 18 | so you can still apply per-file-ignores if you add an additional pattern: 19 | 20 | .. sourcecode:: ini 21 | 22 | [flake8] 23 | per-file-ignores = 24 | examples/*.ipynb: E402 25 | examples/*nbqa_ipynb.py: E402 26 | 27 | The directory and the stem of the filename are preserved, so e.g. ``path/to/mynotebook.ipynb`` will be ``path/to/mynotebook{randomstring}_nbqa_ipynb.py`` when nbqa passes it to the linter. 28 | 29 | Any other limitation is likely unintentional - if you run into any, please do report an issue. 30 | -------------------------------------------------------------------------------- /tests/data/notebook_for_testing.md: -------------------------------------------------------------------------------- 1 | --- 2 | jupytext: 3 | text_representation: 4 | extension: .md 5 | format_name: myst 6 | format_version: 0.13 7 | jupytext_version: 1.14.1 8 | kernelspec: 9 | display_name: Python 3 10 | language: python 11 | name: python3 12 | substitutions: 13 | extra_dependencies: bokeh 14 | --- 15 | 16 | ```{code-cell} ipython3 17 | :tags: [skip-flake8] 18 | 19 | import os 20 | 21 | import glob 22 | 23 | import nbqa 24 | ``` 25 | 26 | # Some markdown cell containing \\n 27 | 28 | +++ {"tags": ["skip-mdformat"]} 29 | 30 | # First level heading 31 | 32 | ```{code-cell} ipython3 33 | :tags: [flake8-skip] 34 | 35 | %%time foo 36 | def hello(name: str = "world\n"): 37 | """ 38 | Greet user. 39 | 40 | Examples 41 | -------- 42 | >>> hello() 43 | 'hello world\\n' 44 | 45 | >>> hello("goodbye") 46 | 'hello goodbye' 47 | """ 48 | 49 | return 'hello {}'.format(name) 50 | 51 | 52 | !ls 53 | hello(3) 54 | ``` 55 | 56 | ```python 57 | 2 +2 58 | ``` 59 | 60 | ```{code-cell} ipython3 61 | %%bash 62 | 63 | pwd 64 | ``` 65 | 66 | ```{code-cell} ipython3 67 | from random import randint 68 | 69 | if __debug__: 70 | %time randint(5,10) 71 | ``` 72 | 73 | ```{code-cell} ipython3 74 | import pprint 75 | import sys 76 | 77 | if __debug__: 78 | pretty_print_object = pprint.PrettyPrinter( 79 | indent=4, width=80, stream=sys.stdout, compact=True, depth=5 80 | ) 81 | 82 | pretty_print_object.isreadable(["Hello", "World"]) 83 | ``` 84 | -------------------------------------------------------------------------------- /tests/test_map_python_line_to_nb_lines.py: -------------------------------------------------------------------------------- 1 | """Check output from third-party tool is correctly parsed.""" 2 | 3 | from textwrap import dedent 4 | 5 | from nbqa.output_parser import map_python_line_to_nb_lines 6 | 7 | 8 | def test_map_python_line_to_nb_lines() -> None: 9 | """Check that the output is correctly parsed if there is a warning about line 0.""" 10 | out = "notebook.ipynb:0:1: WPS102 Found incorrect module name pattern" 11 | err = "" 12 | notebook = "notebook.ipynb" 13 | cell_mapping = {0: "cell_0:0"} 14 | result, _ = map_python_line_to_nb_lines("flake8", out, err, notebook, cell_mapping) 15 | expected = "notebook.ipynb:cell_0:0:1: WPS102 Found incorrect module name pattern" 16 | assert result == expected 17 | 18 | 19 | def test_black_unparseable_output() -> None: 20 | """Check that the output is correctly parsed if ``black`` fails to reformat.""" 21 | out = "" 22 | err = dedent( 23 | """\ 24 | error: cannot format notebook.ipynb: Cannot parse: 38:5: invalid syntax 25 | Oh no! 💥 💔 💥 26 | 1 file failed to reformat. 27 | """ 28 | ) 29 | notebook = "notebook.ipynb" 30 | cell_mapping = {38: "cell_10:1"} 31 | _, result = map_python_line_to_nb_lines("black", out, err, notebook, cell_mapping) 32 | expected = dedent( 33 | """\ 34 | error: cannot format notebook.ipynb: Cannot parse: cell_10:1:5: invalid syntax 35 | Oh no! 💥 💔 💥 36 | 1 file failed to reformat. 37 | """ 38 | ) 39 | assert result == expected 40 | -------------------------------------------------------------------------------- /.github/workflows/dispatch_pre-commit.yml: -------------------------------------------------------------------------------- 1 | name: run pre-commit 2 | 3 | on: 4 | repository_dispatch: 5 | types: [pre-commit-run-command] 6 | jobs: 7 | runPreCommit: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v4 11 | with: 12 | repository: ${{github.event.client_payload.pull_request.head.repo.full_name}} 13 | ref: ${{github.event.client_payload.pull_request.head.ref}} 14 | token: ${{ secrets.ACTION_TRIGGER_TOKEN }} 15 | - name: Cache multiple paths 16 | uses: actions/cache@v4 17 | env: 18 | # Increase this value to reset cache if requirements.txt has not changed 19 | CACHE_NUMBER: 0 20 | with: 21 | path: | 22 | ~/.cache/pip 23 | $RUNNER_TOOL_CACHE/Python/* 24 | ~\AppData\Local\pip\Cache 25 | ~/.cache/pre-commit 26 | key: ${{ runner.os }}-build-${{ matrix.python-version }}-${{ 27 | hashFiles('.pre-commit-config.yaml') }} 28 | - uses: actions/setup-python@v4 29 | with: 30 | python-version: 3.8 31 | - name: install-pre-commit 32 | run: python -m pip install --upgrade pre-commit 33 | - name: Slash Command Dispatch 34 | run: pre-commit run --all-files || (exit 0) 35 | - run: | 36 | git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com" 37 | git config --local user.name "github-actions[bot]" 38 | git commit -m "Run pre-commit" -a 39 | git push 40 | -------------------------------------------------------------------------------- /tests/tools/test_mdformat.py: -------------------------------------------------------------------------------- 1 | """Check mdformat works.""" 2 | 3 | import difflib 4 | import os 5 | from pathlib import Path 6 | from typing import TYPE_CHECKING 7 | 8 | from nbqa.__main__ import main 9 | 10 | if TYPE_CHECKING: 11 | from _pytest.capture import CaptureFixture 12 | 13 | 14 | def test_mdformat(tmp_notebook_for_testing: Path) -> None: 15 | """Check mdformat works""" 16 | with open(tmp_notebook_for_testing, encoding="utf-8") as handle: 17 | before = handle.readlines() 18 | path = os.path.join("tests", "data", "notebook_for_testing.ipynb") 19 | 20 | main(["mdformat", os.path.abspath(path), "--nbqa-md"]) 21 | with open(tmp_notebook_for_testing, encoding="utf-8") as handle: 22 | after = handle.readlines() 23 | 24 | diff = difflib.unified_diff(before, after) 25 | result = "".join(i for i in diff if any([i.startswith("+ "), i.startswith("- ")])) 26 | expected = ( 27 | '- "First level heading\\n",\n- "==="\n+ "# First level heading"\n' 28 | ) 29 | assert result == expected 30 | 31 | 32 | def test_mdformat_works_with_empty_file(capsys: "CaptureFixture") -> None: 33 | """ 34 | Check mdformat works with empty notebook. 35 | 36 | Parameters 37 | ---------- 38 | capsys 39 | Pytest fixture to capture stdout and stderr. 40 | """ 41 | path = os.path.abspath(os.path.join("tests", "data", "footer.ipynb")) 42 | 43 | main(["mdformat", path, "--nbqa-diff", "--nbqa-md"]) 44 | 45 | out, err = capsys.readouterr() 46 | assert out == "Notebook(s) would be left unchanged\n" 47 | assert err == "" 48 | -------------------------------------------------------------------------------- /tests/tools/test_blacken_docs.py: -------------------------------------------------------------------------------- 1 | """Check blacken-docs runs.""" 2 | 3 | import os 4 | from typing import TYPE_CHECKING 5 | 6 | from nbqa.__main__ import main 7 | 8 | if TYPE_CHECKING: 9 | from _pytest.capture import CaptureFixture 10 | 11 | 12 | def test_blacken_docs(capsys: "CaptureFixture") -> None: 13 | """ 14 | Check blacken-docs. 15 | 16 | Parameters 17 | ---------- 18 | capsys 19 | Pytest fixture to capture stdout and stderr. 20 | """ 21 | path = os.path.join("tests", "data", "notebook_for_testing.ipynb") 22 | main(["blacken-docs", path, "--nbqa-diff", "--nbqa-md"]) 23 | out, err = capsys.readouterr() 24 | expected_out = ( 25 | "\x1b[1mCell 2\x1b[0m\n" 26 | "------\n" 27 | f"\x1b[1;37m--- {path}\n" 28 | f"\x1b[0m\x1b[1;37m+++ {path}\n" 29 | "\x1b[0m\x1b[36m@@ -1 +1 @@\n" 30 | "\x1b[0m\x1b[31m-set(())\n" 31 | "\x1b[0m\x1b[32m+set()\n" 32 | "\x1b[0m\n" 33 | "To apply these changes, remove the `--nbqa-diff` flag\n" 34 | ) 35 | expected_out = ( 36 | "\x1b[1mCell 3\x1b[0m\n" 37 | "------\n" 38 | f"\x1b[1;37m--- {path}\n" 39 | f"\x1b[0m\x1b[1;37m+++ {path}\n" 40 | "\x1b[0m\x1b[36m@@ -1,3 +1,3 @@\n" 41 | "\x1b[0m\x1b[31m-2 +2\n" 42 | "\x1b[0m\x1b[32m+2 + 2\n" 43 | "\x1b[0m\n" 44 | f"{path}: Rewriting...\n" 45 | "To apply these changes, remove the `--nbqa-diff` flag\n" 46 | ) 47 | expected_err = "" 48 | assert out.replace("\r\n", "\n") == expected_out 49 | assert err.replace("\r\n", "\n") == expected_err 50 | -------------------------------------------------------------------------------- /tests/data/notebook_for_autoflake.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Notebook used for testing autoflake" 8 | ] 9 | }, 10 | { 11 | "cell_type": "code", 12 | "execution_count": null, 13 | "metadata": {}, 14 | "outputs": [], 15 | "source": [ 16 | "def say_hello(name: str):\n", 17 | " unused_var = \"not used\"\n", 18 | " print(\"hello\")" 19 | ] 20 | }, 21 | { 22 | "cell_type": "code", 23 | "execution_count": null, 24 | "metadata": {}, 25 | "outputs": [], 26 | "source": [ 27 | "# expand wildcard imports\n", 28 | "from os.path import *\n", 29 | "\n", 30 | "pwd_full_path = abspath(\".\")" 31 | ] 32 | }, 33 | { 34 | "cell_type": "code", 35 | "execution_count": null, 36 | "metadata": {}, 37 | "outputs": [], 38 | "source": [ 39 | "# unused imports\n", 40 | "import numpy as np\n", 41 | "import pandas as pd\n", 42 | "\n", 43 | "X = 5 * np.random.rand(10, 1)" 44 | ] 45 | } 46 | ], 47 | "metadata": { 48 | "kernelspec": { 49 | "display_name": "Python 3.8.0 64-bit ('env38')", 50 | "name": "python38064bitenv38402d408776534fc5af35f0cc2441a95a" 51 | }, 52 | "language_info": { 53 | "codemirror_mode": { 54 | "name": "ipython", 55 | "version": 3 56 | }, 57 | "file_extension": ".py", 58 | "mimetype": "text/x-python", 59 | "name": "python", 60 | "nbconvert_exporter": "python", 61 | "pygments_lexer": "ipython3", 62 | "version": "3.8.0-final" 63 | } 64 | }, 65 | "nbformat": 4, 66 | "nbformat_minor": 2 67 | } 68 | -------------------------------------------------------------------------------- /tests/data/simple_imports.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": { 7 | "tags": [ 8 | "skip-flake8" 9 | ] 10 | }, 11 | "outputs": [], 12 | "source": [ 13 | "import os\n", 14 | "\n", 15 | "import numpy as np" 16 | ] 17 | }, 18 | { 19 | "cell_type": "code", 20 | "execution_count": null, 21 | "metadata": {}, 22 | "outputs": [], 23 | "source": [ 24 | "cwd = os.getcwd()\n", 25 | "x = np.arange(1, 10)" 26 | ] 27 | }, 28 | { 29 | "cell_type": "code", 30 | "execution_count": null, 31 | "metadata": {}, 32 | "outputs": [], 33 | "source": [ 34 | "import os\n", 35 | "\n", 36 | "import numpy as np" 37 | ] 38 | }, 39 | { 40 | "cell_type": "code", 41 | "execution_count": null, 42 | "metadata": {}, 43 | "outputs": [], 44 | "source": [ 45 | "class Foo:\n", 46 | " ..." 47 | ] 48 | } 49 | ], 50 | "metadata": { 51 | "anaconda-cloud": {}, 52 | "kernelspec": { 53 | "display_name": ".venv", 54 | "language": "python", 55 | "name": "python3" 56 | }, 57 | "language_info": { 58 | "codemirror_mode": { 59 | "name": "ipython", 60 | "version": 3 61 | }, 62 | "file_extension": ".py", 63 | "mimetype": "text/x-python", 64 | "name": "python", 65 | "nbconvert_exporter": "python", 66 | "pygments_lexer": "ipython3", 67 | "version": "3.10.6" 68 | }, 69 | "vscode": { 70 | "interpreter": { 71 | "hash": "864e9cd7dbf32e150cc52d563f4c3a35053498b9e9d82e11d8edd2e505b61bc9" 72 | } 73 | } 74 | }, 75 | "nbformat": 4, 76 | "nbformat_minor": 4 77 | } 78 | -------------------------------------------------------------------------------- /tests/data/starting_with_comment.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "# default_exp core" 10 | ] 11 | }, 12 | { 13 | "cell_type": "markdown", 14 | "metadata": {}, 15 | "source": [ 16 | "# module name here\n", 17 | "\n", 18 | "> API details." 19 | ] 20 | }, 21 | { 22 | "cell_type": "code", 23 | "execution_count": null, 24 | "metadata": {}, 25 | "outputs": [], 26 | "source": [ 27 | "# hide\n", 28 | "from nbdev.showdoc import *" 29 | ] 30 | }, 31 | { 32 | "cell_type": "code", 33 | "execution_count": null, 34 | "metadata": {}, 35 | "outputs": [], 36 | "source": [ 37 | "# export\n", 38 | "def example_func(hi = \"yo\"):\n", 39 | " pass" 40 | ] 41 | }, 42 | { 43 | "cell_type": "code", 44 | "execution_count": null, 45 | "metadata": {}, 46 | "outputs": [], 47 | "source": [ 48 | "# hide\n", 49 | "from nbdev.export import notebook2script\n", 50 | "\n", 51 | "notebook2script()" 52 | ] 53 | } 54 | ], 55 | "metadata": { 56 | "kernelspec": { 57 | "display_name": "Python 3", 58 | "language": "python", 59 | "name": "python3" 60 | }, 61 | "language_info": { 62 | "codemirror_mode": { 63 | "name": "ipython", 64 | "version": 3 65 | }, 66 | "file_extension": ".py", 67 | "mimetype": "text/x-python", 68 | "name": "python", 69 | "nbconvert_exporter": "python", 70 | "pygments_lexer": "ipython3", 71 | "version": "3.7.8" 72 | } 73 | }, 74 | "nbformat": 4, 75 | "nbformat_minor": 4 76 | } 77 | -------------------------------------------------------------------------------- /tests/test_local_script.py: -------------------------------------------------------------------------------- 1 | """Tets running local script.""" 2 | 3 | import os 4 | from typing import TYPE_CHECKING 5 | 6 | import pytest 7 | 8 | from nbqa.__main__ import main 9 | 10 | if TYPE_CHECKING: 11 | from _pytest.capture import CaptureFixture 12 | 13 | 14 | def test_local_script() -> None: 15 | """Test local script is picked up.""" 16 | cwd = os.getcwd() 17 | os.chdir(os.path.join("tests", "invalid_data")) 18 | try: 19 | main(["foobarqux", "."]) 20 | finally: 21 | os.chdir(cwd) 22 | 23 | 24 | def test_local_module() -> None: 25 | """Test local module is picked up.""" 26 | cwd = os.getcwd() 27 | os.chdir(os.path.join("tests", "invalid_data")) 28 | try: 29 | main(["mymod", "."]) 30 | finally: 31 | os.chdir(cwd) 32 | 33 | 34 | def test_local_submodule() -> None: 35 | """Test local submodule is picked up.""" 36 | cwd = os.getcwd() 37 | os.chdir(os.path.join("tests", "invalid_data")) 38 | try: 39 | main(["mymod.mysubmod", "."]) 40 | finally: 41 | os.chdir(cwd) 42 | 43 | 44 | def test_local_nonfound() -> None: 45 | """Test local module is picked up.""" 46 | cwd = os.getcwd() 47 | os.chdir(os.path.join("tests", "invalid_data")) 48 | try: 49 | with pytest.raises(ModuleNotFoundError): 50 | main(["fdsfda", "."]) 51 | finally: 52 | os.chdir(cwd) 53 | 54 | 55 | def test_with_subcommand(capsys: "CaptureFixture") -> None: 56 | """Check subcommand is picked up by module.""" 57 | main(["tests.local_script foo", "."]) 58 | out, _ = capsys.readouterr() 59 | assert out.replace("\r\n", "\n") == "['foo']\n" 60 | -------------------------------------------------------------------------------- /tests/test_running_from_different_dir.py: -------------------------------------------------------------------------------- 1 | """Check configs are picked up when running in different directory.""" 2 | 3 | import os 4 | from pathlib import Path 5 | from textwrap import dedent 6 | from typing import TYPE_CHECKING 7 | 8 | import pytest 9 | 10 | from nbqa.__main__ import main 11 | 12 | if TYPE_CHECKING: 13 | from _pytest.capture import CaptureFixture 14 | 15 | 16 | @pytest.mark.parametrize( 17 | "arg, cwd", 18 | [ 19 | (Path("tests"), Path.cwd()), 20 | (Path("data"), Path.cwd() / "tests"), 21 | (Path("notebook_for_testing.ipynb"), Path.cwd() / "tests/data"), 22 | (Path.cwd() / "tests/data/notebook_for_testing.ipynb", Path.cwd().parent), 23 | ], 24 | ) 25 | def test_running_in_different_dir_works( 26 | arg: Path, cwd: Path, capsys: "CaptureFixture" 27 | ) -> None: 28 | """ 29 | Check .nbqa.ini config is picked up when running from non-root directory. 30 | 31 | Parameters 32 | ---------- 33 | arg 34 | Directory or notebook to run command on. 35 | cwd 36 | Directory from which to run command. 37 | """ 38 | config_path = Path("pyproject.toml") 39 | config_path.write_text( 40 | dedent( 41 | """\ 42 | [tool.nbqa.addopts] 43 | flake8 = ["--ignore=F401"] \ 44 | """ 45 | ), 46 | encoding="utf8", 47 | ) 48 | original_cwd = os.getcwd() 49 | try: 50 | os.chdir(str(cwd)) 51 | main(["flake8", str(arg)]) 52 | out, _ = capsys.readouterr() 53 | assert "W291" in out 54 | assert "F401" not in out 55 | finally: 56 | os.chdir(original_cwd) 57 | Path("pyproject.toml").unlink() 58 | -------------------------------------------------------------------------------- /tests/test_skip_bad_cells.py: -------------------------------------------------------------------------------- 1 | """Test the skip bad cells flag.""" 2 | 3 | import os 4 | from typing import TYPE_CHECKING 5 | 6 | from nbqa.__main__ import main 7 | 8 | if TYPE_CHECKING: 9 | from _pytest.capture import CaptureFixture 10 | 11 | 12 | def test_cmdline(capsys: "CaptureFixture") -> None: 13 | """Check running from command-line.""" 14 | file = os.path.join("tests", "invalid_data", "automagic.ipynb") 15 | main(["black", file, "--nbqa-diff"]) 16 | out, _ = capsys.readouterr() 17 | expected_out = ( 18 | "\x1b[1mCell 1\x1b[0m\n" 19 | "------\n" 20 | f"\x1b[1;37m--- {file}\n" 21 | f"\x1b[0m\x1b[1;37m+++ {file}\n" 22 | "\x1b[0m\x1b[36m@@ -1,2 +1,2 @@\n" 23 | "\x1b[0m\x1b[31m- print('definitely valid')\n" 24 | '\x1b[0m\x1b[32m+ print("definitely valid")\n' 25 | "\x1b[0m\n" 26 | "To apply these changes, remove the `--nbqa-diff` flag\n" 27 | ) 28 | assert out == expected_out 29 | 30 | 31 | def test_config_file(capsys: "CaptureFixture") -> None: 32 | """Test setting in config file.""" 33 | file = os.path.join("tests", "invalid_data", "automagic.ipynb") 34 | main(["black", file, "--nbqa-diff"]) 35 | out, _ = capsys.readouterr() 36 | expected_out = ( 37 | "\x1b[1mCell 1\x1b[0m\n" 38 | "------\n" 39 | f"\x1b[1;37m--- {file}\n" 40 | f"\x1b[0m\x1b[1;37m+++ {file}\n" 41 | "\x1b[0m\x1b[36m@@ -1,2 +1,2 @@\n" 42 | "\x1b[0m\x1b[31m- print('definitely valid')\n" 43 | '\x1b[0m\x1b[32m+ print("definitely valid")\n' 44 | "\x1b[0m\n" 45 | "To apply these changes, remove the `--nbqa-diff` flag\n" 46 | ) 47 | assert out == expected_out 48 | -------------------------------------------------------------------------------- /tests/data/notebook_for_testing_copy.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "import os\n", 10 | "\n", 11 | "import glob\n", 12 | "\n", 13 | "import nbqa" 14 | ] 15 | }, 16 | { 17 | "cell_type": "markdown", 18 | "metadata": {}, 19 | "source": [ 20 | "# Some markdown cell containing \\n" 21 | ] 22 | }, 23 | { 24 | "cell_type": "code", 25 | "execution_count": null, 26 | "metadata": {}, 27 | "outputs": [], 28 | "source": [ 29 | "%%time\n", 30 | "def hello(name: str = \"world\\n\"):\n", 31 | " \"\"\"\n", 32 | " Greet user.\n", 33 | "\n", 34 | " Examples\n", 35 | " --------\n", 36 | " >>> hello()\n", 37 | " 'hello world\\\\n'\n", 38 | " >>> hello(\"goodbye\")\n", 39 | " 'hello goodby'\n", 40 | " \"\"\"\n", 41 | " if True:\n", 42 | " %time # indented magic!\n", 43 | " return f'hello {name}'\n", 44 | "\n", 45 | "\n", 46 | "hello(3)" 47 | ] 48 | }, 49 | { 50 | "cell_type": "code", 51 | "execution_count": null, 52 | "metadata": {}, 53 | "outputs": [], 54 | "source": [] 55 | } 56 | ], 57 | "metadata": { 58 | "anaconda-cloud": {}, 59 | "kernelspec": { 60 | "display_name": "Python 3", 61 | "language": "python", 62 | "name": "python3" 63 | }, 64 | "language_info": { 65 | "codemirror_mode": { 66 | "name": "ipython", 67 | "version": 3 68 | }, 69 | "file_extension": ".py", 70 | "mimetype": "text/x-python", 71 | "name": "python", 72 | "nbconvert_exporter": "python", 73 | "pygments_lexer": "ipython3", 74 | "version": "3.7.7" 75 | } 76 | }, 77 | "nbformat": 4, 78 | "nbformat_minor": 4 79 | } 80 | -------------------------------------------------------------------------------- /docs/_templates/autosummary/module.rst: -------------------------------------------------------------------------------- 1 | {% block module %} 2 | {{ name | escape | underline}} 3 | 4 | .. currentmodule:: {{ module }} 5 | 6 | .. automodule:: {{ fullname }} 7 | 8 | 9 | {% block modules %} 10 | {% if modules %} 11 | 12 | .. rubric:: Modules 13 | 14 | .. autosummary:: 15 | :toctree: {{ name }} 16 | :recursive: 17 | 18 | {% for item in modules %} 19 | {{ item }} 20 | {%- endfor %} 21 | 22 | {% endif %} 23 | 24 | {% endblock %} 25 | 26 | 27 | {% block attributes %} 28 | {% if attributes %} 29 | .. rubric:: Module Attributes 30 | 31 | .. autosummary:: 32 | :toctree: {{ name }} 33 | 34 | {% for item in attributes %} 35 | {{ item }} 36 | {%- endfor %} 37 | 38 | {% endif %} 39 | 40 | {% endblock %} 41 | 42 | {% block functions %} 43 | {% if functions %} 44 | 45 | 46 | .. rubric:: Functions 47 | 48 | .. autosummary:: 49 | :toctree: {{ name }}/functions 50 | :nosignatures: 51 | 52 | {% for item in functions %} 53 | {{ item }} 54 | {%- endfor %} 55 | 56 | {% endif %} 57 | 58 | {% endblock %} 59 | 60 | {% block classes %} 61 | {% if classes %} 62 | 63 | .. rubric:: Classes 64 | 65 | .. autosummary:: 66 | :toctree: {{ name }}/classes 67 | :nosignatures: 68 | 69 | {% for item in classes %} 70 | {{ item }} 71 | {%- endfor %} 72 | 73 | {% endif %} 74 | 75 | {% endblock %} 76 | 77 | 78 | {% block exceptions %} 79 | {% if exceptions %} 80 | 81 | 82 | .. rubric:: Exceptions 83 | 84 | .. autosummary:: 85 | :toctree: {{ name }}/exceptions 86 | :nosignatures: 87 | 88 | {% for item in exceptions %} 89 | {{ item }} 90 | {%- endfor %} 91 | 92 | {% endif %} 93 | 94 | {% endblock %} 95 | 96 | 97 | {% endblock %} 98 | -------------------------------------------------------------------------------- /tests/data/footer.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "%%javascript\n", 10 | "\n", 11 | "// Code to dynamically generate table of contents at the top of the HTML file\n", 12 | "var tocEntries = ['')\n", 25 | " subList = false;\n", 26 | " }\n", 27 | " tocEntries.push('
  • ' + hText + '
  • ')\n", 28 | " }\n", 29 | " else if (hType == 'H3') {\n", 30 | " if (!subList) {\n", 31 | " subList = true;\n", 32 | " tocEntries.push('')\n", 38 | "$('#toc').html(tocEntries.join(' '))" 39 | ] 40 | } 41 | ], 42 | "metadata": { 43 | "kernelspec": { 44 | "display_name": "Python 3", 45 | "language": "python", 46 | "name": "python3" 47 | }, 48 | "language_info": { 49 | "codemirror_mode": { 50 | "name": "ipython", 51 | "version": 3 52 | }, 53 | "file_extension": ".py", 54 | "mimetype": "text/x-python", 55 | "name": "python", 56 | "nbconvert_exporter": "python", 57 | "pygments_lexer": "ipython3", 58 | "version": "3.6.4" 59 | } 60 | }, 61 | "nbformat": 4, 62 | "nbformat_minor": 1 63 | } 64 | -------------------------------------------------------------------------------- /tests/data/notebook_starting_with_md.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "Let's start with a markdown cell, shall we?" 8 | ] 9 | }, 10 | { 11 | "cell_type": "code", 12 | "execution_count": null, 13 | "metadata": {}, 14 | "outputs": [], 15 | "source": [ 16 | "import os\n", 17 | "\n", 18 | "import glob\n", 19 | "\n", 20 | "import nbqa" 21 | ] 22 | }, 23 | { 24 | "cell_type": "code", 25 | "execution_count": null, 26 | "metadata": {}, 27 | "outputs": [], 28 | "source": [] 29 | }, 30 | { 31 | "cell_type": "markdown", 32 | "metadata": {}, 33 | "source": [ 34 | "# Some markdown cell containing \\n" 35 | ] 36 | }, 37 | { 38 | "cell_type": "code", 39 | "execution_count": null, 40 | "metadata": {}, 41 | "outputs": [], 42 | "source": [ 43 | "%time\n", 44 | "def hello(name: str = \"world\\n\"):\n", 45 | " \"\"\"\n", 46 | " Greet user.\n", 47 | "\n", 48 | " Examples\n", 49 | " --------\n", 50 | " >>> hello()\n", 51 | " 'hello world\\\\n'\n", 52 | "\n", 53 | " >>> hello(\"goodbye\")\n", 54 | " 'hello goodbye'\n", 55 | " \"\"\"\n", 56 | "\n", 57 | " return f'hello {name}'\n", 58 | "\n", 59 | "\n", 60 | "hello(3)" 61 | ] 62 | }, 63 | { 64 | "cell_type": "code", 65 | "execution_count": null, 66 | "metadata": {}, 67 | "outputs": [], 68 | "source": [] 69 | } 70 | ], 71 | "metadata": { 72 | "anaconda-cloud": {}, 73 | "kernelspec": { 74 | "display_name": "Python 3", 75 | "language": "python", 76 | "name": "python3" 77 | }, 78 | "language_info": { 79 | "codemirror_mode": { 80 | "name": "ipython", 81 | "version": 3 82 | }, 83 | "file_extension": ".py", 84 | "mimetype": "text/x-python", 85 | "name": "python", 86 | "nbconvert_exporter": "python", 87 | "pygments_lexer": "ipython3", 88 | "version": "3.7.7" 89 | } 90 | }, 91 | "nbformat": 4, 92 | "nbformat_minor": 4 93 | } 94 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = nbqa 3 | version = attr: nbqa.__version__ 4 | description = Run any standard Python code quality tool on a Jupyter Notebook 5 | long_description = file: README.md 6 | long_description_content_type = text/markdown 7 | url = https://github.com/nbQA-dev/nbQA 8 | author = Marco Gorelli, Girish Pasupathy, Sebastian Weigand 9 | license = MIT 10 | license_files = LICENSE 11 | classifiers = 12 | Development Status :: 4 - Beta 13 | Environment :: Console 14 | Framework :: Jupyter 15 | Intended Audience :: Developers 16 | Natural Language :: English 17 | Operating System :: OS Independent 18 | Programming Language :: Python :: 3 19 | Programming Language :: Python :: 3 :: Only 20 | Topic :: Software Development :: Quality Assurance 21 | keywords = jupyter, notebook, format, lint 22 | project_urls = 23 | Documentation = https://nbQA.readthedocs.io/en/latest/ 24 | Source = https://github.com/nbQA-dev/nbQA 25 | Tracker = https://github.com/nbQA-dev/nbQA/issues 26 | 27 | [options] 28 | packages = find: 29 | py_modules = nbqa 30 | install_requires = 31 | autopep8>=1.5 32 | ipython>=7.8.0 33 | tokenize-rt>=3.2.0 34 | tomli 35 | python_requires = >=3.9 36 | 37 | [options.packages.find] 38 | exclude = 39 | tests* 40 | 41 | [options.entry_points] 42 | console_scripts = 43 | nbqa = nbqa.__main__:main 44 | 45 | [options.extras_require] 46 | toolchain = 47 | black 48 | blacken-docs 49 | flake8 50 | isort 51 | jupytext 52 | mypy 53 | pylint 54 | pyupgrade 55 | ruff 56 | 57 | [flake8] 58 | extend-ignore = E203,E503 59 | max-line-length = 120 60 | exclude = venv,.* 61 | 62 | [darglint] 63 | ignore = DAR101,DAR103,DAR201,DAR301 64 | docstring_style = numpy 65 | 66 | [pydocstyle] 67 | add-ignore = D104 68 | 69 | [mypy] 70 | strict = True 71 | allow_untyped_decorators = True 72 | 73 | [mypy-pytest] 74 | ignore_missing_imports = True 75 | 76 | [mypy-_pytest.capture] 77 | ignore_missing_imports = True 78 | 79 | [mypy-setuptools] 80 | ignore_missing_imports = True 81 | 82 | [mypy-setup] 83 | ignore_errors = True 84 | 85 | [mypy-conf] 86 | ignore_errors = True 87 | -------------------------------------------------------------------------------- /tests/test_nbqa_diff.py: -------------------------------------------------------------------------------- 1 | """Check --nbqa-diff flag.""" 2 | 3 | import os 4 | from pathlib import Path 5 | from typing import TYPE_CHECKING 6 | 7 | from nbqa.__main__ import main 8 | 9 | if TYPE_CHECKING: 10 | from _pytest.capture import CaptureFixture 11 | 12 | 13 | SPARKLES = "\N{SPARKLES}" 14 | SHORTCAKE = "\N{SHORTCAKE}" 15 | COLLISION = "\N{COLLISION SYMBOL}" 16 | BROKEN_HEART = "\N{BROKEN HEART}" 17 | TESTS_DIR = Path("tests") 18 | TEST_DATA_DIR = TESTS_DIR / "data" 19 | 20 | DIRTY_NOTEBOOK = TEST_DATA_DIR / "notebook_for_testing.ipynb" 21 | CLEAN_NOTEBOOK = TEST_DATA_DIR / "clean_notebook.ipynb" 22 | 23 | 24 | def test_diff_present(capsys: "CaptureFixture") -> None: 25 | """Test the results on --nbqa-diff on a dirty notebook.""" 26 | main(["black", str(DIRTY_NOTEBOOK), "--nbqa-diff"]) 27 | out, err = capsys.readouterr() 28 | expected_out = ( 29 | "\x1b[1mCell 2\x1b[0m\n" 30 | "------\n" 31 | f"\x1b[1;37m--- {str(DIRTY_NOTEBOOK)}\n" 32 | f"\x1b[0m\x1b[1;37m+++ {str(DIRTY_NOTEBOOK)}\n" 33 | "\x1b[0m\x1b[36m@@ -12,8 +12,8 @@\n" 34 | "\x1b[0m\x1b[31m- return 'hello {}'.format(name)\n" 35 | '\x1b[0m\x1b[32m+ return "hello {}".format(name)\n' 36 | "\x1b[0m\x1b[31m-hello(3) \n" 37 | "\x1b[0m\x1b[32m+hello(3)\n" 38 | "\x1b[0m\n" 39 | "To apply these changes, remove the `--nbqa-diff` flag\n" 40 | ) 41 | assert out == expected_out 42 | assert "1 file reformatted" in err 43 | 44 | 45 | def test_invalid_syntax_with_nbqa_diff(capsys: "CaptureFixture") -> None: 46 | """ 47 | Check that using nbqa-diff when there's invalid syntax doesn't have empty output. 48 | 49 | Parameters 50 | ---------- 51 | capsys 52 | Pytest fixture to capture stdout and stderr. 53 | """ 54 | path = os.path.join("tests", "invalid_data", "assignment_to_literal.ipynb") 55 | 56 | main(["black", os.path.abspath(path), "--nbqa-diff", "--nbqa-dont-skip-bad-cells"]) 57 | 58 | out, err = capsys.readouterr() 59 | expected_out = "Notebook(s) would be left unchanged\n" 60 | assert expected_out == out 61 | assert "1 file failed to reformat" in err 62 | -------------------------------------------------------------------------------- /nbqa/find_root.py: -------------------------------------------------------------------------------- 1 | """ 2 | Find project root. 3 | 4 | Taken from https://github.com/psf/black/blob/master/src/black/__init__.py 5 | """ 6 | 7 | from functools import lru_cache 8 | from pathlib import Path 9 | from typing import Iterable 10 | 11 | # files and folders known to indicate a project root 12 | KNOWN_PROJECT_ROOT_DIRS = [".git", ".hg"] 13 | KNOW_PROJECT_ROOT_FILES = [ 14 | "setup.py", 15 | "setup.cfg", 16 | "pyproject.toml", 17 | "MANIFEST.in", 18 | ] 19 | 20 | 21 | @lru_cache 22 | def find_project_root( 23 | srcs: Iterable[str], 24 | root_files: Iterable[str] = tuple(KNOW_PROJECT_ROOT_FILES), 25 | root_dirs: Iterable[str] = tuple(KNOWN_PROJECT_ROOT_DIRS), 26 | ) -> Path: 27 | """ 28 | Return a directory containing .git, .hg, or nbqa.ini. 29 | 30 | That directory will be a common parent of all files and directories 31 | passed in `srcs`. 32 | If no directory in the tree contains a marker that would specify it's the 33 | project root, the root of the file system is returned. 34 | 35 | Parameters 36 | ---------- 37 | srcs 38 | Source paths. 39 | root_files 40 | Files indicating that the current directory is the project root. 41 | 42 | Returns 43 | ------- 44 | Path 45 | Project root. 46 | """ 47 | path_srcs = [Path(Path.cwd(), src).resolve() for src in srcs] 48 | 49 | # A list of lists of parents for each 'src'. 'src' is included as a 50 | # "parent" of itself if it is a directory 51 | src_parents = [ 52 | list(path.parents) + ([path] if path.is_dir() else []) for path in path_srcs 53 | ] 54 | 55 | common_base = max( 56 | set.intersection(*(set(parents) for parents in src_parents)), 57 | key=lambda path: path.parts, 58 | ) 59 | 60 | for directory in (common_base, *common_base.parents): 61 | for known_project_root_dir in root_dirs: 62 | if (directory / known_project_root_dir).is_dir(): 63 | return directory 64 | for know_project_root_file in root_files: 65 | if (directory / know_project_root_file).is_file(): 66 | return directory 67 | 68 | return Path("/").resolve() 69 | -------------------------------------------------------------------------------- /tests/tools/test_mypy_works.py: -------------------------------------------------------------------------------- 1 | """Check :code:`mypy` works as intended.""" 2 | 3 | import os 4 | from pathlib import Path 5 | from textwrap import dedent 6 | from typing import TYPE_CHECKING 7 | 8 | from nbqa.__main__ import main 9 | 10 | if TYPE_CHECKING: 11 | from _pytest.capture import CaptureFixture 12 | 13 | 14 | def test_mypy_works(capsys: "CaptureFixture") -> None: 15 | """ 16 | Check mypy works. Shouldn't alter the notebook content. 17 | 18 | Parameters 19 | ---------- 20 | capsys 21 | Pytest fixture to capture stdout and stderr. 22 | """ 23 | main( 24 | [ 25 | "mypy", 26 | "--ignore-missing-imports", 27 | "--allow-untyped-defs", 28 | str(Path("tests") / "data"), 29 | ] 30 | ) 31 | 32 | # check out and err 33 | out, err = capsys.readouterr() 34 | expected_out = dedent( 35 | """\ 36 | has incompatible type 37 | has incompatible type 38 | has incompatible type 39 | """ 40 | ) 41 | # Unfortunately, the colours don't show up in CI. Seems to work fine locally though. 42 | # So, we can only do a partial test. 43 | for result, expected in zip( 44 | sorted(out.splitlines()[:-1]), sorted(expected_out.splitlines()) 45 | ): 46 | assert expected in result 47 | assert err == "" 48 | 49 | 50 | def test_mypy_with_local_import(capsys: "CaptureFixture") -> None: 51 | """ 52 | Check mypy can find local import. 53 | 54 | Parameters 55 | ---------- 56 | capsys 57 | Pytest fixture to capture stdout and stderr 58 | """ 59 | main( 60 | [ 61 | "mypy", 62 | str(Path("tests") / "data/notebook_with_local_import.ipynb"), 63 | ] 64 | ) 65 | 66 | # check out and err 67 | out, _ = capsys.readouterr() 68 | expected = "Success: no issues found in 1 source file" 69 | assert expected in out 70 | 71 | 72 | def test_notebook_doesnt_shadow_python_module(capsys: "CaptureFixture") -> None: 73 | """Check that notebook with same name as a Python file doesn't overshadow it.""" 74 | cwd = os.getcwd() 75 | try: 76 | os.chdir(os.path.join("tests", "data")) 77 | main(["mypy", "t.ipynb"]) 78 | finally: 79 | os.chdir(cwd) 80 | result, _ = capsys.readouterr() 81 | expected = "Success: no issues found in 1 source file" 82 | assert expected in result 83 | -------------------------------------------------------------------------------- /tests/tools/test_yapf.py: -------------------------------------------------------------------------------- 1 | """Check that :code:`yapf` works as intended.""" 2 | 3 | import os 4 | from pathlib import Path 5 | from shutil import copyfile 6 | from typing import TYPE_CHECKING 7 | 8 | from nbqa.__main__ import main 9 | 10 | if TYPE_CHECKING: 11 | from _pytest.capture import CaptureFixture 12 | from py._path.local import LocalPath 13 | 14 | 15 | def test_successive_runs_using_yapf( 16 | tmpdir: "LocalPath", capsys: "CaptureFixture" 17 | ) -> None: 18 | """Check yapf returns 0 on the second run given a dirty notebook.""" 19 | src_notebook = Path(os.path.join("tests", "data", "notebook_for_testing.ipynb")) 20 | test_notebook = Path(tmpdir) / src_notebook.name 21 | copyfile(src_notebook, test_notebook) 22 | main(["yapf", str(test_notebook), "--in-place", "--nbqa-diff"]) 23 | out, _ = capsys.readouterr() 24 | expected_out = ( 25 | "\x1b[1mCell 2\x1b[0m\n" 26 | "------\n" 27 | f"\x1b[1;37m--- {str(test_notebook)}\n" 28 | f"\x1b[0m\x1b[1;37m+++ {str(test_notebook)}\n" 29 | "\x1b[0m\x1b[36m@@ -16,4 +16,4 @@\n" 30 | "\x1b[0m\x1b[31m-hello(3) \n" 31 | "\x1b[0m\x1b[32m+hello(3)\n" 32 | "\x1b[0m\n" 33 | "\x1b[1mCell 5\x1b[0m\n" 34 | "------\n" 35 | f"\x1b[1;37m--- {str(test_notebook)}\n" 36 | f"\x1b[0m\x1b[1;37m+++ {str(test_notebook)}\n" 37 | "\x1b[0m\x1b[36m@@ -2,8 +2,10 @@\n" 38 | "\x1b[0m\x1b[31m- pretty_print_object = pprint.PrettyPrinter(\n" 39 | "\x1b[0m\x1b[31m- indent=4, width=80, stream=sys.stdout, compact=True, depth=5\n" 40 | "\x1b[0m\x1b[31m- )\n" 41 | "\x1b[0m\x1b[32m+ pretty_print_object = pprint.PrettyPrinter(indent=4,\n" 42 | "\x1b[0m\x1b[32m+ width=80,\n" 43 | "\x1b[0m\x1b[32m+ stream=sys.stdout,\n" 44 | "\x1b[0m\x1b[32m+ compact=True,\n" 45 | "\x1b[0m\x1b[32m+ depth=5)\n" 46 | "\x1b[0m\nTo apply these changes, remove the `--nbqa-diff` flag\n" 47 | ) 48 | assert out == expected_out 49 | 50 | main(["yapf", str(test_notebook), "--in-place"]) 51 | main(["yapf", str(test_notebook), "--in-place", "--nbqa-diff"]) 52 | 53 | out, _ = capsys.readouterr() 54 | expected_out = "Notebook(s) would be left unchanged\n" 55 | assert out == expected_out 56 | -------------------------------------------------------------------------------- /tests/test_pyproject_toml.py: -------------------------------------------------------------------------------- 1 | """Check configs from :code:`pyproject.toml` are picked up.""" 2 | 3 | import os 4 | from pathlib import Path 5 | from textwrap import dedent 6 | from typing import TYPE_CHECKING 7 | 8 | from nbqa.__main__ import main 9 | 10 | if TYPE_CHECKING: 11 | from _pytest.capture import CaptureFixture 12 | 13 | 14 | def test_pyproject_toml_works(capsys: "CaptureFixture") -> None: 15 | """ 16 | Check if config is picked up from pyproject.toml works. 17 | 18 | Parameters 19 | ---------- 20 | capsys 21 | Pytest fixture to capture stdout and stderr. 22 | """ 23 | Path("pyproject.toml").write_text( 24 | dedent( 25 | """ 26 | [tool.nbqa.addopts] 27 | flake8 = [ 28 | "--ignore=F401,E302", 29 | "--select=E303", 30 | ] 31 | """ 32 | ), 33 | encoding="utf-8", 34 | ) 35 | 36 | main(["flake8", "tests"]) 37 | Path("pyproject.toml").unlink() 38 | 39 | out, _ = capsys.readouterr() 40 | expected_out = "" 41 | assert out == expected_out 42 | 43 | 44 | def test_cli_extends_pyprojecttoml(capsys: "CaptureFixture") -> None: 45 | """ 46 | Check CLI args overwrite pyproject.toml 47 | 48 | Parameters 49 | ---------- 50 | capsys 51 | Pytest fixture to capture stdout and stderr. 52 | """ 53 | Path("pyproject.toml").write_text( 54 | dedent( 55 | """ 56 | [tool.nbqa.addopts] 57 | flake8 = [ 58 | "--ignore=F401", 59 | ] 60 | """ 61 | ), 62 | encoding="utf-8", 63 | ) 64 | 65 | main( 66 | [ 67 | "flake8", 68 | os.path.join("tests", "data", "notebook_for_testing.ipynb"), 69 | "--ignore=E402,W291", 70 | ] 71 | ) 72 | out, _ = capsys.readouterr() 73 | Path("pyproject.toml").unlink() 74 | 75 | # if arguments are specified on command line, they will take precedence 76 | # over those specified in the pyproject.toml 77 | notebook_path = os.path.join("tests", "data", "notebook_for_testing.ipynb") 78 | expected_out = dedent( 79 | f"""\ 80 | {notebook_path}:cell_1:1:1: F401 'os' imported but unused 81 | {notebook_path}:cell_1:3:1: F401 'glob' imported but unused 82 | {notebook_path}:cell_1:5:1: F401 'nbqa' imported but unused 83 | {notebook_path}:cell_4:1:1: F401 'random.randint' imported but unused 84 | """ 85 | ) 86 | assert out == expected_out 87 | -------------------------------------------------------------------------------- /tests/test_skip_celltags.py: -------------------------------------------------------------------------------- 1 | """Test the skip_celltags option.""" 2 | 3 | import os 4 | from typing import TYPE_CHECKING 5 | 6 | from nbqa.__main__ import main 7 | 8 | if TYPE_CHECKING: 9 | from _pytest.capture import CaptureFixture 10 | 11 | 12 | def test_skip_celltags_cli(capsys: "CaptureFixture") -> None: 13 | """ 14 | Check flake8 works. Shouldn't alter the notebook content. 15 | 16 | Parameters 17 | ---------- 18 | capsys 19 | Pytest fixture to capture stdout and stderr. 20 | """ 21 | # check passing both absolute and relative paths 22 | 23 | path = os.path.join("tests", "data", "notebook_for_testing.ipynb") 24 | main(["flake8", path, "--nbqa-skip-celltags=skip-flake8,flake8-skip"]) 25 | 26 | out, err = capsys.readouterr() 27 | expected_out = f"{path}:cell_4:1:1: F401 'random.randint' imported but unused\n" 28 | expected_err = "" 29 | 30 | assert out == expected_out 31 | assert err == expected_err 32 | 33 | 34 | def test_skip_celltags_cli_md(capsys: "CaptureFixture") -> None: 35 | """ 36 | Check flake8 works. Shouldn't alter the notebook content. 37 | 38 | Parameters 39 | ---------- 40 | capsys 41 | Pytest fixture to capture stdout and stderr. 42 | """ 43 | # check passing both absolute and relative paths 44 | 45 | path = os.path.join("tests", "data", "notebook_for_testing.ipynb") 46 | main( 47 | [ 48 | "mdformat", 49 | path, 50 | "--nbqa-skip-celltags=skip-mdformat", 51 | "--nbqa-md", 52 | "--nbqa-diff", 53 | ] 54 | ) 55 | 56 | out, err = capsys.readouterr() 57 | expected_out = "Notebook(s) would be left unchanged\n" 58 | expected_err = "" 59 | 60 | assert out == expected_out 61 | assert err == expected_err 62 | 63 | 64 | def test_skip_celltags_pyprojecttoml(capsys: "CaptureFixture") -> None: 65 | """ 66 | Check flake8 works. Shouldn't alter the notebook content. 67 | 68 | Parameters 69 | ---------- 70 | capsys 71 | Pytest fixture to capture stdout and stderr. 72 | """ 73 | # check passing both absolute and relative paths 74 | with open("pyproject.toml", "w", encoding="utf-8") as handle: 75 | handle.write( 76 | "[tool.nbqa.skip_celltags]\nflake8 = ['skip-flake8', 'flake8-skip']\n" 77 | ) 78 | path = os.path.join("tests", "data", "notebook_for_testing.ipynb") 79 | main(["flake8", path]) 80 | 81 | out, err = capsys.readouterr() 82 | expected_out = f"{path}:cell_4:1:1: F401 'random.randint' imported but unused\n" 83 | expected_err = "" 84 | 85 | assert out == expected_out 86 | assert err == expected_err 87 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v5.0.0 4 | hooks: 5 | - id: end-of-file-fixer 6 | exclude: ^tests/data/ 7 | - id: requirements-txt-fixer 8 | - id: trailing-whitespace 9 | - id: debug-statements 10 | - repo: https://github.com/pre-commit/pre-commit 11 | rev: v4.2.0 12 | hooks: 13 | - id: validate_manifest 14 | - repo: https://github.com/hauntsaninja/black-pre-commit-mirror 15 | # black compiled with mypyc 16 | rev: 25.1.0 17 | hooks: 18 | - id: black 19 | - repo: https://github.com/PyCQA/pylint 20 | rev: v3.3.7 21 | hooks: 22 | - id: pylint 23 | files: ^(nbqa|tests)/ 24 | exclude: ^tests/data/ 25 | - repo: https://github.com/PyCQA/flake8 26 | rev: 7.2.0 27 | hooks: 28 | - id: flake8 29 | additional_dependencies: [flake8-typing-imports==1.14.0] 30 | exclude: tests/data 31 | - repo: https://github.com/PyCQA/isort 32 | rev: 6.0.1 33 | hooks: 34 | - id: isort 35 | - repo: https://github.com/PyCQA/pydocstyle 36 | rev: 6.3.0 37 | hooks: 38 | - id: pydocstyle 39 | files: ^nbqa/ 40 | - repo: https://github.com/pre-commit/mirrors-mypy 41 | rev: v1.16.0 42 | hooks: 43 | - id: mypy 44 | exclude: ^docs/ 45 | additional_dependencies: [types-setuptools, types-toml, tokenize-rt] 46 | - repo: https://github.com/asottile/pyupgrade 47 | rev: v3.20.0 48 | hooks: 49 | - id: pyupgrade 50 | args: [--py38-plus] 51 | - repo: https://github.com/MarcoGorelli/auto-walrus 52 | rev: 0.3.4 53 | hooks: 54 | - id: auto-walrus 55 | - repo: https://github.com/codespell-project/codespell 56 | rev: v2.4.1 57 | hooks: 58 | - id: codespell 59 | files: \.(py|rst|md)$ 60 | - repo: https://github.com/terrencepreilly/darglint 61 | rev: v1.8.1 62 | hooks: 63 | - id: darglint 64 | - repo: https://github.com/pre-commit/pygrep-hooks 65 | rev: v1.10.0 66 | hooks: 67 | - id: rst-backticks 68 | - id: rst-directive-colons 69 | - id: rst-inline-touching-normal 70 | - repo: https://github.com/asottile/setup-cfg-fmt 71 | rev: v2.8.0 72 | hooks: 73 | - id: setup-cfg-fmt 74 | - repo: https://github.com/PyCQA/autoflake 75 | rev: v2.3.1 76 | hooks: 77 | - id: autoflake 78 | args: [--remove-all-unused-imports, -i] 79 | - repo: meta 80 | hooks: 81 | - id: check-hooks-apply 82 | - id: check-useless-excludes 83 | - repo: https://github.com/kynan/nbstripout 84 | rev: 0.8.1 85 | hooks: 86 | - id: nbstripout 87 | exclude: ^tests/data/(databricks_notebook|notebook_for_testing)\.ipynb$ 88 | -------------------------------------------------------------------------------- /tests/tools/test_autopep8.py: -------------------------------------------------------------------------------- 1 | """Check that :code:`autopep8` works as intended.""" 2 | 3 | import os 4 | import sys 5 | from pathlib import Path 6 | from shutil import copyfile 7 | from typing import TYPE_CHECKING 8 | 9 | import pytest 10 | 11 | from nbqa.__main__ import main 12 | 13 | if TYPE_CHECKING: 14 | from _pytest.capture import CaptureFixture 15 | from py._path.local import LocalPath 16 | 17 | 18 | @pytest.mark.skipif( 19 | sys.version_info >= (3, 11), reason="Some deprecation warning shows up" 20 | ) 21 | def test_successive_runs_using_autopep8( 22 | tmpdir: "LocalPath", capsys: "CaptureFixture" 23 | ) -> None: 24 | """Check autopep8 returns 0 on the second run given a dirty notebook.""" 25 | src_notebook = Path(os.path.join("tests", "data", "notebook_for_testing.ipynb")) 26 | test_notebook = Path(tmpdir) / src_notebook.name 27 | copyfile(src_notebook, test_notebook) 28 | main(["autopep8", str(test_notebook), "-i", "--nbqa-diff"]) 29 | out, _ = capsys.readouterr() 30 | expected_out = ( 31 | "\x1b[1mCell 1\x1b[0m\n" 32 | "------\n" 33 | f"\x1b[1;37m--- {str(test_notebook)}\n" 34 | f"\x1b[0m\x1b[1;37m+++ {str(test_notebook)}\n" 35 | "\x1b[0m\x1b[36m@@ -1,3 +1,6 @@\n" 36 | "\x1b[0m\x1b[32m+import sys\n" 37 | "\x1b[0m\x1b[32m+import pprint\n" 38 | "\x1b[0m\x1b[32m+from random import randint\n" 39 | "\x1b[0m\n" 40 | "\x1b[1mCell 2\x1b[0m\n" 41 | "------\n" 42 | f"\x1b[1;37m--- {str(test_notebook)}\n" 43 | f"\x1b[0m\x1b[1;37m+++ {str(test_notebook)}\n" 44 | "\x1b[0m\x1b[36m@@ -16,4 +16,4 @@\n" 45 | "\x1b[0m\x1b[31m-hello(3) \n" 46 | "\x1b[0m\x1b[32m+hello(3)\n" 47 | "\x1b[0m\n" 48 | "\x1b[1mCell 4\x1b[0m\n" 49 | "------\n" 50 | f"\x1b[1;37m--- {str(test_notebook)}\n" 51 | f"\x1b[0m\x1b[1;37m+++ {str(test_notebook)}\n" 52 | "\x1b[0m\x1b[36m@@ -1,4 +1,2 @@\n" 53 | "\x1b[0m\x1b[31m-from random import randint\n" 54 | "\x1b[0m\x1b[31m-\n" 55 | "\x1b[0m\n" 56 | "\x1b[1mCell 5\x1b[0m\n" 57 | "------\n" 58 | f"\x1b[1;37m--- {str(test_notebook)}\n" 59 | f"\x1b[0m\x1b[1;37m+++ {str(test_notebook)}\n" 60 | "\x1b[0m\x1b[36m@@ -1,6 +1,3 @@\n" 61 | "\x1b[0m\x1b[31m-import pprint\n" 62 | "\x1b[0m\x1b[31m-import sys\n" 63 | "\x1b[0m\x1b[31m-\n" 64 | "\x1b[0m\n" 65 | "To apply these changes, remove the `--nbqa-diff` flag\n" 66 | ) 67 | assert out == expected_out 68 | 69 | main(["autopep8", str(test_notebook), "-i"]) 70 | main(["autopep8", str(test_notebook), "-i", "--nbqa-diff"]) 71 | 72 | out, err = capsys.readouterr() 73 | assert out == "Notebook(s) would be left unchanged\n" 74 | assert err == "" 75 | -------------------------------------------------------------------------------- /tests/tools/test_doctest.py: -------------------------------------------------------------------------------- 1 | """Check that running :code:`doctest` works.""" 2 | 3 | import os 4 | import sys 5 | from typing import TYPE_CHECKING 6 | 7 | from nbqa.__main__ import main 8 | 9 | if TYPE_CHECKING: 10 | from _pytest.capture import CaptureFixture 11 | GOOD_EXAMPLE_NOTEBOOK = os.path.join("tests", "data", "notebook_for_testing.ipynb") 12 | WRONG_EXAMPLE_NOTEBOOK = os.path.join( 13 | "tests", "data", "notebook_for_testing_copy.ipynb" 14 | ) 15 | INVALID_IMPORT_NOTEBOOK = os.path.join( 16 | "tests", "data", "invalid_import_in_doctest.ipynb" 17 | ) 18 | 19 | 20 | def test_doctest_works(capsys: "CaptureFixture") -> None: 21 | """ 22 | Check doctest works. 23 | 24 | Parameters 25 | ---------- 26 | capsys 27 | Pytest fixture to capture stdout and stderr. 28 | """ 29 | main(["doctest", GOOD_EXAMPLE_NOTEBOOK]) 30 | 31 | # check out and err 32 | out, err = capsys.readouterr() 33 | assert out == "" 34 | assert err == "" 35 | 36 | main(["doctest", WRONG_EXAMPLE_NOTEBOOK]) 37 | 38 | # check out and err 39 | out, err = capsys.readouterr() 40 | 41 | expected_out = ( 42 | "**********************************************************************\n" 43 | f'File "{WRONG_EXAMPLE_NOTEBOOK}", cell_2:10, in notebook_for_testing_copy.hello\n' 44 | "Failed example:\n" 45 | ' hello("goodbye")\n' 46 | "Expected:\n" 47 | " 'hello goodby'\n" 48 | "Got:\n" 49 | " 'hello goodbye'\n" 50 | "**********************************************************************\n" 51 | "1 items had failures:\n" 52 | " 1 of 2 in notebook_for_testing_copy.hello\n" 53 | "***Test Failed*** 1 failures.\n" 54 | ) 55 | if sys.version_info >= (3, 13): 56 | expected_out = expected_out.replace("1 failures", "1 failure") 57 | expected_out = expected_out.replace("1 items", "1 item") 58 | 59 | try: 60 | assert out.replace("\r\n", "\n") == expected_out 61 | except AssertionError: 62 | # observed this in CI, some jobs pass with absolute path, 63 | # others with relative... 64 | assert out.replace("\r\n", "\n") == expected_out.replace( 65 | WRONG_EXAMPLE_NOTEBOOK, os.path.abspath(WRONG_EXAMPLE_NOTEBOOK) 66 | ) 67 | assert err == "" 68 | 69 | 70 | def test_doctest_invalid_import(capsys: "CaptureFixture") -> None: 71 | """ 72 | Check that correct error is reported if notebook contains unimportable imports. 73 | 74 | Parameters 75 | ---------- 76 | capsys 77 | Pytest fixture to capture stdout and stderr. 78 | """ 79 | main(["doctest", INVALID_IMPORT_NOTEBOOK]) 80 | 81 | _, err = capsys.readouterr() 82 | assert "ModuleNotFoundError: No module named 'thisdoesnotexist'" in err 83 | -------------------------------------------------------------------------------- /tests/test_include_exclude.py: -------------------------------------------------------------------------------- 1 | """Check include-exclude work.""" 2 | 3 | import re 4 | from pathlib import Path 5 | from textwrap import dedent 6 | from typing import TYPE_CHECKING 7 | 8 | from nbqa.__main__ import main 9 | 10 | if TYPE_CHECKING: 11 | from _pytest.capture import CaptureFixture 12 | 13 | 14 | def test_cli_files(capsys: "CaptureFixture") -> None: 15 | """ 16 | Test --nbqa-files is picked up correctly. 17 | 18 | Parameters 19 | ---------- 20 | capsys 21 | Pytest fixture to capture stdout and stderr. 22 | """ 23 | main(["flake8", "tests", "--nbqa-files", "^tests/data/notebook_for"]) 24 | 25 | out, _ = capsys.readouterr() 26 | assert out and all( 27 | re.search(r"tests.data.notebook_for", i) for i in out.splitlines() 28 | ) 29 | 30 | 31 | def test_cli_exclude(capsys: "CaptureFixture") -> None: 32 | """ 33 | Test --nbqa-exclude is picked up correctly. 34 | 35 | Parameters 36 | ---------- 37 | capsys 38 | Pytest fixture to capture stdout and stderr. 39 | """ 40 | main(["flake8", "tests", "--nbqa-exclude", "^tests/data/notebook_for"]) 41 | 42 | out, _ = capsys.readouterr() 43 | assert out and all( 44 | re.search(r"tests.data.notebook_for", i) is None for i in out.splitlines() 45 | ) 46 | 47 | 48 | def test_config_files(capsys: "CaptureFixture") -> None: 49 | """ 50 | Test [nbqa.files] config is picked up correctly. 51 | 52 | Parameters 53 | ---------- 54 | capsys 55 | Pytest fixture to capture stdout and stderr. 56 | """ 57 | Path("pyproject.toml").write_text( 58 | dedent( 59 | """\ 60 | [tool.nbqa.files] 61 | flake8 = "^tests/data/notebook_for" 62 | """ 63 | ), 64 | encoding="utf-8", 65 | ) 66 | main(["flake8", "tests"]) 67 | Path("pyproject.toml").unlink() 68 | 69 | out, _ = capsys.readouterr() 70 | assert out and all( 71 | re.search(r"tests.data.notebook_for", i) for i in out.splitlines() 72 | ) 73 | 74 | 75 | def test_config_exclude(capsys: "CaptureFixture") -> None: 76 | """ 77 | Test [nbqa.exclude] config is picked up correctly. 78 | 79 | Parameters 80 | ---------- 81 | capsys 82 | Pytest fixture to capture stdout and stderr. 83 | """ 84 | Path("pyproject.toml").write_text( 85 | dedent( 86 | """\ 87 | [tool.nbqa.exclude] 88 | flake8 = "^tests/data/notebook_for" 89 | """ 90 | ), 91 | encoding="utf-8", 92 | ) 93 | 94 | main(["flake8", "tests"]) 95 | Path("pyproject.toml").unlink() 96 | 97 | out, _ = capsys.readouterr() 98 | assert out and all( 99 | re.search(r"tests.data.notebook_for", i) is None for i in out.splitlines() 100 | ) 101 | -------------------------------------------------------------------------------- /tests/test_nbqa_shell.py: -------------------------------------------------------------------------------- 1 | """Ensure the --nbqa-shell flag correctly calls the underlying command.""" 2 | 3 | import os 4 | import sys 5 | from shutil import which 6 | from subprocess import CompletedProcess 7 | from typing import List 8 | 9 | import pytest 10 | from _pytest.capture import CaptureFixture 11 | from _pytest.monkeypatch import MonkeyPatch 12 | 13 | from nbqa.__main__ import CommandNotFoundError, main 14 | 15 | 16 | def _message(args: List[str]) -> str: 17 | return f"I would have run `{args[:-1]}`" 18 | 19 | 20 | def subprocess_run(args: List[str], *_, **__): # type: ignore 21 | """Mock subprocess run to print and return ok.""" 22 | print(_message(args=args), file=sys.stderr) 23 | return CompletedProcess(args, 0, b"", b"") 24 | 25 | 26 | def test_nbqa_shell(monkeypatch: MonkeyPatch, capsys: CaptureFixture) -> None: 27 | """Check nbqa shell command call.""" 28 | path = os.path.join("tests", "data", "notebook_for_testing.ipynb") 29 | monkeypatch.setattr("subprocess.run", subprocess_run) 30 | 31 | args = ["black", "--nbqa-shell", path] 32 | expected_run = [which("black"), path] 33 | main(args) 34 | out, err = capsys.readouterr() 35 | received = err.strip().splitlines()[1] 36 | expected = _message(args=expected_run) # type:ignore[arg-type] 37 | assert received == expected 38 | assert out == "", f"No stdout expected. Received `{out}`" 39 | 40 | 41 | def test_nbqa_not_shell(monkeypatch: MonkeyPatch, capsys: CaptureFixture) -> None: 42 | """Check nbqa without --nbqa-shell command call.""" 43 | path = os.path.join("tests", "data", "notebook_for_testing.ipynb") 44 | monkeypatch.setattr("subprocess.run", subprocess_run) 45 | 46 | args = ["black", path] 47 | expected_run = [sys.executable, "-m", "black", path] 48 | main(args) 49 | out, err = capsys.readouterr() 50 | received = err.strip().splitlines()[1] 51 | expected = _message(args=expected_run) 52 | assert received == expected 53 | assert out == "", f"No stdout expected. Received `{out}`" 54 | 55 | 56 | def test_nbqa_shell_not_found(monkeypatch: MonkeyPatch) -> None: 57 | """Check --nbqa-shell command call with inexistend command.""" 58 | path = os.path.join("tests", "data", "notebook_for_testing.ipynb") 59 | monkeypatch.setattr("subprocess.run", subprocess_run) 60 | 61 | args = ["some-fictional-command", "--nbqa-shell", path] 62 | msg = "\x1b\\[1mnbqa was unable to find some-fictional-command.\x1b\\[0m" 63 | with pytest.raises(CommandNotFoundError, match=msg): 64 | main(args) 65 | 66 | 67 | @pytest.mark.skipif(sys.platform != "linux", reason="needs grep") 68 | def test_grep(capsys: CaptureFixture) -> None: 69 | """Check grep with string works.""" 70 | main(["grep 'import pandas'", ".", "--nbqa-shell"]) 71 | out, _ = capsys.readouterr() 72 | assert out == "tests/data/notebook_for_autoflake.ipynb:import pandas as pd\n" 73 | -------------------------------------------------------------------------------- /.github/workflows/publish_to_pypi.yml: -------------------------------------------------------------------------------- 1 | name: Publish Python 🐍 distribution 📦 to PyPI and TestPyPI 2 | 3 | on: push 4 | 5 | jobs: 6 | build: 7 | name: Build distribution 📦 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - uses: actions/checkout@v4 12 | - name: Set up Python 13 | uses: actions/setup-python@v4 14 | with: 15 | python-version: "3.x" 16 | - name: Install pypa/build 17 | run: >- 18 | python3 -m 19 | pip install 20 | build 21 | --user 22 | - name: Build a binary wheel and a source tarball 23 | run: python3 -m build 24 | - name: Store the distribution packages 25 | uses: actions/upload-artifact@v4 26 | with: 27 | name: python-package-distributions 28 | path: dist/ 29 | 30 | publish-to-pypi: 31 | name: >- 32 | Publish Python 🐍 distribution 📦 to PyPI 33 | if: startsWith(github.ref, 'refs/tags/') # only publish to PyPI on tag pushes 34 | needs: 35 | - build 36 | runs-on: ubuntu-latest 37 | environment: 38 | name: pypi 39 | url: https://pypi.org/p/nbqa # Replace with your PyPI project name 40 | permissions: 41 | id-token: write # IMPORTANT: mandatory for trusted publishing 42 | 43 | steps: 44 | - name: Download all the dists 45 | uses: actions/download-artifact@v4 46 | with: 47 | name: python-package-distributions 48 | path: dist/ 49 | - name: Publish distribution 📦 to PyPI 50 | uses: pypa/gh-action-pypi-publish@release/v1 51 | 52 | github-release: 53 | name: >- 54 | Sign the Python 🐍 distribution 📦 with Sigstore 55 | and upload them to GitHub Release 56 | needs: 57 | - publish-to-pypi 58 | runs-on: ubuntu-latest 59 | 60 | permissions: 61 | contents: write # IMPORTANT: mandatory for making GitHub Releases 62 | id-token: write # IMPORTANT: mandatory for sigstore 63 | 64 | steps: 65 | - name: Download all the dists 66 | uses: actions/download-artifact@v4 67 | with: 68 | name: python-package-distributions 69 | path: dist/ 70 | - name: Sign the dists with Sigstore 71 | uses: sigstore/gh-action-sigstore-python@v3.0.0 72 | with: 73 | inputs: >- 74 | ./dist/*.tar.gz 75 | ./dist/*.whl 76 | - name: Create GitHub Release 77 | env: 78 | GITHUB_TOKEN: ${{ github.token }} 79 | run: >- 80 | gh release create 81 | '${{ github.ref_name }}' 82 | --repo '${{ github.repository }}' 83 | --notes "" 84 | - name: Upload artifact signatures to GitHub Release 85 | env: 86 | GITHUB_TOKEN: ${{ github.token }} 87 | # Upload to GitHub Release using the `gh` CLI. 88 | # `dist/` contains the built packages, and the 89 | # sigstore-produced signatures and certificates. 90 | run: >- 91 | gh release upload 92 | '${{ github.ref_name }}' dist/** 93 | --repo '${{ github.repository }}' 94 | -------------------------------------------------------------------------------- /docs/tutorial.rst: -------------------------------------------------------------------------------- 1 | ======== 2 | Tutorial 3 | ======== 4 | 5 | Welcome! Here's a little tutorial, assuming you're brand-new here. We'll walk you through: 6 | 7 | - creating a virtual environment; 8 | - installing ``nbqa``, and checking your installation; 9 | - running ``black`` on your notebook; 10 | - configuring ``nbqa``. 11 | 12 | Creating a virtual environment 13 | ------------------------------ 14 | 15 | Rather than using your system installation of Python, we recommend using a virtual environment so that your dependencies don't clash with each other. 16 | Here's one way to set one up using Conda - see `this tutorial `_ for other options. 17 | 18 | 1. Install the `Miniconda distribution of Python `_; 19 | 2. Create a new virtual environment. Here, we'll call it ``nbqa-env`` 20 | 21 | .. code-block:: bash 22 | 23 | conda create -n nbqa-env python=3.8 -y 24 | 25 | 3. Activate your virtual environment 26 | 27 | .. code-block:: bash 28 | 29 | conda activate nbqa-env 30 | 31 | Install nbqa and black 32 | ---------------------- 33 | 34 | 1. Install ``nbqa`` and at least one Python code quality tool - here, we'll use ``black`` 35 | 36 | .. code-block:: bash 37 | 38 | pip install -U nbqa black 39 | 40 | 2. Check your installation 41 | 42 | .. code-block:: bash 43 | 44 | nbqa --version 45 | black --version 46 | 47 | Neither of these commands should error. 48 | 49 | Run nbqa black 50 | -------------- 51 | 52 | 1. Locate a Jupyter Notebook on your system. If you don't have one, `here `_ 53 | is a nice one you can download. 54 | 55 | 2. Run the ``black`` formatter on your notebook via ``nbqa`` 56 | 57 | .. code-block:: bash 58 | 59 | nbqa black notebook.ipynb --line-length=96 60 | 61 | 3. Reload your notebook, and admire the difference! 62 | 63 | Configuring nbqa 64 | ---------------- 65 | 66 | Rather than having to type ``--line-length=96`` from the command-line for 67 | each notebook you want to reformat, you can configure ``nbqa`` in your ``pyproject.toml`` file. 68 | Open up your ``pyproject.toml`` file (or create one if you don't have one already) and add in the following lines :: 69 | 70 | [tool.black] 71 | line-length = 96 72 | 73 | Now, you'll be able to run the command from the previous section with just 74 | 75 | .. code-block:: bash 76 | 77 | nbqa black notebook.ipynb 78 | 79 | Much simpler! 80 | 81 | See :ref:`configuration` for how to further configure how ``nbqa``. 82 | 83 | Writing your own tool 84 | --------------------- 85 | 86 | You can use ``nbqa`` to run your own custom tool on Jupyter Notebooks too. You just need to make sure you can 87 | run it as a module on a given set of Python files. For example, if your tool is called ``my_amazing_tool``, then 88 | as long as you can run 89 | 90 | .. code-block:: bash 91 | 92 | python -m my_amazing_tool file_1.py file_2.py 93 | 94 | then you will be able to run 95 | 96 | .. code-block:: bash 97 | 98 | nbqa my_amazing_tool notebook_1.ipynb notebook_2.ipynb 99 | -------------------------------------------------------------------------------- /docs/pre-commit.rst: -------------------------------------------------------------------------------- 1 | ========== 2 | Pre-commit 3 | ========== 4 | 5 | Usage 6 | ----- 7 | 8 | You can easily use ``nbqa`` as a `pre-commit `_ hook. 9 | 10 | Here's an example of what you could include in your ``.pre-commit-config.yaml`` file: 11 | 12 | .. code-block:: yaml 13 | 14 | repos: 15 | - repo: https://github.com/nbQA-dev/nbQA 16 | rev: 1.9.1 17 | hooks: 18 | - id: nbqa-black 19 | additional_dependencies: [black==20.8b1] 20 | - id: nbqa-pyupgrade 21 | additional_dependencies: [pyupgrade==2.7.3] 22 | - id: nbqa-isort 23 | additional_dependencies: [isort==5.6.4] 24 | 25 | For best reproducibility, you should pin your dependencies (as above). Running ``pre-commit autoupdate`` will update your hooks' versions, but 26 | versions of additional dependencies need to be updated manually. 27 | 28 | See `.pre-commit-hooks.yaml `_ for all available built-in hooks. 29 | 30 | Custom hooks 31 | ------------ 32 | 33 | If you have your own custom tool (e.g. ``customtool``) for which we currently don't have a built-in hook, you can define your own one with: 34 | 35 | .. code-block:: yaml 36 | 37 | - repo: https://github.com/nbQA-dev/nbQA 38 | rev: 1.9.1 39 | hooks: 40 | - id: nbqa 41 | entry: nbqa customtool 42 | name: nbqa-customtool 43 | alias: nbqa-customtool 44 | additional_dependencies: [customtool==] 45 | 46 | If there are additional Python code quality tools you would like us to make a hook for, please :ref:`open a pull request` 47 | or let us know in the `issue tracker `_! 48 | 49 | Configuration 50 | ------------- 51 | 52 | To pass command line arguments, use the `pre-commit args `_ option: 53 | 54 | .. code-block:: yaml 55 | 56 | repos: 57 | - repo: https://github.com/nbQA-dev/nbQA 58 | rev: 1.9.1 59 | hooks: 60 | - id: nbqa-pyupgrade 61 | args: [--py38-plus] 62 | - id: nbqa-isort 63 | args: [--profile=black] 64 | - id: nbqa-flake8 65 | args: [--ignore=E402] # E402 module level import not at top of file 66 | 67 | Note that some tools like ``flake8`` require the flag and its value to be joined by an equal sign in order to not interpret the value as a 68 | filename (`GH issue `_). 69 | 70 | See :ref:`configuration` for how to further configure how ``nbqa`` should run each tool. Also, see the `pre-commit documentation `_ 71 | for how to further configure these hooks. 72 | 73 | Temporarily disable hooks 74 | ------------------------- 75 | 76 | Although not recommended, it is still possible to temporarily **disable all checks** 77 | using ``git commit --no-verify``, or **just specific ones** using the ``SKIP`` 78 | environment variable. For example, on a Unix-like operating system: 79 | 80 | .. code:: bash 81 | 82 | SKIP=nbqa-black git commit -m "foo" 83 | 84 | 85 | For more details, please check out 86 | `the pre-commit documentation `_. 87 | -------------------------------------------------------------------------------- /tests/data/notebook_for_testing.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": { 7 | "tags": [ 8 | "skip-flake8" 9 | ] 10 | }, 11 | "outputs": [], 12 | "source": [ 13 | "import os\n", 14 | "\n", 15 | "import glob\n", 16 | "\n", 17 | "import nbqa" 18 | ] 19 | }, 20 | { 21 | "cell_type": "markdown", 22 | "metadata": {}, 23 | "source": [ 24 | "# Some markdown cell containing \\\\n" 25 | ] 26 | }, 27 | { 28 | "cell_type": "markdown", 29 | "metadata": { 30 | "tags": [ 31 | "skip-mdformat" 32 | ] 33 | }, 34 | "source": [ 35 | "First level heading\n", 36 | "===" 37 | ] 38 | }, 39 | { 40 | "cell_type": "code", 41 | "execution_count": null, 42 | "metadata": { 43 | "tags": [ 44 | "flake8-skip" 45 | ] 46 | }, 47 | "outputs": [], 48 | "source": [ 49 | "%%time foo\n", 50 | "def hello(name: str = \"world\\n\"):\n", 51 | " \"\"\"\n", 52 | " Greet user.\n", 53 | "\n", 54 | " Examples\n", 55 | " --------\n", 56 | " >>> hello()\n", 57 | " 'hello world\\\\n'\n", 58 | "\n", 59 | " >>> hello(\"goodbye\")\n", 60 | " 'hello goodbye'\n", 61 | " \"\"\"\n", 62 | "\n", 63 | " return 'hello {}'.format(name)\n", 64 | "\n", 65 | "\n", 66 | "!ls\n", 67 | "hello(3) " 68 | ] 69 | }, 70 | { 71 | "cell_type": "markdown", 72 | "metadata": {}, 73 | "source": [ 74 | "```python\n", 75 | "2 +2\n", 76 | "```" 77 | ] 78 | }, 79 | { 80 | "cell_type": "code", 81 | "execution_count": null, 82 | "metadata": {}, 83 | "outputs": [], 84 | "source": [ 85 | " %%bash\n", 86 | "\n", 87 | " pwd" 88 | ] 89 | }, 90 | { 91 | "cell_type": "code", 92 | "execution_count": null, 93 | "metadata": {}, 94 | "outputs": [], 95 | "source": [ 96 | "from random import randint\n", 97 | "\n", 98 | "if __debug__:\n", 99 | " %time randint(5,10)" 100 | ] 101 | }, 102 | { 103 | "cell_type": "code", 104 | "execution_count": null, 105 | "metadata": {}, 106 | "outputs": [], 107 | "source": [ 108 | "import pprint\n", 109 | "import sys\n", 110 | "\n", 111 | "if __debug__:\n", 112 | " pretty_print_object = pprint.PrettyPrinter(\n", 113 | " indent=4, width=80, stream=sys.stdout, compact=True, depth=5\n", 114 | " )\n", 115 | "\n", 116 | "pretty_print_object.isreadable([\"Hello\", \"World\"])" 117 | ] 118 | } 119 | ], 120 | "metadata": { 121 | "anaconda-cloud": {}, 122 | "kernelspec": { 123 | "display_name": "Python 3", 124 | "language": "python", 125 | "name": "python3" 126 | }, 127 | "language_info": { 128 | "codemirror_mode": { 129 | "name": "ipython", 130 | "version": 3 131 | }, 132 | "file_extension": ".py", 133 | "mimetype": "text/x-python", 134 | "name": "python", 135 | "nbconvert_exporter": "python", 136 | "pygments_lexer": "ipython3", 137 | "version": "3.8.5" 138 | } 139 | }, 140 | "nbformat": 4, 141 | "nbformat_minor": 4 142 | } -------------------------------------------------------------------------------- /tests/test_return_code.py: -------------------------------------------------------------------------------- 1 | """Check that return code from third-party tool is preserved.""" 2 | 3 | import subprocess 4 | from functools import partial 5 | from pathlib import Path 6 | from typing import Sequence 7 | 8 | TESTS_DIR = Path("tests") 9 | TEST_DATA_DIR = TESTS_DIR / "data" 10 | DIRTY_NOTEBOOK = TEST_DATA_DIR / "notebook_for_testing.ipynb" 11 | CLEAN_NOTEBOOK = TEST_DATA_DIR / "clean_notebook.ipynb" 12 | EMPTY_NOTEBOOK = TEST_DATA_DIR / "empty_notebook.ipynb" 13 | INVALID_SYNTAX_NOTEBOOK = TESTS_DIR / "invalid_data" / "invalid_syntax.ipynb" 14 | 15 | # Interpret the below constants in the same context as that of pre-commit tool 16 | # Success indicates the QA tool reported no issues. 17 | PASSED = 0 18 | 19 | 20 | def _run_nbqa_with(command: str, notebooks: Sequence[Path], *args: str) -> int: 21 | """Run nbqa with the QA tool specified by command parameter.""" 22 | notebook_paths = map(str, notebooks) 23 | output = subprocess.run(["nbqa", command, *notebook_paths, *args]) 24 | return output.returncode 25 | 26 | 27 | def test_flake8_return_code() -> None: 28 | """Check flake8 returns 0 if it passes, 1 otherwise.""" 29 | flake8_runner = partial(_run_nbqa_with, "flake8") 30 | assert flake8_runner([DIRTY_NOTEBOOK]) != PASSED 31 | assert flake8_runner([CLEAN_NOTEBOOK]) == PASSED 32 | 33 | 34 | def test_autoflake_return_code() -> None: 35 | """Check flake8 returns 0 if it passes, 1 otherwise.""" 36 | autoflake_options = [ 37 | "--check", 38 | "--expand-star-imports", 39 | "--remove-all-unused-imports", 40 | "--remove-unused-variables", 41 | ] 42 | autoflake_runner = partial(_run_nbqa_with, "autoflake") 43 | assert autoflake_runner([CLEAN_NOTEBOOK], *autoflake_options) == PASSED 44 | assert ( 45 | autoflake_runner( 46 | [TEST_DATA_DIR / "notebook_for_autoflake.ipynb"], *autoflake_options 47 | ) 48 | != PASSED 49 | ) 50 | 51 | 52 | def test_pylint_return_code() -> None: 53 | """Check pylint returns 0 if it passes, 20 otherwise.""" 54 | pylint_runner = partial(_run_nbqa_with, "pylint") 55 | assert pylint_runner([DIRTY_NOTEBOOK]) != PASSED 56 | assert pylint_runner([CLEAN_NOTEBOOK], "--disable=C0114") == PASSED 57 | 58 | 59 | def test_black_return_code() -> None: 60 | """Check black returns 0 if it passes, 1 otherwise.""" 61 | black_runner = partial(_run_nbqa_with, "black") 62 | 63 | assert black_runner([DIRTY_NOTEBOOK], "--check") != PASSED 64 | 65 | clean_notebooks = [ 66 | CLEAN_NOTEBOOK, 67 | TEST_DATA_DIR / "notebook_with_cell_after_def.ipynb", 68 | TEST_DATA_DIR / "clean_notebook_with_trailing_semicolon.ipynb", 69 | EMPTY_NOTEBOOK, 70 | ] 71 | assert black_runner(clean_notebooks, "--check") == PASSED 72 | 73 | # This is to test if the tool ran on all the notebooks in a given directory 74 | assert black_runner([TESTS_DIR], "--check") != PASSED 75 | 76 | 77 | def test_check_ast_return_code() -> None: 78 | """Check check-ast returns 0 if it passes, 1 otherwise.""" 79 | check_ast_runner = partial(_run_nbqa_with, "pre_commit_hooks.check_ast") 80 | 81 | assert check_ast_runner([DIRTY_NOTEBOOK]) == PASSED 82 | assert ( 83 | check_ast_runner([INVALID_SYNTAX_NOTEBOOK], "--nbqa-dont-skip-bad-cells") 84 | != PASSED 85 | ) 86 | -------------------------------------------------------------------------------- /tests/tools/test_pylint_works.py: -------------------------------------------------------------------------------- 1 | """Check that :code:`black` works as intended.""" 2 | 3 | import os 4 | from typing import TYPE_CHECKING 5 | 6 | from nbqa.__main__ import main 7 | 8 | if TYPE_CHECKING: 9 | from _pytest.capture import CaptureFixture 10 | 11 | 12 | def test_pylint_works(capsys: "CaptureFixture") -> None: 13 | """ 14 | Check pylint works. Check all the warnings raised by pylint on the notebook. 15 | 16 | Parameters 17 | ---------- 18 | capsys 19 | Pytest fixture to capture stdout and stderr. 20 | """ 21 | # Pass one file with absolute path and the other one with relative path 22 | notebook1 = os.path.join("tests", "data", "notebook_for_testing.ipynb") 23 | notebook2 = os.path.join("tests", "data", "notebook_with_indented_magics.ipynb") 24 | 25 | main(["pylint", notebook1, notebook2, "--disable=C0114"]) 26 | 27 | # check out and err 28 | out, _ = capsys.readouterr() 29 | 30 | expected_out = ( 31 | "************* Module tests.data.notebook_for_testing\n" 32 | f"{notebook1}:cell_2:19:8: C0303: Trailing whitespace (trailing-whitespace)\n" # noqa: E501 33 | f"{notebook1}:cell_2:15:11: C0209: Formatting a regular string which could be an f-string (consider-using-f-string)\n" # noqa: E501 34 | f'{notebook1}:cell_4:1:0: C0413: Import "from random import randint" should be placed at the top of the module (wrong-import-position)\n' # noqa: E501 35 | f'{notebook1}:cell_5:1:0: C0413: Import "import pprint" should be placed at the top of the module (wrong-import-position)\n' # noqa: E501 36 | f'{notebook1}:cell_5:2:0: C0413: Import "import sys" should be placed at the top of the module (wrong-import-position)\n' # noqa: E501 37 | f"{notebook1}:cell_5:9:0: E0606: Possibly using variable 'pretty_print_object' before assignment (possibly-used-before-assignment)\n" # noqa: E501 38 | f'{notebook1}:cell_4:1:0: C0411: standard import "random.randint" should be placed before first party import "nbqa" (wrong-import-order)\n' # noqa: E501 39 | f'{notebook1}:cell_5:1:0: C0411: standard import "pprint" should be placed before first party import "nbqa" (wrong-import-order)\n' # noqa: E501 40 | f'{notebook1}:cell_5:2:0: C0411: standard import "sys" should be placed before first party import "nbqa" (wrong-import-order)\n' # noqa: E501 41 | f"{notebook1}:cell_1:1:0: W0611: Unused import os (unused-import)\n" 42 | f"{notebook1}:cell_1:3:0: W0611: Unused import glob (unused-import)\n" 43 | f"{notebook1}:cell_1:5:0: W0611: Unused import nbqa (unused-import)\n" 44 | f"{notebook1}:cell_4:1:0: W0611: Unused randint imported from random (unused-import)\n" 45 | "************* Module tests.data.notebook_with_indented_magics\n" 46 | f'{notebook2}:cell_3:3:0: C0411: standard import "operator" should be placed before third party import "IPython.get_ipython" (wrong-import-order)\n' # noqa: E501 47 | f"{notebook2}:cell_1:1:0: W0611: Unused randint imported from random (unused-import)\n" 48 | f"{notebook2}:cell_1:2:0: W0611: Unused get_ipython imported from IPython (unused-import)\n" 49 | "\n" 50 | "-----------------------------------\n" 51 | "Your code has been rated at 5.45/10\n" 52 | "\n" 53 | ) 54 | horizontal_bar = "-----------------------------------" 55 | assert ( 56 | out.replace("\r\n", "\n").split(horizontal_bar)[0] 57 | == expected_out.split(horizontal_bar, maxsplit=1)[0] 58 | ) 59 | -------------------------------------------------------------------------------- /tests/tools/test_pyupgrade.py: -------------------------------------------------------------------------------- 1 | """Check pyupgrade works.""" 2 | 3 | import difflib 4 | import os 5 | from pathlib import Path 6 | from textwrap import dedent 7 | from typing import TYPE_CHECKING 8 | 9 | from nbqa.__main__ import main 10 | 11 | if TYPE_CHECKING: 12 | from _pytest.capture import CaptureFixture 13 | 14 | 15 | def test_pyupgrade(tmp_notebook_for_testing: Path, capsys: "CaptureFixture") -> None: 16 | """ 17 | Check pyupgrade works. Should only reformat code cells. 18 | 19 | Parameters 20 | ---------- 21 | tmp_notebook_for_testing 22 | Temporary copy of :code:`tmp_notebook_for_testing.ipynb`. 23 | capsys 24 | Pytest fixture to capture stdout and stderr. 25 | """ 26 | # check diff 27 | with open(tmp_notebook_for_testing, encoding="utf-8") as handle: 28 | before = handle.readlines() 29 | path = os.path.join("tests", "data", "notebook_for_testing.ipynb") 30 | 31 | Path("pyproject.toml").write_text( 32 | dedent( 33 | """\ 34 | [tool.nbqa.addopts] 35 | pyupgrade = ['--py36-plus'] 36 | """ 37 | ), 38 | encoding="utf-8", 39 | ) 40 | main(["pyupgrade", os.path.abspath(path)]) 41 | Path("pyproject.toml").unlink() 42 | with open(tmp_notebook_for_testing, encoding="utf-8") as handle: 43 | after = handle.readlines() 44 | 45 | diff = difflib.unified_diff(before, after) 46 | result = "".join(i for i in diff if any([i.startswith("+ "), i.startswith("- ")])) 47 | expected = dedent( 48 | """\ 49 | - \" return 'hello {}'.format(name)\\n\", 50 | + \" return f'hello {name}'\\n\", 51 | """ 52 | ) 53 | assert result == expected 54 | 55 | # check out and err 56 | out, err = capsys.readouterr() 57 | expected_out = "" 58 | expected_err = f"Rewriting {path}\n" 59 | assert out.replace("\r\n", "\n") == expected_out 60 | assert err.replace("\r\n", "\n") == expected_err 61 | 62 | 63 | def test_pyupgrade_works_with_empty_file(capsys: "CaptureFixture") -> None: 64 | """ 65 | Check pyupgrade works with empty notebook. 66 | 67 | Parameters 68 | ---------- 69 | capsys 70 | Pytest fixture to capture stdout and stderr. 71 | """ 72 | path = os.path.abspath(os.path.join("tests", "data", "footer.ipynb")) 73 | 74 | main(["pyupgrade", path, "--py3-plus"]) 75 | 76 | out, err = capsys.readouterr() 77 | assert out == "" 78 | assert err == "" 79 | 80 | 81 | def test_pyupgrade_works_with_weird_databricks_file(capsys: "CaptureFixture") -> None: 82 | """ 83 | Check pyupgrade works with unusual databricks notebooks. 84 | 85 | Parameters 86 | ---------- 87 | capsys 88 | Pytest fixture to capture stdout and stderr. 89 | """ 90 | path = os.path.join("tests", "data", "databricks_notebook.ipynb") 91 | main(["pyupgrade", path, "--nbqa-diff", "--py3-plus"]) 92 | out, err = capsys.readouterr() 93 | expected_out = ( 94 | "\x1b[1mCell 2\x1b[0m\n" 95 | "------\n" 96 | f"\x1b[1;37m--- {path}\n" 97 | f"\x1b[0m\x1b[1;37m+++ {path}\n" 98 | "\x1b[0m\x1b[36m@@ -1 +1 @@\n" 99 | "\x1b[0m\x1b[31m-set(())\n" 100 | "\x1b[0m\x1b[32m+set()\n" 101 | "\x1b[0m\n" 102 | "To apply these changes, remove the `--nbqa-diff` flag\n" 103 | ) 104 | expected_err = f"Rewriting {path}\n" 105 | assert out.replace("\r\n", "\n") == expected_out 106 | assert err.replace("\r\n", "\n") == expected_err 107 | -------------------------------------------------------------------------------- /tests/tools/test_autoflake.py: -------------------------------------------------------------------------------- 1 | """Check configs from :code:`pyproject.toml` are picked up.""" 2 | 3 | import difflib 4 | from pathlib import Path 5 | from textwrap import dedent 6 | from typing import TYPE_CHECKING, Sequence, Tuple 7 | 8 | from nbqa.__main__ import main 9 | 10 | if TYPE_CHECKING: 11 | from py._path.local import LocalPath 12 | 13 | 14 | def _run_nbqa( 15 | command: str, notebook: str, *args: str 16 | ) -> Tuple[Sequence[str], Sequence[str]]: 17 | """ 18 | Run nbQA on the given notebook using the input command. 19 | 20 | Parameters 21 | ---------- 22 | command 23 | Third party tool to run 24 | notebook 25 | Notebook given to nbQA 26 | 27 | Returns 28 | ------- 29 | Tuple[Sequence[str], Sequence[str]] 30 | Content of the notebook before and after running nbQA 31 | """ 32 | with open(notebook, encoding="utf-8") as handle: 33 | before = handle.readlines() 34 | 35 | main([command, notebook, *args]) 36 | 37 | with open(notebook, encoding="utf-8") as handle: 38 | after = handle.readlines() 39 | 40 | return (before, after) 41 | 42 | 43 | def _validate(before: Sequence[str], after: Sequence[str]) -> bool: 44 | """ 45 | Validate the state of the notebook before and after running nbqa with autoflake. 46 | 47 | Parameters 48 | ---------- 49 | before 50 | Notebook contents before running nbqa with autoflake 51 | after 52 | Notebook contents after running nbqa with autoflake 53 | 54 | Returns 55 | ------- 56 | bool 57 | True if validation succeeded else False 58 | """ 59 | diff = difflib.unified_diff(before, after) 60 | result = "".join(i for i in diff if any([i.startswith("+ "), i.startswith("- ")])) 61 | 62 | expected = dedent( 63 | """\ 64 | - " unused_var = \\"not used\\"\\n", 65 | - "from os.path import *\\n", 66 | + "from os.path import abspath\\n", 67 | - "import pandas as pd\\n", 68 | """ 69 | ) 70 | return result == expected 71 | 72 | 73 | def test_autoflake_cli(tmp_notebook_for_autoflake: "LocalPath") -> None: 74 | """Check if autoflake works as expected using the command line configuration.""" 75 | before, after = _run_nbqa( 76 | "autoflake", 77 | str(tmp_notebook_for_autoflake), 78 | "--in-place", 79 | "--expand-star-imports", 80 | "--remove-all-unused-imports", 81 | "--remove-unused-variables", 82 | ) 83 | 84 | assert _validate(before, after) 85 | 86 | 87 | def _create_toml_config(config_file: Path) -> None: 88 | """ 89 | Create TOML configuration in the test directory. 90 | 91 | Parameters 92 | ---------- 93 | config_file : Path 94 | nbqa configuration file 95 | """ 96 | config_file.write_text( 97 | dedent( 98 | """ 99 | [tool.nbqa.addopts] 100 | autoflake = [ 101 | "--in-place", 102 | "--expand-star-imports", 103 | "--remove-all-unused-imports", 104 | "--remove-unused-variables" 105 | ] 106 | """ 107 | ) 108 | ) 109 | 110 | 111 | def test_autoflake_toml(tmp_notebook_for_autoflake: "LocalPath") -> None: 112 | """Check if autoflake works as expected using the configuration from pyproject.toml.""" 113 | _create_toml_config(Path("pyproject.toml")) 114 | 115 | before, after = _run_nbqa("autoflake", str(tmp_notebook_for_autoflake)) 116 | 117 | assert _validate(before, after) 118 | -------------------------------------------------------------------------------- /docs/examples.rst: -------------------------------------------------------------------------------- 1 | ===================== 2 | Command-line examples 3 | ===================== 4 | 5 | .. note:: 6 | Cell numbers in the output refer to code cells, enumerated starting from 1. 7 | 8 | Reformat your notebooks with `black`_: 9 | 10 | .. code:: console 11 | 12 | $ nbqa black my_notebook.ipynb 13 | reformatted my_notebook.ipynb 14 | All done! ✨ 🍰 ✨ 15 | 1 files reformatted. 16 | 17 | Run your docstring tests with `doctest`_: 18 | 19 | .. code:: console 20 | 21 | $ nbqa doctest my_notebook.ipynb 22 | ********************************************************************** 23 | File "my_notebook.ipynb", cell_2:11, in my_notebook.add 24 | Failed example: 25 | add(2, 2) 26 | Expected: 27 | 4 28 | Got: 29 | 5 30 | ********************************************************************** 31 | 1 items had failures: 32 | 1 of 2 in my_notebook.hello 33 | ***Test Failed*** 1 failures. 34 | 35 | Check for style guide enforcement with `flake8`_: 36 | 37 | .. code:: console 38 | 39 | $ nbqa flake8 my_notebook.ipynb --extend-ignore=E203,E302,E305,E703 40 | my_notebook.ipynb:cell_3:1:1: F401 'import pandas as pd' imported but unused 41 | 42 | Sort your imports with `isort`_: 43 | 44 | .. code:: console 45 | 46 | $ nbqa isort my_notebook.ipynb 47 | Fixing my_notebook.ipynb 48 | 49 | Check your type annotations with `mypy`_: 50 | 51 | .. code:: console 52 | 53 | $ nbqa mypy my_notebook.ipynb --ignore-missing-imports 54 | my_notebook.ipynb:cell_10:5: error: Argument "num1" to "add" has incompatible type "str"; expected "int" 55 | 56 | Perform static code analysis with `pylint`_: 57 | 58 | .. code:: console 59 | 60 | $ nbqa pylint my_notebook.ipynb --disable=C0114 61 | my_notebook.ipynb:cell_1:5:0: W0611: Unused import datetime (unused-import) 62 | 63 | Upgrade your syntax with `pyupgrade`_: 64 | 65 | .. code:: console 66 | 67 | $ nbqa pyupgrade my_notebook.ipynb --py36-plus 68 | Rewriting my_notebook.ipynb 69 | 70 | Format code with `yapf`_: 71 | 72 | .. code:: console 73 | 74 | $ nbqa yapf --in-place my_notebook.ipynb 75 | 76 | Format code with `autopep8`_: 77 | 78 | .. code:: console 79 | 80 | $ nbqa autopep8 -i my_notebook.ipynb 81 | 82 | Check docstring style with `pydocstyle`_: 83 | 84 | .. code:: console 85 | 86 | $ nbqa pydocstyle my_notebook.ipynb 87 | 88 | Format markdown cells with `blacken-docs`_: 89 | 90 | .. code:: console 91 | 92 | $ nbqa blacken-docs my_notebook.ipynb --nbqa-md 93 | 94 | Format ``.md`` file saved via `Jupytext`_ (note: requires ``jupytext`` to be installed): 95 | 96 | .. code:: console 97 | 98 | $ nbqa black my_notebook.md 99 | 100 | Perform linting on a notebook with `ruff`_: 101 | 102 | .. code:: console 103 | 104 | $ nbqa ruff my_notebook.ipynb 105 | 106 | you can also try to auto-fix reported issues via 107 | 108 | .. code:: console 109 | 110 | $ nbqa ruff --fix my_notebook.ipynb 111 | 112 | .. _black: https://black.readthedocs.io/en/stable/ 113 | .. _doctest: https://docs.python.org/3/library/doctest.html 114 | .. _flake8: https://flake8.pycqa.org/en/latest/ 115 | .. _isort: https://timothycrosley.github.io/isort/ 116 | .. _Jupytext: https://github.com/mwouts/jupytext 117 | .. _mypy: http://mypy-lang.org/ 118 | .. _pylint: https://github.com/PyCQA/pylint 119 | .. _pyupgrade: https://github.com/asottile/pyupgrade 120 | .. _yapf: https://github.com/google/yapf 121 | .. _autopep8: https://github.com/hhatto/autopep8 122 | .. _pydocstyle: http://www.pydocstyle.org/en/stable/ 123 | .. _blacken-docs: https://github.com/asottile/blacken-docs 124 | .. _ruff: https://github.com/charliermarsh/ruff 125 | -------------------------------------------------------------------------------- /.pre-commit-hooks.yaml: -------------------------------------------------------------------------------- 1 | - id: nbqa 2 | name: nbqa 3 | description: Run any standard Python code quality tool on a Jupyter Notebook 4 | entry: nbqa 5 | language: python 6 | require_serial: true 7 | types_or: [jupyter, markdown] 8 | - id: nbqa-black 9 | name: nbqa-black 10 | description: Run 'black' on a Jupyter Notebook 11 | entry: nbqa black 12 | language: python 13 | require_serial: true 14 | types_or: [jupyter, markdown] 15 | additional_dependencies: [black] 16 | - id: nbqa-check-ast 17 | name: nbqa-check-ast 18 | description: Run 'check-ast' on a Jupyter Notebook 19 | entry: nbqa pre_commit_hooks.check_ast 20 | language: python 21 | require_serial: true 22 | types_or: [jupyter, markdown] 23 | additional_dependencies: [pre-commit-hooks] 24 | args: [--nbqa-dont-skip-bad-cells] 25 | - id: nbqa-flake8 26 | name: nbqa-flake8 27 | description: Run 'flake8' on a Jupyter Notebook 28 | entry: nbqa flake8 29 | language: python 30 | require_serial: true 31 | types_or: [jupyter, markdown] 32 | additional_dependencies: [flake8] 33 | - id: nbqa-isort 34 | name: nbqa-isort 35 | description: Run 'isort' on a Jupyter Notebook 36 | entry: nbqa isort 37 | language: python 38 | require_serial: true 39 | types_or: [jupyter, markdown] 40 | additional_dependencies: [isort] 41 | - id: nbqa-mypy 42 | name: nbqa-mypy 43 | description: Run 'mypy' on a Jupyter Notebook 44 | entry: nbqa mypy 45 | language: python 46 | require_serial: true 47 | types_or: [jupyter, markdown] 48 | additional_dependencies: [mypy] 49 | - id: nbqa-pylint 50 | name: nbqa-pylint 51 | description: Run 'pylint' on a Jupyter Notebook 52 | entry: nbqa pylint 53 | language: python 54 | require_serial: true 55 | types_or: [jupyter, markdown] 56 | additional_dependencies: [pylint] 57 | - id: nbqa-pyupgrade 58 | name: nbqa-pyupgrade 59 | description: Run 'pyupgrade' on a Jupyter Notebook 60 | entry: nbqa pyupgrade 61 | language: python 62 | require_serial: true 63 | types_or: [jupyter, markdown] 64 | additional_dependencies: [pyupgrade] 65 | - id: nbqa-yapf 66 | name: nbqa-yapf 67 | description: Run 'yapf' on a Jupyter Notebook 68 | entry: nbqa yapf --in-place 69 | language: python 70 | require_serial: true 71 | types_or: [jupyter, markdown] 72 | additional_dependencies: [yapf] 73 | - id: nbqa-autopep8 74 | name: nbqa-autopep8 75 | description: Run 'autopep8' on a Jupyter Notebook 76 | entry: nbqa autopep8 -i 77 | language: python 78 | require_serial: true 79 | types_or: [jupyter, markdown] 80 | additional_dependencies: [autopep8] 81 | - id: nbqa-pydocstyle 82 | name: nbqa-pydocstyle 83 | description: Run 'pydocstyle' on a Jupyter Notebook 84 | entry: nbqa pydocstyle 85 | language: python 86 | require_serial: true 87 | types_or: [jupyter, markdown] 88 | additional_dependencies: [pydocstyle] 89 | - id: nbqa-ruff 90 | name: nbqa-ruff 91 | description: Run 'ruff check' on a Jupyter Notebook # for backwards compatiblity 92 | entry: nbqa "ruff check" 93 | language: python 94 | additional_dependencies: [ruff] 95 | require_serial: true 96 | types_or: [jupyter, markdown] 97 | - id: nbqa-ruff-check 98 | name: nbqa-ruff-check 99 | description: Run 'ruff check' on a Jupyter Notebook 100 | entry: nbqa "ruff check" 101 | language: python 102 | additional_dependencies: [ruff] 103 | require_serial: true 104 | types_or: [jupyter, markdown] 105 | - id: nbqa-ruff-format 106 | name: nbqa-ruff-format 107 | description: Run 'ruff format' on a Jupyter Notebook 108 | entry: nbqa "ruff format" 109 | language: python 110 | additional_dependencies: [ruff] 111 | require_serial: true 112 | types_or: [jupyter, markdown] 113 | -------------------------------------------------------------------------------- /docs/contributing.rst: -------------------------------------------------------------------------------- 1 | .. highlight:: shell 2 | 3 | .. _contributing: 4 | 5 | ============ 6 | Contributing 7 | ============ 8 | 9 | Contributions are welcome, and they are greatly appreciated! Every little bit 10 | helps, and credit will always be given. 11 | 12 | You can contribute in many ways: 13 | 14 | Types of Contributions 15 | ---------------------- 16 | 17 | Report Bugs 18 | ~~~~~~~~~~~ 19 | 20 | Report bugs at https://github.com/nbQA-dev/nbQA/issues. 21 | 22 | If you are reporting a bug, please include: 23 | 24 | * Your operating system name and version. 25 | * Any details about your local setup that might be helpful in troubleshooting. 26 | * Detailed steps to reproduce the bug. 27 | 28 | Fix Bugs 29 | ~~~~~~~~ 30 | 31 | Look through the GitHub issues for bugs. Anything tagged with "bug" and "help 32 | wanted" is open to whoever wants to implement it. 33 | 34 | Implement Features 35 | ~~~~~~~~~~~~~~~~~~ 36 | 37 | Look through the GitHub issues for features. Anything tagged with "enhancement" 38 | and "help wanted" is open to whoever wants to implement it. 39 | 40 | Write Documentation 41 | ~~~~~~~~~~~~~~~~~~~ 42 | 43 | nbqa could always use more documentation, whether as part of the 44 | official nbqa docs, in docstrings, or even on the web in blog posts, 45 | articles, and such. 46 | 47 | Submit Feedback 48 | ~~~~~~~~~~~~~~~ 49 | 50 | The best way to send feedback is to file an issue at https://github.com/nbQA-dev/nbQA/issues. 51 | 52 | If you are proposing a feature: 53 | 54 | * Explain in detail how it would work. 55 | * Keep the scope as narrow as possible, to make it easier to implement. 56 | * Remember that this is a volunteer-driven project, and that contributions 57 | are welcome :) 58 | 59 | Get Started! 60 | ------------ 61 | 62 | Ready to contribute? Here's how to set up ``nbqa`` for local development. 63 | 64 | 1. Fork the ``nbqa`` repo on GitHub at https://github.com/nbQA-dev/nbQA. 65 | 2. Clone your fork locally:: 66 | 67 | $ git clone git@github.com:your_name_here/nbqa.git nbqa-dev 68 | $ cd nbqa-dev 69 | 70 | 3. Create a virtual environment for local development:: 71 | 72 | $ tox --devenv venv 73 | $ . venv/bin/activate 74 | 75 | 4. Create a branch for local development:: 76 | 77 | $ git checkout -b name-of-your-bugfix-or-feature 78 | 79 | Now you can make your changes locally. 80 | 81 | 5. When you're done making changes, check that your changes pass tests: 82 | 83 | $ tox -e py 84 | 85 | 6. Commit your changes and push your branch to GitHub:: 86 | 87 | $ git add . 88 | $ git commit -m "Your detailed description of your changes." 89 | $ git push origin name-of-your-bugfix-or-feature 90 | 91 | 7. Submit a pull request through the GitHub website. 92 | 93 | Pull Request Guidelines 94 | ----------------------- 95 | 96 | Before you submit a pull request, check that it meets these guidelines: 97 | 98 | 1. The pull request should include tests. 99 | 2. If the pull request adds functionality, the docs should be updated. Put 100 | your new functionality into a function with a docstring, and add the 101 | feature to the list in README.md. 102 | 3. The pull request should work for Python 3.6, 3.7, 3.8, and 3.9. 103 | 104 | Add yourself as a contributor 105 | ----------------------------- 106 | 107 | To add yourself to the table of contributors, please follow the `bot usage 108 | instructions `_. 109 | 110 | Example of comment:: 111 | 112 | @all-contributors please add @ for documentation 113 | 114 | .. note:: 115 | 116 | It's considered a good practice to always prefix usernames with ``@`` 117 | 118 | Tips 119 | ---- 120 | 121 | Enable pre-commit to catch errors early-on: 122 | 123 | .. code-block:: 124 | 125 | pre-commit install 126 | -------------------------------------------------------------------------------- /tests/test_ipython_magics.py: -------------------------------------------------------------------------------- 1 | """Check user can check for other magics.""" 2 | 3 | import difflib 4 | import os 5 | from pathlib import Path 6 | from shutil import copyfile 7 | from typing import TYPE_CHECKING, Callable, Sequence 8 | 9 | import pytest 10 | 11 | from nbqa.__main__ import main 12 | 13 | if TYPE_CHECKING: 14 | from _pytest.capture import CaptureFixture 15 | from py._path.local import LocalPath 16 | 17 | 18 | def _create_ignore_cell_config(config_file_path: Path, config: str) -> None: 19 | """Create configuration file for nbqa.""" 20 | config_file_path.write_text(config) 21 | 22 | 23 | def _copy_notebook(src_nb: Path, target_dir: Path) -> Path: 24 | """Copy source notebook to the target directory.""" 25 | test_nb_path = target_dir / src_nb.name 26 | copyfile(src_nb, test_nb_path) 27 | return test_nb_path 28 | 29 | 30 | def _validate_magics_with_black(before: Sequence[str], after: Sequence[str]) -> bool: 31 | """ 32 | Validate the state of the notebook before and after running nbqa with black. 33 | 34 | Parameters 35 | ---------- 36 | before 37 | Notebook contents before running nbqa with black 38 | after 39 | Notebook contents after running nbqa with black 40 | 41 | Returns 42 | ------- 43 | bool 44 | True if validation succeeded else False 45 | """ 46 | diff = difflib.unified_diff(before, after) 47 | result = "".join(i for i in diff if any([i.startswith("+ "), i.startswith("- ")])) 48 | expected = ( 49 | '- "def compute(operand1,operand2, bin_op):\\n",\n' 50 | '+ "def compute(operand1, operand2, bin_op):\\n",\n' 51 | '- "compute(5,1, operator.add)"\n' 52 | '+ "compute(5, 1, operator.add)"\n' 53 | '- " ?str.splitlines"\n' 54 | '+ "str.splitlines?"\n' 55 | '- " %time randint(5,10)"\n' 56 | '+ "%time randint(5,10)"\n' 57 | '- "result = str.split??"\n' 58 | '+ "str.split??"\n' 59 | ) 60 | return result == expected 61 | 62 | 63 | def test_indented_magics( 64 | tmp_notebook_with_indented_magics: "LocalPath", 65 | ) -> None: 66 | """Check if the indented line magics are retained properly after mutating.""" 67 | with open(str(tmp_notebook_with_indented_magics), encoding="utf-8") as handle: 68 | before = handle.readlines() 69 | main( 70 | ["black", os.path.join("tests", "data", "notebook_with_indented_magics.ipynb")] 71 | ) 72 | with open(str(tmp_notebook_with_indented_magics), encoding="utf-8") as handle: 73 | after = handle.readlines() 74 | 75 | assert _validate_magics_with_black(before, after) 76 | 77 | 78 | def _validate_magics_flake8_warnings(actual: str, test_nb_path: Path) -> bool: 79 | """Validate the results of notebooks with warnings.""" 80 | expected = ( 81 | f"{str(test_nb_path)}:cell_1:1:1: F401 'random.randint' imported but unused\n" 82 | f"{str(test_nb_path)}:cell_1:2:1: F401 'IPython.get_ipython' imported but unused\n" 83 | f"{str(test_nb_path)}:cell_3:6:21: E231 missing whitespace after ','\n" 84 | f"{str(test_nb_path)}:cell_3:11:10: E231 missing whitespace after ','\n" 85 | ) 86 | return actual == expected 87 | 88 | 89 | @pytest.mark.parametrize( 90 | "config, validate", 91 | [ 92 | ( 93 | "--extend-ignore=F821", 94 | _validate_magics_flake8_warnings, 95 | ), 96 | ], 97 | ) 98 | def test_magics_with_flake8( 99 | config: str, 100 | validate: Callable[..., bool], 101 | tmpdir: "LocalPath", 102 | capsys: "CaptureFixture", 103 | ) -> None: 104 | """Test nbqa with flake8 on notebook with different types of ipython magics.""" 105 | test_nb_path = _copy_notebook( 106 | Path("tests/data/notebook_with_indented_magics.ipynb"), Path(tmpdir) 107 | ) 108 | 109 | main(["flake8", str(test_nb_path), config]) 110 | 111 | out, _ = capsys.readouterr() 112 | assert validate(out, test_nb_path) 113 | -------------------------------------------------------------------------------- /nbqa/output_parser.py: -------------------------------------------------------------------------------- 1 | """Parse output from code quality tools.""" 2 | 3 | import re 4 | from functools import partial 5 | from typing import Callable, Mapping, Match, NamedTuple, Sequence, Tuple, Union 6 | 7 | from nbqa.path_utils import get_relative_and_absolute_paths 8 | 9 | 10 | def _line_to_cell(match: Match[str], cell_mapping: Mapping[int, str]) -> str: 11 | """Replace Python line with corresponding Jupyter notebook cell.""" 12 | return str(cell_mapping[int(match.group())]) 13 | 14 | 15 | class Output(NamedTuple): 16 | """Captured stdout and stderr.""" 17 | 18 | out: str 19 | err: str 20 | 21 | 22 | def _get_pattern( 23 | notebook: str, command: str, cell_mapping: Mapping[int, str] 24 | ) -> Sequence[Tuple[str, Union[str, Callable[[Match[str]], str]]]]: 25 | """ 26 | Get pattern and substitutions with which to process code quality tool's output. 27 | 28 | Parameters 29 | ---------- 30 | notebook 31 | Notebook command is being run on. 32 | command 33 | Code quality tool. 34 | cell_mapping 35 | Mapping from Python file lines to Jupyter notebook cells. 36 | 37 | Returns 38 | ------- 39 | List 40 | Patterns and substitutions for reported output. 41 | """ 42 | standard_substitution = partial(_line_to_cell, cell_mapping=cell_mapping) 43 | 44 | relative_path, absolute_path = get_relative_and_absolute_paths(notebook) 45 | 46 | if command == "black": 47 | return [ 48 | ( 49 | rf"(?<=^error: cannot format {re.escape(relative_path)}: Cannot parse: )\d+" 50 | rf"|(?<=^error: cannot format {re.escape(absolute_path)}: Cannot parse: )\d+", 51 | standard_substitution, 52 | ), 53 | (r"(?<=line )\d+(?=\)\nOh no! )", standard_substitution), 54 | (r"line cell_(?=\d+:\d+\)\nOh no! )", "cell_"), 55 | ] 56 | 57 | if command == "doctest": 58 | return [ 59 | ( 60 | rf'(?<=^File "{re.escape(relative_path)}", line )\d+' 61 | rf'|(?<=^File "{re.escape(absolute_path)}", line )\d+', 62 | standard_substitution, 63 | ), 64 | ( 65 | rf'(?<=^File "{re.escape(relative_path)}",) line' 66 | rf'|(?<=^File "{re.escape(absolute_path)}",) line', 67 | "", 68 | ), 69 | ] 70 | 71 | if command.startswith("ruff"): 72 | return [ 73 | ( 74 | rf"(?<=--> {re.escape(relative_path)}:)\d+" 75 | rf"|(?<=--> {re.escape(absolute_path)}:)\d+", 76 | standard_substitution, 77 | ) 78 | ] 79 | 80 | # This is the most common one and is used by flake, pylint, mypy, and more. 81 | return [ 82 | ( 83 | rf"(?<=^{re.escape(absolute_path)}:)\d+" 84 | rf"|(?<=^{re.escape(relative_path)}:)\d+", 85 | standard_substitution, 86 | ) 87 | ] 88 | 89 | 90 | def map_python_line_to_nb_lines( 91 | command: str, out: str, err: str, notebook: str, cell_mapping: Mapping[int, str] 92 | ) -> Output: 93 | """ 94 | Make sure stdout and stderr make reference to Jupyter Notebook cells and lines. 95 | 96 | Parameters 97 | ---------- 98 | command 99 | Code quality tool. 100 | out 101 | Captured stdout from third-party tool. 102 | err 103 | Captured stdout from third-party tool. 104 | notebook 105 | Original Jupyter notebook. 106 | cell_mapping 107 | Mapping from Python file lines to Jupyter notebook cells. 108 | 109 | Returns 110 | ------- 111 | Output 112 | Stdout, stderr with references to temporary Python file's lines replaced with references 113 | to notebook's cells and lines. 114 | """ 115 | patterns = _get_pattern(notebook, command, cell_mapping) 116 | for pattern_, substitution_ in patterns: 117 | try: 118 | out = re.sub(pattern_, substitution_, out, flags=re.MULTILINE) 119 | except KeyError: 120 | pass 121 | try: 122 | err = re.sub(pattern_, substitution_, err, flags=re.MULTILINE) 123 | except KeyError: # pragma: nocover (defensive check) 124 | pass 125 | 126 | return Output(out, err) 127 | -------------------------------------------------------------------------------- /nbqa/path_utils.py: -------------------------------------------------------------------------------- 1 | """Utility functions to deal with paths.""" 2 | 3 | import json 4 | import os 5 | import string 6 | from pathlib import Path 7 | from typing import Any, Dict, Optional, Tuple, cast 8 | 9 | 10 | def remove_prefix(string_: str, prefix: str) -> str: 11 | """ 12 | Remove prefix from string. 13 | 14 | Parameters 15 | ---------- 16 | string_ 17 | Given string to remove prefix from. 18 | prefix 19 | Prefix to remove. 20 | 21 | Raises 22 | ------ 23 | AssertionError 24 | If string doesn't start with given prefix. 25 | """ 26 | if string_.startswith(prefix): 27 | string_ = string_[len(prefix) :] 28 | else: # pragma: nocover 29 | raise AssertionError(f"{string_} doesn't start with prefix {prefix}") 30 | return string_ 31 | 32 | 33 | def remove_suffix(string_: str, suffix: str) -> str: 34 | """ 35 | Remove suffix from string. 36 | 37 | Parameters 38 | ---------- 39 | string_ 40 | Given string to remove suffix from. 41 | suffix 42 | Suffix to remove. 43 | 44 | Raises 45 | ------ 46 | AssertionError 47 | If string doesn't end with given suffix. 48 | """ 49 | if string_.endswith(suffix): 50 | string_ = string_[: -len(suffix)] 51 | else: # pragma: nocover 52 | raise AssertionError(f"{string_} doesn't end with suffix {suffix}") 53 | return string_ 54 | 55 | 56 | def get_relative_and_absolute_paths(path: str) -> Tuple[str, str]: 57 | """Get relative (if possible) and absolute versions of path.""" 58 | absolute_path = Path(path).resolve() 59 | try: 60 | relative_path = absolute_path.relative_to(Path.cwd()) 61 | except ValueError: 62 | relative_path = absolute_path 63 | return str(relative_path), str(absolute_path) 64 | 65 | 66 | def read_notebook(notebook: str) -> Tuple[Optional[Dict[str, Any]], Optional[bool]]: 67 | """ 68 | Read notebook. 69 | 70 | If it's .md, try reading it with jupytext. If can't, ignore it. 71 | 72 | Parameters 73 | ---------- 74 | notebook 75 | Path of notebook. 76 | 77 | Returns 78 | ------- 79 | notebook_json 80 | Parsed notebook 81 | trailing_newline 82 | Whether the notebook originally had a trailing newline 83 | """ 84 | trailing_newline = True 85 | _, ext = os.path.splitext(notebook) 86 | with open(notebook, encoding="utf-8") as handle: 87 | content = handle.read() 88 | if ext == ".ipynb": 89 | trailing_newline = content.endswith("\n") 90 | return json.loads(content), trailing_newline 91 | assert ext == ".md" 92 | try: 93 | import jupytext # pylint: disable=import-outside-toplevel 94 | from markdown_it import MarkdownIt # pylint: disable=import-outside-toplevel 95 | except ImportError: # pragma: nocover (how to test this?) 96 | return None, None 97 | 98 | from jupytext.config import ( # pylint: disable=import-outside-toplevel 99 | JupytextConfigurationError, 100 | load_jupytext_config, 101 | ) 102 | 103 | try: 104 | config = load_jupytext_config(os.path.abspath(notebook)) 105 | except JupytextConfigurationError: 106 | config = None 107 | 108 | try: 109 | # jupytext isn't typed unfortunately 110 | md_content = cast( # type: ignore[missing-attribute,unused-ignore] 111 | Any, jupytext.jupytext.read(notebook, config=config) 112 | ) 113 | except: # noqa: E72a # pylint: disable=bare-except 114 | return None, None 115 | 116 | if ("kernelspec" not in md_content.get("metadata", {})) or ( 117 | ( 118 | md_content.get("metadata", {}) 119 | .get("kernelspec", {}) 120 | .get("language", "") 121 | .rstrip(string.digits) 122 | != "python" 123 | ) 124 | and ( 125 | md_content.get("metadata", {}) 126 | .get("kernelspec", {}) 127 | .get("name", "") 128 | .rstrip(string.digits) 129 | != "python" 130 | ) 131 | ): 132 | # Not saved with jupytext, or not Python 133 | return None, None 134 | 135 | # get lexer: see https://github.com/mwouts/jupytext/issues/993 136 | parsed = MarkdownIt("commonmark").disable("inline", True).parse(content) 137 | lexer = None 138 | for token in parsed: 139 | if token.type == "fence" and token.info.startswith("{code-cell}"): 140 | lexer = remove_prefix(token.info, "{code-cell}").strip() 141 | md_content["metadata"]["language_info"] = {"pygments_lexer": lexer} 142 | break 143 | 144 | for cell in md_content["cells"]: 145 | cell["source"] = cell["source"].splitlines(keepends=True) 146 | if "format_name" in md_content.get("metadata", {}).get("jupytext", {}).get( 147 | "text_representation", {} 148 | ): 149 | return md_content, True 150 | return None, None # pragma: nocover (defensive check, shouldn't get here) 151 | -------------------------------------------------------------------------------- /tests/tools/test_ruff_works.py: -------------------------------------------------------------------------------- 1 | """Check :code:`ruff` works as intended.""" 2 | 3 | import os 4 | from pathlib import Path 5 | from typing import TYPE_CHECKING 6 | 7 | import pytest 8 | 9 | from nbqa.__main__ import main 10 | 11 | if TYPE_CHECKING: 12 | from _pytest.capture import CaptureFixture 13 | 14 | 15 | @pytest.mark.parametrize( 16 | "path_0, path_1, path_2", 17 | ( 18 | ( 19 | os.path.abspath( 20 | os.path.join("tests", "data", "notebook_for_testing.ipynb") 21 | ), 22 | os.path.abspath( 23 | os.path.join("tests", "data", "notebook_for_testing_copy.ipynb") 24 | ), 25 | os.path.abspath( 26 | os.path.join("tests", "data", "notebook_starting_with_md.ipynb") 27 | ), 28 | ), 29 | ( 30 | os.path.join("tests", "data", "notebook_for_testing.ipynb"), 31 | os.path.join("tests", "data", "notebook_for_testing_copy.ipynb"), 32 | os.path.join("tests", "data", "notebook_starting_with_md.ipynb"), 33 | ), 34 | ), 35 | ) 36 | def test_ruff_works( 37 | path_0: str, path_1: str, path_2: str, capsys: "CaptureFixture" 38 | ) -> None: 39 | """ 40 | Check flake8 works. Shouldn't alter the notebook content. 41 | 42 | Parameters 43 | ---------- 44 | capsys 45 | Pytest fixture to capture stdout and stderr. 46 | """ 47 | # check passing both absolute and relative paths 48 | 49 | main(["ruff", path_0, path_1, path_2]) 50 | 51 | expected_path_0 = os.path.join("tests", "data", "notebook_for_testing.ipynb") 52 | expected_path_1 = os.path.join("tests", "data", "notebook_for_testing_copy.ipynb") 53 | expected_path_2 = os.path.join("tests", "data", "notebook_starting_with_md.ipynb") 54 | 55 | out, err = capsys.readouterr() 56 | 57 | # ignore ruff's suggestions 58 | prev = "" 59 | output: list[str] = [] 60 | for line in out.splitlines(): 61 | if "cell_" in line: 62 | # append previous line and matching line 63 | output.append(prev) 64 | output.append(line) 65 | prev = line 66 | 67 | expected_out = ( 68 | f"F401 [*] `os` imported but unused\n --> {expected_path_1}:cell_1:1:8\n" 69 | f"F401 [*] `glob` imported but unused\n --> {expected_path_1}:cell_1:3:8\n" 70 | f"F401 [*] `nbqa` imported but unused\n --> {expected_path_1}:cell_1:5:8\n" 71 | f"F401 [*] `os` imported but unused\n --> {expected_path_0}:cell_1:1:8\n" 72 | f"F401 [*] `glob` imported but unused\n --> {expected_path_0}:cell_1:3:8\n" 73 | f"F401 [*] `nbqa` imported but unused\n --> {expected_path_0}:cell_1:5:8\n" 74 | f"E402 Module level import not at top of file\n --> {expected_path_0}:cell_4:1:1\n" 75 | f"F401 [*] `random.randint` imported but unused\n --> {expected_path_0}:cell_4:1:20\n" 76 | f"E402 Module level import not at top of file\n --> {expected_path_0}:cell_5:1:1\n" 77 | f"E402 Module level import not at top of file\n --> {expected_path_0}:cell_5:2:1\n" 78 | f"F401 [*] `os` imported but unused\n --> {expected_path_2}:cell_1:1:8\n" 79 | f"F401 [*] `glob` imported but unused\n --> {expected_path_2}:cell_1:3:8\n" 80 | f"F401 [*] `nbqa` imported but unused\n --> {expected_path_2}:cell_1:5:8\n" 81 | ) 82 | 83 | # simple dedent of '\s+-->' 84 | out = "\n".join(sorted([x.lstrip() for x in output])) 85 | exp = "\n".join(sorted([x.lstrip() for x in expected_out.splitlines()])) 86 | 87 | assert out == exp 88 | assert err == "" 89 | 90 | 91 | def test_cell_with_all_magics(capsys: "CaptureFixture") -> None: 92 | """ 93 | Should ignore cell with all magics. 94 | 95 | Parameters 96 | ---------- 97 | capsys 98 | Pytest fixture to capture stdout and stderr. 99 | """ 100 | 101 | path = os.path.join("tests", "data", "all_magic_cell.ipynb") 102 | main(["ruff", "--quiet", path]) 103 | 104 | out, err = capsys.readouterr() 105 | assert out == "" 106 | assert err == "" 107 | 108 | 109 | def test_ruff_isort(capsys: "CaptureFixture") -> None: 110 | """ 111 | Should ignore cell with all magics. 112 | 113 | Parameters 114 | ---------- 115 | capsys 116 | Pytest fixture to capture stdout and stderr. 117 | """ 118 | 119 | path = os.path.join("tests", "data", "simple_imports.ipynb") 120 | main(["ruff", "--quiet", path, "--select=I"]) 121 | 122 | out, err = capsys.readouterr() 123 | assert out == "" 124 | assert err == "" 125 | 126 | 127 | def test_ruff_format(capsys: "CaptureFixture", tmp_notebook_for_testing: Path) -> None: 128 | """ 129 | Should ignore cell with all magics. 130 | 131 | Parameters 132 | ---------- 133 | capsys 134 | Pytest fixture to capture stdout and stderr. 135 | """ 136 | 137 | main(["ruff format", str(tmp_notebook_for_testing)]) 138 | 139 | out, err = capsys.readouterr() 140 | assert out == "1 file reformatted\n" 141 | assert err == "" 142 | -------------------------------------------------------------------------------- /nbqa/save_markdown_source.py: -------------------------------------------------------------------------------- 1 | """ 2 | Extract markdown cells from notebook and save them to temporary markdown file. 3 | 4 | Python cells, output, and metadata are ignored. 5 | """ 6 | 7 | import secrets 8 | from collections import defaultdict 9 | from typing import Any, DefaultDict, Mapping, MutableMapping, NamedTuple, Sequence 10 | 11 | from nbqa.handle_magics import MagicHandler 12 | from nbqa.notebook_info import NotebookInfo 13 | 14 | MARKDOWN_SEPARATOR = f"# %%NBQA-MD-SEP{secrets.token_hex(3)}\n" 15 | 16 | 17 | class Index(NamedTuple): 18 | """Keep track of line and cell number while iterating over cells.""" 19 | 20 | line_number: int 21 | cell_number: int 22 | 23 | 24 | def _parse_cell( 25 | source: Sequence[str], 26 | ) -> str: 27 | """ 28 | Parse cell, replacing line magics with python code as placeholder. 29 | 30 | Parameters 31 | ---------- 32 | source 33 | Source from notebook cell. 34 | 35 | Returns 36 | ------- 37 | str 38 | Parsed cell. 39 | """ 40 | parsed_cell = MARKDOWN_SEPARATOR 41 | parsed_cell += "".join(source) 42 | return f"{parsed_cell}\n" 43 | 44 | 45 | def _get_line_numbers_for_mapping( 46 | cell_source: str, 47 | ) -> Mapping[int, int]: 48 | """Get the line number mapping from python file to notebook cell. 49 | 50 | Parameters 51 | ---------- 52 | cell_source 53 | Source code of the notebook cell 54 | 55 | Returns 56 | ------- 57 | Mapping[int, int] 58 | Line number mapping from temporary python file to notebook cell 59 | """ 60 | lines_in_cell = cell_source.splitlines() 61 | line_mapping: MutableMapping[int, int] = {} 62 | 63 | line_mapping.update({i: i for i in range(len(lines_in_cell))}) 64 | 65 | return line_mapping 66 | 67 | 68 | def _should_ignore_markdown_cell( 69 | source: Sequence[str], 70 | skip_celltags: Sequence[str], 71 | tags: Sequence[str], 72 | ) -> bool: 73 | """ 74 | Return True if the current cell should be ignored from processing. 75 | 76 | Parameters 77 | ---------- 78 | source 79 | Source from the notebook cell 80 | 81 | Returns 82 | ------- 83 | bool 84 | True if the cell should ignored else False 85 | """ 86 | joined_source = "".join(source) 87 | if not joined_source or set(tags).intersection(skip_celltags): 88 | return True 89 | return False 90 | 91 | 92 | def main( # pylint: disable=R0914 93 | notebook_json: MutableMapping[str, Any], 94 | file_descriptor: int, 95 | skip_celltags: Sequence[str], 96 | ) -> NotebookInfo: 97 | """ 98 | Extract code cells from notebook and save them in temporary Python file. 99 | 100 | Parameters 101 | ---------- 102 | notebook_json 103 | Jupyter Notebook third-party tool is being run against. 104 | 105 | Returns 106 | ------- 107 | NotebookInfo 108 | 109 | Raises 110 | ------ 111 | AssertionError 112 | If hash collision (extremely rare event!) 113 | """ 114 | cells = notebook_json["cells"] 115 | 116 | result = [] 117 | cell_mapping = {0: "cell_0:0"} 118 | index = Index(line_number=0, cell_number=0) 119 | temporary_lines: DefaultDict[int, Sequence[MagicHandler]] = defaultdict(list) 120 | markdown_cells_to_ignore = set() 121 | 122 | whole_src = "".join( 123 | ["".join(cell["source"]) for cell in cells if cell["cell_type"] == "markdown"] 124 | ) 125 | if MARKDOWN_SEPARATOR.strip() in whole_src: 126 | raise AssertionError( 127 | "Extremely rare hash collision occurred - please re-run nbQA to fix this" 128 | ) 129 | 130 | for cell in cells: 131 | if cell["cell_type"] == "markdown": 132 | index = index._replace(cell_number=index.cell_number + 1) 133 | 134 | if _should_ignore_markdown_cell( 135 | cell["source"], 136 | skip_celltags, 137 | cell.get("metadata", {}).get("tags", []), 138 | ): 139 | markdown_cells_to_ignore.add(index.cell_number) 140 | continue 141 | 142 | parsed_cell = _parse_cell(cell["source"]) 143 | 144 | cell_mapping.update( 145 | { 146 | py_line 147 | + index.line_number 148 | + 1: f"cell_{index.cell_number}:{cell_line}" 149 | for py_line, cell_line in _get_line_numbers_for_mapping( 150 | parsed_cell, 151 | ).items() 152 | } 153 | ) 154 | result.append(parsed_cell) 155 | index = index._replace( 156 | line_number=index.line_number + len(parsed_cell.splitlines()) 157 | ) 158 | temporary_lines[index.cell_number] = [] # compatibility 159 | 160 | result_txt = "".join(result).rstrip("\n") + "\n" if result else "" 161 | with open(file_descriptor, "w", encoding="utf-8") as handle: 162 | handle.write(result_txt) 163 | 164 | return NotebookInfo(cell_mapping, set(), temporary_lines, markdown_cells_to_ignore) 165 | -------------------------------------------------------------------------------- /tests/tools/test_flake8_works.py: -------------------------------------------------------------------------------- 1 | """Check :code:`flake8` works as intended.""" 2 | 3 | import os 4 | from pathlib import Path 5 | from textwrap import dedent 6 | from typing import TYPE_CHECKING 7 | 8 | import pytest 9 | 10 | from nbqa.__main__ import main 11 | 12 | if TYPE_CHECKING: 13 | from _pytest.capture import CaptureFixture 14 | 15 | 16 | @pytest.mark.parametrize( 17 | "path_0, path_1, path_2", 18 | ( 19 | ( 20 | os.path.abspath( 21 | os.path.join("tests", "data", "notebook_for_testing.ipynb") 22 | ), 23 | os.path.abspath( 24 | os.path.join("tests", "data", "notebook_for_testing_copy.ipynb") 25 | ), 26 | os.path.abspath( 27 | os.path.join("tests", "data", "notebook_starting_with_md.ipynb") 28 | ), 29 | ), 30 | ( 31 | os.path.join("tests", "data", "notebook_for_testing.ipynb"), 32 | os.path.join("tests", "data", "notebook_for_testing_copy.ipynb"), 33 | os.path.join("tests", "data", "notebook_starting_with_md.ipynb"), 34 | ), 35 | ), 36 | ) 37 | def test_flake8_works( 38 | path_0: str, path_1: str, path_2: str, capsys: "CaptureFixture" 39 | ) -> None: 40 | """ 41 | Check flake8 works. Shouldn't alter the notebook content. 42 | 43 | Parameters 44 | ---------- 45 | capsys 46 | Pytest fixture to capture stdout and stderr. 47 | """ 48 | # check passing both absolute and relative paths 49 | 50 | main(["flake8", path_0, path_1, path_2]) 51 | 52 | expected_path_0 = os.path.join("tests", "data", "notebook_for_testing.ipynb") 53 | expected_path_1 = os.path.join("tests", "data", "notebook_for_testing_copy.ipynb") 54 | expected_path_2 = os.path.join("tests", "data", "notebook_starting_with_md.ipynb") 55 | 56 | out, err = capsys.readouterr() 57 | expected_out = dedent( 58 | f"""\ 59 | {expected_path_0}:cell_1:1:1: F401 'os' imported but unused 60 | {expected_path_0}:cell_1:3:1: F401 'glob' imported but unused 61 | {expected_path_0}:cell_1:5:1: F401 'nbqa' imported but unused 62 | {expected_path_0}:cell_2:19:9: W291 trailing whitespace 63 | {expected_path_0}:cell_4:1:1: E402 module level import not at top of file 64 | {expected_path_0}:cell_4:1:1: F401 'random.randint' imported but unused 65 | {expected_path_0}:cell_5:1:1: E402 module level import not at top of file 66 | {expected_path_0}:cell_5:2:1: E402 module level import not at top of file 67 | {expected_path_1}:cell_1:1:1: F401 'os' imported but unused 68 | {expected_path_1}:cell_1:3:1: F401 'glob' imported but unused 69 | {expected_path_1}:cell_1:5:1: F401 'nbqa' imported but unused 70 | {expected_path_2}:cell_1:1:1: F401 'os' imported but unused 71 | {expected_path_2}:cell_1:3:1: F401 'glob' imported but unused 72 | {expected_path_2}:cell_1:5:1: F401 'nbqa' imported but unused 73 | """ 74 | ) 75 | expected_err = "" 76 | assert sorted(out.splitlines()) == sorted(expected_out.splitlines()) 77 | assert sorted(err.splitlines()) == sorted(expected_err.splitlines()) 78 | 79 | 80 | def test_cell_with_all_magics(capsys: "CaptureFixture") -> None: 81 | """ 82 | Should ignore cell with all magics. 83 | 84 | Parameters 85 | ---------- 86 | capsys 87 | Pytest fixture to capture stdout and stderr. 88 | """ 89 | path = os.path.join("tests", "data", "all_magic_cell.ipynb") 90 | main(["flake8", path]) 91 | 92 | out, err = capsys.readouterr() 93 | assert out == "" 94 | assert err == "" 95 | 96 | 97 | def test_per_file_ignores( 98 | tmp_notebook_for_testing: Path, capsys: "CaptureFixture" 99 | ) -> None: 100 | """ 101 | Check flake8 per-file-ignore patterns work. 102 | 103 | Parameters 104 | ---------- 105 | tmp_notebook_for_testing 106 | notebook Path to test 107 | capsys 108 | Pytest fixture to capture stdout and stderr. 109 | """ 110 | # enable per-file ignores with nbqa glob 111 | flake8_ini = Path(".flake8") 112 | flake8_ini.write_text( 113 | dedent( 114 | """ 115 | [flake8] 116 | per-file-ignores = 117 | **/*.ipynb: E402 118 | **/*nbqa_ipynb.py: E402 119 | """ 120 | ), 121 | encoding="utf-8", 122 | ) 123 | 124 | main(["flake8", str(tmp_notebook_for_testing)]) 125 | flake8_ini.unlink() 126 | 127 | expected_path_0 = os.path.join("tests", "data", "notebook_for_testing.ipynb") 128 | 129 | out, err = capsys.readouterr() 130 | expected_out = dedent( 131 | f"""\ 132 | {expected_path_0}:cell_1:1:1: F401 'os' imported but unused 133 | {expected_path_0}:cell_1:3:1: F401 'glob' imported but unused 134 | {expected_path_0}:cell_1:5:1: F401 'nbqa' imported but unused 135 | {expected_path_0}:cell_2:19:9: W291 trailing whitespace 136 | {expected_path_0}:cell_4:1:1: F401 'random.randint' imported but unused 137 | """ 138 | ) 139 | assert err == "" 140 | assert sorted(out.splitlines()) == sorted(expected_out.splitlines()) 141 | -------------------------------------------------------------------------------- /tests/data/notebook_with_indented_magics.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# IPython magics\n", 8 | "\n", 9 | "This notebook is used for testing nbqa with ipython magics." 10 | ] 11 | }, 12 | { 13 | "cell_type": "code", 14 | "execution_count": null, 15 | "metadata": { 16 | "tags": [] 17 | }, 18 | "outputs": [], 19 | "source": [ 20 | "from random import randint\n", 21 | "from IPython import get_ipython" 22 | ] 23 | }, 24 | { 25 | "cell_type": "markdown", 26 | "metadata": {}, 27 | "source": [ 28 | "## Cell magics" 29 | ] 30 | }, 31 | { 32 | "cell_type": "code", 33 | "execution_count": null, 34 | "metadata": {}, 35 | "outputs": [], 36 | "source": [ 37 | "%%bash\n", 38 | "\n", 39 | "for n in {1..10}\n", 40 | "do\n", 41 | " echo -n \"$n \"\n", 42 | "done" 43 | ] 44 | }, 45 | { 46 | "cell_type": "code", 47 | "execution_count": null, 48 | "metadata": {}, 49 | "outputs": [], 50 | "source": [ 51 | "%%time\n", 52 | "\n", 53 | "import operator\n", 54 | "\n", 55 | "\n", 56 | "def compute(operand1,operand2, bin_op):\n", 57 | " \"\"\"Perform input binary operation over the given operands.\"\"\"\n", 58 | " return bin_op(operand1, operand2)\n", 59 | "\n", 60 | "\n", 61 | "compute(5,1, operator.add)" 62 | ] 63 | }, 64 | { 65 | "cell_type": "markdown", 66 | "metadata": {}, 67 | "source": [ 68 | "## Help Magics" 69 | ] 70 | }, 71 | { 72 | "cell_type": "code", 73 | "execution_count": null, 74 | "metadata": {}, 75 | "outputs": [], 76 | "source": [ 77 | "str.split??" 78 | ] 79 | }, 80 | { 81 | "cell_type": "code", 82 | "execution_count": null, 83 | "metadata": {}, 84 | "outputs": [], 85 | "source": [ 86 | "# would this comment also be considered as magic?\n", 87 | "str.split?" 88 | ] 89 | }, 90 | { 91 | "cell_type": "code", 92 | "execution_count": null, 93 | "metadata": {}, 94 | "outputs": [], 95 | "source": [ 96 | " ?str.splitlines" 97 | ] 98 | }, 99 | { 100 | "cell_type": "markdown", 101 | "metadata": {}, 102 | "source": [ 103 | "## Shell magics" 104 | ] 105 | }, 106 | { 107 | "cell_type": "code", 108 | "execution_count": null, 109 | "metadata": {}, 110 | "outputs": [], 111 | "source": [ 112 | "!grep -r '%%HTML' . | wc -l" 113 | ] 114 | }, 115 | { 116 | "cell_type": "code", 117 | "execution_count": null, 118 | "metadata": {}, 119 | "outputs": [], 120 | "source": [ 121 | "flake8_version = !pip list 2>&1 | grep flake8\n", 122 | "\n", 123 | "if flake8_version:\n", 124 | " my_var = flake8_version" 125 | ] 126 | }, 127 | { 128 | "cell_type": "markdown", 129 | "metadata": {}, 130 | "source": [ 131 | "## Line magics" 132 | ] 133 | }, 134 | { 135 | "cell_type": "code", 136 | "execution_count": null, 137 | "metadata": {}, 138 | "outputs": [], 139 | "source": [ 140 | " %time randint(5,10)" 141 | ] 142 | }, 143 | { 144 | "cell_type": "code", 145 | "execution_count": null, 146 | "metadata": {}, 147 | "outputs": [], 148 | "source": [ 149 | "if __debug__:\n", 150 | " %time compute(5,1, operator.mul)" 151 | ] 152 | }, 153 | { 154 | "cell_type": "code", 155 | "execution_count": null, 156 | "metadata": {}, 157 | "outputs": [], 158 | "source": [ 159 | "%time get_ipython().run_line_magic(\"lsmagic\", \"\")" 160 | ] 161 | }, 162 | { 163 | "cell_type": "code", 164 | "execution_count": null, 165 | "metadata": {}, 166 | "outputs": [], 167 | "source": [ 168 | "import pprint\n", 169 | "import sys\n", 170 | "\n", 171 | "%time pretty_print_object = pprint.PrettyPrinter(\\\n", 172 | " indent=4, width=80, stream=sys.stdout, compact=True, depth=5\\\n", 173 | " )" 174 | ] 175 | }, 176 | { 177 | "cell_type": "code", 178 | "execution_count": null, 179 | "metadata": {}, 180 | "outputs": [], 181 | "source": [ 182 | "# Add an ipython magic line in incomplete state\n", 183 | "# nbqa should handle such scenarios gracefully\n", 184 | "!pip list 2>&1 |\\" 185 | ] 186 | }, 187 | { 188 | "cell_type": "code", 189 | "execution_count": null, 190 | "metadata": {}, 191 | "outputs": [], 192 | "source": [ 193 | "# Though this is rare, this test case ensures the nbqa\\n\n", 194 | "# does not crash when such input is present\\n\",\n", 195 | "result = str.split??" 196 | ] 197 | } 198 | ], 199 | "metadata": { 200 | "kernelspec": { 201 | "display_name": "Python 3", 202 | "language": "python", 203 | "name": "python3" 204 | }, 205 | "language_info": { 206 | "codemirror_mode": { 207 | "name": "ipython", 208 | "version": 3 209 | }, 210 | "file_extension": ".py", 211 | "mimetype": "text/x-python", 212 | "name": "python", 213 | "nbconvert_exporter": "python", 214 | "pygments_lexer": "ipython3", 215 | "version": "3.8.5" 216 | } 217 | }, 218 | "nbformat": 4, 219 | "nbformat_minor": 4 220 | } 221 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # nbqa documentation build configuration file, created by 4 | # sphinx-quickstart on Fri Jun 9 13:47:02 2017. 5 | # 6 | # This file is execfile()d with the current directory set to its 7 | # containing dir. 8 | # 9 | # Note that not all possible configuration values are present in this 10 | # autogenerated file. 11 | # 12 | # All configuration values have a default; values that are commented out 13 | # serve to show the default. 14 | 15 | # If extensions (or modules to document with autodoc) are in another 16 | # directory, add these directories to sys.path here. If the directory is 17 | # relative to the documentation root, use os.path.abspath to make it 18 | # absolute, like shown here. 19 | # 20 | 21 | import nbqa 22 | 23 | # -- General configuration --------------------------------------------- 24 | 25 | # If your documentation needs a minimal Sphinx version, state it here. 26 | # 27 | # needs_sphinx = '1.0' 28 | 29 | # Add any Sphinx extension module names here, as strings. They can be 30 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 31 | extensions = [ 32 | "sphinx_copybutton", 33 | "sphinx.ext.autodoc", 34 | "sphinx.ext.viewcode", 35 | "sphinx.ext.napoleon", 36 | "myst_parser", 37 | ] 38 | 39 | add_module_names = False 40 | autodoc_member_order = "bysource" 41 | autodoc_typehints = "description" 42 | 43 | myst_title_to_header = True 44 | myst_heading_anchors = 3 45 | 46 | # Add any paths that contain templates here, relative to this directory. 47 | templates_path = ["_templates"] 48 | 49 | # The suffix(es) of source filenames. 50 | # You can specify multiple suffix as a list of string: 51 | # 52 | source_suffix = [".rst", ".md"] 53 | # source_suffix = ".rst" 54 | 55 | # The master toctree document. 56 | master_doc = "index" 57 | 58 | # General information about the project. 59 | project = "nbQA" 60 | copyright = "2020, Marco Gorelli" 61 | author = "Marco Gorelli" 62 | 63 | # The version info for the project you're documenting, acts as replacement 64 | # for |version| and |release|, also used in various other places throughout 65 | # the built documents. 66 | # 67 | # The short X.Y version. 68 | version = nbqa.__version__ 69 | # The full version, including alpha/beta/rc tags. 70 | release = nbqa.__version__ 71 | 72 | # The language for content autogenerated by Sphinx. Refer to documentation 73 | # for a list of supported languages. 74 | # 75 | # This is also used if you do content translation via gettext catalogs. 76 | # Usually you set "language" from the command line for these cases. 77 | language = "en" 78 | 79 | # List of patterns, relative to source directory, that match files and 80 | # directories to ignore when looking for source files. 81 | # This patterns also effect to html_static_path and html_extra_path 82 | exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] 83 | 84 | # The name of the Pygments (syntax highlighting) style to use. 85 | pygments_style = "sphinx" 86 | 87 | # If true, `todo` and `todoList` produce output, else they produce nothing. 88 | todo_include_todos = False 89 | 90 | 91 | # -- Options for HTML output ------------------------------------------- 92 | 93 | # The theme to use for HTML and HTML Help pages. See the documentation for 94 | # a list of builtin themes. 95 | # 96 | html_theme = "sphinx_rtd_theme" 97 | html_theme_options = { 98 | "navigation_depth": -1, 99 | } 100 | 101 | # Theme options are theme-specific and customize the look and feel of a 102 | # theme further. For a list of options available for each theme, see the 103 | # documentation. 104 | # 105 | # html_theme_options = {} 106 | 107 | # Add any paths that contain custom static files (such as style sheets) here, 108 | # relative to this directory. They are copied after the builtin static files, 109 | # so a file named "default.css" will overwrite the builtin "default.css". 110 | # html_static_path = ["_static"] 111 | 112 | 113 | # -- Options for HTMLHelp output --------------------------------------- 114 | 115 | # Output file base name for HTML help builder. 116 | htmlhelp_basename = "nbqadoc" 117 | 118 | copybutton_prompt_text = "$ " 119 | 120 | 121 | # -- Options for LaTeX output ------------------------------------------ 122 | 123 | latex_elements = { 124 | # The paper size ('letterpaper' or 'a4paper'). 125 | # 126 | # 'papersize': 'letterpaper', 127 | # The font size ('10pt', '11pt' or '12pt'). 128 | # 129 | # 'pointsize': '10pt', 130 | # Additional stuff for the LaTeX preamble. 131 | # 132 | # 'preamble': '', 133 | # Latex figure (float) alignment 134 | # 135 | # 'figure_align': 'htbp', 136 | } 137 | 138 | # Grouping the document tree into LaTeX files. List of tuples 139 | # (source start file, target name, title, author, documentclass 140 | # [howto, manual, or own class]). 141 | latex_documents = [ 142 | (master_doc, "nbqa.tex", "nbQA Documentation", "Marco Gorelli", "manual"), 143 | ] 144 | 145 | linkcheck_ignore = [ 146 | "https://github.com/pre-commit/pre-commit-hooks#", 147 | "https://twitter.com/HEPfeickert/status/1324823925898027008", 148 | ] 149 | 150 | # -- Options for manual page output ------------------------------------ 151 | 152 | # One entry per manual page. List of tuples 153 | # (source start file, name, description, authors, manual section). 154 | man_pages = [(master_doc, "nbqa", "nbQA Documentation", [author], 1)] 155 | 156 | 157 | # -- Options for Texinfo output ---------------------------------------- 158 | 159 | # Grouping the document tree into Texinfo files. List of tuples 160 | # (source start file, target name, title, author, 161 | # dir menu entry, description, category) 162 | texinfo_documents = [ 163 | ( 164 | master_doc, 165 | "nbqa", 166 | "nbQA Documentation", 167 | author, 168 | "nbqa", 169 | "One line description of project.", 170 | "Miscellaneous", 171 | ), 172 | ] 173 | -------------------------------------------------------------------------------- /docs/configuration.rst: -------------------------------------------------------------------------------- 1 | .. _configuration: 2 | 3 | Configuration 4 | ------------- 5 | 6 | You can configure :code:`nbQA` either at the command line, or by using a :code:`pyproject.toml` file. We'll see some examples below. 7 | 8 | .. note:: 9 | Please note that if you pass the same option via both the :code:`pyproject.toml` file and via the command-line, the command-line will take precedence. 10 | 11 | Preview / CI 12 | ~~~~~~~~~~~~ 13 | 14 | To preview changes without modifying your notebook, using the :code:`--nbqa-diff` flag. The return code will be ``1`` if ``nbQA`` would've modified any of 15 | your notebooks, and ``0`` otherwise. 16 | 17 | .. note:: 18 | You should not use ``-nbqa-diff`` alongside tools such as ``flake8`` which only check your code. Instead, use it with formatters such as ``isort``. 19 | 20 | Extra flags 21 | ~~~~~~~~~~~ 22 | 23 | If you wish to pass extra flags (e.g. :code:`--extend-ignore E203` to :code:`flake8`) you can either run 24 | 25 | .. code-block:: bash 26 | 27 | nbqa flake8 my_notebook.ipynb --extend-ignore E203 28 | 29 | or you can put the following in your :code:`pyproject.toml` file 30 | 31 | .. code-block:: toml 32 | 33 | [tool.nbqa.addopts] 34 | flake8 = [ 35 | "--extend-ignore=E203" 36 | ] 37 | 38 | .. note:: 39 | If you specify extra flags via both the :code:`pyproject.toml` file and the command-line, both will be passed on to the underlying command-line tool, 40 | with the options specified in :code:`pyproject.toml` passed first. In this case the exact behaviour will depend on the tool and the option in question. 41 | It's common that subsequent flags override earlier ones, but check the documentation for the tool and option in question to be sure. 42 | 43 | Cell magics 44 | ~~~~~~~~~~~ 45 | 46 | By default, :code:`nbQA` will ignore line magics (e.g. :code:`%matplotlib inline`), as well as most cell magics. 47 | To process code in cells with cell magics, you can use the :code:`--nbqa-process-cells` CLI argument. E.g. to process code within :code:`%%add_to` cell magics, use 48 | 49 | .. code-block:: bash 50 | 51 | nbqa black my_notebook.ipynb --nbqa-process-cells add_to 52 | 53 | or use the :code:`process_cells` option in your :code:`pyproject.toml` file: 54 | 55 | .. code-block:: toml 56 | 57 | [tool.nbqa.process_cells] 58 | black = ["add_to"] 59 | 60 | Include / exclude 61 | ~~~~~~~~~~~~~~~~~ 62 | 63 | To include or exclude notebooks from being processed, we recommend using ``nbQA``'s own ``--nbqa-files`` and ``--nbqa-exclude`` flags. 64 | These take regex patterns and match posix-like paths, `exactly like the same options from pre-commit `_. 65 | These can be set from the command-line with the ``--nbqa-files`` and ``--nbqa-exclude`` flags, or in your ``.pyproject.toml`` file in the 66 | ``[tool.nbqa.files]`` and ``[tool.nbqa.exclude]`` sections. 67 | 68 | Say you're running ``nbqa isort`` on a directory ``my_directory``. Here are some examples of how to include/exclude files: 69 | 70 | - exclude notebooks in ``my_directory`` whose name starts with ``poc_``: 71 | 72 | .. code-block:: toml 73 | 74 | [tool.nbqa.exclude] 75 | isort = "^my_directory/poc_" 76 | 77 | - exclude notebooks in subdirectory ``my_directory/my_subdirectory``: 78 | 79 | .. code-block:: toml 80 | 81 | [tool.nbqa.exclude] 82 | isort = "^my_directory/my_subdirectory/" 83 | 84 | - only include notebooks in ``my_directory`` whose name starts with ``EDA``: 85 | 86 | .. code-block:: toml 87 | 88 | [tool.nbqa.files] 89 | isort = "^my_directory/EDA" 90 | 91 | All the above examples can equivalently be run from the command-line, e.g. as 92 | 93 | .. code-block:: bash 94 | 95 | nbqa isort my_directory --nbqa-exclude ^my_directory/poc_ 96 | 97 | Don't skip bad cells 98 | ~~~~~~~~~~~~~~~~~~~~ 99 | 100 | By default, ``nbQA`` will skip cells with invalid syntax. To process cells with syntax errors, you can use the :code:`--nbqa-dont-skip-bad-cells` CLI argument. 101 | 102 | This can be set from the command-line with the ``--nbqa-dont-skip-bad-cells`` flag, or in your ``.pyproject.toml`` file in the 103 | ``[tool.nbqa.dont_skip_bad_cells]`` section. 104 | 105 | For example, to process "bad" cells when running ``black`` on ``notebook.ipynb``, you could 106 | add the following to your :code:`pyproject.toml` file: 107 | 108 | .. code-block:: toml 109 | 110 | [tool.nbqa.dont_skip_bad_cells] 111 | black = true 112 | 113 | or, from the command-line: 114 | 115 | .. code-block:: bash 116 | 117 | nbqa black notebook.ipynb --nbqa-dont-skip-bad-cells 118 | 119 | Skip cells based on celltags 120 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 121 | 122 | You can skip cells based on the celltags in their metadata using the :code:`--nbqa-skip-celltags` CLI argument. 123 | 124 | For example, to skip cells which contain either the ``skip-flake8`` or ``flake8-skip`` tags, you could 125 | add the following to your :code:`pyproject.toml` file: 126 | 127 | .. code-block:: toml 128 | 129 | [tool.nbqa.skip_celltags] 130 | black = ["skip-flake8", "flake8-skip"] 131 | 132 | or, from the command-line: 133 | 134 | .. code-block:: bash 135 | 136 | nbqa black notebook.ipynb --nbqa-skip-celltags=skip-flake8,flake8-skip 137 | 138 | Process markdown cells 139 | ~~~~~~~~~~~~~~~~~~~~~~~ 140 | 141 | You can process markdown cells (instead of code cells) by using the :code:`--nbqa-md` CLI argument. 142 | 143 | This is useful when running tools which run on markdown files, such as ``blacken-docs``. 144 | 145 | For example, you could add the following to your :code:`pyproject.toml` file: 146 | 147 | .. code-block:: toml 148 | 149 | [tool.nbqa.md] 150 | blacken-docs = true 151 | 152 | or, from the command-line: 153 | 154 | .. code-block:: bash 155 | 156 | nbqa blacken-docs notebook.ipynb --nbqa-md 157 | 158 | Shell commands 159 | ~~~~~~~~~~~~~~ 160 | 161 | By default, ``nbQA`` runs a command ``command`` as ``python -m command``. 162 | To instead run it as ``command`` (e.g. ``flake8heavened``, which can't be 163 | run as ``python -m flake8heavened``), 164 | you could add the following to your :code:`pyproject.toml` file: 165 | 166 | .. code-block:: toml 167 | 168 | [tool.nbqa.shell] 169 | flake8heavened = true 170 | 171 | or, from the command-line: 172 | 173 | .. code:: console 174 | 175 | $ nbqa flake8heavened my_notebook.ipynb --nbqa-shell 176 | -------------------------------------------------------------------------------- /nbqa/cmdline.py: -------------------------------------------------------------------------------- 1 | """Parses the command line arguments provided.""" 2 | 3 | import argparse 4 | import sys 5 | from textwrap import dedent 6 | from typing import Optional, Sequence 7 | 8 | from nbqa import __version__ 9 | from nbqa.text import BOLD, RESET 10 | 11 | CONFIGURATION_URL = "https://nbqa.readthedocs.io/en/latest/configuration.html" 12 | DOCS_URL = "https://nbqa.readthedocs.io/en/latest/index.html" 13 | USAGE_MSG = dedent( 14 | f"""\ 15 | nbqa 16 | 17 | {BOLD}Please specify:{RESET} 18 | - 1) a code quality tool (e.g. `black`, `pyupgrade`, `flake`, ...) 19 | - 2) some notebooks (or, if supported by the tool, directories) 20 | - 3) (optional) flags for nbqa (e.g. `--nbqa-diff`, `--nbqa-shell`) 21 | - 4) (optional) flags for code quality tool (e.g. `--line-length` for `black`) 22 | 23 | {BOLD}Examples:{RESET} 24 | nbqa flake8 notebook.ipynb 25 | nbqa black notebook.ipynb --line-length=96 26 | nbqa pyupgrade notebook_1.ipynb notebook_2.ipynb 27 | 28 | See {DOCS_URL} for more details on how to run `nbqa`. 29 | """ 30 | ) 31 | DEPRECATED = { 32 | "--nbqa-skip-bad-cells": ( 33 | "was deprecated in 0.13.0\n" 34 | "Cells with invalid syntax are now skipped by default" 35 | ), 36 | "--nbqa-ignore-cells": "was deprecated in 0.8.0 and is now unnecessary", 37 | "--nbqa-config": "was deprecated in 0.8.0 and is now unnecessary", 38 | "--nbqa-mutate": "was deprecated in 1.0.0 and is now unnecessary", 39 | } 40 | 41 | 42 | class CLIArgs: # pylint: disable=R0902 43 | """Stores the command line arguments passed.""" 44 | 45 | command: str 46 | root_dirs: Sequence[str] 47 | addopts: Optional[Sequence[str]] 48 | process_cells: Optional[Sequence[str]] 49 | diff: Optional[bool] 50 | files: Optional[str] 51 | exclude: Optional[str] 52 | dont_skip_bad_cells: Optional[bool] 53 | md: Optional[bool] 54 | shell: Optional[bool] 55 | 56 | def __init__(self, args: argparse.Namespace, cmd_args: Sequence[str]) -> None: 57 | """ 58 | Initialize this instance with the parsed command line arguments. 59 | 60 | Parameters 61 | ---------- 62 | args 63 | Command line arguments passed to nbqa 64 | cmd_args 65 | Additional options to pass to the tool 66 | """ 67 | if cmd_args: 68 | for flag, msg in DEPRECATED.items(): 69 | if flag in cmd_args: 70 | sys.stderr.write(f"Flag {flag} {msg}\n") 71 | cmd_args = [arg for arg in cmd_args if arg != flag] 72 | self.command = args.command 73 | self.root_dirs = args.root_dirs 74 | self.addopts = cmd_args or None 75 | if args.nbqa_process_cells is not None: 76 | self.process_cells = args.nbqa_process_cells.split(",") 77 | else: 78 | self.process_cells = None 79 | self.diff = args.nbqa_diff or None 80 | self.files = args.nbqa_files 81 | self.exclude = args.nbqa_exclude 82 | self.dont_skip_bad_cells = args.nbqa_dont_skip_bad_cells or None 83 | if args.nbqa_skip_celltags is not None: 84 | self.skip_celltags = args.nbqa_skip_celltags.split(",") 85 | else: 86 | self.skip_celltags = None 87 | self.md = args.nbqa_md or None 88 | self.shell = args.nbqa_shell or None 89 | 90 | def __repr__(self) -> str: # pragma: nocover 91 | """Print prettily.""" 92 | return str(self.__dict__) 93 | 94 | @staticmethod 95 | def parse_args(argv: Optional[Sequence[str]]) -> "CLIArgs": 96 | """ 97 | Parse command-line arguments. 98 | 99 | Parameters 100 | ---------- 101 | argv 102 | Passed via command-line. 103 | Returns 104 | ------- 105 | CLIArgs 106 | Object that holds all the parsed command line arguments. 107 | """ 108 | parser = argparse.ArgumentParser( 109 | description="Run any standard Python code-quality tool on a Jupyter notebook.", 110 | usage=USAGE_MSG, 111 | ) 112 | parser.add_argument("command", help="Command to run, e.g. `flake8`.") 113 | parser.add_argument( 114 | "root_dirs", nargs="+", help="Notebooks or directories to run command on." 115 | ) 116 | parser.add_argument( 117 | "--nbqa-files", 118 | help="Global file include pattern.", 119 | ) 120 | parser.add_argument( 121 | "--nbqa-exclude", 122 | help="Global file exclude pattern.", 123 | ) 124 | parser.add_argument( 125 | "--nbqa-diff", 126 | action="store_true", 127 | help="Show diff which would result from running tool.", 128 | ) 129 | parser.add_argument( 130 | "--nbqa-shell", 131 | action="store_true", 132 | help="Run `command` directly rather than `python -m command`", 133 | ) 134 | parser.add_argument( 135 | "--nbqa-process-cells", 136 | required=False, 137 | help=dedent( 138 | r""" 139 | Process code within these cell magics. You can pass multiple options, 140 | e.g. `nbqa black my_notebook.ipynb --nbqa-process-cells add_to,write_to` 141 | by placing commas between them. 142 | """ 143 | ), 144 | ) 145 | parser.add_argument( 146 | "--version", action="version", version=f"nbqa {__version__}" 147 | ) 148 | parser.add_argument( 149 | "--nbqa-dont-skip-bad-cells", 150 | action="store_true", 151 | help="Don't skip cells with invalid syntax.", 152 | ) 153 | parser.add_argument( 154 | "--nbqa-skip-celltags", 155 | required=False, 156 | help=dedent( 157 | r""" 158 | Skip cells with have any of the given celltags. 159 | """ 160 | ), 161 | ) 162 | parser.add_argument( 163 | "--nbqa-md", 164 | action="store_true", 165 | help=dedent( 166 | r""" 167 | Process markdown cells, rather than Python ones. 168 | """ 169 | ), 170 | ) 171 | args, cmd_args = parser.parse_known_args(argv) 172 | return CLIArgs(args, cmd_args) 173 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | """Define some fixtures that can be reused between tests.""" 2 | 3 | import shutil 4 | import sys 5 | from pathlib import Path 6 | from shutil import copytree # pylint: disable=E0611,W4901 7 | from typing import TYPE_CHECKING, Iterator 8 | 9 | import pytest 10 | 11 | if TYPE_CHECKING: 12 | from py._path.local import LocalPath 13 | 14 | 15 | @pytest.fixture(autouse=True) 16 | def tmp_pyprojecttoml(tmpdir: "LocalPath") -> Iterator[Path]: 17 | """ 18 | Temporarily delete pyproject.toml so it can be recreated during tests. 19 | 20 | Parameters 21 | ---------- 22 | tmpdir 23 | Pytest fixture, gives us a temporary directory. 24 | """ 25 | filename = Path("pyproject.toml") 26 | temp_file = Path(tmpdir) / filename 27 | shutil.copy(str(filename), str(temp_file)) 28 | filename.unlink() 29 | yield filename 30 | shutil.copy(str(temp_file), str(filename)) 31 | 32 | 33 | @pytest.fixture(autouse=True) 34 | def tmp_setupcfg(tmpdir: "LocalPath") -> Iterator[None]: 35 | """ 36 | Temporarily delete setup.cfg so it can be recreated during tests. 37 | 38 | Parameters 39 | ---------- 40 | tmpdir 41 | Pytest fixture, gives us a temporary directory. 42 | """ 43 | filename = Path("setup.cfg") 44 | temp_file = Path(tmpdir) / filename 45 | shutil.copy(str(filename), str(temp_file)) 46 | filename.unlink() 47 | yield 48 | shutil.copy(str(temp_file), str(filename)) 49 | 50 | 51 | @pytest.fixture 52 | def tmp_notebook_for_testing(tmpdir: "LocalPath") -> Iterator[Path]: 53 | """ 54 | Make temporary copy of test notebook before it's operated on, then revert it. 55 | 56 | Parameters 57 | ---------- 58 | tmpdir 59 | Pytest fixture, gives us a temporary directory. 60 | 61 | Yields 62 | ------ 63 | Path 64 | Temporary copy of test notebook. 65 | """ 66 | filename = Path("tests/data") / "notebook_for_testing.ipynb" 67 | temp_file = Path(tmpdir) / "tmp.ipynb" 68 | shutil.copy(str(filename), str(temp_file)) 69 | yield filename 70 | shutil.copy(str(temp_file), str(filename)) 71 | 72 | 73 | @pytest.fixture 74 | def tmp_notebook_with_multiline(tmpdir: "LocalPath") -> Iterator[Path]: 75 | """ 76 | Make temporary copy of test notebook before it's operated on, then revert it. 77 | 78 | Parameters 79 | ---------- 80 | tmpdir 81 | Pytest fixture, gives us a temporary directory. 82 | 83 | Yields 84 | ------ 85 | Path 86 | Temporary copy of test notebook. 87 | """ 88 | filename = Path("tests/data") / "clean_notebook_with_multiline.ipynb" 89 | temp_file = Path(tmpdir) / "tmp.ipynb" 90 | shutil.copy(str(filename), str(temp_file)) 91 | yield filename 92 | shutil.copy(str(temp_file), str(filename)) 93 | 94 | 95 | @pytest.fixture 96 | def tmp_notebook_starting_with_md(tmpdir: "LocalPath") -> Iterator[Path]: 97 | """ 98 | Make temporary copy of test notebook before it's operated on, then revert it. 99 | 100 | Parameters 101 | ---------- 102 | tmpdir 103 | Pytest fixture, gives us a temporary directory. 104 | 105 | Yields 106 | ------ 107 | Path 108 | Temporary copy of notebook. 109 | """ 110 | filename = Path("tests/data") / "notebook_starting_with_md.ipynb" 111 | temp_file = Path(tmpdir) / "tmp.ipynb" 112 | shutil.copy(str(filename), str(temp_file)) 113 | yield filename 114 | shutil.copy(str(temp_file), str(filename)) 115 | 116 | 117 | @pytest.fixture 118 | def tmp_test_data(tmpdir: "LocalPath") -> Iterator[Path]: 119 | """ 120 | Make temporary copy of test data before it's operated on, then revert it. 121 | 122 | Parameters 123 | ---------- 124 | tmpdir 125 | Pytest fixture, gives us a temporary directory. 126 | 127 | Yields 128 | ------ 129 | Path 130 | Temporary copy of test data. 131 | """ 132 | dirname = Path("tests/data") 133 | temp_dir = Path(tmpdir) 134 | copytree(str(dirname), str(temp_dir / dirname)) 135 | yield dirname 136 | copytree(str(temp_dir / dirname), str(dirname), dirs_exist_ok=True) 137 | 138 | 139 | @pytest.fixture 140 | def tmp_notebook_with_trailing_semicolon(tmpdir: "LocalPath") -> Iterator[Path]: 141 | """ 142 | Make temporary copy of test notebook before it's operated on, then revert it. 143 | 144 | Parameters 145 | ---------- 146 | tmpdir 147 | Pytest fixture, gives us a temporary directory. 148 | 149 | Yields 150 | ------ 151 | Path 152 | Temporary copy of notebook. 153 | """ 154 | filename = Path("tests/data") / "notebook_with_trailing_semicolon.ipynb" 155 | temp_file = Path(tmpdir) / "tmp.ipynb" 156 | shutil.copy(str(filename), str(temp_file)) 157 | yield filename 158 | shutil.copy(str(temp_file), str(filename)) 159 | 160 | 161 | @pytest.fixture 162 | def tmp_notebook_with_indented_magics(tmpdir: "LocalPath") -> Iterator[Path]: 163 | """ 164 | Make temporary copy of test notebook before it's operated on, then revert it. 165 | 166 | Parameters 167 | ---------- 168 | tmpdir 169 | Pytest fixture, gives us a temporary directory. 170 | 171 | Yields 172 | ------ 173 | Path 174 | Temporary copy of notebook. 175 | """ 176 | filename = Path("tests/data") / "notebook_with_indented_magics.ipynb" 177 | temp_file = Path(tmpdir) / "tmp.ipynb" 178 | shutil.copy(str(filename), str(temp_file)) 179 | yield filename 180 | shutil.copy(str(temp_file), str(filename)) 181 | 182 | 183 | @pytest.fixture 184 | def tmp_notebook_for_autoflake(tmpdir: "LocalPath") -> Iterator[Path]: 185 | """ 186 | Make temporary copy of test notebook before it's operated on, then revert it. 187 | 188 | Parameters 189 | ---------- 190 | tmpdir 191 | Pytest fixture, gives us a temporary directory. 192 | 193 | Yields 194 | ------ 195 | Path 196 | Temporary copy of notebook. 197 | """ 198 | filename = Path("tests/data") / "notebook_for_autoflake.ipynb" 199 | temp_file = Path(tmpdir) / "tmp.ipynb" 200 | shutil.copy(str(filename), str(temp_file)) 201 | yield filename 202 | shutil.copy(str(temp_file), str(filename)) 203 | 204 | 205 | @pytest.fixture 206 | def tmp_remove_comments() -> Iterator[None]: 207 | """Make temporary copy of ``tests/remove_comments.py`` in root dir.""" 208 | temp_file = Path("remove_comments.py") 209 | shutil.copy(str(Path("tests") / temp_file), str(temp_file)) 210 | yield 211 | temp_file.unlink() 212 | if "remove_comments" in sys.modules: 213 | del sys.modules["remove_comments"] 214 | 215 | 216 | @pytest.fixture 217 | def tmp_remove_all() -> Iterator[None]: 218 | """Make temporary copy of ``tests/remove_all.py`` in root dir.""" 219 | temp_file = Path("remove_all.py") 220 | shutil.copy(str(Path("tests") / temp_file), str(temp_file)) 221 | yield 222 | temp_file.unlink() 223 | if "remove_all" in sys.modules: 224 | del sys.modules["remove_all"] 225 | 226 | 227 | @pytest.fixture 228 | def tmp_print_6174() -> Iterator[None]: 229 | """Make temporary copy of ``tests/print_6174.py`` in root dir.""" 230 | temp_file = Path("print_6174.py") 231 | shutil.copy(str(Path("tests") / temp_file), str(temp_file)) 232 | yield 233 | temp_file.unlink() 234 | --------------------------------------------------------------------------------