diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6c29f34..cefde86 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,7 +2,7 @@ default_stages: [pre-commit] repos: - repo: https://github.com/psf/black-pre-commit-mirror - rev: 25.11.0 + rev: 25.12.0 hooks: - id: black @@ -13,10 +13,10 @@ repos: - id: end-of-file-fixer - id: debug-statements - - repo: https://github.com/christopher-hacker/enforce-notebook-run-order - rev: 2.1.1 - hooks: - - id: enforce-notebook-run-order +# - repo: https://github.com/christopher-hacker/enforce-notebook-run-order +# rev: 2.1.1 +# hooks: +# - id: enforce-notebook-run-order - repo: local hooks: @@ -38,3 +38,11 @@ repos: args: [--fix-header, --repo-name=devops_tests] language: python types: [jupyter] + + - id: notebooks-output + name: notebooks output + entry: notebooks_output + additional_dependencies: + - nbformat + language: python + types: [jupyter] diff --git a/.pre-commit-hooks.yaml b/.pre-commit-hooks.yaml index 922a4c2..9a644d7 100644 --- a/.pre-commit-hooks.yaml +++ b/.pre-commit-hooks.yaml @@ -13,3 +13,11 @@ language: python stages: [pre-commit] types: [jupyter] + +- id: notebooks-output + name: notebooks output + description: check if notebooks are executed and without errors and warnings + entry: notebooks_output + language: python + stages: [pre-commit] + types: [jupyter] diff --git a/hooks/check_notebooks.py b/hooks/check_notebooks.py index 870a124..bf3f2e1 100755 --- a/hooks/check_notebooks.py +++ b/hooks/check_notebooks.py @@ -1,4 +1,5 @@ #!/usr/bin/env python3 +# pylint: disable=duplicate-code #TODO #62 """ Checks notebook execution status for Jupyter notebooks""" from __future__ import annotations @@ -13,26 +14,6 @@ class NotebookTestError(Exception): """Raised when a notebook validation test fails.""" -def test_cell_contains_output(notebook): - """checks if all notebook cells have an output present""" - for cell in notebook.cells: - if cell.cell_type == "code" and cell.source != "": - if cell.execution_count is None: - raise ValueError("Cell does not contain output!") - - -def test_no_errors_or_warnings_in_output(notebook): - """checks if all example Jupyter notebooks have clear std-err output - (i.e., no errors or warnings) visible; except acceptable - diagnostics from the joblib package""" - for cell in notebook.cells: - if cell.cell_type == "code": - for output in cell.outputs: - if "name" in output and output["name"] == "stderr": - if not output["text"].startswith("[Parallel(n_jobs="): - raise ValueError(output["text"]) - - def test_show_plot_used_instead_of_matplotlib(notebook): """checks if plotting is done with open_atmos_jupyter_utils show_plot()""" matplot_used = False @@ -82,20 +63,13 @@ def test_jetbrains_bug_py_66491(notebook): ) -def main(argv: Sequence[str] | None = None) -> int: - """test all notebooks""" +def open_and_test_notebooks(argv, test_functions): + """Create argparser and run notebook tests""" parser = argparse.ArgumentParser() parser.add_argument("filenames", nargs="*", help="Filenames to check.") args = parser.parse_args(argv) retval = 0 - test_functions = [ - test_cell_contains_output, - test_no_errors_or_warnings_in_output, - test_jetbrains_bug_py_66491, - test_show_anim_used_instead_of_matplotlib, - test_show_plot_used_instead_of_matplotlib, - ] for filename in args.filenames: with open(filename, encoding="utf8") as notebook_file: notebook = nbformat.read(notebook_file, nbformat.NO_CONVERT) @@ -108,5 +82,17 @@ def main(argv: Sequence[str] | None = None) -> int: return retval +def main(argv: Sequence[str] | None = None) -> int: + """test all notebooks""" + return open_and_test_notebooks( + argv=argv, + test_functions=[ + test_jetbrains_bug_py_66491, + test_show_anim_used_instead_of_matplotlib, + test_show_plot_used_instead_of_matplotlib, + ], + ) + + if __name__ == "__main__": raise SystemExit(main()) diff --git a/hooks/notebooks_output.py b/hooks/notebooks_output.py new file mode 100755 index 0000000..c2245b9 --- /dev/null +++ b/hooks/notebooks_output.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python3 +# pylint: disable=duplicate-code #TODO #62 +"""checks if notebook is executed and do not contain 'stderr""" + +from __future__ import annotations + +import argparse +from collections.abc import Sequence + +import nbformat + + +class NotebookTestError(Exception): + """Raised when a notebook validation test fails.""" + + +def test_cell_contains_output(notebook): + """checks if all notebook cells have an output present""" + for cell_idx, cell in enumerate(notebook.cells): + if cell.cell_type == "code" and cell.source != "": + if cell.execution_count is None: + raise ValueError(f"Cell {cell_idx} does not contain output") + + +def test_no_errors_or_warnings_in_output(notebook): + """checks if all example Jupyter notebooks have clear std-err output + (i.e., no errors or warnings) visible; except acceptable + diagnostics from the joblib package""" + for cell_idx, cell in enumerate(notebook.cells): + if cell.cell_type == "code": + for output in cell.outputs: + ot = output.get("output_type") + if ot == "error": + raise ValueError( + f"Cell [{cell_idx}] contain error or warning. \n\n" + f"Cell [{cell_idx}] output:\n{output}\n" + ) + if ot == "stream" and output.get("name") == "stderr": + out_text = output.get("text") + if out_text and not out_text.startswith("[Parallel(n_jobs="): + raise ValueError(f" Cell [{cell_idx}]: {out_text}") + + +def open_and_test_notebooks(argv, test_functions): + """Create argparser and run notebook tests""" + parser = argparse.ArgumentParser() + parser.add_argument("filenames", nargs="*", help="Filenames to check.") + args = parser.parse_args(argv) + + retval = 0 + for filename in args.filenames: + with open(filename, encoding="utf8") as notebook_file: + notebook = nbformat.read(notebook_file, nbformat.NO_CONVERT) + for func in test_functions: + try: + func(notebook) + except NotebookTestError as e: + print(f"{filename} : {e}") + retval = 1 + return retval + + +def main(argv: Sequence[str] | None = None) -> int: + """test all notebooks""" + return open_and_test_notebooks( + argv=argv, + test_functions=[ + test_cell_contains_output, + test_no_errors_or_warnings_in_output, + ], + ) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/pyproject.toml b/pyproject.toml index e3dfb25..d76fbb5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,3 +29,4 @@ dynamic = ['version'] [project.scripts] check_notebooks = "hooks.check_notebooks:main" check_badges = "hooks.check_badges:main" +notebooks_output = "hooks.notebooks_output:main"