├── 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",
13 | "var anchors = $('a.anchor-link');\n",
14 | "var headingTypes = $(anchors).parent().map(function() { return $(this).prop('tagName')});\n",
15 | "var headingTexts = $(anchors).parent().map(function() { return $(this).text()});\n",
16 | "var subList = false;\n",
17 | "\n",
18 | "$.each(anchors, function(i, anch) {\n",
19 | " var hType = headingTypes[i];\n",
20 | " var hText = headingTexts[i];\n",
21 | " hText = hText.substr(0, hText.length - 1);\n",
22 | " if (hType == 'H2') {\n",
23 | " if (subList) {\n",
24 | " tocEntries.push('
')\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",
33 | " }\n",
34 | " tocEntries.push('- ' + hText + '
')\n",
35 | " }\n",
36 | "});\n",
37 | "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 |
--------------------------------------------------------------------------------