├── tests ├── parsers │ ├── __init__.py │ ├── test_notebook_parsers.py │ └── test_cell_parsers.py ├── flake8_integration │ ├── __init__.py │ ├── conftest.py │ ├── test_formatter.py │ └── test_cli.py ├── data │ ├── notebooks │ │ ├── falsy_python_file.py │ │ ├── not_a_notebook.ipynb │ │ ├── notebook_with_out_ipython_magic.ipynb │ │ ├── cell_with_source_string.ipynb │ │ ├── notebook_with_out_flake8_tags.ipynb │ │ └── notebook_with_flake8_tags.ipynb │ ├── intermediate_py_files │ │ ├── notebook_with_out_ipython_magic.ipynb_parsed │ │ ├── cell_with_source_string.ipynb_parsed │ │ ├── notebook_with_out_flake8_tags.ipynb_parsed │ │ └── notebook_with_flake8_tags.ipynb_parsed │ ├── expected_output_config_test.txt │ ├── expected_output_exec_count.txt │ ├── expected_output_total_cell_count.txt │ └── expected_output_code_cell_count.txt ├── __init__.py ├── conftest.py └── test__main__.py ├── docs ├── authors.rst ├── contributing.rst ├── readme.md ├── changelog.md ├── notebooks │ ├── with_tags.nblink │ └── with_out_tags.nblink ├── _templates │ └── autosummary │ │ ├── exception.rst │ │ ├── function.rst │ │ ├── method.rst │ │ ├── class.rst │ │ └── module.rst ├── examples.rst ├── api_docs.rst ├── requirements.txt ├── index.rst ├── Makefile ├── make.bat ├── installation.rst ├── usage.rst ├── conf.py └── _static │ └── interrogate_badge.svg ├── .gitattributes ├── setup.py ├── flake8_nb ├── flake8_integration │ ├── __init__.py │ ├── formatter.py │ └── cli.py ├── parsers │ ├── __init__.py │ └── cell_parsers.py ├── __main__.py └── __init__.py ├── readthedocs.yml ├── .pre-commit-hooks.yaml ├── binder └── environment.yml ├── .github ├── dependabot.yml ├── workflows │ ├── trigger-binder.yml │ ├── test-nightly-schedule.yml │ ├── codeql.yml │ ├── test-pre-coomit-hook.yml │ ├── binder-on-pr.yml │ ├── autoupdate-pre-commit-config.yml │ └── test.yml ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md └── hook_tester.py ├── MANIFEST.in ├── .sourcery.yaml ├── .editorconfig ├── requirements_dev.txt ├── AUTHORS.rst ├── pyproject.toml ├── tox.ini ├── .gitignore ├── Makefile ├── setup.cfg ├── changelog.md ├── CODE_OF_CONDUCT.md ├── .pre-commit-config.yaml ├── .all-contributorsrc ├── CONTRIBUTING.rst ├── README.md └── LICENSE /tests/parsers/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/flake8_integration/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/authors.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../AUTHORS.rst 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=lf 2 | *.bat text eol=crlf 3 | -------------------------------------------------------------------------------- /docs/contributing.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../CONTRIBUTING.rst 2 | -------------------------------------------------------------------------------- /docs/readme.md: -------------------------------------------------------------------------------- 1 | ```{include} ../README.md 2 | 3 | ``` 4 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup() 4 | -------------------------------------------------------------------------------- /tests/data/notebooks/falsy_python_file.py: -------------------------------------------------------------------------------- 1 | import foo 2 | -------------------------------------------------------------------------------- /docs/changelog.md: -------------------------------------------------------------------------------- 1 | ```{include} ../changelog.md 2 | 3 | ``` 4 | -------------------------------------------------------------------------------- /tests/data/notebooks/not_a_notebook.ipynb: -------------------------------------------------------------------------------- 1 | This isn't an actual notebook and should give a warning at parsing! 2 | -------------------------------------------------------------------------------- /docs/notebooks/with_tags.nblink: -------------------------------------------------------------------------------- 1 | { 2 | "path": "../../tests/data/notebooks/notebook_with_flake8_tags.ipynb" 3 | } 4 | -------------------------------------------------------------------------------- /docs/_templates/autosummary/exception.rst: -------------------------------------------------------------------------------- 1 | {{ name | escape | underline }} 2 | 3 | .. autoexception:: {{ fullname }} 4 | -------------------------------------------------------------------------------- /docs/_templates/autosummary/function.rst: -------------------------------------------------------------------------------- 1 | {{ name | escape | underline }} 2 | 3 | .. autofunction:: {{ fullname }} 4 | -------------------------------------------------------------------------------- /flake8_nb/flake8_integration/__init__.py: -------------------------------------------------------------------------------- 1 | """Package containing code to integrate the parserers and hacking flake8.""" 2 | -------------------------------------------------------------------------------- /docs/notebooks/with_out_tags.nblink: -------------------------------------------------------------------------------- 1 | { 2 | "path": "../../tests/data/notebooks/notebook_with_out_flake8_tags.ipynb" 3 | } 4 | -------------------------------------------------------------------------------- /docs/_templates/autosummary/method.rst: -------------------------------------------------------------------------------- 1 | {{ name | escape | underline }} 2 | 3 | .. automethod:: {{ fullname }} 4 | :noindex: 5 | -------------------------------------------------------------------------------- /tests/data/intermediate_py_files/notebook_with_out_ipython_magic.ipynb_parsed: -------------------------------------------------------------------------------- 1 | # INTERMEDIATE_CELL_SEPARATOR (1,1,3) 2 | 3 | 4 | 1 + 1 5 | -------------------------------------------------------------------------------- /docs/examples.rst: -------------------------------------------------------------------------------- 1 | ======== 2 | Examples 3 | ======== 4 | 5 | 6 | .. toctree:: 7 | :maxdepth: 2 8 | 9 | notebooks/with_out_tags 10 | notebooks/with_tags 11 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | """Unit test package for flake8_nb.""" 2 | import os 3 | 4 | TEST_NOTEBOOK_BASE_PATH = os.path.abspath( 5 | os.path.join(os.path.dirname(__file__), "data", "notebooks") 6 | ) 7 | -------------------------------------------------------------------------------- /tests/data/expected_output_config_test.txt: -------------------------------------------------------------------------------- 1 | cell_with_source_string.ipynb#In[1]:3:1: E302 expected 2 blank lines, found 1 2 | notebook_with_out_ipython_magic.ipynb#In[1]:1:4: E222 multiple spaces after operator 3 | -------------------------------------------------------------------------------- /tests/data/intermediate_py_files/cell_with_source_string.ipynb_parsed: -------------------------------------------------------------------------------- 1 | # INTERMEDIATE_CELL_SEPARATOR (1,1,1) 2 | 3 | 4 | from ipywidgets import interact 5 | 6 | def f(x): 7 | return x 8 | 9 | 10 | interact(f, x=10) 11 | -------------------------------------------------------------------------------- /readthedocs.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | formats: all 4 | 5 | build: 6 | image: latest 7 | 8 | python: 9 | version: 3.8 10 | install: 11 | - method: pip 12 | path: . 13 | - requirements: docs/requirements.txt 14 | -------------------------------------------------------------------------------- /docs/api_docs.rst: -------------------------------------------------------------------------------- 1 | ============== 2 | Inner workings 3 | ============== 4 | 5 | This is the detailed documentation of the inner workings of ``flake8_nb``. 6 | 7 | .. autosummary:: 8 | :toctree: api 9 | :recursive: 10 | 11 | flake8_nb 12 | -------------------------------------------------------------------------------- /.pre-commit-hooks.yaml: -------------------------------------------------------------------------------- 1 | - id: flake8-nb 2 | name: flake8-nb 3 | description: "flake8 checking for jupyter notebooks" 4 | entry: flake8-nb 5 | language: python 6 | language_version: python3 7 | require_serial: true 8 | types: [file] 9 | files: \.(py|ipynb)$ 10 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | # docs requirements 2 | Sphinx>=3.2.0 3 | sphinx-rtd-theme>=0.5.0 4 | sphinx-copybutton>=0.3.0 5 | myst-parser>=0.12.0 6 | Sphinx-Substitution-Extensions>=2020.7.4.1 7 | # notebook integration in docs 8 | nbsphinx>=0.8.6 9 | nbsphinx-link>=1.3.0 10 | ipykernel>=5.1.2 11 | -------------------------------------------------------------------------------- /binder/environment.yml: -------------------------------------------------------------------------------- 1 | channels: 2 | - conda-forge 3 | 4 | dependencies: 5 | # runtime dependencies 6 | - python >=3.8,<3.9.0a0 7 | - jupyterlab >=3,<4.0.0a0 8 | # labextension build dependencies 9 | - nodejs >=14,<15 10 | - pip 11 | - wheel 12 | - pip: 13 | - -e .. 14 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: pip 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "04:00" 8 | 9 | # Maintain dependencies for GitHub Actions 10 | - package-ecosystem: "github-actions" 11 | directory: "/" 12 | schedule: 13 | interval: "weekly" 14 | -------------------------------------------------------------------------------- /.github/workflows/trigger-binder.yml: -------------------------------------------------------------------------------- 1 | name: "Trigger-Binder-build" 2 | on: 3 | push: 4 | branches: 5 | - main 6 | workflow_dispatch: 7 | 8 | jobs: 9 | trigger-binder-build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: s-weigand/trigger-mybinder-build@v1 13 | with: 14 | target-repo: s-weigand/flake8-nb 15 | target-state: main 16 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | Welcome to flake8-nb's documentation! 2 | ===================================== 3 | 4 | .. toctree:: 5 | :maxdepth: 2 6 | :caption: Contents: 7 | 8 | readme 9 | installation 10 | usage 11 | examples 12 | api_docs 13 | contributing 14 | authors 15 | changelog 16 | 17 | Indices and tables 18 | ================== 19 | * :ref:`genindex` 20 | * :ref:`modindex` 21 | * :ref:`search` 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include AUTHORS.rst 2 | include CONTRIBUTING.rst 3 | include changelog.md 4 | include LICENSE 5 | include README.md 6 | 7 | recursive-include tests * 8 | recursive-exclude * __pycache__ 9 | recursive-exclude * *.py[co] 10 | 11 | recursive-exclude docs * 12 | recursive-exclude binder * 13 | recursive-exclude **/.ipynb_checkpoints/** * 14 | 15 | exclude . .* Makefile readthedocs.yml requirements_dev.txt CODE_OF_CONDUCT.md tox.ini 16 | -------------------------------------------------------------------------------- /.sourcery.yaml: -------------------------------------------------------------------------------- 1 | ignore: 2 | - tests/* 3 | 4 | refactor: 5 | skip: [] 6 | recommendation_level: suggestion 7 | python_version: "3.7" 8 | 9 | metrics: 10 | quality_threshold: 25.0 11 | 12 | clone_detection: 13 | min_lines: 3 14 | min_duplicates: 2 15 | identical_clones_only: false 16 | 17 | github: 18 | labels: [] 19 | ignore_labels: [sourcery-ignore] 20 | request_review: author 21 | sourcery_branch: sourcery/{base_branch} 22 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | indent_style = space 7 | indent_size = 2 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | charset = utf-8 11 | end_of_line = lf 12 | 13 | [*.bat] 14 | indent_style = tab 15 | end_of_line = crlf 16 | 17 | [.gitattributes] 18 | indent_style = tab 19 | 20 | [*.py] 21 | indent_size = 4 22 | 23 | [LICENSE] 24 | insert_final_newline = false 25 | 26 | [Makefile] 27 | indent_style = tab 28 | -------------------------------------------------------------------------------- /requirements_dev.txt: -------------------------------------------------------------------------------- 1 | pip>=19.2.3 2 | wheel>=0.33.6 3 | # quality asurence 4 | black>=19.10b0 5 | isort>=5.2.2 6 | pre-commit>=2.6.0 7 | pydocstyle>=5.0.2 8 | # test requirements 9 | tox>=3.5.2 10 | pytest>=5.1.3 11 | pytest-runner>=5.1 12 | pytest-cov>=2.5.1 13 | coverage[toml]>=4.5.4 14 | # doc requirements 15 | -r docs/requirements.txt 16 | # package runtime requirements 17 | flake8==5.0.4 18 | nbconvert==7.6.0; python_version > '3.7' 19 | ipython==8.14.0; python_version >= '3.9' 20 | -------------------------------------------------------------------------------- /AUTHORS.rst: -------------------------------------------------------------------------------- 1 | ======= 2 | Credits 3 | ======= 4 | 5 | Many thanks go to the creators and contributors of flake8_. 6 | Which inspired me to write this hack on top of it and supplies nearly all 7 | of the functionality. 8 | 9 | This packages skeleton was created with Cookiecutter_ and the 10 | `audreyr/cookiecutter-pypackage`_ project template. 11 | 12 | The idea to use cell tags was inspired by the use of cell tags in nbval_. 13 | 14 | 15 | .. _flake8: https://github.com/pycqa/flake8 16 | .. _Cookiecutter: https://github.com/cookiecutter/cookiecutter 17 | .. _`audreyr/cookiecutter-pypackage`: https://github.com/audreyr/cookiecutter-pypackage 18 | .. _nbval: https://github.com/computationalmodelling/nbval 19 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "" 5 | labels: enhancement 6 | assignees: s-weigand 7 | --- 8 | 9 | **Is your feature request related to a problem? Please describe.** 10 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 11 | 12 | **Describe the solution you'd like** 13 | A clear and concise description of what you want to happen. 14 | 15 | **Describe alternatives you've considered** 16 | A clear and concise description of any alternative solutions or features you've considered. 17 | 18 | **Additional context** 19 | Add any other context or screenshots about the feature request here. 20 | -------------------------------------------------------------------------------- /.github/hook_tester.py: -------------------------------------------------------------------------------- 1 | """Test file for the precommit hook.""" 2 | from pathlib import Path 3 | 4 | REPO_ROOT = Path(__file__).parent.parent 5 | 6 | 7 | def test_hook_output(): 8 | """Tests that flake8-nb output is contained in the hooks output.""" 9 | flake8_nb_output_lines = (REPO_ROOT / "flake8-nb_run_output.txt").read_text().splitlines() 10 | hook_output_lines = (REPO_ROOT / "hook_run_output.txt").read_text().splitlines() 11 | # the first line is removed due to different location of the executable 12 | flake8_nb_output_lines = flake8_nb_output_lines[1:] 13 | assert all( 14 | flake8_nb_output_line in hook_output_lines 15 | for flake8_nb_output_line in flake8_nb_output_lines 16 | ) 17 | -------------------------------------------------------------------------------- /flake8_nb/parsers/__init__.py: -------------------------------------------------------------------------------- 1 | """Package responsible for transforming notebooks to valid python files.""" 2 | from typing import Any 3 | from typing import Dict 4 | from typing import NamedTuple 5 | 6 | NotebookCell = Dict[str, Any] 7 | 8 | 9 | class CellId(NamedTuple): 10 | """Container to hold information to identify a cell. 11 | 12 | The information are: 13 | * ``input_nr`` 14 | Execution count, " " for not executed cells 15 | * ``code_cell_nr`` 16 | Count of the code cell starting at 1, ignoring raw and markdown cells 17 | * ``total_cell_nr`` 18 | Total count of the cell starting at 1, considering raw and markdown cells. 19 | """ 20 | 21 | input_nr: str 22 | code_cell_nr: int 23 | total_cell_nr: int 24 | -------------------------------------------------------------------------------- /flake8_nb/__main__.py: -------------------------------------------------------------------------------- 1 | """Command-line implementation of flake8_nb.""" 2 | from __future__ import annotations 3 | 4 | import sys 5 | 6 | from flake8_nb.flake8_integration.cli import Flake8NbApplication 7 | 8 | 9 | def main(argv: list[str] | None = None) -> None: 10 | """Execute the main bit of the application. 11 | 12 | This handles the creation of an instance of :class:`Application`, runs it, 13 | and then exits the application. 14 | 15 | 16 | Parameters 17 | ---------- 18 | argv: list[str] | None 19 | The arguments to be passed to the application for parsing. 20 | """ 21 | app = Flake8NbApplication() 22 | app.run(sys.argv[1:] if argv is None else argv[1:]) 23 | app.exit() 24 | 25 | 26 | if __name__ == "__main__": 27 | main() 28 | -------------------------------------------------------------------------------- /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 = flake8_nb 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_all: 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 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[BUG]" 5 | labels: bug 6 | assignees: s-weigand 7 | --- 8 | 9 | **Describe the bug** 10 | A clear and concise description of what the bug is. 11 | 12 | **To Reproduce** 13 | Steps to reproduce the behavior: 14 | 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | 28 | - OS: [e.g. iOS] 29 | - Version [e.g. 22] 30 | 31 | **Additional context** 32 | Add any other context about the problem here. 33 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import os 2 | from typing import Iterator 3 | 4 | import pytest 5 | 6 | from flake8_nb.parsers.notebook_parsers import InvalidNotebookWarning 7 | from flake8_nb.parsers.notebook_parsers import NotebookParser 8 | from tests.parsers.test_notebook_parsers import TEST_NOTEBOOK_BASE_PATH 9 | 10 | 11 | @pytest.fixture(scope="function") 12 | def notebook_parser() -> Iterator[NotebookParser]: 13 | notebooks = [ 14 | "not_a_notebook.ipynb", 15 | "notebook_with_flake8_tags.ipynb", 16 | "notebook_with_out_flake8_tags.ipynb", 17 | "notebook_with_out_ipython_magic.ipynb", 18 | ] 19 | notebook_paths = [os.path.join(TEST_NOTEBOOK_BASE_PATH, notebook) for notebook in notebooks] 20 | with pytest.warns(InvalidNotebookWarning): 21 | parser_instance = NotebookParser(notebook_paths) 22 | yield parser_instance 23 | parser_instance.clean_up() 24 | -------------------------------------------------------------------------------- /tests/data/expected_output_exec_count.txt: -------------------------------------------------------------------------------- 1 | falsy_python_file.py:1:1: F401 'foo' imported but unused 2 | cell_with_source_string.ipynb#In[1]:3:1: E302 expected 2 blank lines, found 1 3 | notebook_with_flake8_tags.ipynb#In[1]:2:5: E231 missing whitespace after ':' 4 | notebook_with_flake8_tags.ipynb#In[2]:2:5: E231 missing whitespace after ':' 5 | notebook_with_flake8_tags.ipynb#In[4]:3:5: E231 missing whitespace after ':' 6 | notebook_with_flake8_tags.ipynb#In[5]:3:5: E231 missing whitespace after ':' 7 | notebook_with_flake8_tags.ipynb#In[7]:2:5: E231 missing whitespace after ':' 8 | notebook_with_out_flake8_tags.ipynb#In[1]:1:1: F401 'not_a_package' imported but unused 9 | notebook_with_out_flake8_tags.ipynb#In[2]:1:5: E231 missing whitespace after ':' 10 | notebook_with_out_flake8_tags.ipynb#In[5]:1:5: E231 missing whitespace after ':' 11 | notebook_with_out_ipython_magic.ipynb#In[1]:1:4: E222 multiple spaces after operator 12 | -------------------------------------------------------------------------------- /tests/data/expected_output_total_cell_count.txt: -------------------------------------------------------------------------------- 1 | falsy_python_file.py:1:1: F401 'foo' imported but unused 2 | cell_with_source_string.ipynb:cell#1:3:1: E302 expected 2 blank lines, found 1 3 | notebook_with_flake8_tags.ipynb:cell#4:2:5: E231 missing whitespace after ':' 4 | notebook_with_flake8_tags.ipynb:cell#6:2:5: E231 missing whitespace after ':' 5 | notebook_with_flake8_tags.ipynb:cell#11:3:5: E231 missing whitespace after ':' 6 | notebook_with_flake8_tags.ipynb:cell#13:3:5: E231 missing whitespace after ':' 7 | notebook_with_flake8_tags.ipynb:cell#18:2:5: E231 missing whitespace after ':' 8 | notebook_with_out_flake8_tags.ipynb:cell#3:1:1: F401 'not_a_package' imported but unused 9 | notebook_with_out_flake8_tags.ipynb:cell#5:1:5: E231 missing whitespace after ':' 10 | notebook_with_out_flake8_tags.ipynb:cell#13:1:5: E231 missing whitespace after ':' 11 | notebook_with_out_ipython_magic.ipynb:cell#3:1:4: E222 multiple spaces after operator 12 | -------------------------------------------------------------------------------- /flake8_nb/__init__.py: -------------------------------------------------------------------------------- 1 | """Top-level package for flake8-nb.""" 2 | 3 | __author__ = """Sebastian Weigand""" 4 | __email__ = "s.weigand.phy@gmail.com" 5 | __version__ = "0.5.3" 6 | 7 | import flake8 8 | 9 | from flake8_nb.flake8_integration.formatter import IpynbFormatter 10 | 11 | __all__ = ["IpynbFormatter"] 12 | 13 | 14 | def save_cast_int(int_str: str) -> int: 15 | """Cast version string to tuple, in a save manner. 16 | 17 | This is needed so the version number of prereleases (i.e. 3.8.0rc1) 18 | don't not throw exceptions. 19 | 20 | Parameters 21 | ---------- 22 | int_str : str 23 | String which should represent a number. 24 | 25 | Returns 26 | ------- 27 | int 28 | Int representation of int_str 29 | """ 30 | try: 31 | return int(int_str) 32 | except ValueError: 33 | return 0 34 | 35 | 36 | FLAKE8_VERSION_TUPLE = tuple(map(save_cast_int, flake8.__version__.split("."))) 37 | -------------------------------------------------------------------------------- /.github/workflows/test-nightly-schedule.yml: -------------------------------------------------------------------------------- 1 | name: "Scheduled Tests" 2 | on: 3 | schedule: 4 | - cron: "0 7 * * 1" # At 07:00 on each Monday. 5 | workflow_dispatch: 6 | 7 | jobs: 8 | flake8-nightly: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v3 12 | - name: Set up Python 3.7 13 | uses: actions/setup-python@v4 14 | with: 15 | python-version: 3.7 16 | - name: Install dependencies 17 | env: 18 | TOX_ENV_NAME: flake8-nightly 19 | run: | 20 | python -m pip install -U pip 21 | python -m pip install -U -r requirements_dev.txt 22 | python -m pip install . 23 | python -m pip install --force-reinstall -U -q git+https://github.com/pycqa/flake8 24 | - name: Show flake8 version 25 | run: | 26 | pip freeze | grep flake8 27 | - name: Run tests 28 | run: | 29 | python -m pytest -vv 30 | -------------------------------------------------------------------------------- /tests/data/expected_output_code_cell_count.txt: -------------------------------------------------------------------------------- 1 | falsy_python_file.py:1:1: F401 'foo' imported but unused 2 | cell_with_source_string.ipynb:code_cell#1:3:1: E302 expected 2 blank lines, found 1 3 | notebook_with_flake8_tags.ipynb:code_cell#1:2:5: E231 missing whitespace after ':' 4 | notebook_with_flake8_tags.ipynb:code_cell#2:2:5: E231 missing whitespace after ':' 5 | notebook_with_flake8_tags.ipynb:code_cell#4:3:5: E231 missing whitespace after ':' 6 | notebook_with_flake8_tags.ipynb:code_cell#5:3:5: E231 missing whitespace after ':' 7 | notebook_with_flake8_tags.ipynb:code_cell#7:2:5: E231 missing whitespace after ':' 8 | notebook_with_out_flake8_tags.ipynb:code_cell#1:1:1: F401 'not_a_package' imported but unused 9 | notebook_with_out_flake8_tags.ipynb:code_cell#2:1:5: E231 missing whitespace after ':' 10 | notebook_with_out_flake8_tags.ipynb:code_cell#6:1:5: E231 missing whitespace after ':' 11 | notebook_with_out_ipython_magic.ipynb:code_cell#1:1:4: E222 multiple spaces after operator 12 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: ["main"] 6 | pull_request: 7 | branches: ["main"] 8 | schedule: 9 | - cron: "15 2 * * 1" 10 | 11 | jobs: 12 | analyze: 13 | name: Analyze 14 | runs-on: ubuntu-latest 15 | permissions: 16 | actions: read 17 | contents: read 18 | security-events: write 19 | 20 | strategy: 21 | fail-fast: false 22 | matrix: 23 | language: [python] 24 | 25 | steps: 26 | - name: Checkout 27 | uses: actions/checkout@v3 28 | 29 | - name: Initialize CodeQL 30 | uses: github/codeql-action/init@v2 31 | with: 32 | languages: ${{ matrix.language }} 33 | queries: +security-and-quality 34 | setup-python-dependencies: false 35 | 36 | - name: Autobuild 37 | uses: github/codeql-action/autobuild@v2 38 | 39 | - name: Perform CodeQL Analysis 40 | uses: github/codeql-action/analyze@v2 41 | with: 42 | category: "/language:${{ matrix.language }}" 43 | -------------------------------------------------------------------------------- /tests/data/intermediate_py_files/notebook_with_out_flake8_tags.ipynb_parsed: -------------------------------------------------------------------------------- 1 | from IPython import get_ipython 2 | 3 | 4 | # INTERMEDIATE_CELL_SEPARATOR (1,1,3) 5 | 6 | 7 | import not_a_package 8 | 9 | 10 | # INTERMEDIATE_CELL_SEPARATOR (2,2,5) 11 | 12 | 13 | {"1":1} 14 | 15 | 16 | # INTERMEDIATE_CELL_SEPARATOR (3,3,7) 17 | 18 | 19 | def func(): 20 | return "foo" 21 | 22 | 23 | # INTERMEDIATE_CELL_SEPARATOR (4,4,9) 24 | 25 | 26 | class Bar: 27 | def foo(self): 28 | return "foo" 29 | 30 | 31 | # INTERMEDIATE_CELL_SEPARATOR (5,6,13) 32 | 33 | 34 | {"1":1} 35 | 36 | 37 | # INTERMEDIATE_CELL_SEPARATOR (6,7,15) 38 | 39 | 40 | get_ipython().system('flake8_nb notebook_with_out_flake8_tags.ipynb') 41 | 42 | 43 | # INTERMEDIATE_CELL_SEPARATOR (7,8,17) 44 | 45 | 46 | get_ipython().system("flake8_nb --notebook-cell-format '{nb_path}:code_cell#{code_cell_count}' notebook_with_out_flake8_tags.ipynb") # noqa: E501 47 | 48 | 49 | # INTERMEDIATE_CELL_SEPARATOR (8,9,19) 50 | 51 | 52 | get_ipython().system("flake8_nb --notebook-cell-format '{nb_path}:cell#{total_cell_count}' notebook_with_out_flake8_tags.ipynb") # noqa: E501 53 | -------------------------------------------------------------------------------- /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 | {%- endfor %} 48 | 49 | {% endif %} 50 | {% endblock %} 51 | -------------------------------------------------------------------------------- /.github/workflows/test-pre-coomit-hook.yml: -------------------------------------------------------------------------------- 1 | name: Test pre-commit Hook 2 | 3 | on: 4 | push: 5 | tags: 6 | - v** 7 | branches-ignore: 8 | - "dependabot/**" 9 | - "sourcery/**" 10 | - "create-pr-action/pre-commit-config-update-*" 11 | pull_request: 12 | workflow_dispatch: 13 | 14 | jobs: 15 | test-pre-commit-hook: 16 | name: Test pre-commit hook 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v3 20 | - name: Set up Python 21 | uses: actions/setup-python@v4 22 | with: 23 | python-version: 3.7 24 | - uses: actions/cache@v3 25 | with: 26 | path: ~/.cache/pre-commit 27 | key: pre-commit-hook-test-${{ hashFiles('requirements_dev.txt') }} 28 | - name: Install dependencies 29 | run: | 30 | python -m pip install -U pip 31 | python -m pip install -U -r requirements_dev.txt 32 | pip install . 33 | - name: Run flake8-nb 34 | run: flake8-nb tests/data/notebooks/ 2>&1 | tee flake8-nb_run_output.txt 35 | - name: Run hook 36 | run: pre-commit try-repo . -a 2>&1 | tee hook_run_output.txt 37 | - name: Test hook output 38 | run: pytest .github/hook_tester.py 39 | -------------------------------------------------------------------------------- /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=flake8_nb 13 | set API_TOCTREE_DIR=api 14 | set SPHINXOPTS= 15 | 16 | if "%1" == "" goto help 17 | 18 | %SPHINXBUILD% >NUL 2>NUL 19 | if errorlevel 9009 ( 20 | echo. 21 | echo.The Sphinx module was not found. Make sure you have Sphinx installed, 22 | echo.then set the SPHINXBUILD environment variable to point to the full 23 | echo.path of the 'sphinx-build' executable. Alternatively you may add the 24 | echo.Sphinx directory to PATH. 25 | echo. 26 | echo.If you don't have Sphinx installed, grab it from 27 | echo.http://sphinx-doc.org/ 28 | exit /b 1 29 | ) 30 | 31 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 32 | goto end 33 | 34 | :help 35 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 36 | 37 | if "%1" == "clean_all" ( 38 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 39 | del /q /s %BUILDDIR%\* 40 | for /d %%i in (%API_TOCTREE_DIR%\*) do rmdir /q /s %%i 41 | del /q /s %API_TOCTREE_DIR%\* 42 | goto end 43 | ) 44 | 45 | :end 46 | popd 47 | -------------------------------------------------------------------------------- /.github/workflows/binder-on-pr.yml: -------------------------------------------------------------------------------- 1 | # Reference https://mybinder.readthedocs.io/en/latest/howto/gh-actions-badges.html 2 | name: Binder Badge 3 | on: 4 | pull_request_target: 5 | types: [opened] 6 | 7 | permissions: 8 | pull-requests: write 9 | 10 | jobs: 11 | binder: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: comment on PR with Binder link 15 | uses: actions/github-script@v6 16 | with: 17 | github-token: ${{secrets.GITHUB_TOKEN}} 18 | script: | 19 | var PR_HEAD_USERREPO = process.env.PR_HEAD_USERREPO; 20 | var PR_HEAD_REF = process.env.PR_HEAD_REF; 21 | github.rest.issues.createComment({ 22 | issue_number: context.issue.number, 23 | owner: context.repo.owner, 24 | repo: context.repo.repo, 25 | body: `[![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/${PR_HEAD_USERREPO}/${PR_HEAD_REF}?urlpath=lab%2Ftree%2Ftests%2Fdata%2Fnotebooks) :point_left: Launch a binder notebook on branch _${PR_HEAD_USERREPO}/${PR_HEAD_REF}_` 26 | }) 27 | env: 28 | PR_HEAD_REF: ${{ github.event.pull_request.head.ref }} 29 | PR_HEAD_USERREPO: ${{ github.event.pull_request.head.repo.full_name }} 30 | -------------------------------------------------------------------------------- /tests/data/notebooks/notebook_with_out_ipython_magic.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "## Example notebook using iPython magic" 8 | ] 9 | }, 10 | { 11 | "cell_type": "markdown", 12 | "metadata": {}, 13 | "source": [ 14 | "The next cell should not report `E222 multiple spaces after operator`" 15 | ] 16 | }, 17 | { 18 | "cell_type": "code", 19 | "execution_count": 1, 20 | "metadata": {}, 21 | "outputs": [ 22 | { 23 | "data": { 24 | "text/plain": [ 25 | "2" 26 | ] 27 | }, 28 | "execution_count": 1, 29 | "metadata": {}, 30 | "output_type": "execute_result" 31 | } 32 | ], 33 | "source": [ 34 | "1 + 1" 35 | ] 36 | } 37 | ], 38 | "metadata": { 39 | "kernelspec": { 40 | "display_name": "Python 3", 41 | "language": "python", 42 | "name": "python3" 43 | }, 44 | "language_info": { 45 | "codemirror_mode": { 46 | "name": "ipython", 47 | "version": 3 48 | }, 49 | "file_extension": ".py", 50 | "mimetype": "text/x-python", 51 | "name": "python", 52 | "nbconvert_exporter": "python", 53 | "pygments_lexer": "ipython3", 54 | "version": "3.8.8" 55 | } 56 | }, 57 | "nbformat": 4, 58 | "nbformat_minor": 4 59 | } 60 | -------------------------------------------------------------------------------- /tests/data/notebooks/cell_with_source_string.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "metadata": {}, 7 | "outputs": [ 8 | { 9 | "data": { 10 | "text/plain": [ 11 | "10" 12 | ] 13 | }, 14 | "metadata": {}, 15 | "output_type": "display_data" 16 | }, 17 | { 18 | "data": { 19 | "text/plain": [ 20 | "" 21 | ] 22 | }, 23 | "execution_count": 1, 24 | "metadata": {}, 25 | "output_type": "execute_result" 26 | } 27 | ], 28 | "source": "from ipywidgets import interact\n\ndef f(x):\n return x\n\n\ninteract(f, x=10)" 29 | } 30 | ], 31 | "metadata": { 32 | "kernelspec": { 33 | "display_name": "Python 3", 34 | "language": "python", 35 | "name": "python3" 36 | }, 37 | "language_info": { 38 | "codemirror_mode": { 39 | "name": "ipython", 40 | "version": 3 41 | }, 42 | "file_extension": ".py", 43 | "mimetype": "text/x-python", 44 | "name": "python", 45 | "nbconvert_exporter": "python", 46 | "pygments_lexer": "ipython3", 47 | "version": "3.8.8" 48 | } 49 | }, 50 | "nbformat": 4, 51 | "nbformat_minor": 4 52 | } 53 | -------------------------------------------------------------------------------- /.github/workflows/autoupdate-pre-commit-config.yml: -------------------------------------------------------------------------------- 1 | name: "Update pre-commit config" 2 | # You need to set an access token as a secret ACTION_TRIGGER_TOKEN on this repo 3 | # in order for the PR to trigger the CI. 4 | # For more details see: https://github.com/technote-space/create-pr-action#github_token 5 | # If you don't want the PR to trigger the CI (NOT RECOMMENDED), comment out the GITHUB_TOKEN line. 6 | 7 | on: 8 | schedule: 9 | - cron: "0 7 * * 1" # At 07:00 on each Monday. 10 | workflow_dispatch: 11 | 12 | jobs: 13 | update-pre-commit: 14 | if: github.repository_owner == 's-weigand' 15 | name: Autoupdate pre-commit config 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: Set up Python 19 | uses: actions/setup-python@v4 20 | with: 21 | python-version: 3.7 22 | - uses: actions/cache@v3 23 | with: 24 | path: ~/.cache/pre-commit 25 | key: pre-commit-autoupdate 26 | - name: Update pre-commit config packages 27 | uses: technote-space/create-pr-action@v2 28 | with: 29 | GITHUB_TOKEN: ${{ secrets.ACTION_TRIGGER_TOKEN }} 30 | EXECUTE_COMMANDS: | 31 | pip install pre-commit 32 | pre-commit autoupdate || (exit 0); 33 | pre-commit run -a || (exit 0); 34 | COMMIT_MESSAGE: "⬆️ UPGRADE: Autoupdate pre-commit config" 35 | PR_BRANCH_NAME: "pre-commit-config-update-${PR_ID}" 36 | PR_TITLE: "⬆️ UPGRADE: Autoupdate pre-commit config" 37 | -------------------------------------------------------------------------------- /docs/installation.rst: -------------------------------------------------------------------------------- 1 | .. highlight:: shell 2 | 3 | ============ 4 | Installation 5 | ============ 6 | 7 | 8 | Stable release 9 | -------------- 10 | 11 | To install flake8-nb, run this command in your terminal: 12 | 13 | .. code-block:: console 14 | 15 | $ pip install flake8-nb 16 | 17 | This is the preferred method to install flake8-nb, as it will always install the most recent stable release. 18 | 19 | If you don't have `pip`_ installed, this `Python installation guide`_ can guide 20 | you through the process. 21 | 22 | .. _pip: https://pip.pypa.io/en/stable/ 23 | .. _Python installation guide: https://docs.python-guide.org/starting/installation/ 24 | 25 | 26 | From sources 27 | ------------ 28 | 29 | You can either pip install it directly from github: 30 | 31 | .. code-block:: console 32 | 33 | $ pip install git+git://github.com/s-weigand/flake8-nb@ 34 | 35 | Or get the sources for flake8-nb, which can be downloaded from the `Github repo`_. 36 | 37 | By cloning the public repository: 38 | 39 | .. code-block:: console 40 | 41 | $ git clone git://github.com/s-weigand/flake8-nb 42 | 43 | Or downloading the `tarball`_: 44 | 45 | .. code-block:: console 46 | 47 | $ curl -OJL https://github.com/s-weigand/flake8-nb/tarball/main 48 | 49 | Once you have a copy of the source, you can install it with: 50 | 51 | .. code-block:: console 52 | 53 | $ python setup.py install 54 | 55 | 56 | .. _Github repo: https://github.com/s-weigand/flake8-nb 57 | .. _tarball: https://github.com/s-weigand/flake8-nb/tarball/main 58 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.black] 2 | line-length = 99 3 | target-version = ['py36', 'py37', 'py38'] 4 | exclude = ''' 5 | /( 6 | \.eggs 7 | | \.git 8 | | \.hg 9 | | \.mypy_cache 10 | | \.tox 11 | | \.venv 12 | | _build 13 | | buck-out 14 | | build 15 | | dist 16 | )/ 17 | ''' 18 | 19 | [tool.isort] 20 | profile = "hug" 21 | src_paths = ["flake8_nb", "test"] 22 | include_trailing_comma = true 23 | line_length = 99 24 | multi_line_output = 3 25 | known_first_party = ["flake8_nb", "tests"] 26 | force_single_line = true 27 | 28 | [tool.interrogate] 29 | exclude = ["setup.py", "docs", "tests", ".eggs","flake8_nb/flake8_integration/hacked_config.py"] 30 | ignore-init-module = true 31 | fail-under=100 32 | verbose = 1 33 | 34 | 35 | [tool.coverage.run] 36 | branch = true 37 | relative_files = true 38 | omit = [ 39 | 'setup.py', 40 | 'flake8_nb/__init__.py', 41 | 'flake8_nb/*/__init__.py', 42 | 'tests/__init__.py', 43 | '*/tests/*', 44 | # comment the above line if you want to see if all tests did run 45 | ] 46 | 47 | [tool.coverage.report] 48 | # Regexes for lines to exclude from consideration 49 | exclude_lines = [ 50 | # Have to re-enable the standard pragma 51 | 'pragma: no cover', 52 | 53 | # Don't complain about missing debug-only code: 54 | 'def __repr__', 55 | 'if self\.debug', 56 | 57 | # Don't complain if tests don't hit defensive assertion code: 58 | 'raise AssertionError', 59 | 'raise NotImplementedError', 60 | 61 | # Don't complain if non-runnable code isn't run: 62 | 'if 0:', 63 | 'if __name__ == .__main__.:', 64 | ] 65 | -------------------------------------------------------------------------------- /tests/data/intermediate_py_files/notebook_with_flake8_tags.ipynb_parsed: -------------------------------------------------------------------------------- 1 | from IPython import get_ipython 2 | 3 | 4 | # INTERMEDIATE_CELL_SEPARATOR (1,1,4) 5 | 6 | 7 | import not_a_package # noqa: F401 8 | {"1":1} # noqa: F401 9 | 10 | 11 | # INTERMEDIATE_CELL_SEPARATOR (2,2,6) 12 | 13 | 14 | {"2":1} # noqa: E231 15 | {"2":2} 16 | 17 | 18 | # INTERMEDIATE_CELL_SEPARATOR (3,3,8) 19 | 20 | 21 | {"3":1} # noqa: E231 22 | {"3":2} # noqa: E231 23 | 24 | 25 | # INTERMEDIATE_CELL_SEPARATOR (4,4,11) 26 | 27 | 28 | # flake8-noqa-cell-E402-F401-F811 # noqa: E402, F401, F811 29 | import not_a_package # noqa: E402, F401, F811 30 | {"4":1} # noqa: E402, F401, F811 31 | 32 | 33 | # INTERMEDIATE_CELL_SEPARATOR (5,5,13) 34 | 35 | 36 | # flake8-noqa-line-2-E231 37 | {"5":1} # noqa: E231 38 | {"5":2} 39 | 40 | 41 | # INTERMEDIATE_CELL_SEPARATOR (6,6,15) 42 | 43 | 44 | # flake8-noqa-cell-E231 # noqa: E231 45 | {"6":1} # noqa: E231 46 | {"6":2} # noqa: E231 47 | 48 | 49 | # INTERMEDIATE_CELL_SEPARATOR (7,7,18) 50 | 51 | 52 | {"5":1} # noqa: E231 53 | {"5":2} 54 | 55 | 56 | # INTERMEDIATE_CELL_SEPARATOR (8,8,20) 57 | 58 | 59 | get_ipython().system('flake8_nb notebook_with_flake8_tags.ipynb') 60 | 61 | 62 | # INTERMEDIATE_CELL_SEPARATOR (9,9,22) 63 | 64 | 65 | get_ipython().system("flake8_nb --notebook-cell-format '{nb_path}:code_cell#{code_cell_count}' notebook_with_flake8_tags.ipynb") # noqa: E501 66 | 67 | 68 | # INTERMEDIATE_CELL_SEPARATOR (10,10,24) 69 | 70 | 71 | get_ipython().system("flake8_nb --notebook-cell-format '{nb_path}:cell#{total_cell_count}' notebook_with_flake8_tags.ipynb") # noqa: E501 72 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | minversion = 3.4.0 3 | skip_missing_interpreters=true 4 | envlist = py{37,38,39,310}, flake8-nightly, flake8-legacy, pre-commit, docs, docs-links 5 | 6 | 7 | [flake8_nb] 8 | max-line-length = 100 9 | ; notebook_cell_format = {nb_path}#In[{exec_count}]test 10 | ; exclude= 11 | ; *.ipynb_checkpoints/* 12 | ; *.tox* 13 | ; filename= 14 | ; *.ipynb_parsed 15 | ; *.py 16 | 17 | [pytest] 18 | addopts = --cov=. --cov-report term --cov-report xml --cov-report html --cov-config=pyproject.toml 19 | filterwarnings = 20 | ignore:.*not_a_notebook.ipynb 21 | 22 | [testenv:docs] 23 | whitelist_externals = make 24 | commands = 25 | make --directory=docs clean_all html 26 | 27 | [testenv:docs-links] 28 | whitelist_externals = make 29 | commands = 30 | make --directory=docs clean_all linkcheck 31 | 32 | 33 | [testenv:pre-commit] 34 | basepython=python 35 | skip_install=true 36 | commands_pre = 37 | {envpython} -m pip install -U -q pre-commit 38 | commands=pre-commit run --all 39 | 40 | [testenv:flake8-nightly] 41 | passenv = * 42 | commands_pre = 43 | {[testenv]commands_pre} 44 | {envpython} -m pip install -U -q --force-reinstall git+https://github.com/pycqa/flake8 45 | commands = 46 | {envpython} -c "import flake8_nb;print('FLAKE8 VERSION: ', flake8_nb.FLAKE8_VERSION_TUPLE)" 47 | {envpython} -m pytest -vv 48 | 49 | [testenv:flake8-legacy] 50 | passenv = * 51 | commands_pre = 52 | {[testenv]commands_pre} 53 | {envpython} -m pip install -U -q 'flake8==3.8.0' 54 | commands = 55 | {envpython} -m pytest -vv 56 | 57 | [testenv] 58 | passenv = * 59 | install_command=python -m pip install {opts} {packages} 60 | commands_pre = 61 | {envpython} -m pip install -U -q -r {toxinidir}/requirements_dev.txt 62 | commands = 63 | {envpython} -m pytest 64 | -------------------------------------------------------------------------------- /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 | Functions 46 | --------- 47 | 48 | .. rubric:: Summary 49 | 50 | .. autosummary:: 51 | :toctree: {{ name }}/functions 52 | :nosignatures: 53 | 54 | {% for item in functions %} 55 | {{ item }} 56 | {%- endfor %} 57 | 58 | {% endif %} 59 | 60 | {% endblock %} 61 | 62 | {% block classes %} 63 | {% if classes %} 64 | 65 | Classes 66 | ------- 67 | 68 | .. rubric:: Summary 69 | 70 | .. autosummary:: 71 | :toctree: {{ name }}/classes 72 | :nosignatures: 73 | 74 | {% for item in classes %} 75 | {{ item }} 76 | {%- endfor %} 77 | 78 | {% endif %} 79 | 80 | {% endblock %} 81 | 82 | 83 | {% block exceptions %} 84 | {% if exceptions %} 85 | 86 | Exceptions 87 | ---------- 88 | 89 | .. rubric:: Exception Summary 90 | 91 | .. autosummary:: 92 | :toctree: {{ name }}/exceptions 93 | :nosignatures: 94 | 95 | {% for item in exceptions %} 96 | {{ item }} 97 | {%- endfor %} 98 | 99 | {% endif %} 100 | 101 | {% endblock %} 102 | 103 | 104 | {% endblock %} 105 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | docs/api/ 69 | 70 | # PyBuilder 71 | target/ 72 | 73 | # Jupyter Notebook 74 | .ipynb_checkpoints 75 | 76 | # pyenv 77 | .python-version 78 | 79 | # celery beat schedule file 80 | celerybeat-schedule 81 | 82 | # SageMath parsed files 83 | *.sage.py 84 | 85 | # Environments 86 | .env 87 | .venv 88 | env/ 89 | venv/ 90 | ENV/ 91 | env.bak/ 92 | venv.bak/ 93 | 94 | # Spyder project settings 95 | .spyderproject 96 | .spyproject 97 | 98 | # Rope project settings 99 | .ropeproject 100 | 101 | # mkdocs documentation 102 | /site 103 | 104 | # mypy 105 | .mypy_cache/ 106 | 107 | # vscode config 108 | .vscode 109 | 110 | # my local testing folder 111 | # testing 112 | 113 | # local hook testing files 114 | flake8-nb_run_output.txt 115 | hook_run_output.txt 116 | flake8_nb/flake8_integration/hacked_config.py 117 | -------------------------------------------------------------------------------- /tests/flake8_integration/conftest.py: -------------------------------------------------------------------------------- 1 | import os 2 | from typing import List 3 | from typing import Tuple 4 | 5 | import pytest 6 | 7 | from tests.parsers.test_notebook_parsers import TEST_NOTEBOOK_BASE_PATH 8 | 9 | TEST_NOTEBOOK_PATHS = [ 10 | os.path.normcase(os.path.join(TEST_NOTEBOOK_BASE_PATH, filename)) 11 | for filename in [ 12 | "not_a_notebook.ipynb", 13 | "cell_with_source_string.ipynb", 14 | "notebook_with_flake8_tags.ipynb", 15 | "notebook_with_out_flake8_tags.ipynb", 16 | "notebook_with_out_ipython_magic.ipynb", 17 | ] 18 | ] 19 | 20 | 21 | @pytest.mark.usefixtures("tmpdir") 22 | class TempIpynbArgs: 23 | def __init__(self, kind: str, tmpdir_factory): 24 | self.kind = kind 25 | tmpdir = tmpdir_factory.mktemp("ipynb_folder") 26 | self.top_level = tmpdir.join("top_level.ipynb") 27 | self.top_level.write("top_level") 28 | self.sub_level_dir = tmpdir.mkdir("sub") 29 | self.sub_level = self.sub_level_dir.join("sub_level.ipynb") 30 | self.sub_level.write("sub_level") 31 | 32 | def get_args_and_result(self) -> Tuple[List[str], Tuple[List[str], List[str]]]: 33 | if self.kind == "file": 34 | return ( 35 | [str(self.top_level), "random_arg"], 36 | (["random_arg"], [str(os.path.normcase(self.top_level))]), 37 | ) 38 | elif self.kind == "dir": 39 | return ( 40 | [str(self.sub_level_dir), "random_arg"], 41 | ( 42 | [self.sub_level_dir, "random_arg"], 43 | [os.path.normcase(self.sub_level)], 44 | ), 45 | ) 46 | elif self.kind == "random": 47 | return (["random_arg"], (["random_arg"], [])) 48 | else: 49 | return ([], ([os.curdir], TEST_NOTEBOOK_PATHS)) 50 | 51 | 52 | @pytest.fixture(scope="session", params=["file", "dir", "random", None]) 53 | def temp_ipynb_args(request, tmpdir_factory): 54 | return TempIpynbArgs(request.param, tmpdir_factory) 55 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: clean clean-test clean-pyc clean-build docs help 2 | .DEFAULT_GOAL := help 3 | 4 | define BROWSER_PYSCRIPT 5 | import os, webbrowser, sys 6 | 7 | try: 8 | from urllib import pathname2url 9 | except: 10 | from urllib.request import pathname2url 11 | 12 | webbrowser.open("file://" + pathname2url(os.path.abspath(sys.argv[1]))) 13 | endef 14 | export BROWSER_PYSCRIPT 15 | 16 | define PRINT_HELP_PYSCRIPT 17 | import re, sys 18 | 19 | for line in sys.stdin: 20 | match = re.match(r'^([a-zA-Z_-]+):.*?## (.*)$$', line) 21 | if match: 22 | target, help = match.groups() 23 | print("%-20s %s" % (target, help)) 24 | endef 25 | export PRINT_HELP_PYSCRIPT 26 | 27 | BROWSER := python -c "$$BROWSER_PYSCRIPT" 28 | 29 | help: 30 | @python -c "$$PRINT_HELP_PYSCRIPT" < $(MAKEFILE_LIST) 31 | 32 | clean: clean-build clean-pyc clean-test ## remove all build, test, coverage and Python artifacts 33 | 34 | clean-build: ## remove build artifacts 35 | rm -fr build/ 36 | rm -fr dist/ 37 | rm -fr .eggs/ 38 | find . -name '*.egg-info' -exec rm -fr {} + 39 | find . -name '*.egg' -exec rm -f {} + 40 | 41 | clean-pyc: ## remove Python file artifacts 42 | find . -name '*.pyc' -exec rm -f {} + 43 | find . -name '*.pyo' -exec rm -f {} + 44 | find . -name '*~' -exec rm -f {} + 45 | find . -name '__pycache__' -exec rm -fr {} + 46 | 47 | clean-test: ## remove test and coverage artifacts 48 | rm -fr .tox/ 49 | rm -f .coverage 50 | rm -fr htmlcov/ 51 | rm -fr .pytest_cache 52 | 53 | lint: ## check style with flake8 54 | flake8 flake8_nb tests 55 | 56 | test: ## run tests quickly with the default Python 57 | pytest 58 | 59 | test-all: ## run tests on every Python version with tox 60 | tox 61 | 62 | coverage: ## check code coverage quickly with the default Python 63 | coverage run --source flake8_nb -m pytest 64 | coverage report -m 65 | coverage html 66 | $(BROWSER) htmlcov/index.html 67 | 68 | docs: ## generate Sphinx HTML documentation, including API docs 69 | rm -f docs/flake8_nb.rst 70 | rm -f docs/modules.rst 71 | sphinx-apidoc -o docs/ flake8_nb 72 | $(MAKE) -C docs clean 73 | $(MAKE) -C docs html 74 | $(BROWSER) docs/_build/html/index.html 75 | 76 | servedocs: docs ## compile the docs watching for changes 77 | watchmedo shell-command -p '*.rst' -c '$(MAKE) -C docs html' -R -D . 78 | 79 | release: dist ## package and upload a release 80 | twine upload dist/* 81 | 82 | dist: clean ## builds source and wheel package 83 | python setup.py sdist 84 | python setup.py bdist_wheel 85 | ls -l dist 86 | 87 | install: clean ## install the package to the active Python's site-packages 88 | python setup.py install 89 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = flake8_nb 3 | version = attr: flake8_nb.__version__ 4 | description = Flake8 based checking for jupyter notebooks 5 | long_description = file: README.md 6 | long_description_content_type = text/markdown 7 | url = https://github.com/s-weigand/flake8-nb 8 | author = Sebastian Weigand 9 | author_email = s.weigand.phy@gmail.com 10 | license = Apache-2.0 11 | license_file = LICENSE 12 | platforms = any 13 | classifiers = 14 | Development Status :: 4 - Beta 15 | Environment :: Console 16 | Framework :: Flake8 17 | Framework :: Jupyter 18 | Intended Audience :: Developers 19 | License :: OSI Approved :: Apache Software License 20 | Natural Language :: English 21 | Operating System :: OS Independent 22 | Programming Language :: Python :: 3 23 | Programming Language :: Python :: 3 :: Only 24 | Programming Language :: Python :: 3.7 25 | Programming Language :: Python :: 3.8 26 | Programming Language :: Python :: 3.9 27 | Programming Language :: Python :: 3.10 28 | Programming Language :: Python :: 3.11 29 | Programming Language :: Python :: Implementation :: CPython 30 | Topic :: Software Development :: Libraries :: Python Modules 31 | Topic :: Software Development :: Quality Assurance 32 | keywords = flake8_nb flake8 lint notebook jupyter ipython 33 | project_urls = 34 | Documentation=https://flake8-nb.readthedocs.io/en/latest/ 35 | Source=https://github.com/s-weigand/flake8-nb 36 | Tracker=https://github.com/s-weigand/flake8-nb/issues 37 | Changelog=https://flake8-nb.readthedocs.io/en/latest/changelog.html 38 | 39 | [options] 40 | packages = find: 41 | install_requires = 42 | flake8>=3.8.0,<5.0.5 43 | ipython>=7.8.0 44 | nbconvert>=5.6.0 45 | python_requires = >=3.7 46 | include_package_data = True 47 | setup_requires = 48 | setuptools>=41.2 49 | tests_require = pytest>=3 50 | zip_safe = False 51 | 52 | [options.packages.find] 53 | include = 54 | flake8_nb 55 | flake8_nb.* 56 | 57 | [options.entry_points] 58 | console_scripts = 59 | flake8_nb = flake8_nb.__main__:main 60 | flake8-nb = flake8_nb.__main__:main 61 | flake8.report = 62 | default_notebook = flake8_nb:IpynbFormatter 63 | 64 | [flake8] 65 | max-line-length = 99 66 | exclude = 67 | docs 68 | *.ipynb_checkpoints/* 69 | *.tox* 70 | tests/data/notebooks/* 71 | 72 | [darglint] 73 | docstring_style = numpy 74 | 75 | [aliases] 76 | test = pytest 77 | 78 | [tool:pytest] 79 | collect_ignore = ['setup.py'] 80 | 81 | [rstcheck] 82 | ignore_directives = autosummary,code-block 83 | 84 | [mypy] 85 | strict = True 86 | ignore_missing_imports = True 87 | scripts_are_modules = True 88 | show_error_codes = True 89 | warn_unused_ignores = False 90 | -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 0.5.3 (2023-03-28) 4 | 5 | - ✨ Official python 3.11 support [#291](https://github.com/s-weigand/flake8-nb/pull/291) 6 | - 🩹 Fixed bug with pre-commit-ci due to read-only filesystem by @lkeegan in [#290](https://github.com/s-weigand/flake8-nb/pull/290) 7 | 8 | ## 0.5.2 (2022-08-17) 9 | 10 | - 🩹 Fix config file discovery with flake8>=5.0.0 [#255](https://github.com/s-weigand/flake8-nb/pull/255) 11 | 12 | ## 0.5.1 (2022-08-16) 13 | 14 | - 🩹 Fix config discovery with flake8>=5.0.0 [#251](https://github.com/s-weigand/flake8-nb/pull/251) 15 | 16 | ## 0.5.0 (2022-08-15) 17 | 18 | - Drop support for `flake8<3.8.0` [#240](https://github.com/s-weigand/flake8-nb/pull/240) 19 | - Set max supported version of `flake8` to be `<5.0.5` [#240](https://github.com/s-weigand/flake8-nb/pull/240) 20 | - Enable calling `flake8_nb` as python module [#240](https://github.com/s-weigand/flake8-nb/pull/240) 21 | 22 | ## 0.4.0 (2022-02-21) 23 | 24 | - Drop official python 3.6 support 25 | 26 | ## 0.3.1 (2021-10-19) 27 | 28 | - Set max supported version of `flake8` to be `<4.0.2` 29 | - Added official Python 3.10 support and tests 30 | 31 | ## 0.3.0 (2020-05-16) 32 | 33 | - Set max supported version of `flake8` to be `<3.9.3` 34 | - Report formatting is configurable via `--notebook-cell-format` option 35 | with formatting options `nb_path`, `exec_count`, `code_cell_count` and `total_cell_count`. 36 | 37 | ## 0.2.7 (2020-04-16) 38 | 39 | - Set max supported version of `flake8` to be `<3.9.2` 40 | 41 | ## 0.2.6 (2020-03-21) 42 | 43 | - Set max supported version of `flake8` to be `<3.9.1` 44 | 45 | ## 0.2.5 (2020-10-06) 46 | 47 | - Added official Python 3.9 support and tests 48 | 49 | ## 0.2.4 (2020-10-04) 50 | 51 | - Set max supported version of `flake8` to be `<3.8.5` 52 | 53 | ## 0.2.3 (2020-10-02) 54 | 55 | - Fixed pre-commit hook file association so it support python and juypter notebooks 56 | 57 | ## 0.2.1 (2020-08-11) 58 | 59 | - Forced uft8 encoding when reading notebooks, 60 | this prevents errors on windows when console codepage is assumed 61 | 62 | ## 0.2.0 (2020-07-18) 63 | 64 | - Added pre-commit hook ([#47](https://github.com/s-weigand/flake8-nb/pull/47)) 65 | 66 | ## 0.1.8 (2020-06-09) 67 | 68 | - Set max supported version of `flake8` to be `<=3.8.3` 69 | 70 | ## 0.1.7 (2020-05-25) 71 | 72 | - Set max supported version of `flake8` to be `<=3.8.2` 73 | 74 | ## 0.1.6 (2020-05-20) 75 | 76 | - Set max supported version of `flake8` to be `<=3.8.1` 77 | - Fixed bug with `--exclude` option 78 | 79 | ## 0.1.4 (2020-01-01) 80 | 81 | - Set max supported version of `flake8` to be `<3.8.0`, to prevent breaking due to changes of `flake8`'s inner workings. 82 | 83 | ## 0.1.3 (2019-11-13) 84 | 85 | - Added official Python 3.8 support and tests 86 | 87 | ## 0.1.2 (2019-10-29) 88 | 89 | - Fixed compatibility with `flake8==3.7.9` 90 | 91 | ## 0.1.1 (2019-10-24) 92 | 93 | - Added console-script 'flake8-nb' as an alias for 'flake8_nb' 94 | 95 | ## 0.1.0 (2019-10-22) 96 | 97 | - First release on PyPI. 98 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | - Using welcoming and inclusive language 18 | - Being respectful of differing viewpoints and experiences 19 | - Gracefully accepting constructive criticism 20 | - Focusing on what is best for the community 21 | - Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | - The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | 28 | - Trolling, insulting/derogatory comments, and personal or political attacks 29 | 30 | - Public or private harassment 31 | 32 | - Publishing others' private information, such as a physical or electronic 33 | address, without explicit permission 34 | 35 | - Other conduct which could reasonably be considered inappropriate in a 36 | professional setting 37 | 38 | ## Our Responsibilities 39 | 40 | Project maintainers are responsible for clarifying the standards of acceptable 41 | behavior and are expected to take appropriate and fair corrective action in 42 | response to any instances of unacceptable behavior. 43 | 44 | Project maintainers have the right and responsibility to remove, edit, or 45 | reject comments, commits, code, wiki edits, issues, and other contributions 46 | that are not aligned to this Code of Conduct, or to ban temporarily or 47 | permanently any contributor for other behaviors that they deem inappropriate, 48 | threatening, offensive, or harmful. 49 | 50 | ## Scope 51 | 52 | This Code of Conduct applies both within project spaces and in public spaces 53 | when an individual is representing the project or its community. Examples of 54 | representing a project or community include using an official project e-mail 55 | address, posting via an official social media account, or acting as an appointed 56 | representative at an online or offline event. Representation of a project may be 57 | further defined and clarified by project maintainers. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported by contacting the project team at s.weigand.phy@gmail.com. All 63 | complaints will be reviewed and investigated and will result in a response that 64 | is deemed necessary and appropriate to the circumstances. The project team is 65 | obligated to maintain confidentiality with regard to the reporter of an incident. 66 | Further details of specific enforcement policies may be posted separately. 67 | 68 | Project maintainers who do not follow or enforce the Code of Conduct in good 69 | faith may face temporary or permanent repercussions as determined by other 70 | members of the project's leadership. 71 | 72 | ## Attribution 73 | 74 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 75 | available at 76 | 77 | For answers to common questions about this code of conduct, see 78 | 79 | 80 | [homepage]: https://www.contributor-covenant.org 81 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v4.4.0 4 | hooks: 5 | - id: check-ast 6 | - id: check-builtin-literals 7 | - id: end-of-file-fixer 8 | - id: trailing-whitespace 9 | - id: debug-statements 10 | - id: fix-encoding-pragma 11 | args: [--remove] 12 | - repo: https://github.com/pre-commit/pre-commit 13 | rev: v3.2.2 14 | hooks: 15 | - id: validate_manifest 16 | 17 | - repo: https://github.com/pre-commit/mirrors-mypy 18 | rev: v1.2.0 19 | hooks: 20 | - id: mypy 21 | exclude: "^(docs|tests|.github)" 22 | additional_dependencies: [types-all] 23 | 24 | - repo: https://github.com/MarcoGorelli/absolufy-imports 25 | rev: v0.3.1 26 | hooks: 27 | - id: absolufy-imports 28 | types: [file] 29 | types_or: [python, pyi] 30 | 31 | - repo: https://github.com/asottile/pyupgrade 32 | rev: v3.3.2 33 | hooks: 34 | - id: pyupgrade 35 | args: [--py37-plus] 36 | - repo: https://github.com/python/black 37 | rev: 23.3.0 38 | hooks: 39 | - id: black 40 | language_version: python3 41 | 42 | - repo: https://github.com/kynan/nbstripout 43 | rev: 0.6.1 44 | hooks: 45 | - id: nbstripout 46 | name: nbstripout no notebook_with_out_flake8_tags 47 | args: [--keep-count, --keep-output, --drop-empty-cells] 48 | exclude: "not_a_notebook|notebook_with_out_flake8_tags|cell_with_source_string" 49 | 50 | - repo: https://github.com/kynan/nbstripout 51 | rev: 0.6.1 52 | hooks: 53 | - id: nbstripout 54 | name: nbstripout only notebook_with_out_flake8_tags 55 | args: [--keep-count, --keep-output] 56 | files: "notebook_with_out_flake8_tags" 57 | 58 | - repo: https://github.com/asottile/setup-cfg-fmt 59 | rev: v2.2.0 60 | hooks: 61 | - id: setup-cfg-fmt 62 | args: [--include-version-classifiers] 63 | 64 | - repo: https://github.com/pre-commit/mirrors-prettier 65 | rev: v3.0.0-alpha.9-for-vscode # Use the sha or tag you want to point at 66 | hooks: 67 | - id: prettier 68 | - repo: https://github.com/pycqa/flake8 69 | rev: 6.0.0 70 | hooks: 71 | - id: flake8 72 | - repo: https://github.com/PyCQA/isort 73 | rev: 5.12.0 74 | hooks: 75 | - id: isort 76 | - repo: https://github.com/PyCQA/pydocstyle 77 | rev: 6.3.0 78 | hooks: 79 | - id: pydocstyle 80 | exclude: "^(docs|tests|setup.py)" 81 | - repo: https://github.com/terrencepreilly/darglint 82 | rev: v1.8.1 83 | hooks: 84 | - id: darglint 85 | exclude: "^(docs|tests|setup.py)" 86 | 87 | - repo: https://github.com/econchick/interrogate 88 | rev: 1.5.0 89 | hooks: 90 | - id: interrogate 91 | exclude: "docs|tests" 92 | pass_filenames: false 93 | 94 | - repo: https://github.com/rstcheck/rstcheck 95 | rev: "v6.1.2" 96 | hooks: 97 | - id: rstcheck 98 | additional_dependencies: [sphinx] 99 | exclude: "^docs/_templates/autosummary" 100 | 101 | - repo: https://github.com/pre-commit/pygrep-hooks 102 | rev: v1.10.0 103 | hooks: 104 | - id: rst-backticks 105 | 106 | - repo: https://github.com/codespell-project/codespell 107 | rev: v2.2.4 108 | hooks: 109 | - id: codespell 110 | files: ".py|.rst|.md" 111 | 112 | - repo: https://github.com/asottile/yesqa 113 | rev: v1.4.0 114 | hooks: 115 | - id: yesqa 116 | additional_dependencies: [flake8-docstrings] 117 | -------------------------------------------------------------------------------- /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "README.md" 4 | ], 5 | "imageSize": 100, 6 | "commit": false, 7 | "contributors": [ 8 | { 9 | "login": "s-weigand", 10 | "name": "Sebastian Weigand", 11 | "avatar_url": "https://avatars2.githubusercontent.com/u/9513634?v=4", 12 | "profile": "https://github.com/s-weigand", 13 | "contributions": [ 14 | "code", 15 | "ideas", 16 | "maintenance", 17 | "projectManagement", 18 | "infra", 19 | "test", 20 | "doc" 21 | ] 22 | }, 23 | { 24 | "login": "jtmiclat", 25 | "name": "Jt Miclat", 26 | "avatar_url": "https://avatars0.githubusercontent.com/u/30991698?v=4", 27 | "profile": "https://jtmiclat.me", 28 | "contributions": [ 29 | "bug" 30 | ] 31 | }, 32 | { 33 | "login": "peisenha", 34 | "name": "Philipp Eisenhauer", 35 | "avatar_url": "https://avatars3.githubusercontent.com/u/3607591?v=4", 36 | "profile": "http://eisenhauer.io", 37 | "contributions": [ 38 | "bug" 39 | ] 40 | }, 41 | { 42 | "login": "shmokmt", 43 | "name": "Shoma Okamoto", 44 | "avatar_url": "https://avatars1.githubusercontent.com/u/32533860?v=4", 45 | "profile": "https://shmokmt.github.io/", 46 | "contributions": [ 47 | "test" 48 | ] 49 | }, 50 | { 51 | "login": "MarcoGorelli", 52 | "name": "Marco Gorelli", 53 | "avatar_url": "https://avatars2.githubusercontent.com/u/33491632?v=4", 54 | "profile": "https://marcogorelli.github.io/", 55 | "contributions": [ 56 | "tool", 57 | "doc" 58 | ] 59 | }, 60 | { 61 | "login": "psychemedia", 62 | "name": "Tony Hirst", 63 | "avatar_url": "https://avatars.githubusercontent.com/u/82988?v=4", 64 | "profile": "http://blog.ouseful.info", 65 | "contributions": [ 66 | "ideas" 67 | ] 68 | }, 69 | { 70 | "login": "Dobatymo", 71 | "name": "Dobatymo", 72 | "avatar_url": "https://avatars.githubusercontent.com/u/7647594?v=4", 73 | "profile": "https://github.com/Dobatymo", 74 | "contributions": [ 75 | "bug" 76 | ] 77 | }, 78 | { 79 | "login": "AlpAribal", 80 | "name": "Alp Arıbal", 81 | "avatar_url": "https://avatars.githubusercontent.com/u/6286038?v=4", 82 | "profile": "https://github.com/AlpAribal", 83 | "contributions": [ 84 | "bug" 85 | ] 86 | }, 87 | { 88 | "login": "1kastner", 89 | "name": "1kastner", 90 | "avatar_url": "https://avatars.githubusercontent.com/u/5236165?v=4", 91 | "profile": "https://www.tuhh.de/mls/en/institute/associates/marvin-kastner-msc.html", 92 | "contributions": [ 93 | "bug" 94 | ] 95 | }, 96 | { 97 | "login": "dominiquesydow", 98 | "name": "Dominique Sydow", 99 | "avatar_url": "https://avatars.githubusercontent.com/u/7207093?v=4", 100 | "profile": "https://github.com/dominiquesydow", 101 | "contributions": [ 102 | "bug" 103 | ] 104 | }, 105 | { 106 | "login": "lkeegan", 107 | "name": "Liam Keegan", 108 | "avatar_url": "https://avatars.githubusercontent.com/u/12845624?v=4", 109 | "profile": "https://www.keegan.ch", 110 | "contributions": [ 111 | "bug", 112 | "code" 113 | ] 114 | } 115 | ], 116 | "contributorsPerLine": 7, 117 | "projectName": "flake8-nb", 118 | "projectOwner": "s-weigand", 119 | "repoType": "github", 120 | "repoHost": "https://github.com", 121 | "skipCi": true, 122 | "commitConvention": "angular" 123 | } 124 | -------------------------------------------------------------------------------- /CONTRIBUTING.rst: -------------------------------------------------------------------------------- 1 | .. highlight:: shell 2 | 3 | ============ 4 | Contributing 5 | ============ 6 | 7 | Contributions are welcome, and they are greatly appreciated! Every little bit 8 | helps, and credit will always be given. 9 | 10 | You can contribute in many ways: 11 | 12 | Types of Contributions 13 | ---------------------- 14 | 15 | Report Bugs 16 | ~~~~~~~~~~~ 17 | 18 | Report bugs at https://github.com/s-weigand/flake8-nb/issues. 19 | 20 | If you are reporting a bug, please include: 21 | 22 | * Your operating system name and version. 23 | * Any details about your local setup that might be helpful in troubleshooting. 24 | * Detailed steps to reproduce the bug. 25 | 26 | Fix Bugs 27 | ~~~~~~~~ 28 | 29 | Look through the GitHub issues for bugs. Anything tagged with "bug" and "help 30 | wanted" is open to whoever wants to implement it. 31 | 32 | Implement Features 33 | ~~~~~~~~~~~~~~~~~~ 34 | 35 | Look through the GitHub issues for features. Anything tagged with "enhancement" 36 | and "help wanted" is open to whoever wants to implement it. 37 | 38 | Write Documentation 39 | ~~~~~~~~~~~~~~~~~~~ 40 | 41 | flake8-nb could always use more documentation, whether as part of the 42 | official flake8-nb docs, in docstrings, or even on the web in blog posts, 43 | articles, and such. 44 | 45 | Submit Feedback 46 | ~~~~~~~~~~~~~~~ 47 | 48 | The best way to send feedback is to file an issue at https://github.com/s-weigand/flake8-nb/issues. 49 | 50 | If you are proposing a feature: 51 | 52 | * Explain in detail how it would work. 53 | * Keep the scope as narrow as possible, to make it easier to implement. 54 | * Remember that this is a volunteer-driven project, and that contributions 55 | are welcome :) 56 | 57 | Get Started! 58 | ------------ 59 | 60 | Ready to contribute? Here's how to set up ``flake8_nb`` for local development. 61 | 62 | 1. Fork the ``flake8_nb`` repo on GitHub. 63 | 2. Clone your fork locally:: 64 | 65 | $ git clone git@github.com:your_name_here/flake8_nb.git 66 | 67 | 3. Install your local copy into a virtualenv. Assuming you have virtualenvwrapper installed, 68 | this is how you set up your fork for local development:: 69 | 70 | $ mkvirtualenv flake8_nb 71 | $ cd flake8_nb/ 72 | $ pip install -r requirements_dev.txt 73 | $ pip install -e . 74 | 75 | 4. Install the ``pre-commit`` hooks, for quality assurance:: 76 | 77 | $ pre-commit install 78 | 79 | 5. Create a branch for local development:: 80 | 81 | $ git checkout -b name-of-your-bugfix-or-feature 82 | 83 | Now you can make your changes locally. 84 | 85 | 6. When you're done making changes, check that your changes pass flake8 and the 86 | tests, including testing other Python versions with tox:: 87 | 88 | $ tox 89 | 90 | To get flake8 and tox, just pip install them into your virtualenv. 91 | 92 | 7. Commit your changes and push your branch to GitHub:: 93 | 94 | $ git add . 95 | $ git commit -m "Your detailed description of your changes." 96 | $ git push origin name-of-your-bugfix-or-feature 97 | 98 | 8. Submit a pull request through the GitHub website. 99 | 100 | Pull Request Guidelines 101 | ----------------------- 102 | 103 | Before you submit a pull request, check that it meets these guidelines: 104 | 105 | 1. The pull request should include tests. 106 | 2. If the pull request adds functionality, the docs should be updated. Put 107 | your new functionality into a function with a docstring, and add the 108 | feature to the list in README.rst. 109 | 3. The pull request should work for Python 3.7, 3.8, 3.9 and 3.10. Check 110 | https://github.com/s-weigand/flake8-nb/actions 111 | and make sure that the tests pass for all supported Python versions. 112 | 113 | Tips 114 | ---- 115 | 116 | To run a subset of tests:: 117 | 118 | $ pytest tests.test_flake8_nb 119 | 120 | 121 | Deploying 122 | --------- 123 | 124 | A reminder for the maintainers on how to deploy. 125 | Make sure all your changes are committed (including an entry in HISTORY.rst). 126 | Then run:: 127 | 128 | $ bump2version patch # possible: major / minor / patch 129 | $ git push 130 | $ git push --tags 131 | 132 | Travis will then deploy to PyPI if tests pass. 133 | -------------------------------------------------------------------------------- /flake8_nb/flake8_integration/formatter.py: -------------------------------------------------------------------------------- 1 | """Module containing the report formatter. 2 | 3 | This also includes the code to map parsed error back to the 4 | original notebook and the cell the code in. 5 | """ 6 | 7 | from __future__ import annotations 8 | 9 | import os 10 | from typing import cast 11 | 12 | from flake8.formatting.default import Default 13 | from flake8.style_guide import Violation 14 | 15 | from flake8_nb.parsers.notebook_parsers import NotebookParser 16 | from flake8_nb.parsers.notebook_parsers import map_intermediate_to_input 17 | 18 | try: 19 | from flake8.formatting.default import COLORS 20 | from flake8.formatting.default import COLORS_OFF 21 | except ImportError: 22 | COLORS = COLORS_OFF = {} 23 | 24 | 25 | def map_notebook_error(violation: Violation, format_str: str) -> tuple[str, int] | None: 26 | """Map the violation caused in an intermediate file back to its cause. 27 | 28 | The cause is resolved as the notebook, the input cell and 29 | the respective line number in that cell. 30 | 31 | Parameters 32 | ---------- 33 | violation : Violation 34 | Reported violation from checking the parsed notebook 35 | format_str: str 36 | Format string used to format the notebook path and cell reporting. 37 | 38 | Returns 39 | ------- 40 | tuple[str, int] | None 41 | (filename, input_cell_line_number) 42 | ``filename`` being the name of the original notebook and 43 | the input cell were the violation was reported. 44 | ``input_cell_line_number`` line number in the input cell 45 | were the violation was reported. 46 | """ 47 | intermediate_filename = os.path.abspath(violation.filename) 48 | intermediate_line_number = violation.line_number 49 | mappings = NotebookParser.get_mappings() 50 | for original_notebook, intermediate_py, input_line_mapping in mappings: 51 | if os.path.samefile(intermediate_py, intermediate_filename): 52 | input_id, input_cell_line_number = map_intermediate_to_input( 53 | input_line_mapping, intermediate_line_number 54 | ) 55 | exec_count, code_cell_count, total_cell_count = input_id 56 | filename = format_str.format( 57 | nb_path=original_notebook, 58 | exec_count=exec_count, 59 | code_cell_count=code_cell_count, 60 | total_cell_count=total_cell_count, 61 | ) 62 | 63 | return filename, input_cell_line_number 64 | return None 65 | 66 | 67 | class IpynbFormatter(Default): # type: ignore[misc] 68 | r"""Default flake8_nb formatter for jupyter notebooks. 69 | 70 | If the file to be formatted is a ``*.py`` file, 71 | it uses flake8's default formatter. 72 | """ 73 | 74 | def after_init(self) -> None: 75 | """Check for a custom format string.""" 76 | if self.options.format.lower() != "default_notebook": 77 | self.error_format = self.options.format 78 | if not hasattr(self, "color"): 79 | self.color = True 80 | 81 | def format(self, violation: Violation) -> str | None: 82 | r"""Format the error detected by a flake8 checker. 83 | 84 | Depending on if the violation was caused by a ``*.py`` file 85 | or by a parsed notebook. 86 | 87 | Parameters 88 | ---------- 89 | violation : Violation 90 | Error a checker reported. 91 | 92 | Returns 93 | ------- 94 | str | None 95 | Formatted error message, which will be displayed 96 | in the terminal. 97 | """ 98 | filename = violation.filename 99 | if filename.lower().endswith(".ipynb_parsed"): 100 | map_result = map_notebook_error(violation, self.options.notebook_cell_format) 101 | if map_result: 102 | filename, line_number = map_result 103 | return cast( 104 | str, 105 | self.error_format 106 | % { 107 | "code": violation.code, 108 | "text": violation.text, 109 | "path": filename, 110 | "row": line_number, 111 | "col": violation.column_number, 112 | **(COLORS if self.color else COLORS_OFF), 113 | }, 114 | ) 115 | return cast(str, super().format(violation)) 116 | -------------------------------------------------------------------------------- /tests/flake8_integration/test_formatter.py: -------------------------------------------------------------------------------- 1 | import os 2 | from optparse import Values 3 | from typing import List 4 | 5 | import pytest 6 | from flake8.style_guide import Violation 7 | 8 | from flake8_nb.flake8_integration.formatter import IpynbFormatter 9 | from flake8_nb.flake8_integration.formatter import map_notebook_error 10 | from flake8_nb.parsers.notebook_parsers import NotebookParser 11 | 12 | TEST_NOTEBOOK_PATH = os.path.join("tests", "data", "notebooks", "notebook_with_flake8_tags.ipynb") 13 | 14 | 15 | def get_test_intermediate_path(intermediate_names): 16 | return [ 17 | filename 18 | for filename in intermediate_names 19 | if filename.endswith("notebook_with_flake8_tags.ipynb_parsed") 20 | ][0] 21 | 22 | 23 | def get_mocked_option(notebook_cell_format: str, formatter="default_notebook") -> Values: 24 | return Values( 25 | { 26 | "output_file": "", 27 | "format": formatter, 28 | "notebook_cell_format": notebook_cell_format, 29 | "color": "off", 30 | } 31 | ) 32 | 33 | 34 | def get_mocked_violation(filename: str, line_number: int) -> Violation: 35 | return Violation( 36 | filename=os.path.normpath(filename), 37 | line_number=line_number, 38 | physical_line=0, 39 | column_number=2, 40 | code="AB123", 41 | text="This is just for the coverage", 42 | ) 43 | 44 | 45 | @pytest.mark.parametrize( 46 | "line_number,cell_nr,expected_line_number", 47 | [ 48 | (8, 1, 2), 49 | (15, 2, 2), 50 | (29, 4, 2), 51 | (30, 4, 3), 52 | (38, 5, 3), 53 | ], 54 | ) 55 | @pytest.mark.parametrize( 56 | "notebook_cell_format,cell_format_str", 57 | ( 58 | ("{nb_path}#In[{exec_count}]", "#In[{}]"), 59 | ("{nb_path}:code_cell#{exec_count}", ":code_cell#{}"), 60 | ), 61 | ) 62 | def test_IpynbFormatter__map_notebook_error( 63 | notebook_parser: NotebookParser, 64 | notebook_cell_format: str, 65 | cell_format_str: str, 66 | line_number: int, 67 | cell_nr: int, 68 | expected_line_number: int, 69 | ): 70 | expected_filename = f"{TEST_NOTEBOOK_PATH}{cell_format_str.format(cell_nr)}" 71 | filename = get_test_intermediate_path(notebook_parser.intermediate_py_file_paths) 72 | mock_error = get_mocked_violation(filename, line_number) 73 | map_result = map_notebook_error(mock_error, notebook_cell_format) 74 | assert map_result is not None 75 | filename, input_cell_line_number = map_result 76 | assert input_cell_line_number == expected_line_number 77 | assert filename == expected_filename 78 | 79 | 80 | @pytest.mark.parametrize( 81 | "format_str,file_path_list,expected_result_str", 82 | [ 83 | ( 84 | "default_notebook", 85 | [], 86 | "{expected_filename}:2:2: AB123 This is just for the coverage", 87 | ), 88 | ( 89 | "%(path)s:%(row)d: %(text)s", 90 | [], 91 | "{expected_filename}:2: This is just for the coverage", 92 | ), 93 | ( 94 | "default_notebook", 95 | ["tests", "data", "notebooks", "falsy_python_file.py"], 96 | "{expected_filename}:8:2: AB123 This is just for the coverage", 97 | ), 98 | ( 99 | "default_notebook", 100 | [ 101 | "tests", 102 | "data", 103 | "intermediate_py_files", 104 | "notebook_with_flake8_tags.ipynb_parsed", 105 | ], 106 | "{expected_filename}:8:2: AB123 This is just for the coverage", 107 | ), 108 | ], 109 | ) 110 | @pytest.mark.parametrize( 111 | "notebook_cell_format,cell_format_str", 112 | ( 113 | ("{nb_path}#In[{exec_count}]", "#In[1]"), 114 | ("{nb_path}:code_cell#{exec_count}", ":code_cell#1"), 115 | ), 116 | ) 117 | def test_IpynbFormatter__format( 118 | notebook_cell_format: str, 119 | cell_format_str: str, 120 | notebook_parser: NotebookParser, 121 | file_path_list: List[str], 122 | format_str: str, 123 | expected_result_str: str, 124 | ): 125 | mocked_option = get_mocked_option(notebook_cell_format, format_str) 126 | formatter = IpynbFormatter(mocked_option) # type: ignore 127 | if file_path_list: 128 | filename = expected_filename = os.path.join(*file_path_list) 129 | else: 130 | expected_filename = f"{TEST_NOTEBOOK_PATH}{cell_format_str}" 131 | filename = get_test_intermediate_path(notebook_parser.intermediate_py_file_paths) 132 | mock_error = get_mocked_violation(filename, 8) 133 | result = formatter.format(mock_error) 134 | expected_result = expected_result_str.format(expected_filename=expected_filename) 135 | assert result == expected_result 136 | -------------------------------------------------------------------------------- /tests/flake8_integration/test_cli.py: -------------------------------------------------------------------------------- 1 | import contextlib 2 | import os 3 | import re 4 | 5 | import flake8 6 | import pytest 7 | 8 | from flake8_nb import FLAKE8_VERSION_TUPLE 9 | from flake8_nb import __version__ 10 | from flake8_nb.flake8_integration.cli import Flake8NbApplication 11 | from flake8_nb.flake8_integration.cli import get_notebooks_from_args 12 | from flake8_nb.flake8_integration.cli import hack_option_manager_generate_versions 13 | from flake8_nb.parsers.notebook_parsers import InvalidNotebookWarning 14 | from flake8_nb.parsers.notebook_parsers import NotebookParser 15 | from tests.flake8_integration.conftest import TempIpynbArgs 16 | 17 | 18 | def test_get_notebooks_from_args(temp_ipynb_args: TempIpynbArgs): 19 | orig_args, (expected_args, expected_nb_list) = temp_ipynb_args.get_args_and_result() 20 | args, nb_list = get_notebooks_from_args( 21 | orig_args, exclude=["*.tox/*", ".ipynb_checkpoints", "*/docs/*"] 22 | ) 23 | assert sorted(args) == sorted(expected_args) 24 | assert sorted(nb_list) == sorted(expected_nb_list) 25 | 26 | 27 | def test_hack_option_manager_generate_versions(): 28 | pattern = re.compile(rf"flake8: {flake8.__version__}, original_input") 29 | 30 | def test_func(*args, **kwargs): 31 | return "original_input" 32 | 33 | hacked_output = hack_option_manager_generate_versions(test_func)() 34 | assert re.match(pattern, hacked_output) is not None 35 | 36 | 37 | def test_Flake8NbApplication__generate_versions(): 38 | generate_versions_pattern = re.compile( 39 | rf"flake8: {flake8.__version__}(, [\w\-_]+: \d+\.\d+\.\d+)+" 40 | ) 41 | generate_epilog_pattern = re.compile( 42 | rf"Installed plugins: flake8: {flake8.__version__}(, [\w\-_]+: \d+\.\d+\.\d+)+" 43 | ) 44 | orig_args = [os.path.join("tests", "data", "notebooks")] 45 | app = Flake8NbApplication() 46 | app.initialize(orig_args) 47 | 48 | if FLAKE8_VERSION_TUPLE < (5, 0, 0): 49 | app.option_manager.generate_epilog() 50 | 51 | hacked_generate_versions = app.option_manager.generate_versions() 52 | assert re.match(generate_versions_pattern, hacked_generate_versions) is not None 53 | 54 | hacked_generate_epilog: str = app.option_manager.parser.epilog # type: ignore 55 | 56 | assert re.match(generate_epilog_pattern, hacked_generate_epilog) is not None 57 | 58 | 59 | def test_Flake8NbApplication__hack_flake8_program_and_version(): 60 | app = Flake8NbApplication() 61 | app.initialize([]) 62 | program = "flake8_nb" 63 | 64 | assert app.program == program 65 | assert app.version == __version__ 66 | assert app.option_manager.parser.prog == program 67 | assert app.option_manager.parser.version == __version__ # type: ignore 68 | assert app.option_manager.program_name == program 69 | assert app.option_manager.version == __version__ 70 | 71 | 72 | def test_Flake8NbApplication__option_defaults(): 73 | app = Flake8NbApplication() 74 | app.initialize([]) 75 | 76 | option_dict = app.option_manager.config_options_dict 77 | assert option_dict["format"].default == "default_notebook" 78 | assert option_dict["filename"].default == "*.py,*.ipynb_parsed" 79 | assert option_dict["exclude"].default.endswith(",.ipynb_checkpoints") # type: ignore 80 | assert option_dict["keep_parsed_notebooks"].default is False 81 | 82 | 83 | @pytest.mark.filterwarnings(InvalidNotebookWarning) 84 | def test_Flake8NbApplication__hack_args(temp_ipynb_args: TempIpynbArgs): 85 | orig_args, (expected_args, _) = temp_ipynb_args.get_args_and_result() 86 | result = Flake8NbApplication.hack_args( 87 | orig_args, exclude=["*.tox/*", "*.ipynb_checkpoints*", "*/docs/*"] 88 | ) 89 | expected_parsed_nb_list = NotebookParser.intermediate_py_file_paths 90 | 91 | assert result == expected_args + expected_parsed_nb_list 92 | 93 | 94 | @pytest.mark.filterwarnings(InvalidNotebookWarning) 95 | def test_Flake8NbApplication__parse_configuration_and_cli(): 96 | orig_args = [os.path.join("tests", "data", "notebooks")] 97 | app = Flake8NbApplication() 98 | # parse_configuration_and_cli is called by initialize 99 | app.initialize(orig_args) 100 | expected_parsed_nb_list = NotebookParser.intermediate_py_file_paths 101 | 102 | assert app.args == orig_args + expected_parsed_nb_list 103 | 104 | 105 | @pytest.mark.parametrize("keep_parsed_notebooks", [False, True]) 106 | def test_Flake8NbApplication__exit(keep_parsed_notebooks: bool): 107 | with pytest.warns(InvalidNotebookWarning): 108 | orig_args = [] 109 | if keep_parsed_notebooks is True: 110 | orig_args += ["--keep-parsed-notebooks"] 111 | app = Flake8NbApplication() 112 | app.initialize([*orig_args, os.path.join("tests", "data", "notebooks")]) 113 | temp_path = NotebookParser.temp_path 114 | with contextlib.suppress(SystemExit): 115 | app.exit() 116 | assert os.path.exists(temp_path) == keep_parsed_notebooks 117 | NotebookParser.clean_up() 118 | -------------------------------------------------------------------------------- /docs/usage.rst: -------------------------------------------------------------------------------- 1 | .. highlight:: shell 2 | 3 | ===== 4 | Usage 5 | ===== 6 | 7 | Since ``flake8_nb`` is basically a hacked version of 8 | ``flake8`` its usage is identically. 9 | The only key difference is the appended ``_nb`` is the commands and 10 | configurations name. 11 | 12 | Command line usage 13 | ------------------ 14 | 15 | The basic usage is to call ``flake8_nb`` with the files/paths, 16 | which should be checked as arguments (see `flake8 invocation`_). 17 | 18 | .. code-block:: console 19 | 20 | $ flake8_nb path-to-notebooks-or-folder 21 | 22 | To customize the behavior you can use the many options provided 23 | by ``flake8``'s CLI. To see all the provided option just call: 24 | 25 | .. code-block:: console 26 | 27 | $ flake8_nb --help 28 | 29 | Additional flags/options provided by ``flake8_nb``: 30 | 31 | * ``--keep-parsed-notebooks`` 32 | If this flag is activated the the parsed notebooks will be kept 33 | and the path they were saved in will be displayed, for further 34 | debugging or trouble shooting. 35 | 36 | * ``--notebook-cell-format`` 37 | Template string used to format the filename and cell part of error report. 38 | Possible variables which will be replaced are ``nb_path``, ``exec_count``, 39 | ``code_cell_count`` and ``total_cell_count``. 40 | 41 | Project wide configuration 42 | -------------------------- 43 | 44 | Configuration of a project can be saved in one of the following files 45 | ``setup.cfg``, ``tox.ini`` or ``.flake8_nb``, on the top level of your project 46 | (see `flake8 configuration`_). 47 | 48 | .. code-block:: ini 49 | 50 | [flake8_nb] 51 | ; Default values 52 | keep_parsed_notebooks = False 53 | notebook_cell_format = {nb_path}#In[{exec_count}] 54 | 55 | For a detailed explanation on how to use and configure it, 56 | you can consult the official `flake8 documentation`_ 57 | 58 | 59 | Per cell/line configuration 60 | --------------------------- 61 | 62 | There are multiple ways to fine grade configure ``flake8_nb`` 63 | on a line or cell basis. 64 | 65 | flake8 ``noqa`` comments 66 | ^^^^^^^^^^^^^^^^^^^^^^^^ 67 | The most intuitive way for experienced ``flake8`` users is 68 | to utilize the known `flake8 noqa`_ comment on a line, to ignore specific 69 | or all errors, ``flake8`` would report on that given line. 70 | 71 | .. note:: 72 | 73 | If a normal ``flake8 noqa comment`` ends with a string, which doesn't 74 | match the error code pattern (``\w+\d+``), this comment will be ignored. 75 | 76 | 77 | Cell tags 78 | ^^^^^^^^^ 79 | Cell tags are meta information, which can be added to cells, 80 | to augment their behavior (for ``jupyterlab<2.0``, you will need to install `jupyterlab-celltags`_ ). 81 | Depending on the editor you use for the notebook, they aren't 82 | directly visible, which is a nice way to hide certain internals 83 | which aren't important for the user/reader. 84 | For example if write a book like notebook and want to demonstrate 85 | some bad code examples an still pass your ``flake8_nb`` tests you 86 | can use ``flake8-noqa-tags``. 87 | Or if you want to demonstrate a raised exception and still want 88 | then whole notebook to be executed when you run all cells, you 89 | can use the ``raises-exception`` tag. 90 | 91 | The patterns for ``flake8-noqa-tags`` are the following: 92 | 93 | * ``flake8-noqa-cell`` 94 | ignores all reports from a cell 95 | 96 | * ``flake8-noqa-cell--`` 97 | ignores given rules for the cell 98 | i.e. ``flake8-noqa-cell-F401-F811`` 99 | 100 | * ``flake8-noqa-line-`` 101 | ignores all reports from a given line in a cell, 102 | i.e. ``flake8-noqa-line-1`` 103 | 104 | * ``flake8-noqa-line---`` 105 | ignores given rules from a given line for the cell 106 | i.e. ``flake8-noqa-line-1-F401-F811`` 107 | 108 | 109 | Inline cell tags 110 | ^^^^^^^^^^^^^^^^ 111 | If you want your users/reader to directly see which ``flake8`` rules 112 | are ignored, you can also use the ``flake8-noqa-tag`` pattern as 113 | comment in a cell. 114 | 115 | 116 | .. note:: 117 | 118 | If you use jupyter magic to run code other than Python (i.e. ``%%bash``) 119 | you should ignore the whole cell with ``flake8-noqa-cell``. 120 | 121 | As pre-commit hook 122 | ------------------ 123 | 124 | Add the following to your :code:`.pre-commit-config.yaml` file: 125 | 126 | .. code-block:: yaml 127 | :substitutions: 128 | 129 | - repo: https://github.com/s-weigand/flake8-nb 130 | rev: |version| # specify version here 131 | hooks: 132 | - id: flake8-nb 133 | 134 | See `pre-commit docs`_ for more on pre-commit. 135 | 136 | .. _`flake8 invocation`: https://flake8.pycqa.org/en/latest/user/invocation.html 137 | .. _`flake8 configuration`: https://flake8.pycqa.org/en/latest/user/configuration.html 138 | .. _`flake8 documentation`: https://flake8.pycqa.org/en/latest/index.html 139 | .. _`flake8 noqa`: https://flake8.pycqa.org/en/latest/user/violations.html#in-line-ignoring-errors 140 | .. _`jupyterlab-celltags`: https://github.com/jupyterlab/jupyterlab-celltags 141 | .. _`pre-commit docs`: https://pre-commit.com/ 142 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | tags: 6 | - v** 7 | branches-ignore: 8 | - "dependabot/**" 9 | - "sourcery/**" 10 | - "create-pr-action/pre-commit-config-update-*" 11 | pull_request: 12 | 13 | jobs: 14 | misspell: 15 | name: misspell 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/checkout@v3 19 | - name: misspell 20 | uses: reviewdog/action-misspell@v1 21 | with: 22 | reporter: github-pr-review 23 | github_token: ${{ secrets.github_token }} 24 | level: warning 25 | locale: "US" 26 | 27 | pre-commit: 28 | runs-on: ubuntu-latest 29 | steps: 30 | - name: Check out repo 31 | uses: actions/checkout@v3 32 | - name: Set up Python 33 | uses: actions/setup-python@v4 34 | with: 35 | python-version: 3.8 36 | - name: Run pre-commit 37 | uses: pre-commit/action@v3.0.0 38 | 39 | check-manifest: 40 | name: Check Manifest 41 | runs-on: ubuntu-latest 42 | steps: 43 | - name: Check out repo 44 | uses: actions/checkout@v3 45 | - name: Set up Python 3.8 46 | uses: actions/setup-python@v4 47 | with: 48 | python-version: 3.8 49 | - name: Install check manifest 50 | run: python -m pip install check-manifest 51 | - name: Run check manifest 52 | run: check-manifest 53 | 54 | docs: 55 | runs-on: ubuntu-latest 56 | steps: 57 | - uses: actions/checkout@v3 58 | - name: Setup conda 59 | uses: s-weigand/setup-conda@v1 60 | with: 61 | conda-channels: conda-forge 62 | - name: Install dependencies 63 | run: | 64 | conda install -y pandoc 65 | pip install -U -q -r docs/requirements.txt 66 | pip install . 67 | - name: Show installed dependencies 68 | run: | 69 | pip freeze 70 | pandoc -v 71 | - name: Build docs 72 | run: make --directory=docs clean_all html 73 | 74 | docs-link: 75 | runs-on: ubuntu-latest 76 | steps: 77 | - uses: actions/checkout@v3 78 | - name: Setup conda 79 | uses: s-weigand/setup-conda@v1 80 | with: 81 | conda-channels: conda-forge 82 | - name: Install dependencies 83 | run: | 84 | conda install -y pandoc 85 | pip install -U -q -r docs/requirements.txt 86 | pip install . 87 | - name: Show installed dependencies 88 | continue-on-error: true 89 | run: | 90 | pip freeze 91 | pandoc -v 92 | - name: Build docs 93 | run: make --directory=docs clean_all linkcheck 94 | 95 | # flake8-nightly: 96 | # runs-on: ubuntu-latest 97 | # needs: [pre-commit, docs] 98 | 99 | # steps: 100 | # - uses: actions/checkout@v3 101 | # - name: Set up Python 3.7 102 | # uses: actions/setup-python@v4 103 | # with: 104 | # python-version: 3.7 105 | # - name: Install dependencies 106 | # env: 107 | # TOX_ENV_NAME: flake8-nightly 108 | # run: | 109 | # python -m pip install -U pip 110 | # python -m pip install -U -r requirements_dev.txt 111 | # pip install . 112 | # python -m pip install --force-reinstall -U -q git+https://github.com/pycqa/flake8 113 | # - name: Show flake8 version 114 | # run: | 115 | # pip freeze | grep flake8 116 | # - name: Run tests 117 | # run: | 118 | # python -m pytest -vv 119 | # - name: Codecov Upload 120 | # uses: codecov/codecov-action@v3 121 | # with: 122 | # file: ./coverage.xml 123 | # name: flake8-nightly 124 | 125 | flake8-legacy: 126 | runs-on: ubuntu-latest 127 | needs: [pre-commit, docs] 128 | 129 | steps: 130 | - uses: actions/checkout@v3 131 | - name: Set up Python 3.7 132 | uses: actions/setup-python@v4 133 | with: 134 | python-version: 3.7 135 | - name: Install dependencies 136 | run: | 137 | python -m pip install -U pip 138 | pip install . 139 | python -m pip install -U -r requirements_dev.txt 140 | python -m pip install -U -q 'flake8==3.8.0' 141 | - name: Show flake8 version 142 | run: | 143 | pip freeze | grep flake8 144 | - name: Run tests 145 | run: | 146 | python -m pytest -vv 147 | - name: Codecov Upload 148 | uses: codecov/codecov-action@v3 149 | with: 150 | file: ./coverage.xml 151 | name: flake8-legacy 152 | 153 | test: 154 | runs-on: ${{ matrix.os }} 155 | needs: [pre-commit, docs] 156 | strategy: 157 | matrix: 158 | os: [ubuntu-latest, windows-latest, macOS-latest] 159 | python-version: [3.7, 3.8, 3.9, "3.10", 3.11] 160 | 161 | steps: 162 | - uses: actions/checkout@v3 163 | - name: Set up Python ${{ matrix.python-version }} 164 | uses: actions/setup-python@v4 165 | with: 166 | python-version: ${{ matrix.python-version }} 167 | - name: Install dependencies 168 | run: | 169 | python -m pip install -U pip 170 | pip install . 171 | python -m pip install -U -r requirements_dev.txt 172 | - name: Run tests 173 | run: | 174 | python -m pytest 175 | - name: Codecov Upload 176 | uses: codecov/codecov-action@v3 177 | with: 178 | file: ./coverage.xml 179 | name: ${{ matrix.os }}-py${{ matrix.python-version }} 180 | 181 | deploy: 182 | runs-on: ubuntu-latest 183 | if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags') 184 | needs: [test, flake8-legacy] 185 | steps: 186 | - uses: actions/checkout@v3 187 | - name: Set up Python 3.7 188 | uses: actions/setup-python@v4 189 | with: 190 | python-version: 3.7 191 | - name: Install dependencies 192 | run: | 193 | python -m pip install -U pip 194 | pip install -U -r requirements_dev.txt 195 | pip install -U . 196 | - name: Build dist 197 | run: | 198 | python setup.py sdist bdist_wheel 199 | 200 | - name: Publish package 201 | uses: pypa/gh-action-pypi-publish@v1.8.7 202 | with: 203 | user: __token__ 204 | password: ${{ secrets.pypi_password }} 205 | -------------------------------------------------------------------------------- /tests/data/notebooks/notebook_with_out_flake8_tags.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# This notebook demonstrates `flake8_nb` reporting" 8 | ] 9 | }, 10 | { 11 | "cell_type": "markdown", 12 | "metadata": {}, 13 | "source": [ 14 | "The next cell should report `F401 'not_a_package' imported but unused`" 15 | ] 16 | }, 17 | { 18 | "cell_type": "code", 19 | "execution_count": 1, 20 | "metadata": { 21 | "tags": [ 22 | "raises-exception" 23 | ] 24 | }, 25 | "outputs": [ 26 | { 27 | "ename": "ModuleNotFoundError", 28 | "evalue": "No module named 'not_a_package'", 29 | "output_type": "error", 30 | "traceback": [ 31 | "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m", 32 | "\u001b[1;31mModuleNotFoundError\u001b[0m Traceback (most recent call last)", 33 | "\u001b[1;32m\u001b[0m in \u001b[0;36m\u001b[1;34m\u001b[0m\n\u001b[1;32m----> 1\u001b[1;33m \u001b[1;32mimport\u001b[0m \u001b[0mnot_a_package\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m", 34 | "\u001b[1;31mModuleNotFoundError\u001b[0m: No module named 'not_a_package'" 35 | ] 36 | } 37 | ], 38 | "source": [ 39 | "import not_a_package" 40 | ] 41 | }, 42 | { 43 | "cell_type": "markdown", 44 | "metadata": {}, 45 | "source": [ 46 | "The next cell should report `E231 missing whitespace after ':'`" 47 | ] 48 | }, 49 | { 50 | "cell_type": "code", 51 | "execution_count": 2, 52 | "metadata": {}, 53 | "outputs": [ 54 | { 55 | "data": { 56 | "text/plain": [ 57 | "{'1': 1}" 58 | ] 59 | }, 60 | "execution_count": 2, 61 | "metadata": {}, 62 | "output_type": "execute_result" 63 | } 64 | ], 65 | "source": [ 66 | "{\"1\":1}" 67 | ] 68 | }, 69 | { 70 | "cell_type": "markdown", 71 | "metadata": {}, 72 | "source": [ 73 | "The next cell should not be reported, since it is valid syntax" 74 | ] 75 | }, 76 | { 77 | "cell_type": "code", 78 | "execution_count": 3, 79 | "metadata": {}, 80 | "outputs": [], 81 | "source": [ 82 | "def func():\n", 83 | " return \"foo\"" 84 | ] 85 | }, 86 | { 87 | "cell_type": "markdown", 88 | "metadata": {}, 89 | "source": [ 90 | "The next cell should not be reported, since it is valid syntax" 91 | ] 92 | }, 93 | { 94 | "cell_type": "code", 95 | "execution_count": 4, 96 | "metadata": {}, 97 | "outputs": [], 98 | "source": [ 99 | "class Bar:\n", 100 | " def foo(self):\n", 101 | " return \"foo\"" 102 | ] 103 | }, 104 | { 105 | "cell_type": "markdown", 106 | "metadata": {}, 107 | "source": [ 108 | "The next cell should be ignored in the generated intermediate `*.py` file since it is empty" 109 | ] 110 | }, 111 | { 112 | "cell_type": "code", 113 | "execution_count": null, 114 | "metadata": { 115 | "tags": [ 116 | "keep_output" 117 | ] 118 | }, 119 | "outputs": [], 120 | "source": [] 121 | }, 122 | { 123 | "cell_type": "markdown", 124 | "metadata": {}, 125 | "source": [ 126 | "The next cell should report `E231 missing whitespace after ':'`" 127 | ] 128 | }, 129 | { 130 | "cell_type": "code", 131 | "execution_count": 5, 132 | "metadata": {}, 133 | "outputs": [ 134 | { 135 | "data": { 136 | "text/plain": [ 137 | "{'1': 1}" 138 | ] 139 | }, 140 | "execution_count": 5, 141 | "metadata": {}, 142 | "output_type": "execute_result" 143 | } 144 | ], 145 | "source": [ 146 | "{\"1\":1}" 147 | ] 148 | }, 149 | { 150 | "cell_type": "markdown", 151 | "metadata": {}, 152 | "source": [ 153 | "## Report using execution count" 154 | ] 155 | }, 156 | { 157 | "cell_type": "code", 158 | "execution_count": 6, 159 | "metadata": { 160 | "tags": [] 161 | }, 162 | "outputs": [ 163 | { 164 | "name": "stdout", 165 | "output_type": "stream", 166 | "text": [ 167 | "notebook_with_out_flake8_tags.ipynb#In[1]:1:1: F401 'not_a_package' imported but unused\n", 168 | "notebook_with_out_flake8_tags.ipynb#In[2]:1:5: E231 missing whitespace after ':'\n", 169 | "notebook_with_out_flake8_tags.ipynb#In[5]:1:5: E231 missing whitespace after ':'\n" 170 | ] 171 | } 172 | ], 173 | "source": [ 174 | "!flake8_nb notebook_with_out_flake8_tags.ipynb" 175 | ] 176 | }, 177 | { 178 | "cell_type": "markdown", 179 | "metadata": {}, 180 | "source": [ 181 | "## Report using code cell count" 182 | ] 183 | }, 184 | { 185 | "cell_type": "code", 186 | "execution_count": 7, 187 | "metadata": { 188 | "tags": [ 189 | "flake8-noqa-cell-E501" 190 | ] 191 | }, 192 | "outputs": [ 193 | { 194 | "name": "stdout", 195 | "output_type": "stream", 196 | "text": [ 197 | "'notebook_with_out_flake8_tags.ipynb:code_cell#1':1:1: F401 'not_a_package' imported but unused\n", 198 | "'notebook_with_out_flake8_tags.ipynb:code_cell#2':1:5: E231 missing whitespace after ':'\n", 199 | "'notebook_with_out_flake8_tags.ipynb:code_cell#6':1:5: E231 missing whitespace after ':'\n" 200 | ] 201 | } 202 | ], 203 | "source": [ 204 | "!flake8_nb --notebook-cell-format '{nb_path}:code_cell#{code_cell_count}' notebook_with_out_flake8_tags.ipynb" 205 | ] 206 | }, 207 | { 208 | "cell_type": "markdown", 209 | "metadata": {}, 210 | "source": [ 211 | "## Report using total cell count" 212 | ] 213 | }, 214 | { 215 | "cell_type": "code", 216 | "execution_count": 8, 217 | "metadata": { 218 | "tags": [ 219 | "flake8-noqa-cell-E501" 220 | ] 221 | }, 222 | "outputs": [ 223 | { 224 | "name": "stdout", 225 | "output_type": "stream", 226 | "text": [ 227 | "'notebook_with_out_flake8_tags.ipynb:cell#3':1:1: F401 'not_a_package' imported but unused\n", 228 | "'notebook_with_out_flake8_tags.ipynb:cell#5':1:5: E231 missing whitespace after ':'\n", 229 | "'notebook_with_out_flake8_tags.ipynb:cell#14':1:5: E231 missing whitespace after ':'\n" 230 | ] 231 | } 232 | ], 233 | "source": [ 234 | "!flake8_nb --notebook-cell-format '{nb_path}:cell#{total_cell_count}' notebook_with_out_flake8_tags.ipynb" 235 | ] 236 | } 237 | ], 238 | "metadata": { 239 | "kernelspec": { 240 | "display_name": "Python 3", 241 | "language": "python", 242 | "name": "python3" 243 | }, 244 | "language_info": { 245 | "codemirror_mode": { 246 | "name": "ipython", 247 | "version": 3 248 | }, 249 | "file_extension": ".py", 250 | "mimetype": "text/x-python", 251 | "name": "python", 252 | "nbconvert_exporter": "python", 253 | "pygments_lexer": "ipython3", 254 | "version": "3.8.8" 255 | } 256 | }, 257 | "nbformat": 4, 258 | "nbformat_minor": 4 259 | } 260 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # flake8_nb 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 | import flake8_nb 21 | 22 | # -- General configuration --------------------------------------------- 23 | 24 | # If your documentation needs a minimal Sphinx version, state it here. 25 | # 26 | # needs_sphinx = '1.0' 27 | 28 | # Add any Sphinx extension module names here, as strings. They can be 29 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 30 | extensions = [ 31 | "nbsphinx", 32 | "nbsphinx_link", 33 | "myst_parser", 34 | "sphinx_copybutton", 35 | "sphinx.ext.autodoc", 36 | "sphinx.ext.autosummary", 37 | "sphinx.ext.viewcode", 38 | "sphinx.ext.napoleon", 39 | "sphinx_substitution_extensions", 40 | ] 41 | 42 | autoclass_content = "both" 43 | autosummary_generate = True 44 | add_module_names = False 45 | autodoc_member_order = "bysource" 46 | numpydoc_show_class_members = False 47 | numpydoc_class_members_toctree = False 48 | 49 | # Add any paths that contain templates here, relative to this directory. 50 | templates_path = ["_templates"] 51 | 52 | # List of arguments to be passed to the kernel that executes the notebooks: 53 | nbsphinx_execute_arguments = [ 54 | "--InlineBackend.figure_formats={'svg', 'pdf'}", 55 | "--InlineBackend.rc={'figure.dpi': 96}", 56 | ] 57 | 58 | # The suffix(es) of source filenames. 59 | # You can specify multiple suffix as a list of string: 60 | # 61 | source_suffix = [".rst", ".md"] 62 | # source_suffix = ".rst" 63 | 64 | # The main toctree document. 65 | main_doc = "index" 66 | 67 | # General information about the project. 68 | project = "flake8-nb" 69 | copyright = "2019, Sebastian Weigand" 70 | author = "Sebastian Weigand" 71 | 72 | # The version info for the project you're documenting, acts as replacement 73 | # for |version| and |release|, also used in various other places throughout 74 | # the built documents. 75 | # 76 | # The short X.Y version. 77 | version = flake8_nb.__version__ 78 | # The full version, including alpha/beta/rc tags. 79 | release = flake8_nb.__version__ 80 | 81 | # The language for content autogenerated by Sphinx. Refer to documentation 82 | # for a list of supported languages. 83 | # 84 | # This is also used if you do content translation via gettext catalogs. 85 | # Usually you set "language" from the command line for these cases. 86 | language = None 87 | 88 | # List of patterns, relative to source directory, that match files and 89 | # directories to ignore when looking for source files. 90 | # This patterns also effect to html_static_path and html_extra_path 91 | exclude_patterns = ["_build", "Thumbs.db", ".DS_Store", "**.ipynb_checkpoints"] 92 | 93 | # The name of the Pygments (syntax highlighting) style to use. 94 | pygments_style = "sphinx" 95 | 96 | # If true, `todo` and `todoList` produce output, else they produce nothing. 97 | todo_include_todos = False 98 | 99 | rst_prolog = f""" 100 | .. |version| replace:: {version} 101 | """ 102 | 103 | 104 | # This is processed by Jinja2 and inserted before each notebook 105 | nbsphinx_prolog = r""" 106 | {% set docname = env.metadata[env.docname]['nbsphinx-link-target'][3:] %} 107 | {% set binder_urls = 'lab/tree/' + env.metadata[env.docname]['nbsphinx-link-target'][3:] %} 108 | .. only:: html 109 | 110 | .. role:: raw-html(raw) 111 | :format: html 112 | 113 | .. nbinfo:: 114 | 115 | This page was generated from `{{ docname }}`__. 116 | Interactive online version: 117 | :raw-html:`Binder badge` # noqa: E501 118 | 119 | __ https://github.com/s-weigand/flake8-nb/blob/main/{{ docname | urlencode }} 120 | 121 | 122 | """ 123 | 124 | # This is processed by Jinja2 and inserted after each notebook 125 | nbsphinx_epilog = r""" 126 | {% set docname = env.metadata[env.docname]['nbsphinx-link-target'][3:] %} 127 | .. raw:: latex 128 | \nbsphinxstopnotebook{\scriptsize\noindent\strut 129 | \textcolor{gray}{\dotfill\ \sphinxcode{\sphinxupquote{\strut 130 | {{ docname | escape_latex }}}} ends here.}} 131 | """ 132 | # -- Options for HTML output ------------------------------------------- 133 | 134 | # The theme to use for HTML and HTML Help pages. See the documentation for 135 | # a list of builtin themes. 136 | # 137 | html_theme = "sphinx_rtd_theme" 138 | html_theme_options = { 139 | "navigation_depth": -1, 140 | } 141 | 142 | # Theme options are theme-specific and customize the look and feel of a 143 | # theme further. For a list of options available for each theme, see the 144 | # documentation. 145 | # 146 | # html_theme_options = {} 147 | 148 | # Add any paths that contain custom static files (such as style sheets) here, 149 | # relative to this directory. They are copied after the builtin static files, 150 | # so a file named "default.css" will overwrite the builtin "default.css". 151 | # html_static_path = ["_static"] 152 | 153 | 154 | # -- Options for HTMLHelp output --------------------------------------- 155 | 156 | # Output file base name for HTML help builder. 157 | htmlhelp_basename = "flake8_nbdoc" 158 | 159 | 160 | # -- Options for LaTeX output ------------------------------------------ 161 | 162 | latex_elements = { 163 | # The paper size ('letterpaper' or 'a4paper'). 164 | # 165 | # 'papersize': 'letterpaper', 166 | # The font size ('10pt', '11pt' or '12pt'). 167 | # 168 | # 'pointsize': '10pt', 169 | # Additional stuff for the LaTeX preamble. 170 | # 171 | # 'preamble': '', 172 | # Latex figure (float) alignment 173 | # 174 | # 'figure_align': 'htbp', 175 | } 176 | 177 | # Grouping the document tree into LaTeX files. List of tuples 178 | # (source start file, target name, title, author, documentclass 179 | # [howto, manual, or own class]). 180 | latex_documents = [ 181 | ( 182 | main_doc, 183 | "flake8_nb.tex", 184 | "flake8-nb Documentation", 185 | "Sebastian Weigand", 186 | "manual", 187 | ) 188 | ] 189 | 190 | 191 | # -- Options for manual page output ------------------------------------ 192 | 193 | # One entry per manual page. List of tuples 194 | # (source start file, name, description, authors, manual section). 195 | man_pages = [(main_doc, "flake8_nb", "flake8-nb Documentation", [author], 1)] 196 | 197 | linkcheck_ignore = [ 198 | r"https://github\.com/s-weigand/flake8-nb/actions", 199 | r"https://github\.com/s-weigand/flake8-nb/workflows/Tests/badge\.svg", 200 | r"https://gitlab\.com/pycqa/flake8/blob/master/src/flake8/main/application\.py#L194", 201 | ] 202 | 203 | # -- Options for Texinfo output ---------------------------------------- 204 | 205 | # Grouping the document tree into Texinfo files. List of tuples 206 | # (source start file, target name, title, author, 207 | # dir menu entry, description, category) 208 | texinfo_documents = [ 209 | ( 210 | main_doc, 211 | "flake8_nb", 212 | "flake8-nb Documentation", 213 | author, 214 | "flake8_nb", 215 | "One line description of project.", 216 | "Miscellaneous", 217 | ) 218 | ] 219 | -------------------------------------------------------------------------------- /tests/test__main__.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import shutil 4 | import subprocess 5 | import sys 6 | from pathlib import Path 7 | 8 | import pytest 9 | from _pytest.capture import CaptureFixture 10 | from _pytest.monkeypatch import MonkeyPatch 11 | from flake8 import __version__ as flake_version 12 | 13 | from flake8_nb import FLAKE8_VERSION_TUPLE 14 | from flake8_nb import __version__ 15 | from flake8_nb.__main__ import main 16 | from flake8_nb.parsers.notebook_parsers import InvalidNotebookWarning 17 | from flake8_nb.parsers.notebook_parsers import NotebookParser 18 | from tests import TEST_NOTEBOOK_BASE_PATH 19 | 20 | 21 | @pytest.mark.parametrize("keep_intermediate", [True, False]) 22 | @pytest.mark.parametrize( 23 | "notebook_cell_format,expected_result", 24 | [ 25 | ("{nb_path}#In[{exec_count}]", "expected_output_exec_count"), 26 | ("{nb_path}:code_cell#{code_cell_count}", "expected_output_code_cell_count"), 27 | ("{nb_path}:cell#{total_cell_count}", "expected_output_total_cell_count"), 28 | ], 29 | ) 30 | def test_run_main( 31 | capsys, keep_intermediate: bool, notebook_cell_format: str, expected_result: str 32 | ): 33 | argv = ["flake8_nb"] 34 | if keep_intermediate: 35 | argv.append("--keep-parsed-notebooks") 36 | argv += ["--notebook-cell-format", notebook_cell_format] 37 | argv += ["--exclude", "*.tox/*,*.ipynb_checkpoints*,*/docs/*"] 38 | with pytest.raises(SystemExit): 39 | with pytest.warns(InvalidNotebookWarning): 40 | main([*argv, TEST_NOTEBOOK_BASE_PATH]) 41 | captured = capsys.readouterr() 42 | result_output = captured.out 43 | result_list = result_output.replace("\r", "").split("\n") 44 | result_list.remove("") 45 | expected_result_path = os.path.join( 46 | os.path.dirname(__file__), "data", f"{expected_result}.txt" 47 | ) 48 | with open(expected_result_path) as result_file: 49 | expected_result_list = result_file.readlines() 50 | assert len(expected_result_list) == len(result_list) 51 | for expected_result in expected_result_list: 52 | assert any(result.endswith(expected_result.rstrip("\n")) for result in result_list) 53 | 54 | if keep_intermediate: 55 | assert os.path.exists(NotebookParser.temp_path) 56 | NotebookParser.clean_up() 57 | 58 | 59 | def test_run_main_use_config(capsys, tmp_path: Path): 60 | test_config = tmp_path / "setup.cfg" 61 | test_config.write_text("[flake8_nb]\nextend-ignore = E231,F401") 62 | 63 | argv = ["flake8_nb", "--config", test_config.resolve().as_posix()] 64 | with pytest.raises(SystemExit): 65 | with pytest.warns(InvalidNotebookWarning): 66 | main([*argv, TEST_NOTEBOOK_BASE_PATH]) 67 | captured = capsys.readouterr() 68 | result_output = captured.out 69 | result_list = result_output.replace("\r", "").split("\n") 70 | result_list.remove("") 71 | expected_result_path = os.path.join( 72 | os.path.dirname(__file__), "data", "expected_output_config_test.txt" 73 | ) 74 | with open(expected_result_path) as result_file: 75 | expected_result_list = result_file.readlines() 76 | assert len(expected_result_list) == len(result_list) 77 | for expected_result in expected_result_list: 78 | assert any(result.endswith(expected_result.rstrip("\n")) for result in result_list) 79 | 80 | 81 | @pytest.mark.parametrize("config_file_name", ("setup.cfg", "tox.ini", ".flake8_nb")) 82 | def test_config_discovered( 83 | config_file_name: str, tmp_path: Path, monkeypatch: MonkeyPatch, capsys: CaptureFixture 84 | ): 85 | """Check that config file is discovered.""" 86 | 87 | test_config = tmp_path / config_file_name 88 | test_config.write_text("[flake8_nb]\nextend-ignore = E231,F401") 89 | 90 | shutil.copytree(TEST_NOTEBOOK_BASE_PATH, tmp_path / "notebooks") 91 | 92 | with monkeypatch.context() as m: 93 | m.chdir(tmp_path) 94 | with pytest.raises(SystemExit): 95 | with pytest.warns(InvalidNotebookWarning): 96 | main(["flake8_nb"]) 97 | captured = capsys.readouterr() 98 | result_output = captured.out 99 | result_list = result_output.replace("\r", "").split("\n") 100 | result_list.remove("") 101 | expected_result_path = os.path.join( 102 | os.path.dirname(__file__), "data", "expected_output_config_test.txt" 103 | ) 104 | with open(expected_result_path) as result_file: 105 | expected_result_list = result_file.readlines() 106 | assert len(expected_result_list) == len(result_list) 107 | for expected_result in expected_result_list: 108 | assert any(result.endswith(expected_result.rstrip("\n")) for result in result_list) 109 | 110 | 111 | def test_run_main_all_excluded(capsys): 112 | argv = ["flake8_nb"] 113 | argv += [ 114 | "--exclude", 115 | f"*.tox/*,*.ipynb_checkpoints*,*/docs/*,{TEST_NOTEBOOK_BASE_PATH}", 116 | ] 117 | with pytest.raises(SystemExit): 118 | with pytest.warns(InvalidNotebookWarning): 119 | main([*argv, TEST_NOTEBOOK_BASE_PATH]) 120 | captured = capsys.readouterr() 121 | result_output = captured.out 122 | result_list = result_output.replace("\r", "").split("\n") 123 | result_list.remove("") 124 | assert len(result_list) == 0 125 | 126 | 127 | @pytest.mark.parametrize("keep_intermediate", [True, False]) 128 | @pytest.mark.parametrize("cli_entrypoint", ["flake8_nb", "flake8-nb"]) 129 | @pytest.mark.parametrize( 130 | "notebook_cell_format,expected_result", 131 | [ 132 | ("{nb_path}#In[{exec_count}]", "expected_output_exec_count"), 133 | ("{nb_path}:code_cell#{code_cell_count}", "expected_output_code_cell_count"), 134 | ("{nb_path}:cell#{total_cell_count}", "expected_output_total_cell_count"), 135 | ], 136 | ) 137 | def test_syscall( 138 | cli_entrypoint: str, keep_intermediate: bool, notebook_cell_format: str, expected_result: str 139 | ): 140 | argv = [cli_entrypoint] 141 | if keep_intermediate: 142 | argv.append("--keep-parsed-notebooks") 143 | argv += ["--notebook-cell-format", notebook_cell_format] 144 | argv += ["--exclude", "*.tox/*,*.ipynb_checkpoints*,*/docs/*"] 145 | proc = subprocess.Popen( 146 | [*argv, TEST_NOTEBOOK_BASE_PATH], stdout=subprocess.PIPE, universal_newlines=True 147 | ) 148 | result_list = [str(line) for line in proc.stdout] 149 | expected_result_path = os.path.join( 150 | os.path.dirname(__file__), "data", f"{expected_result}.txt" 151 | ) 152 | with open(expected_result_path) as result_file: 153 | expected_result_list = result_file.readlines() 154 | 155 | print("\n".join(expected_result_list)) 156 | print("#" * 80) 157 | print("\n".join(result_list)) 158 | assert len(expected_result_list) == len(result_list) 159 | 160 | for expected_result in expected_result_list: 161 | assert any(result.endswith(expected_result) for result in result_list) 162 | 163 | 164 | def test_flake8_nb_module_call(): 165 | """Call flake8_nb as python module ``python -m flake8_nb --help``.""" 166 | output = subprocess.run( 167 | [sys.executable, "-m", "flake8_nb", "--help"], capture_output=True, check=True 168 | ) 169 | assert output.returncode == 0 170 | assert output.stdout.decode().startswith("usage: flake8_nb [options] file file ...") 171 | 172 | 173 | @pytest.mark.skipif(FLAKE8_VERSION_TUPLE < (5, 0, 0), reason="Only implemented for flake8>=5.0.0") 174 | def test_flake8_nb_bug_report(): 175 | """Debug information.""" 176 | output = subprocess.run( 177 | [sys.executable, "-m", "flake8_nb", "--bug-report"], capture_output=True, check=True 178 | ) 179 | assert output.returncode == 0 180 | info = json.loads(output.stdout.decode()) 181 | 182 | assert "flake8-version" in info 183 | assert info["flake8-version"] == flake_version 184 | assert info["version"] == __version__ 185 | 186 | assert not any(plugin["plugin"] == "flake8-nb" for plugin in info["plugins"]) 187 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # flake8-nb 2 | 3 | [![PyPi Version](https://img.shields.io/pypi/v/flake8_nb.svg)](https://pypi.org/project/flake8-nb/) 4 | [![Conda Version](https://img.shields.io/conda/vn/conda-forge/flake8-nb.svg)](https://anaconda.org/conda-forge/flake8-nb) 5 | [![Supported Python Versions](https://img.shields.io/pypi/pyversions/flake8_nb.svg)](https://pypi.org/project/flake8-nb/) 6 | [![Pre-commit](https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit&logoColor=white)](https://github.com/pre-commit/pre-commit) 7 | [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) 8 | 9 | [![Actions Status](https://github.com/s-weigand/flake8-nb/workflows/Tests/badge.svg)](https://github.com/s-weigand/flake8-nb/actions) 10 | [![Documentation Status](https://readthedocs.org/projects/flake8-nb/badge/?version=latest)](https://flake8-nb.readthedocs.io/en/latest/?badge=latest) 11 | [![Testing Coverage](https://codecov.io/gh/s-weigand/flake8-nb/branch/main/graph/badge.svg)](https://codecov.io/gh/s-weigand/flake8-nb) 12 | [![Documentation Coverage](https://flake8-nb.readthedocs.io/en/latest/_static/interrogate_badge.svg)](https://github.com/s-weigand/flake8-nb) 13 | 14 | [![Codacy Badge](https://app.codacy.com/project/badge/Grade/d02b436a637243a1b626b74d018c3bbe)](https://www.codacy.com/gh/s-weigand/flake8-nb/dashboard?utm_source=github.com&utm_medium=referral&utm_content=s-weigand/flake8-nb&utm_campaign=Badge_Grade) 15 | [![All Contributors](https://img.shields.io/github/all-contributors/s-weigand/flake8-nb)](#contributors) 16 | [![Code style Python: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) 17 | [![Binder](https://static.mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/s-weigand/flake8-nb.git/main?urlpath=lab%2Ftree%2Ftests%2Fdata%2Fnotebooks) 18 | 19 | [`flake8`](https://github.com/pycqa/flake8) checking for jupyter notebooks. 20 | 21 | This tool is mainly aimed towards writing tutorials/lecture material, where one might also want 22 | to show off bad practices and/or errors, while still keeping the rest of the code clean and 23 | without adding the complexity of tooling to the readers 24 | (see [docs on cell tags](https://flake8-nb.readthedocs.io/en/latest/usage.html#cell-tags)). 25 | 26 | Basically this is a hack on the `flake8`'s `Application` class, 27 | which adds parsing and a cell based formatter for `*.ipynb` files. 28 | 29 | This is **NOT A PLUGIN** but a stand alone CLI tool/[pre-commit](https://pre-commit.com/) hook to be used instead of the `flake8` command/hook. 30 | 31 | ## Features 32 | 33 | - flake8 CLI tests for jupyter notebooks 34 | - Full base functionality of `flake8` and its plugins 35 | - Input cell based error formatting (Execution count/code cell count/total cellcount) 36 | - Report fine tuning with cell-tags (`flake8-noqa-tags` see [usage](https://flake8-nb.readthedocs.io/en/latest/usage.html#cell-tags)) 37 | - [pre-commit](https://pre-commit.com/) hook 38 | 39 | ## Examples 40 | 41 | ## Default reporting 42 | 43 | If you had a notebook with name `example_notebook.ipynb`, where the code cell 44 | which was executed as 34th cell (`In[34]`) had the following code: 45 | 46 | ```python 47 | bad_formatted_dict = {"missing":"space"} 48 | ``` 49 | 50 | running `flake8_nb` would result in the following output. 51 | 52 | ### Execution count 53 | 54 | ```bash 55 | $ flake8_nb example_notebook.ipynb 56 | example_notebook.ipynb#In[34]:1:31: E231 missing whitespace after ':' 57 | ``` 58 | 59 | ## Custom reporting 60 | 61 | If you prefer the reports to show the cell number rather then the execution count you 62 | can use the `--notebook-cell-format` option, given that the cell is the 5th `code` cell 63 | and 10th total cell (taking `raw` and `markdown` cells into account), 64 | you will get the following output. 65 | 66 | ### Code cell count 67 | 68 | ```bash 69 | $ flake8_nb --notebook-cell-format '{nb_path}:code_cell#{code_cell_count}' example_notebook.ipynb 70 | example_notebook.ipynb:code_cell#5:1:31: E231 missing whitespace after ':' 71 | ``` 72 | 73 | ### Total cell count 74 | 75 | ```bash 76 | $ flake8_nb --notebook-cell-format '{nb_path}:cell#{total_cell_count}' example_notebook.ipynb 77 | example_notebook.ipynb:cell#10:1:31: E231 missing whitespace after ':' 78 | ``` 79 | 80 | ## Similar projects 81 | 82 | - [nbQA](https://github.com/nbQA-dev/nbQA): 83 | Run isort, pyupgrade, mypy, pylint, flake8, mdformat, black, blacken-docs, and more on Jupyter Notebooks 84 | 85 | ## Contributors ✨ 86 | 87 | Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)): 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 |
Sebastian Weigand
Sebastian Weigand

💻 🤔 🚧 📆 🚇 ⚠️ 📖
Jt Miclat
Jt Miclat

🐛
Philipp Eisenhauer
Philipp Eisenhauer

🐛
Shoma Okamoto
Shoma Okamoto

⚠️
Marco Gorelli
Marco Gorelli

🔧 📖
Tony Hirst
Tony Hirst

🤔
Dobatymo
Dobatymo

🐛
Alp Arıbal
Alp Arıbal

🐛
1kastner
1kastner

🐛
Dominique Sydow
Dominique Sydow

🐛
Liam Keegan
Liam Keegan

🐛 💻
111 | 112 | 113 | 114 | 115 | 116 | 117 | This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome! 118 | -------------------------------------------------------------------------------- /docs/_static/interrogate_badge.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | interrogate 13 | interrogate 14 | 100.0% 15 | 100.0% 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /tests/data/notebooks/notebook_with_flake8_tags.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# This notebook demonstrates `flake8_nb` reporting with `flake8-tags`" 8 | ] 9 | }, 10 | { 11 | "cell_type": "markdown", 12 | "metadata": {}, 13 | "source": [ 14 | "## Testing Celltags" 15 | ] 16 | }, 17 | { 18 | "cell_type": "markdown", 19 | "metadata": { 20 | "tags": [ 21 | "flake8-noqa-cell-F401" 22 | ] 23 | }, 24 | "source": [ 25 | "The next cell should not report `F401 'not_a_package' imported but unused`, due to the cell tag `flake8-noqa-cell-F401`.\n", 26 | "\n", 27 | "But it will report `E231 missing whitespace after ':'`, for line 2." 28 | ] 29 | }, 30 | { 31 | "cell_type": "code", 32 | "execution_count": 1, 33 | "metadata": { 34 | "tags": [ 35 | "raises-exception", 36 | "flake8-noqa-cell-F401" 37 | ] 38 | }, 39 | "outputs": [ 40 | { 41 | "ename": "ModuleNotFoundError", 42 | "evalue": "No module named 'not_a_package'", 43 | "output_type": "error", 44 | "traceback": [ 45 | "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m", 46 | "\u001b[1;31mModuleNotFoundError\u001b[0m Traceback (most recent call last)", 47 | "\u001b[1;32m\u001b[0m in \u001b[0;36m\u001b[1;34m\u001b[0m\n\u001b[1;32m----> 1\u001b[1;33m \u001b[1;32mimport\u001b[0m \u001b[0mnot_a_package\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m 2\u001b[0m \u001b[1;33m{\u001b[0m\u001b[1;34m\"1\"\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;36m1\u001b[0m\u001b[1;33m}\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n", 48 | "\u001b[1;31mModuleNotFoundError\u001b[0m: No module named 'not_a_package'" 49 | ] 50 | } 51 | ], 52 | "source": [ 53 | "import not_a_package\n", 54 | "{\"1\":1}" 55 | ] 56 | }, 57 | { 58 | "cell_type": "markdown", 59 | "metadata": {}, 60 | "source": [ 61 | "The next cell should not report `E231 missing whitespace after ':'` for line 1, due to the line tag `flake8-noqa-line-1-E231`.\n", 62 | "\n", 63 | "But it will report `E231 missing whitespace after ':'`, for line 2." 64 | ] 65 | }, 66 | { 67 | "cell_type": "code", 68 | "execution_count": 2, 69 | "metadata": { 70 | "tags": [ 71 | "flake8-noqa-line-1-E231" 72 | ] 73 | }, 74 | "outputs": [ 75 | { 76 | "data": { 77 | "text/plain": [ 78 | "{'2': 2}" 79 | ] 80 | }, 81 | "execution_count": 2, 82 | "metadata": {}, 83 | "output_type": "execute_result" 84 | } 85 | ], 86 | "source": [ 87 | "{\"2\":1}\n", 88 | "{\"2\":2}" 89 | ] 90 | }, 91 | { 92 | "cell_type": "markdown", 93 | "metadata": {}, 94 | "source": [ 95 | "The next cell should not report `E231 missing whitespace after ':'`, due to the line tag `flake8-noqa-cell-E231`." 96 | ] 97 | }, 98 | { 99 | "cell_type": "code", 100 | "execution_count": 3, 101 | "metadata": { 102 | "tags": [ 103 | "flake8-noqa-cell-E231" 104 | ] 105 | }, 106 | "outputs": [ 107 | { 108 | "data": { 109 | "text/plain": [ 110 | "{'3': 2}" 111 | ] 112 | }, 113 | "execution_count": 3, 114 | "metadata": {}, 115 | "output_type": "execute_result" 116 | } 117 | ], 118 | "source": [ 119 | "{\"3\":1}\n", 120 | "{\"3\":2}" 121 | ] 122 | }, 123 | { 124 | "cell_type": "markdown", 125 | "metadata": {}, 126 | "source": [ 127 | "## Testing inline Celltags" 128 | ] 129 | }, 130 | { 131 | "cell_type": "markdown", 132 | "metadata": {}, 133 | "source": [ 134 | "The next cell should not report `F401 'not_a_package' imported but unused`, `F811 redefinition of unused 'not_a_package'` and `E402 module level import not at top of file`, due to the inline cell tag `flake8-noqa-cell-E402-F401-F811`.\n", 135 | "\n", 136 | "But it will report `E231 missing whitespace after ':'`, for line 3." 137 | ] 138 | }, 139 | { 140 | "cell_type": "code", 141 | "execution_count": 4, 142 | "metadata": { 143 | "tags": [ 144 | "raises-exception" 145 | ] 146 | }, 147 | "outputs": [ 148 | { 149 | "ename": "ModuleNotFoundError", 150 | "evalue": "No module named 'not_a_package'", 151 | "output_type": "error", 152 | "traceback": [ 153 | "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m", 154 | "\u001b[1;31mModuleNotFoundError\u001b[0m Traceback (most recent call last)", 155 | "\u001b[1;32m\u001b[0m in \u001b[0;36m\u001b[1;34m\u001b[0m\n\u001b[0;32m 1\u001b[0m \u001b[1;31m# flake8-noqa-cell-E402-F401-F811\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m----> 2\u001b[1;33m \u001b[1;32mimport\u001b[0m \u001b[0mnot_a_package\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m 3\u001b[0m \u001b[1;33m{\u001b[0m\u001b[1;34m\"4\"\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;36m1\u001b[0m\u001b[1;33m}\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n", 156 | "\u001b[1;31mModuleNotFoundError\u001b[0m: No module named 'not_a_package'" 157 | ] 158 | } 159 | ], 160 | "source": [ 161 | "# flake8-noqa-cell-E402-F401-F811\n", 162 | "import not_a_package\n", 163 | "{\"4\":1}" 164 | ] 165 | }, 166 | { 167 | "cell_type": "markdown", 168 | "metadata": {}, 169 | "source": [ 170 | "The next cell should not report `E231 missing whitespace after ':'` for line 2, due to the inline line tag `flake8-noqa-line-2-E231`.\n", 171 | "\n", 172 | "But it will report `E231 missing whitespace after ':'`, for line 3." 173 | ] 174 | }, 175 | { 176 | "cell_type": "code", 177 | "execution_count": 5, 178 | "metadata": {}, 179 | "outputs": [ 180 | { 181 | "data": { 182 | "text/plain": [ 183 | "{'5': 2}" 184 | ] 185 | }, 186 | "execution_count": 5, 187 | "metadata": {}, 188 | "output_type": "execute_result" 189 | } 190 | ], 191 | "source": [ 192 | "# flake8-noqa-line-2-E231\n", 193 | "{\"5\":1}\n", 194 | "{\"5\":2}" 195 | ] 196 | }, 197 | { 198 | "cell_type": "markdown", 199 | "metadata": {}, 200 | "source": [ 201 | "The next cell should not report `E231 missing whitespace after ':'`, due to the inline cell tag `flake8-noqa-cell-E231`." 202 | ] 203 | }, 204 | { 205 | "cell_type": "code", 206 | "execution_count": 6, 207 | "metadata": {}, 208 | "outputs": [ 209 | { 210 | "data": { 211 | "text/plain": [ 212 | "{'6': 2}" 213 | ] 214 | }, 215 | "execution_count": 6, 216 | "metadata": {}, 217 | "output_type": "execute_result" 218 | } 219 | ], 220 | "source": [ 221 | "# flake8-noqa-cell-E231\n", 222 | "{\"6\":1}\n", 223 | "{\"6\":2}" 224 | ] 225 | }, 226 | { 227 | "cell_type": "markdown", 228 | "metadata": {}, 229 | "source": [ 230 | "## Testing normal flake8 noqa comments" 231 | ] 232 | }, 233 | { 234 | "cell_type": "markdown", 235 | "metadata": {}, 236 | "source": [ 237 | "The next cell should not report `E231 missing whitespace after ':'` for line 1, due to the flake8 noqa comment `#noqa: E231`.\n", 238 | "\n", 239 | "But it will report `E231 missing whitespace after ':'`, for line 2." 240 | ] 241 | }, 242 | { 243 | "cell_type": "code", 244 | "execution_count": 7, 245 | "metadata": {}, 246 | "outputs": [ 247 | { 248 | "data": { 249 | "text/plain": [ 250 | "{'5': 2}" 251 | ] 252 | }, 253 | "execution_count": 7, 254 | "metadata": {}, 255 | "output_type": "execute_result" 256 | } 257 | ], 258 | "source": [ 259 | "{\"5\":1} # noqa: E231\n", 260 | "{\"5\":2}" 261 | ] 262 | }, 263 | { 264 | "cell_type": "markdown", 265 | "metadata": {}, 266 | "source": [ 267 | "## Report using execution count" 268 | ] 269 | }, 270 | { 271 | "cell_type": "code", 272 | "execution_count": 8, 273 | "metadata": { 274 | "tags": [] 275 | }, 276 | "outputs": [ 277 | { 278 | "name": "stdout", 279 | "output_type": "stream", 280 | "text": [ 281 | "notebook_with_flake8_tags.ipynb#In[1]:2:5: E231 missing whitespace after ':'\n", 282 | "notebook_with_flake8_tags.ipynb#In[2]:2:5: E231 missing whitespace after ':'\n", 283 | "notebook_with_flake8_tags.ipynb#In[4]:3:5: E231 missing whitespace after ':'\n", 284 | "notebook_with_flake8_tags.ipynb#In[5]:3:5: E231 missing whitespace after ':'\n", 285 | "notebook_with_flake8_tags.ipynb#In[7]:2:5: E231 missing whitespace after ':'\n" 286 | ] 287 | } 288 | ], 289 | "source": [ 290 | "!flake8_nb notebook_with_flake8_tags.ipynb" 291 | ] 292 | }, 293 | { 294 | "cell_type": "markdown", 295 | "metadata": {}, 296 | "source": [ 297 | "## Report using code cell count" 298 | ] 299 | }, 300 | { 301 | "cell_type": "code", 302 | "execution_count": 9, 303 | "metadata": { 304 | "tags": [ 305 | "flake8-noqa-cell-E501" 306 | ] 307 | }, 308 | "outputs": [ 309 | { 310 | "name": "stdout", 311 | "output_type": "stream", 312 | "text": [ 313 | "'notebook_with_flake8_tags.ipynb:code_cell#1':2:5: E231 missing whitespace after ':'\n", 314 | "'notebook_with_flake8_tags.ipynb:code_cell#2':2:5: E231 missing whitespace after ':'\n", 315 | "'notebook_with_flake8_tags.ipynb:code_cell#4':3:5: E231 missing whitespace after ':'\n", 316 | "'notebook_with_flake8_tags.ipynb:code_cell#5':3:5: E231 missing whitespace after ':'\n", 317 | "'notebook_with_flake8_tags.ipynb:code_cell#7':2:5: E231 missing whitespace after ':'\n" 318 | ] 319 | } 320 | ], 321 | "source": [ 322 | "!flake8_nb --notebook-cell-format '{nb_path}:code_cell#{code_cell_count}' notebook_with_flake8_tags.ipynb" 323 | ] 324 | }, 325 | { 326 | "cell_type": "markdown", 327 | "metadata": {}, 328 | "source": [ 329 | "## Report using total cell count" 330 | ] 331 | }, 332 | { 333 | "cell_type": "code", 334 | "execution_count": 10, 335 | "metadata": { 336 | "tags": [ 337 | "flake8-noqa-cell-E501" 338 | ] 339 | }, 340 | "outputs": [ 341 | { 342 | "name": "stdout", 343 | "output_type": "stream", 344 | "text": [ 345 | "'notebook_with_flake8_tags.ipynb:cell#4':2:5: E231 missing whitespace after ':'\n", 346 | "'notebook_with_flake8_tags.ipynb:cell#6':2:5: E231 missing whitespace after ':'\n", 347 | "'notebook_with_flake8_tags.ipynb:cell#11':3:5: E231 missing whitespace after ':'\n", 348 | "'notebook_with_flake8_tags.ipynb:cell#13':3:5: E231 missing whitespace after ':'\n", 349 | "'notebook_with_flake8_tags.ipynb:cell#18':2:5: E231 missing whitespace after ':'\n" 350 | ] 351 | } 352 | ], 353 | "source": [ 354 | "!flake8_nb --notebook-cell-format '{nb_path}:cell#{total_cell_count}' notebook_with_flake8_tags.ipynb" 355 | ] 356 | } 357 | ], 358 | "metadata": { 359 | "kernelspec": { 360 | "display_name": "Python 3", 361 | "language": "python", 362 | "name": "python3" 363 | }, 364 | "language_info": { 365 | "codemirror_mode": { 366 | "name": "ipython", 367 | "version": 3 368 | }, 369 | "file_extension": ".py", 370 | "mimetype": "text/x-python", 371 | "name": "python", 372 | "nbconvert_exporter": "python", 373 | "pygments_lexer": "ipython3", 374 | "version": "3.8.8" 375 | } 376 | }, 377 | "nbformat": 4, 378 | "nbformat_minor": 4 379 | } 380 | -------------------------------------------------------------------------------- /flake8_nb/parsers/cell_parsers.py: -------------------------------------------------------------------------------- 1 | """Module containing parsers for notebook cells. 2 | 3 | This also includes parsers for the cell and inline tags. 4 | It heavily utilizes the mutability of lists. 5 | """ 6 | 7 | from __future__ import annotations 8 | 9 | import re 10 | import warnings 11 | from typing import Dict 12 | from typing import List 13 | 14 | from flake8_nb.parsers import CellId 15 | from flake8_nb.parsers import NotebookCell 16 | 17 | FLAKE8_TAG_PATTERN = re.compile( 18 | r"^flake8-noqa-(cell-(?P(\w+\d+-?)+)" 19 | r"|line-(?P\d+)-(?P(\w+\d+-?)+))$" 20 | r"|^(?Pflake8-noqa-cell)$" 21 | r"|^flake8-noqa-line-(?P\d+)$" 22 | ) 23 | 24 | FLAKE8_INLINE_TAG_PATTERN = re.compile( 25 | r"^.*?(#(?P(\s*flake8-noqa-(cell(-\w+\d+)*|line-\d+(-\w+\d+)*))+))\s*$", 26 | re.DOTALL, 27 | ) 28 | 29 | FLAKE8_NOQA_INLINE_PATTERN = re.compile( 30 | r"^.+?\s*[#]\s*noqa\s*[:]" 31 | r"(?P(\s*\w+\d+[,]?\s*)+)$" 32 | r"|^.+?\s*(?P[#]\s*noqa\s*[:]?\s*)$" 33 | ) 34 | 35 | FLAKE8_NOQA_INLINE_REPLACE_PATTERN = re.compile( 36 | r"^(?P.+?)\s*(?P[#]\s*noqa\s*[:]?.*)$" 37 | ) 38 | 39 | RulesDict = Dict[str, List[str]] 40 | 41 | 42 | class InvalidFlake8TagWarning(UserWarning): 43 | """Warning thrown when a tag is badly formatted. 44 | 45 | When a cell tag starts with 'flake8-noqa-' but doesn't 46 | match the correct pattern needed for cell tags. 47 | This is used to show users that they have a typo in their tags. 48 | """ 49 | 50 | def __init__(self, flake8_tag: str): 51 | """Create InvalidFlake8TagWarning. 52 | 53 | Parameters 54 | ---------- 55 | flake8_tag : str 56 | Used improperly formatted flake8-nb tag 57 | """ 58 | super().__init__( 59 | "flake8-noqa-line/cell-tags should be of form " 60 | "'flake8-noqa-cell--'|'flake8-noqa-cell'/" 61 | "'flake8-noqa-line---'|'flake8-noqa-line-', " 62 | f"you used: '{flake8_tag}'" 63 | ) 64 | 65 | 66 | def extract_flake8_tags(notebook_cell: NotebookCell) -> list[str]: 67 | """Extract all tag that start with 'flake8-noqa-' from a cell. 68 | 69 | Parameters 70 | ---------- 71 | notebook_cell : NotebookCell 72 | Dict representation of a notebook cell as parsed from JSON. 73 | 74 | Returns 75 | ------- 76 | list[str] 77 | List of all tags in the given cell, which started with 'flake8-noqa-'. 78 | """ 79 | return [ 80 | tag for tag in notebook_cell["metadata"].get("tags", []) if tag.startswith("flake8-noqa-") 81 | ] 82 | 83 | 84 | def extract_flake8_inline_tags(notebook_cell: NotebookCell) -> list[str]: 85 | """Extract flake8-tags which were used as comment in a cell. 86 | 87 | Parameters 88 | ---------- 89 | notebook_cell : NotebookCell 90 | Dict representation of a notebook cell as parsed from JSON. 91 | 92 | Returns 93 | ------- 94 | list[str] 95 | List of all inline tags in the given cell, 96 | which matched ``FLAKE8_INLINE_TAG_PATTERN``. 97 | """ 98 | flake8_inline_tags = [] 99 | for source_line in notebook_cell["source"]: 100 | match = re.match(FLAKE8_INLINE_TAG_PATTERN, source_line) 101 | if match and match["flake8_inline_tags"]: 102 | for tag in match["flake8_inline_tags"].split(" "): 103 | tag = tag.strip() 104 | if tag: 105 | flake8_inline_tags.append(tag) 106 | return flake8_inline_tags 107 | 108 | 109 | def extract_inline_flake8_noqa(source_line: str) -> list[str]: 110 | """Extract flake8 noqa rules from normal flake8 comments . 111 | 112 | Parameters 113 | ---------- 114 | source_line : str 115 | Single line of sourcecode from a cell. 116 | 117 | Returns 118 | ------- 119 | list[str] 120 | List of flake8 rules. 121 | """ 122 | match = re.match(FLAKE8_NOQA_INLINE_PATTERN, source_line) 123 | if match: 124 | flake8_noqa_rules_str = match["flake8_noqa_rules"] 125 | if flake8_noqa_rules_str: 126 | flake8_noqa_rules = flake8_noqa_rules_str.split(",") 127 | return [line.strip() for line in flake8_noqa_rules] 128 | elif match["has_flake8_noqa_all"]: # pragma: no branch 129 | return ["noqa"] 130 | return [] 131 | 132 | 133 | def flake8_tag_to_rules_dict(flake8_tag: str) -> RulesDict: 134 | """Parse a flake8 tag to a ``rules_dict``. 135 | 136 | ``rules_dict`` contains lists of rules, depending on if the 137 | tag is a cell or a line tag. 138 | 139 | Parameters 140 | ---------- 141 | flake8_tag : str 142 | String of a flake8-tag. 143 | 144 | Returns 145 | ------- 146 | RulesDict 147 | Dict with cell and line rules. Line rules have the line number 148 | as key and cell rules have 'cell as key'. 149 | 150 | See Also 151 | -------- 152 | get_flake8_rules_dict 153 | """ 154 | match = re.match(FLAKE8_TAG_PATTERN, flake8_tag) 155 | if match: 156 | if match["cell_rules"]: 157 | cell_rules_str = match["cell_rules"] 158 | cell_rules = cell_rules_str.split("-") 159 | return {"cell": cell_rules} 160 | elif match["ignore_cell"]: 161 | return {"cell": ["noqa"]} 162 | elif match["line_nr"] and match["line_rules"]: 163 | line_nr = str(match["line_nr"]) 164 | line_rules_str = match["line_rules"] 165 | line_rules = line_rules_str.split("-") 166 | return {line_nr: line_rules} 167 | elif match["ignore_line_nr"]: # pragma: no branch 168 | line_nr = str(match["ignore_line_nr"]) 169 | return {line_nr: ["noqa"]} 170 | warnings.warn(InvalidFlake8TagWarning(flake8_tag)) 171 | return {} 172 | 173 | 174 | def update_rules_dict(total_rules_dict: RulesDict, new_rules_dict: RulesDict) -> None: 175 | """Update the rules dict ``total_rules_dict`` with ``new_rules_dict``. 176 | 177 | If any entry of a key is 'noqa' (ignore all), the rules will be 178 | set to be only 'noqa'. 179 | 180 | Parameters 181 | ---------- 182 | total_rules_dict : RulesDict 183 | ``rules_dict`` which should be updated. 184 | new_rules_dict : RulesDict 185 | ``rules_dict`` which should be used to update ``total_rules_dict``. 186 | 187 | See Also 188 | -------- 189 | flake8_tag_to_rules_dict, get_flake8_rules_dict 190 | """ 191 | for key, new_rules in new_rules_dict.items(): 192 | old_rules = total_rules_dict.get(key, []) 193 | if "noqa" in old_rules + new_rules: 194 | total_rules_dict[key] = ["noqa"] 195 | else: 196 | total_rules_dict[key] = list(set(old_rules + new_rules)) 197 | 198 | 199 | def get_flake8_rules_dict(notebook_cell: NotebookCell) -> RulesDict: 200 | """Parse all flake8 tags of a cell to a ``rules_dict``. 201 | 202 | ``rules_dict`` contains lists of rules, depending on if the 203 | tag is a cell or a line tag. 204 | 205 | Parameters 206 | ---------- 207 | notebook_cell : NotebookCell 208 | Dict representation of a notebook cell as parsed from JSON. 209 | 210 | Returns 211 | ------- 212 | RulesDict 213 | Dict with all cell and line rules. Line rules have the line number 214 | as key and cell rules have 'cell as key'. 215 | 216 | See Also 217 | -------- 218 | flake8_tag_to_rules_dict, update_rules_dict 219 | """ 220 | flake8_tags = extract_flake8_tags(notebook_cell) 221 | flake8_inline_tags = extract_flake8_inline_tags(notebook_cell) 222 | total_rules_dict: RulesDict = {} 223 | for flake8_tag in set(flake8_tags + flake8_inline_tags): 224 | new_rules_dict = flake8_tag_to_rules_dict(flake8_tag) 225 | update_rules_dict(total_rules_dict, new_rules_dict) 226 | return total_rules_dict 227 | 228 | 229 | def generate_rules_list(source_index: int, rules_dict: RulesDict) -> list[str]: 230 | """Generate a List of rules from ``rules_dict``. 231 | 232 | This list should be applied to the line at ``source_index``. 233 | 234 | Parameters 235 | ---------- 236 | source_index : int 237 | Index of the source code line. 238 | rules_dict : RulesDict 239 | Dict containing lists of rules, depending on if the tag is a 240 | cell or a line tag. 241 | 242 | Returns 243 | ------- 244 | list[str] 245 | List of rules which should be applied to the line at ``source_index``. 246 | 247 | See Also 248 | -------- 249 | flake8_tag_to_rules_dict, get_flake8_rules_dict 250 | """ 251 | line_rules = rules_dict.get(str(source_index + 1), []) 252 | cell_rules = rules_dict.get("cell", []) 253 | return line_rules + cell_rules 254 | 255 | 256 | def update_inline_flake8_noqa(source_line: str, rules_list: list[str]) -> str: 257 | """Update ``source_line`` with flake8 noqa comments. 258 | 259 | This is done extraction flake8-tags as well as inline flake8 260 | comments. 261 | 262 | Parameters 263 | ---------- 264 | source_line : str 265 | Single line of sourcecode from a cell. 266 | rules_list : list[str] 267 | List of rules which should be applied to ``source_line``. 268 | 269 | Returns 270 | ------- 271 | str 272 | ``source_line`` with flake8 noqa comments. 273 | 274 | See Also 275 | -------- 276 | generate_rules_list 277 | """ 278 | inline_flake8_noqa = extract_inline_flake8_noqa(source_line) 279 | source_line = source_line.rstrip("\n") 280 | if inline_flake8_noqa: 281 | rules_list = list(set(inline_flake8_noqa + rules_list)) 282 | source_line = re.sub(FLAKE8_NOQA_INLINE_REPLACE_PATTERN, r"\g", source_line) 283 | rules_list = sorted(rules_list) 284 | if not rules_list: 285 | return f"{source_line}\n" 286 | noqa_str = "" if "noqa" in rules_list else ", ".join(rules_list) 287 | return f"{source_line} # noqa: {noqa_str}\n" 288 | 289 | 290 | def notebook_cell_to_intermediate_dict( 291 | notebook_cell: NotebookCell, 292 | ) -> dict[str, CellId | str | int]: 293 | r"""Parse ``notebook_cell`` to a dict. 294 | 295 | That dict can later be written to a intermediate_py_file. 296 | 297 | Parameters 298 | ---------- 299 | notebook_cell : NotebookCell 300 | Dict representation of a notebook cell as parsed from JSON. 301 | 302 | Returns 303 | ------- 304 | dict[str, CellId | str | int] 305 | Dict which has the keys 'code', 'input_name' and 'code'. 306 | ``code``,``input_name`` is a str of the code cells ``In[\d\*]`` name and ``lines_of_code`` 307 | is the number of lines of corresponding parsed parsed notebook cell. 308 | 309 | See Also 310 | -------- 311 | update_inline_flake8_noqa, flake8_nb.parsers.notebook_parsers.create_intermediate_py_file 312 | """ 313 | updated_source_lines = [] 314 | input_nr = notebook_cell["execution_count"] 315 | total_cell_nr = notebook_cell["total_cell_nr"] 316 | code_cell_nr = notebook_cell["code_cell_nr"] 317 | rules_dict = get_flake8_rules_dict(notebook_cell) 318 | for line_index, source_line in enumerate(notebook_cell["source"]): 319 | rules_list = generate_rules_list(line_index, rules_dict) 320 | updated_source_line = update_inline_flake8_noqa(source_line, rules_list) 321 | updated_source_lines.append(updated_source_line) 322 | if input_nr is None: 323 | input_nr = " " 324 | return { 325 | "code": ( 326 | f"# INTERMEDIATE_CELL_SEPARATOR ({input_nr},{code_cell_nr},{total_cell_nr})\n\n\n" 327 | f"{''.join(updated_source_lines)}\n\n" 328 | ), 329 | "input_id": CellId(str(input_nr), code_cell_nr, total_cell_nr), 330 | "lines_of_code": len(updated_source_lines) + 5, 331 | } 332 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [2019] [Sebastian Weigand] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /tests/parsers/test_notebook_parsers.py: -------------------------------------------------------------------------------- 1 | import os 2 | import warnings 3 | from typing import Dict 4 | from typing import List 5 | from typing import Tuple 6 | from typing import Union 7 | 8 | import pytest 9 | 10 | from flake8_nb.parsers import CellId 11 | from flake8_nb.parsers.notebook_parsers import InputLineMapping 12 | from flake8_nb.parsers.notebook_parsers import InvalidNotebookWarning 13 | from flake8_nb.parsers.notebook_parsers import NotebookParser 14 | from flake8_nb.parsers.notebook_parsers import create_intermediate_py_file 15 | from flake8_nb.parsers.notebook_parsers import create_temp_path 16 | from flake8_nb.parsers.notebook_parsers import get_notebook_code_cells 17 | from flake8_nb.parsers.notebook_parsers import get_rel_paths 18 | from flake8_nb.parsers.notebook_parsers import ignore_cell 19 | from flake8_nb.parsers.notebook_parsers import is_parent_dir 20 | from flake8_nb.parsers.notebook_parsers import map_intermediate_to_input 21 | from flake8_nb.parsers.notebook_parsers import read_notebook_to_cells 22 | from tests import TEST_NOTEBOOK_BASE_PATH 23 | 24 | INTERMEDIATE_PY_FILE_BASE_PATH = os.path.abspath( 25 | os.path.join(os.path.dirname(__file__), "..", "data", "intermediate_py_files") 26 | ) 27 | 28 | 29 | def get_expected_intermediate_file_results(result_name: str, base_path: str) -> Tuple[str, str]: 30 | expected_result_path = os.path.join(base_path, "tests", "data", "notebooks", result_name) 31 | expected_result_file_path = os.path.join(INTERMEDIATE_PY_FILE_BASE_PATH, result_name) 32 | if result_name.startswith("not_a_notebook"): 33 | expected_result_str = "" 34 | else: 35 | with open(expected_result_file_path) as result_file: 36 | expected_result_str = result_file.read() 37 | return expected_result_path, expected_result_str 38 | 39 | 40 | @pytest.mark.parametrize( 41 | "notebook_name,expected_input_line_mapping", 42 | [ 43 | ("not_a_notebook", {"input_ids": [], "code_lines": []}), 44 | ( 45 | "notebook_with_flake8_tags", 46 | { 47 | "input_ids": [ 48 | CellId("1", 1, 4), 49 | CellId("2", 2, 6), 50 | CellId("3", 3, 8), 51 | CellId("4", 4, 11), 52 | CellId("5", 5, 13), 53 | CellId("6", 6, 15), 54 | CellId("7", 7, 18), 55 | CellId("8", 8, 20), 56 | CellId("9", 9, 22), 57 | CellId("10", 10, 24), 58 | ], 59 | "code_lines": [4, 11, 18, 25, 33, 41, 49, 56, 62, 68], 60 | }, 61 | ), 62 | ( 63 | "notebook_with_out_ipython_magic", 64 | {"input_ids": [CellId("1", 1, 3)], "code_lines": [1]}, 65 | ), 66 | ( 67 | "cell_with_source_string", 68 | {"input_ids": [("1", 1, 1)], "code_lines": [1]}, 69 | ), 70 | ( 71 | "notebook_with_out_flake8_tags", 72 | { 73 | "input_ids": [ 74 | CellId("1", 1, 3), 75 | CellId("2", 2, 5), 76 | CellId("3", 3, 7), 77 | CellId("4", 4, 9), 78 | CellId("5", 6, 13), 79 | CellId("6", 7, 15), 80 | CellId("7", 8, 17), 81 | CellId("8", 9, 19), 82 | ], 83 | "code_lines": [4, 10, 16, 23, 31, 37, 43, 49], 84 | }, 85 | ), 86 | ], 87 | ) 88 | def test_create_intermediate_py_file( 89 | tmpdir, notebook_name: str, expected_input_line_mapping: Dict[str, List[Union[str, int]]] 90 | ): 91 | notebook_path = os.path.join(TEST_NOTEBOOK_BASE_PATH, f"{notebook_name}.ipynb") 92 | 93 | tmp_base_path = str(tmpdir) 94 | expected_result_path, expected_result_str = get_expected_intermediate_file_results( 95 | f"{notebook_name}.ipynb_parsed", tmp_base_path 96 | ) 97 | if notebook_name.startswith("not_a_notebook"): 98 | with pytest.warns(InvalidNotebookWarning): 99 | intermediate_file_path, input_line_mapping = create_intermediate_py_file( 100 | notebook_path, tmp_base_path 101 | ) 102 | assert intermediate_file_path == "" 103 | assert input_line_mapping == expected_input_line_mapping 104 | else: 105 | intermediate_file_path, input_line_mapping = create_intermediate_py_file( 106 | notebook_path, tmp_base_path 107 | ) 108 | assert intermediate_file_path == expected_result_path 109 | assert input_line_mapping == expected_input_line_mapping 110 | with open(intermediate_file_path) as result_file: 111 | assert result_file.read() == expected_result_str 112 | 113 | 114 | @pytest.mark.parametrize( 115 | "notebook_path,rel_result_path", 116 | [ 117 | (os.path.join(os.curdir, "file_name.ipynb"), ["file_name.ipynb_parsed"]), 118 | (os.path.join(os.curdir, "../file_name.ipynb"), ["file_name.ipynb_parsed"]), 119 | ( 120 | os.path.join(os.curdir, "sub_dir", "file_name.ipynb"), 121 | ["sub_dir", "file_name.ipynb_parsed"], 122 | ), 123 | ( 124 | os.path.join(os.curdir, "sub_dir", "sub_sub_dir", "file_name.ipynb"), 125 | ["sub_dir", "sub_sub_dir", "file_name.ipynb_parsed"], 126 | ), 127 | ], 128 | ) 129 | def test_create_temp_path(tmpdir, notebook_path: str, rel_result_path: List[str]): 130 | expected_result_path = os.path.join(str(tmpdir), *rel_result_path) 131 | result_path = create_temp_path(notebook_path, str(tmpdir)) 132 | assert result_path == os.path.abspath(expected_result_path) 133 | assert os.path.isdir(os.path.dirname(result_path)) 134 | 135 | 136 | @pytest.mark.parametrize( 137 | "notebook_name,number_of_cells,uses_get_ipython_result", 138 | [ 139 | ("not_a_notebook.ipynb", 0, False), 140 | ("cell_with_source_string.ipynb", 1, False), 141 | ("notebook_with_flake8_tags.ipynb", 10, True), 142 | ("notebook_with_out_flake8_tags.ipynb", 8, True), 143 | ("notebook_with_out_ipython_magic.ipynb", 1, False), 144 | ], 145 | ) 146 | def test_get_notebook_code_cells( 147 | notebook_name: str, number_of_cells: int, uses_get_ipython_result: bool 148 | ): 149 | notebook_path = os.path.join(TEST_NOTEBOOK_BASE_PATH, notebook_name) 150 | if notebook_name.startswith("not_a_notebook"): 151 | with pytest.warns(InvalidNotebookWarning): 152 | uses_get_ipython, notebook_cells = get_notebook_code_cells(notebook_path) 153 | assert uses_get_ipython == uses_get_ipython_result 154 | assert len(notebook_cells) == number_of_cells 155 | else: 156 | uses_get_ipython, notebook_cells = get_notebook_code_cells(notebook_path) 157 | assert uses_get_ipython == uses_get_ipython_result 158 | assert len(notebook_cells) == number_of_cells 159 | 160 | 161 | @pytest.mark.parametrize( 162 | "file_paths,base_path,expected_result", 163 | [ 164 | ( 165 | [os.curdir, os.path.join(os.curdir, "file.foo")], 166 | os.curdir, 167 | [".", "file.foo"], 168 | ), 169 | ( 170 | [os.path.join(os.curdir, "..", "file.foo")], 171 | os.curdir, 172 | [f"..{os.sep}file.foo"], 173 | ), 174 | ], 175 | ) 176 | def test_get_rel_paths(file_paths: List[str], base_path: str, expected_result: List[str]): 177 | assert get_rel_paths(file_paths, base_path) == expected_result 178 | 179 | 180 | @pytest.mark.parametrize( 181 | "notebook_cell,expected_result", 182 | [ 183 | ({"source": ["print('foo')"], "cell_type": "code"}, False), 184 | ({"source": ["## print('foo')"], "cell_type": "markdown"}, True), 185 | ({"source": [], "cell_type": "code"}, True), 186 | ], 187 | ) 188 | def test_ignore_cell(notebook_cell: Dict, expected_result: bool): 189 | assert ignore_cell(notebook_cell) == expected_result 190 | 191 | 192 | @pytest.mark.parametrize( 193 | "parent_dir,path,expected_result", 194 | [ 195 | (os.curdir, os.curdir, True), 196 | (os.curdir, os.path.join(os.curdir, "file.foo"), True), 197 | (os.curdir, os.path.join(os.curdir, "subdir", "file.foo"), True), 198 | (os.curdir, os.path.join(os.curdir, "..", "file.foo"), False), 199 | ], 200 | ) 201 | def test_is_parent_dir(parent_dir: str, path: str, expected_result): 202 | assert is_parent_dir(parent_dir, path) == expected_result 203 | 204 | 205 | @pytest.mark.parametrize( 206 | "notebook_name,number_of_cells", 207 | [ 208 | ("not_a_notebook.ipynb", 0), 209 | ("notebook_with_flake8_tags.ipynb", 24), 210 | ("notebook_with_out_flake8_tags.ipynb", 19), 211 | ("notebook_with_out_ipython_magic.ipynb", 3), 212 | ], 213 | ) 214 | def test_read_notebook_to_cells(notebook_name: str, number_of_cells: int): 215 | notebook_path = os.path.join(TEST_NOTEBOOK_BASE_PATH, notebook_name) 216 | if notebook_name.startswith("not_a_notebook"): 217 | with pytest.warns(InvalidNotebookWarning): 218 | assert len(read_notebook_to_cells(notebook_path)) == number_of_cells 219 | else: 220 | assert len(read_notebook_to_cells(notebook_path)) == number_of_cells 221 | 222 | 223 | def test_InvalidNotebookWarning(): 224 | with pytest.warns( 225 | InvalidNotebookWarning, 226 | match=( 227 | "Error parsing notebook at path 'dummy_path'. " "Make sure this is a valid notebook." 228 | ), 229 | ): 230 | warnings.warn(InvalidNotebookWarning("dummy_path")) 231 | 232 | 233 | @pytest.mark.parametrize( 234 | "line_number,expected_result", 235 | [(15, (("2", 2, 2), 2)), (30, (("4", 4, 5), 3)), (52, (("7", 9, 15), 1))], 236 | ) 237 | def test_map_intermediate_to_input_line(line_number: int, expected_result: Tuple[str, int]): 238 | input_line_mapping: InputLineMapping = { 239 | "input_ids": [ 240 | CellId("1", 1, 1), 241 | CellId("2", 2, 2), 242 | CellId("3", 3, 3), 243 | CellId("4", 4, 5), 244 | CellId("5", 6, 8), 245 | CellId("6", 7, 10), 246 | CellId("7", 9, 15), 247 | ], 248 | "code_lines": [4, 11, 18, 25, 33, 41, 49], 249 | } 250 | assert map_intermediate_to_input(input_line_mapping, line_number) == expected_result 251 | 252 | 253 | ################################# 254 | # NotebookParser Tests # 255 | ################################# 256 | 257 | 258 | def test_NotebookParser_create_intermediate_py_file_paths( 259 | notebook_parser: NotebookParser, 260 | ): 261 | for original_notebook in notebook_parser.original_notebook_paths: 262 | assert os.path.isfile(original_notebook) 263 | for intermediate_py_file in notebook_parser.intermediate_py_file_paths: 264 | assert os.path.isfile(intermediate_py_file) 265 | assert notebook_parser.temp_path != "" 266 | 267 | original_count = len(notebook_parser.original_notebook_paths) 268 | intermediate_count = len(notebook_parser.intermediate_py_file_paths) 269 | input_line_mapping_count = len(notebook_parser.input_line_mappings) 270 | assert original_count == 3 271 | assert intermediate_count == 3 272 | assert input_line_mapping_count == 3 273 | 274 | 275 | def test_NotebookParser_cross_instance_value_propagation( 276 | notebook_parser: NotebookParser, 277 | ): 278 | notebook_parser.get_mappings() 279 | new_parser_instance = NotebookParser() 280 | 281 | original_count = len(new_parser_instance.original_notebook_paths) 282 | intermediate_count = len(new_parser_instance.intermediate_py_file_paths) 283 | input_line_mapping_count = len(new_parser_instance.input_line_mappings) 284 | assert original_count == 3 285 | assert intermediate_count == 3 286 | assert input_line_mapping_count == 3 287 | 288 | 289 | def test_NotebookParser_clean_up(notebook_parser: NotebookParser): 290 | temp_path = notebook_parser.temp_path 291 | notebook_parser.clean_up() 292 | assert not os.path.exists(temp_path) 293 | assert notebook_parser.temp_path == "" 294 | 295 | original_count = len(notebook_parser.original_notebook_paths) 296 | intermediate_count = len(notebook_parser.intermediate_py_file_paths) 297 | input_line_mapping_count = len(notebook_parser.input_line_mappings) 298 | assert original_count == 0 299 | assert intermediate_count == 0 300 | assert input_line_mapping_count == 0 301 | -------------------------------------------------------------------------------- /tests/parsers/test_cell_parsers.py: -------------------------------------------------------------------------------- 1 | import warnings 2 | from typing import Dict 3 | from typing import List 4 | 5 | import pytest 6 | 7 | from flake8_nb.parsers import CellId 8 | from flake8_nb.parsers.cell_parsers import InvalidFlake8TagWarning 9 | from flake8_nb.parsers.cell_parsers import extract_flake8_inline_tags 10 | from flake8_nb.parsers.cell_parsers import extract_flake8_tags 11 | from flake8_nb.parsers.cell_parsers import extract_inline_flake8_noqa 12 | from flake8_nb.parsers.cell_parsers import flake8_tag_to_rules_dict 13 | from flake8_nb.parsers.cell_parsers import generate_rules_list 14 | from flake8_nb.parsers.cell_parsers import get_flake8_rules_dict 15 | from flake8_nb.parsers.cell_parsers import notebook_cell_to_intermediate_dict 16 | from flake8_nb.parsers.cell_parsers import update_inline_flake8_noqa 17 | from flake8_nb.parsers.cell_parsers import update_rules_dict 18 | 19 | 20 | @pytest.mark.parametrize( 21 | "source_index,rules_dict,expected_result", 22 | [ 23 | ( 24 | 0, 25 | {"cell": ["noqa"], "1": ["E402", "F401", "W391"]}, 26 | ["E402", "F401", "W391", "noqa"], 27 | ), 28 | (1, {"cell": ["noqa"], "1": ["E402", "F401", "W391"]}, ["noqa"]), 29 | (1, {"1": ["E402", "F401", "W391"]}, []), 30 | ], 31 | ) 32 | def test_generate_rules_list( 33 | source_index: int, rules_dict: Dict[str, List], expected_result: List 34 | ): 35 | assert sorted(generate_rules_list(source_index, rules_dict)) == expected_result 36 | 37 | 38 | @pytest.mark.parametrize( 39 | "notebook_cell,expected_result", 40 | [ 41 | ( 42 | { 43 | "execution_count": 8, 44 | "metadata": {"tags": ["raises-exception", "flake8-noqa-cell-E402-F401"]}, 45 | "source": ["foo "], 46 | }, 47 | {"cell": ["E402", "F401"]}, 48 | ), 49 | ( 50 | { 51 | "execution_count": 9, 52 | "metadata": { 53 | "tags": [ 54 | "flake8-noqa-cell-E402-F401", 55 | "flake8-noqa-line-1-E402-F401", 56 | "flake8-noqa-line-1-W391", 57 | "flake8-noqa-cell", 58 | ] 59 | }, 60 | "source": ["foo # flake8-noqa-line-3-D402 \n "], 61 | }, 62 | {"cell": ["noqa"], "1": ["W391", "E402", "F401"], "3": ["D402"]}, 63 | ), 64 | ( 65 | { 66 | "execution_count": 8, 67 | "metadata": {"tags": ["raises-exception", "flake8-noqa-cell-E402-F401"]}, 68 | "source": ["foo # flake8-noqa-cell ", "bar # flake8-noqa-line-4"], 69 | }, 70 | {"cell": ["noqa"], "4": ["noqa"]}, 71 | ), 72 | ], 73 | ) 74 | def test_get_flake8_rules_dict(notebook_cell: Dict, expected_result: Dict[str, List]): 75 | flake8_rules_dict = get_flake8_rules_dict(notebook_cell) 76 | for key, value in expected_result.items(): 77 | assert sorted(flake8_rules_dict[key]) == sorted(value) 78 | 79 | 80 | def test_extract_flake8_tags(): 81 | notebook_cell = { 82 | "metadata": { 83 | "tags": [ 84 | "flake8-noqa-cell-E402-F401", 85 | "flake8-noqa-cell", 86 | "flake8-noqa-line-1-E402", 87 | "flake8-noqa-line-1", 88 | "random-tag", 89 | ] 90 | } 91 | } 92 | expected_result = [ 93 | "flake8-noqa-cell-E402-F401", 94 | "flake8-noqa-cell", 95 | "flake8-noqa-line-1-E402", 96 | "flake8-noqa-line-1", 97 | ] 98 | assert extract_flake8_tags(notebook_cell) == expected_result 99 | 100 | 101 | def test_extract_flake8_inline_tags(): 102 | notebook_cell = { 103 | "source": [ 104 | "foo # flake8-noqa-cell-A402-BC403", 105 | "foo # flake8-noqa-cell ", 106 | "foo # flake8-noqa-line-3-D402 \n", 107 | "foo # flake8-noqa-line-4", 108 | "foo # flake8-noqa-cell-E402 flake8-noqa-cell-F403", 109 | "foo # flake8-noqa-line-6-GH402 flake8-noqa-line-6-J403-L43", 110 | "foo # noqa \n", 111 | '"foo # flake8-noqa-cell"', 112 | "foo # noqa : flake8-noqa-cell some randome stuff", 113 | ] 114 | } 115 | expected_result = [ 116 | "flake8-noqa-cell-A402-BC403", 117 | "flake8-noqa-cell", 118 | "flake8-noqa-line-3-D402", 119 | "flake8-noqa-line-4", 120 | "flake8-noqa-cell-E402", 121 | "flake8-noqa-cell-F403", 122 | "flake8-noqa-line-6-GH402", 123 | "flake8-noqa-line-6-J403-L43", 124 | ] 125 | assert sorted(extract_flake8_inline_tags(notebook_cell)) == sorted(expected_result) 126 | 127 | 128 | @pytest.mark.parametrize( 129 | "flake8_noqa_tag,expected_result", 130 | [ 131 | ("flake8-noqa-cell-E402-F401", {"cell": ["E402", "F401"]}), 132 | ("flake8-noqa-cell", {"cell": ["noqa"]}), 133 | ("flake8-noqa-line-1-E402-F401", {"1": ["E402", "F401"]}), 134 | ("flake8-noqa-line-1", {"1": ["noqa"]}), 135 | ("flake8-noqa-line-foo-E402-F401", {}), 136 | ], 137 | ) 138 | def test_flake8_tag_to_rules_dict(flake8_noqa_tag: str, expected_result: Dict[str, List]): 139 | if flake8_noqa_tag == "flake8-noqa-line-foo-E402-F401": 140 | with pytest.warns(InvalidFlake8TagWarning): 141 | assert flake8_tag_to_rules_dict(flake8_noqa_tag) == expected_result 142 | else: 143 | assert flake8_tag_to_rules_dict(flake8_noqa_tag) == expected_result 144 | 145 | 146 | @pytest.mark.parametrize( 147 | "source_index,expected_result", 148 | [ 149 | ("foo # noqa: E402, Fasd401", ["E402", "Fasd401"]), 150 | ("foo # noqa : E402, Fasd401 \n", ["E402", "Fasd401"]), 151 | ("foo # noqa", ["noqa"]), 152 | ("foo # noqa : ", ["noqa"]), 153 | ("foo # noqa \n", ["noqa"]), 154 | ('"foo # noqa : E402, Fasd401"', []), 155 | ("foo # noqa : E402, Fasd401 some randome stuff", []), 156 | ("get_ipython().run_cell_magic('bash', '', 'echo test')\n", []), 157 | ], 158 | ) 159 | def test_extract_inline_flake8_noqa(source_index: str, expected_result: List): 160 | assert extract_inline_flake8_noqa(source_index) == expected_result 161 | 162 | 163 | @pytest.mark.parametrize( 164 | "notebook_cell,expected_result", 165 | [ 166 | ( 167 | { 168 | "execution_count": 8, 169 | "code_cell_nr": 8, 170 | "total_cell_nr": 8, 171 | "metadata": {"tags": ["raises-exception", "flake8-noqa-cell-E402-F401"]}, 172 | "source": ["for i in range(1):\n", " print(i)"], 173 | }, 174 | { 175 | "code": ( 176 | "# INTERMEDIATE_CELL_SEPARATOR (8,8,8)\n\n\n" 177 | "for i in range(1): # noqa: E402, F401\n " 178 | "print(i) # noqa: E402, F401\n\n\n" 179 | ), 180 | "input_id": CellId("8", 8, 8), 181 | "lines_of_code": 7, 182 | }, 183 | ), 184 | ( 185 | { 186 | "execution_count": 9, 187 | "code_cell_nr": 9, 188 | "total_cell_nr": 12, 189 | "metadata": {"tags": ["flake8-noqa-line-1-E402-F401", "flake8-noqa-line-1-W391"]}, 190 | "source": ["for i in range(1):\n", " print(i)"], 191 | }, 192 | { 193 | "code": ( 194 | "# INTERMEDIATE_CELL_SEPARATOR (9,9,12)\n\n\n" 195 | "for i in range(1): # noqa: E402, F401, W391\n" 196 | " print(i)\n\n\n" 197 | ), 198 | "input_id": CellId("9", 9, 12), 199 | "lines_of_code": 7, 200 | }, 201 | ), 202 | ( 203 | { 204 | "execution_count": 2, 205 | "code_cell_nr": 2, 206 | "total_cell_nr": 4, 207 | "metadata": {"tags": ["flake8-noqa-cell-E402", "flake8-noqa-line-1"]}, 208 | "source": [ 209 | "for i in range(1):\n", 210 | " print(i) # noqa:F401, W391", 211 | "print('foo')", 212 | ], 213 | }, 214 | { 215 | "code": ( 216 | "# INTERMEDIATE_CELL_SEPARATOR (2,2,4)\n\n\n" 217 | "for i in range(1): # noqa: \n" 218 | " print(i) # noqa: E402, F401, W391\n" 219 | "print('foo') # noqa: E402\n\n\n" 220 | ), 221 | "input_id": CellId("2", 2, 4), 222 | "lines_of_code": 8, 223 | }, 224 | ), 225 | ( 226 | { 227 | "execution_count": 1, 228 | "code_cell_nr": 1, 229 | "total_cell_nr": 1, 230 | "metadata": {"tags": ["flake8-noqa-cell", "flake8-noqa-line-1"]}, 231 | "source": ["for i in range(1):\n", " print(i) # noqa:F401, W391"], 232 | }, 233 | { 234 | "code": ( 235 | "# INTERMEDIATE_CELL_SEPARATOR (1,1,1)" 236 | "\n\n\nfor i in range(1): # noqa: \n" 237 | " print(i) # noqa: \n\n\n" 238 | ), 239 | "input_id": CellId("1", 1, 1), 240 | "lines_of_code": 7, 241 | }, 242 | ), 243 | ( 244 | { 245 | "execution_count": None, 246 | "code_cell_nr": 1, 247 | "total_cell_nr": 1, 248 | "metadata": {"tags": ["flake8-noqa-cell", "flake8-noqa-line-1"]}, 249 | "source": ["for i in range(1):\n", " print(i) # noqa:F401, W391"], 250 | }, 251 | { 252 | "code": ( 253 | "# INTERMEDIATE_CELL_SEPARATOR ( ,1,1)\n\n\n" 254 | "for i in range(1): # noqa: \n" 255 | " print(i) # noqa: \n\n\n" 256 | ), 257 | "input_id": CellId(" ", 1, 1), 258 | "lines_of_code": 7, 259 | }, 260 | ), 261 | ], 262 | ) 263 | def test_notebook_cell_to_intermediate_py_str( 264 | notebook_cell: Dict, expected_result: Dict[str, str] 265 | ): 266 | intermediate_py_str = notebook_cell_to_intermediate_dict(notebook_cell) 267 | assert intermediate_py_str == expected_result 268 | 269 | 270 | @pytest.mark.parametrize( 271 | "new_rules_dict,expected_result", 272 | [ 273 | ({"cell": ["W391", "F401"]}, {"cell": ["W391", "E402", "F401"], "1": ["W391"]}), 274 | ({"cell": ["noqa"]}, {"cell": ["noqa"], "1": ["W391"]}), 275 | ({"1": ["noqa"]}, {"cell": ["E402", "F401"], "1": ["noqa"]}), 276 | ], 277 | ) 278 | def test_update_rules_dict(new_rules_dict: Dict[str, List], expected_result: Dict[str, List]): 279 | total_rules_dict = {"cell": ["E402", "F401"], "1": ["W391"]} 280 | update_rules_dict(total_rules_dict, new_rules_dict) 281 | assert sorted(total_rules_dict["cell"]) == sorted(expected_result["cell"]) 282 | assert sorted(total_rules_dict["1"]) == sorted(expected_result["1"]) 283 | 284 | 285 | @pytest.mark.parametrize( 286 | "source_index,rules_list,expected_result", 287 | [ 288 | ("foo # noqa: E402, Fasd401", ["noqa"], "foo # noqa: \n"), 289 | ( 290 | "foo # noqa : E402, Fasd401 \n", 291 | ["E402", "F401"], 292 | "foo # noqa: E402, F401, Fasd401\n", 293 | ), 294 | ("foo # noqa", ["E402", "F401"], "foo # noqa: \n"), 295 | ( 296 | '"foo # noqa : E402, Fasd401"', 297 | ["E402", "F401"], 298 | '"foo # noqa : E402, Fasd401" # noqa: E402, F401\n', 299 | ), 300 | ( 301 | "foo # noqa : E402, Fasd401 some randome stuff\n", 302 | [], 303 | "foo # noqa : E402, Fasd401 some randome stuff\n", 304 | ), 305 | ( 306 | "foo # noqa : E402, Fasd401 some randome stuff\n", 307 | ["E402", "F401"], 308 | "foo # noqa : E402, Fasd401 some randome stuff # noqa: E402, F401\n", 309 | ), 310 | ], 311 | ) 312 | def test_update_inline_flake8_noqa(source_index: str, rules_list: List, expected_result: str): 313 | assert update_inline_flake8_noqa(source_index, rules_list) == expected_result 314 | 315 | 316 | def test_InvalidFlake8TagWarning(): 317 | with pytest.warns( 318 | InvalidFlake8TagWarning, 319 | match=( 320 | "flake8-noqa-line/cell-tags should be of form " 321 | "'flake8-noqa-cell--'|'flake8-noqa-cell'/" 322 | "'flake8-noqa-line---'|'flake8-noqa-line-', " 323 | "you used: 'user-pattern'" 324 | ), 325 | ): 326 | warnings.warn(InvalidFlake8TagWarning("user-pattern")) 327 | -------------------------------------------------------------------------------- /flake8_nb/flake8_integration/cli.py: -------------------------------------------------------------------------------- 1 | r"""Module containing the notebook gatherer and hack of flake8. 2 | 3 | This is the main implementation of ``flake8_nb``, it relies on 4 | overwriting ``flake8`` 's CLI default options, searching and parsing 5 | ``*.ipynb`` files and injecting the parsed files, during the loading 6 | of the CLI argv and config of ``flake8``. 7 | """ 8 | from __future__ import annotations 9 | 10 | import configparser 11 | import logging 12 | import os 13 | import sys 14 | import types 15 | from pathlib import Path 16 | from typing import Any 17 | from typing import Callable 18 | 19 | from flake8 import __version__ as flake_version 20 | from flake8 import defaults 21 | from flake8 import utils 22 | from flake8.main.application import Application 23 | from flake8.options import aggregator 24 | from flake8.options import config 25 | from flake8.utils import matches_filename 26 | 27 | from flake8_nb import FLAKE8_VERSION_TUPLE 28 | from flake8_nb import __version__ 29 | from flake8_nb.parsers.notebook_parsers import NotebookParser 30 | 31 | LOG = logging.getLogger(__name__) 32 | 33 | defaults.EXCLUDE = (*defaults.EXCLUDE, ".ipynb_checkpoints") 34 | 35 | 36 | def get_notebooks_from_args( 37 | args: list[str], exclude: list[str] = ["*.tox/*", "*.ipynb_checkpoints*"] 38 | ) -> tuple[list[str], list[str]]: 39 | """Extract the absolute paths to notebooks. 40 | 41 | The paths are relative to the current directory or 42 | to the CLI passes files/folder and returned as list. 43 | 44 | Parameters 45 | ---------- 46 | args : list[str] 47 | The left over arguments that were not parsed by :attr:`option_manager` 48 | exclude : list[str] 49 | File-/Folderpatterns that should be excluded, 50 | by default ["*.tox/*", "*.ipynb_checkpoints*"] 51 | 52 | Returns 53 | ------- 54 | tuple[list[str], list[str]] 55 | List of found notebooks absolute paths. 56 | """ 57 | 58 | def is_notebook(file_path: str, nb_list: list[str], root: str = ".") -> bool: 59 | """Check if a file is a notebook and appends it to nb_list if it is. 60 | 61 | Parameters 62 | ---------- 63 | file_path : str 64 | File to check if it is a notebook 65 | nb_list : list[str] 66 | List of notebooks 67 | root : str 68 | Root directory, by default "." 69 | 70 | Returns 71 | ------- 72 | bool 73 | Whether the given file is a notebook 74 | """ 75 | file_path = os.path.abspath(os.path.join(root, file_path)) 76 | if os.path.isfile(file_path) and file_path.endswith(".ipynb"): 77 | nb_list.append(os.path.normcase(file_path)) 78 | return True 79 | return False 80 | 81 | nb_list: list[str] = [] 82 | if not args: 83 | args = [os.curdir] 84 | for index, arg in list(enumerate(args))[::-1]: 85 | if is_notebook(arg, nb_list): 86 | args.pop(index) 87 | for root, _, filenames in os.walk(arg): 88 | if not matches_filename( # pragma: no branch 89 | root, 90 | patterns=exclude, 91 | log_message='"%(path)s" has %(whether)sbeen excluded', 92 | logger=LOG, 93 | ): 94 | [is_notebook(filename, nb_list, root) for filename in filenames] 95 | 96 | return args, nb_list 97 | 98 | 99 | def hack_option_manager_generate_versions( 100 | generate_versions: Callable[..., str] 101 | ) -> Callable[..., str]: 102 | """Closure to prepend the flake8 version to option_manager.generate_versions . 103 | 104 | Parameters 105 | ---------- 106 | generate_versions : Callable[..., str] 107 | option_manager.generate_versions of flake8.options.manager.OptionManager 108 | 109 | Returns 110 | ------- 111 | Callable[..., str] 112 | hacked_generate_versions 113 | """ 114 | 115 | def hacked_generate_versions(*args: Any, **kwargs: Any) -> str: 116 | """Inner wrapper around option_manager.generate_versions. 117 | 118 | Parameters 119 | ---------- 120 | args: Tuple[Any] 121 | Arbitrary args 122 | kwargs: Dict[str, Any] 123 | Arbitrary kwargs 124 | 125 | Returns 126 | ------- 127 | str 128 | Plugin versions string containing flake8 129 | """ 130 | original_output = generate_versions(*args, **kwargs) 131 | format_str = "%(name)s: %(version)s" 132 | additional_output = format_str % { 133 | "name": "flake8", 134 | "version": flake_version, 135 | } 136 | return f"{additional_output}, {original_output}" 137 | 138 | return hacked_generate_versions 139 | 140 | 141 | def hack_config_module() -> None: 142 | """Create hacked version of ``flake8.options.config`` at runtime. 143 | 144 | Since flake8>=5.0.0 uses hardcoded ``"flake8"`` to discover the config we replace 145 | with it with ``"flake8_nb"`` to create our own hacked version and replace 146 | the references to the original module with the hacked one. 147 | 148 | See: 149 | https://github.com/s-weigand/flake8-nb/issues/249 150 | https://github.com/s-weigand/flake8-nb/issues/254 151 | """ 152 | hacked_config_source = ( 153 | Path(config.__file__) 154 | .read_text() 155 | .replace('"flake8"', '"flake8_nb"') 156 | .replace('".flake8"', '".flake8_nb"') 157 | ) 158 | hacked_config = types.ModuleType("hacked_config") 159 | exec(hacked_config_source, hacked_config.__dict__) 160 | 161 | sys.modules["flake8.options.config"] = hacked_config 162 | aggregator.config = hacked_config 163 | 164 | import flake8.main.application as application_module 165 | 166 | application_module.config = hacked_config 167 | 168 | 169 | class Flake8NbApplication(Application): # type: ignore[misc] 170 | r"""Subclass of ``flake8.main.application.Application``. 171 | 172 | It overwrites the default options and an injection of intermediate parsed 173 | ``*.ipynb`` files to be checked. 174 | """ 175 | 176 | def __init__(self, program: str = "flake8_nb", version: str = __version__): 177 | """Hacked initialization of flake8.Application. 178 | 179 | Parameters 180 | ---------- 181 | program : str 182 | Application name, by default "flake8_nb" 183 | version : str 184 | Application version, by default __version__ 185 | """ 186 | super().__init__() 187 | if FLAKE8_VERSION_TUPLE < (5, 0, 0): 188 | self.apply_hacks() 189 | self.option_manager.generate_versions = hack_option_manager_generate_versions( 190 | self.option_manager.generate_versions 191 | ) 192 | self.parse_configuration_and_cli = ( # type: ignore[assignment] 193 | self.parse_configuration_and_cli_legacy # type: ignore[assignment] 194 | ) 195 | else: 196 | hack_config_module() 197 | self.register_plugin_options = self.hacked_register_plugin_options 198 | 199 | def apply_hacks(self) -> None: 200 | """Apply hacks to flake8 adding options and changing the application name + version.""" 201 | self.hack_flake8_program_and_version("flake8_nb", __version__) 202 | self.hack_options() 203 | self.set_flake8_option( 204 | "--keep-parsed-notebooks", 205 | default=False, 206 | action="store_true", 207 | parse_from_config=True, 208 | help="Keep the temporary parsed notebooks, i.e. for debugging.", 209 | ) 210 | self.set_flake8_option( 211 | "--notebook-cell-format", 212 | metavar="notebook_cell_format", 213 | default="{nb_path}#In[{exec_count}]", 214 | parse_from_config=True, 215 | help="Template string used to format the filename and cell part of error report.\n" 216 | "Possible variables which will be replaces 'nb_path', 'exec_count'," 217 | "'code_cell_count' and 'total_cell_count'. (Default: %default)", 218 | ) 219 | 220 | def hacked_register_plugin_options(self) -> None: 221 | """Register options provided by plugins to our option manager.""" 222 | assert self.plugins is not None 223 | from flake8.main import options 224 | from flake8.options import manager 225 | 226 | plugin_version = ", ".join( 227 | [v for v in self.plugins.versions_str().split(", ") if not v.startswith("flake8-nb")] 228 | ) 229 | 230 | self.option_manager = manager.OptionManager( 231 | version=__version__, 232 | plugin_versions=f"flake8: {flake_version}, {plugin_version}", 233 | parents=[self.prelim_arg_parser], 234 | ) 235 | options.register_default_options(self.option_manager) 236 | self.option_manager.register_plugins(self.plugins) 237 | 238 | def hack_flake8_program_and_version(self, program: str, version: str) -> None: 239 | """Hack to overwrite the program name and version of flake8. 240 | 241 | This is needed because those values are hard coded at creation of `self.option_manager`. 242 | 243 | Parameters 244 | ---------- 245 | program : str 246 | Name of the program 247 | version : str 248 | Version of the program 249 | """ 250 | self.program = program 251 | self.version = version 252 | self.option_manager.parser.prog = program 253 | self.option_manager.parser.version = version 254 | self.option_manager.program_name = program 255 | self.option_manager.version = version 256 | 257 | def set_flake8_option(self, long_option_name: str, *args: Any, **kwargs: Any) -> None: 258 | """Overwrite flake8 options. 259 | 260 | First deletes and than reads an option to `flake8`'s cli options, if it was present. 261 | If the option wasn't present, it just adds it. 262 | 263 | 264 | Parameters 265 | ---------- 266 | long_option_name : str 267 | Long name of the flake8 cli option. 268 | args: Tuple[Any] 269 | Arbitrary args 270 | kwargs: Dict[str, Any] 271 | Arbitrary kwargs 272 | 273 | """ 274 | is_option = False 275 | for option_index, option in enumerate(self.option_manager.options): 276 | if option.long_option_name == long_option_name: 277 | self.option_manager.options.pop(option_index) 278 | is_option = True 279 | if is_option: 280 | # pylint: disable=no-member 281 | parser = self.option_manager.parser 282 | for index, action in enumerate(parser._actions): # pragma: no branch 283 | if long_option_name in action.option_strings: 284 | parser._handle_conflict_resolve( 285 | None, [(long_option_name, parser._actions[index])] 286 | ) 287 | break 288 | self.option_manager.add_option(long_option_name, *args, **kwargs) 289 | 290 | def hack_options(self) -> None: 291 | """Overwrite ``flake8``'s default options, with ``flake8_nb`` defaults.""" 292 | self.set_flake8_option( 293 | "--format", 294 | metavar="format", 295 | default="default_notebook", 296 | parse_from_config=True, 297 | help="Format errors according to the chosen formatter.", 298 | ) 299 | self.set_flake8_option( 300 | "--filename", 301 | metavar="patterns", 302 | default="*.py,*.ipynb_parsed", 303 | parse_from_config=True, 304 | comma_separated_list=True, 305 | help="Only check for filenames matching the patterns in this comma-" 306 | "separated list. (Default: %default)", 307 | ) 308 | 309 | @staticmethod 310 | def hack_args(args: list[str], exclude: list[str]) -> list[str]: 311 | r"""Update args with ``*.ipynb`` files. 312 | 313 | Checks the passed args if ``*.ipynb`` can be found and 314 | appends intermediate parsed files to the list of files, 315 | which should be checked. 316 | 317 | Parameters 318 | ---------- 319 | args : list[str] 320 | List of commandline arguments provided to ``flake8_nb`` 321 | exclude : list[str] 322 | File-/Folderpatterns that should be excluded 323 | 324 | Returns 325 | ------- 326 | list[str] 327 | The original args + intermediate parsed ``*.ipynb`` files. 328 | """ 329 | args, nb_list = get_notebooks_from_args(args, exclude=exclude) 330 | notebook_parser = NotebookParser(nb_list) 331 | return args + notebook_parser.intermediate_py_file_paths 332 | 333 | def parse_configuration_and_cli_legacy( 334 | self, config_finder: config.ConfigFileFinder, argv: list[str] 335 | ) -> None: 336 | """Parse configuration files and the CLI options. 337 | 338 | Parameters 339 | ---------- 340 | config_finder: config.ConfigFileFinder 341 | The finder for finding and reading configuration files. 342 | argv: list[str] 343 | Command-line arguments passed in directly. 344 | """ 345 | self.options, self.args = aggregator.aggregate_options( 346 | self.option_manager, 347 | config_finder, 348 | argv, 349 | ) 350 | 351 | self.args = self.hack_args(self.args, self.options.exclude) 352 | 353 | self.running_against_diff = self.options.diff 354 | if self.running_against_diff: # pragma: no cover 355 | self.parsed_diff = utils.parse_unified_diff() 356 | if not self.parsed_diff: 357 | self.exit() 358 | 359 | self.options._running_from_vcs = False 360 | 361 | self.check_plugins.provide_options(self.option_manager, self.options, self.args) 362 | self.formatting_plugins.provide_options(self.option_manager, self.options, self.args) 363 | 364 | def parse_configuration_and_cli( 365 | self, 366 | cfg: configparser.RawConfigParser, 367 | cfg_dir: str, 368 | argv: list[str], 369 | ) -> None: 370 | """ 371 | Parse configuration files and the CLI options. 372 | 373 | Parameters 374 | ---------- 375 | cfg: configparser.RawConfigParser 376 | Config parser instance 377 | cfg_dir: str 378 | Dir the the config is in. 379 | argv: list[str] 380 | CLI args 381 | 382 | Raises 383 | ------ 384 | SystemExit 385 | If ``--bug-report`` option is passed to the CLI. 386 | """ 387 | assert self.option_manager is not None 388 | assert self.plugins is not None 389 | 390 | self.apply_hacks() 391 | 392 | self.options = aggregator.aggregate_options( 393 | self.option_manager, 394 | cfg, 395 | cfg_dir, 396 | argv, 397 | ) 398 | 399 | argv = self.hack_args(argv, self.options.exclude) 400 | 401 | self.options = aggregator.aggregate_options( 402 | self.option_manager, 403 | cfg, 404 | cfg_dir, 405 | argv, 406 | ) 407 | 408 | import json 409 | 410 | from flake8.main import debug 411 | 412 | if self.options.bug_report: 413 | info = debug.information(__version__, self.plugins) 414 | for index, plugin in enumerate(info["plugins"]): 415 | if plugin["plugin"] == "flake8-nb": 416 | del info["plugins"][index] 417 | info["flake8-version"] = flake_version 418 | print(json.dumps(info, indent=2, sort_keys=True)) 419 | raise SystemExit(0) 420 | 421 | if self.options.diff: # pragma: no cover 422 | LOG.warning( 423 | "the --diff option is deprecated and will be removed in a " "future version." 424 | ) 425 | self.parsed_diff = utils.parse_unified_diff() 426 | 427 | for loaded in self.plugins.all_plugins(): 428 | parse_options = getattr(loaded.obj, "parse_options", None) 429 | if parse_options is None: 430 | continue 431 | 432 | # XXX: ideally we wouldn't have two forms of parse_options 433 | try: 434 | parse_options( 435 | self.option_manager, 436 | self.options, 437 | self.options.filenames, 438 | ) 439 | except TypeError: 440 | parse_options(self.options) 441 | 442 | def exit(self) -> None: 443 | """Handle finalization and exiting the program. 444 | 445 | This should be the last thing called on the application instance. It 446 | will check certain options and exit appropriately. 447 | 448 | Raises 449 | ------ 450 | SystemExit 451 | For flake8>=5.0.0 452 | """ 453 | if self.options.keep_parsed_notebooks: 454 | temp_path = NotebookParser.temp_path 455 | print( 456 | f"The parsed notebooks, are still present at:\n\t{temp_path}", 457 | file=sys.stderr, 458 | ) 459 | else: 460 | NotebookParser.clean_up() 461 | if FLAKE8_VERSION_TUPLE < (5, 0, 0): 462 | super().exit() 463 | else: 464 | raise SystemExit(self.exit_code()) 465 | --------------------------------------------------------------------------------