From b4b8c1ad207c339c29167e88ee0f13e4d9001de8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agnieszka=20=C5=BBaba?= Date: Sun, 24 Aug 2025 14:36:00 +0200 Subject: [PATCH 01/17] start work on pre-commit hooks --- .pre-commit-config.yaml | 13 ++++++++-- pre_commit_hooks/check_notebooks.py | 38 +++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 2 deletions(-) create mode 100644 pre_commit_hooks/check_notebooks.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8155bbd..488ac4a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,4 +1,4 @@ -files: '.py' +files: '.py|.ipynb' exclude: '.git' default_stages: [pre-commit] @@ -23,4 +23,13 @@ repos: - repo: https://github.com/christopher-hacker/enforce-notebook-run-order rev: 2.0.1 hooks: - - id: enforce-notebook-run-order \ No newline at end of file + - id: enforce-notebook-run-order + + - repo: local + hooks: + - id: check_notebooks + name: check notebooks + entry: python ./pre_commit_hooks/check_notebooks.py + language: system + files: '.ipynb' + diff --git a/pre_commit_hooks/check_notebooks.py b/pre_commit_hooks/check_notebooks.py new file mode 100644 index 0000000..7aa14bf --- /dev/null +++ b/pre_commit_hooks/check_notebooks.py @@ -0,0 +1,38 @@ +from __future__ import annotations + +import argparse +from collections.abc import Sequence + +import nbformat + + +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 AssertionError(output["text"]) + + +def main(argv: Sequence[str] | None = None) -> int: + 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) + try: + test_no_errors_or_warnings_in_output(notebook) + except ValueError as exc: + retval = 1 + return retval + + +if __name__ == "__main__": + raise SystemExit(main()) From 2de8ac2e5af514861c161e1f5a026f2b9289a434 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agnieszka=20=C5=BBaba?= Date: Sun, 24 Aug 2025 17:52:48 +0200 Subject: [PATCH 02/17] divide ito check_badges and check_notebooks; add utils --- .pre-commit-config.yaml | 6 +- pre_commit_hooks/check_badges.py | 96 ++++++++++++++++++ pre_commit_hooks/check_notebooks.py | 49 ++++++++++ pre_commit_hooks/utils.py | 45 +++++++++ test_notebooks.py | 147 ---------------------------- 5 files changed, 195 insertions(+), 148 deletions(-) create mode 100644 pre_commit_hooks/check_badges.py create mode 100644 pre_commit_hooks/utils.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 488ac4a..a756a2f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -32,4 +32,8 @@ repos: entry: python ./pre_commit_hooks/check_notebooks.py language: system files: '.ipynb' - + - id: check_badges + name: check badges + entry: python ./pre_commit_hooks/check_badges.py + language: system + files: '.ipynb' \ No newline at end of file diff --git a/pre_commit_hooks/check_badges.py b/pre_commit_hooks/check_badges.py new file mode 100644 index 0000000..4ff6c76 --- /dev/null +++ b/pre_commit_hooks/check_badges.py @@ -0,0 +1,96 @@ +from __future__ import annotations + +import argparse +from collections.abc import Sequence + +import nbformat + +from utils import find_files, relative_path, repo_path + +COLAB_HEADER = f"""import sys +if 'google.colab' in sys.modules: + !pip --quiet install open-atmos-jupyter-utils + from open_atmos_jupyter_utils import pip_install_on_colab + pip_install_on_colab('{repo_path().name}-examples')""" + + +def _preview_badge_markdown(absolute_path): + svg_badge_url = ( + "https://img.shields.io/static/v1?" + + "label=render%20on&logo=github&color=87ce3e&message=GitHub" + ) + link = ( + f"https://github.com/open-atmos/{repo_path().name}/blob/main/" + + f"{relative_path(absolute_path)}" + ) + return f"[![preview notebook]({svg_badge_url})]({link})" + + +def _mybinder_badge_markdown(absolute_path): + svg_badge_url = "https://mybinder.org/badge_logo.svg" + link = ( + f"https://mybinder.org/v2/gh/open-atmos/{repo_path().name}.git/main?urlpath=lab/tree/" + + f"{relative_path(absolute_path)}" + ) + return f"[![launch on mybinder.org]({svg_badge_url})]({link})" + + +def _colab_badge_markdown(absolute_path): + svg_badge_url = "https://colab.research.google.com/assets/colab-badge.svg" + link = ( + f"https://colab.research.google.com/github/open-atmos/{repo_path().name}/blob/main/" + + f"{relative_path(absolute_path)}" + ) + return f"[![launch on Colab]({svg_badge_url})]({link})" + + +def test_first_cell_contains_three_badges(notebook_filename): + """checks if all notebooks feature Github preview, mybinder and Colab badges + (in the first cell)""" + with open(notebook_filename, encoding="utf8") as fp: + nb = nbformat.read(fp, nbformat.NO_CONVERT) + assert len(nb.cells) > 0 + assert nb.cells[0].cell_type == "markdown" + lines = nb.cells[0].source.split("\n") + assert len(lines) == 3 + assert lines[0] == _preview_badge_markdown(notebook_filename) + assert lines[1] == _mybinder_badge_markdown(notebook_filename) + assert lines[2] == _colab_badge_markdown(notebook_filename) + + +def test_second_cell_is_a_markdown_cell(notebook_filename): + """checks if all notebooks have their second cell with some markdown + (hopefully clarifying what the example is about)""" + with open(notebook_filename, encoding="utf8") as fp: + nb = nbformat.read(fp, nbformat.NO_CONVERT) + assert len(nb.cells) > 1 + assert nb.cells[1].cell_type == "markdown" + + +def test_third_cell_contains_colab_header(notebook_filename): + """checks if all notebooks feature a Colab-magic cell""" + with open(notebook_filename, encoding="utf8") as fp: + nb = nbformat.read(fp, nbformat.NO_CONVERT) + assert len(nb.cells) > 2 + assert nb.cells[2].cell_type == "code" + assert nb.cells[2].source == COLAB_HEADER + + +def main(argv: Sequence[str] | None = None) -> int: + parser = argparse.ArgumentParser() + parser.add_argument("filenames", nargs="*", help="Filenames to check.") + args = parser.parse_args(argv) + + retval = 0 + for filename in args.filenames: + try: + test_first_cell_contains_three_badges(filename) + test_second_cell_is_a_markdown_cell(filename) + test_third_cell_contains_colab_header(filename) + except ValueError as exc: + retval = 1 + return retval + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/pre_commit_hooks/check_notebooks.py b/pre_commit_hooks/check_notebooks.py index 7aa14bf..5afa44f 100644 --- a/pre_commit_hooks/check_notebooks.py +++ b/pre_commit_hooks/check_notebooks.py @@ -6,6 +6,13 @@ import nbformat +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 != "": + assert cell.execution_count is not None + + 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 @@ -18,6 +25,47 @@ def test_no_errors_or_warnings_in_output(notebook): raise AssertionError(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 + show_plot_used = False + for cell in notebook.cells: + if cell.cell_type == "code": + if ( + "pyplot.show()" in cell.source + or "plt.show()" in cell.source + or "from matplotlib import pyplot" in cell.source + ): + matplot_used = True + if "show_plot()" in cell.source: + show_plot_used = True + if matplot_used and not show_plot_used: + raise AssertionError( + "if using matplotlib, please use open_atmos_jupyter_utils.show_plot()" + ) + + +def test_show_anim_used_instead_of_matplotlib(notebook): + """checks if animation generation is done with open_atmos_jupyter_utils show_anim()""" + matplot_used = False + show_anim_used = False + for cell in notebook.cells: + if cell.cell_type == "code": + if ( + "funcAnimation" in cell.source + or "matplotlib.animation" in cell.source + or "from matplotlib import animation" in cell.source + ): + matplot_used = True + if "show_anim()" in cell.source: + show_anim_used = True + if matplot_used and not show_anim_used: + raise AssertionError( + """if using matplotlib for animations, + please use open_atmos_jupyter_utils.show_anim()""" + ) + + def main(argv: Sequence[str] | None = None) -> int: parser = argparse.ArgumentParser() parser.add_argument("filenames", nargs="*", help="Filenames to check.") @@ -28,6 +76,7 @@ def main(argv: Sequence[str] | None = None) -> int: with open(filename, encoding="utf8") as notebook_file: notebook = nbformat.read(notebook_file, nbformat.NO_CONVERT) try: + test_cell_contains_output(notebook) test_no_errors_or_warnings_in_output(notebook) except ValueError as exc: retval = 1 diff --git a/pre_commit_hooks/utils.py b/pre_commit_hooks/utils.py new file mode 100644 index 0000000..948be01 --- /dev/null +++ b/pre_commit_hooks/utils.py @@ -0,0 +1,45 @@ +""" +Utils functions to reuse in different parts of the codebase +""" + +import os +import pathlib + +from git.cmd import Git + + +def find_files(path_to_folder_from_project_root=".", file_extension=None): + """ + Returns all files in a current git repo. + The list of returned files may be filtered with `file_extension` param. + """ + all_files = [ + path + for path in Git( + Git(path_to_folder_from_project_root).rev_parse("--show-toplevel") + ) + .ls_files() + .split("\n") + if os.path.isfile(path) + ] + if file_extension is not None: + return list(filter(lambda path: path.endswith(file_extension), all_files)) + + return all_files + + +def relative_path(absolute_path): + """returns a path relative to the repo base (converting backslashes to slashes on Windows)""" + relpath = os.path.relpath(absolute_path, repo_path().absolute()) + posixpath_to_make_it_usable_in_urls_even_on_windows = pathlib.Path( + relpath + ).as_posix() + return posixpath_to_make_it_usable_in_urls_even_on_windows + + +def repo_path(): + """returns absolute path to the repo base (ignoring .git location if in a submodule)""" + path = pathlib.Path(__file__) + while not (path.is_dir() and Git(path).rev_parse("--git-dir") == ".git"): + path = path.parent + return path diff --git a/test_notebooks.py b/test_notebooks.py index ba28eb4..1b3eadb 100644 --- a/test_notebooks.py +++ b/test_notebooks.py @@ -28,23 +28,6 @@ SI = pint.UnitRegistry() -def _relative_path(absolute_path): - """returns a path relative to the repo base (converting backslashes to slashes on Windows)""" - relpath = os.path.relpath(absolute_path, _repo_path().absolute()) - posixpath_to_make_it_usable_in_urls_even_on_windows = pathlib.Path( - relpath - ).as_posix() - return posixpath_to_make_it_usable_in_urls_even_on_windows - - -def _repo_path(): - """returns absolute path to the repo base (ignoring .git location if in a submodule)""" - path = pathlib.Path(__file__) - while not (path.is_dir() and Git(path).rev_parse("--git-dir") == ".git"): - path = path.parent - return path - - COLAB_HEADER = f"""import sys if 'google.colab' in sys.modules: !pip --quiet install open-atmos-jupyter-utils @@ -84,20 +67,6 @@ def test_file_size(notebook_filename): assert os.stat(notebook_filename).st_size * SI.byte < 2 * SI.megabyte -def test_no_errors_or_warnings_in_output(notebook_filename): - """checks if all example Jupyter notebooks have clear std-err output - (i.e., no errors or warnings) visible; with exception of acceptable - diagnostics from the joblib package""" - with open(notebook_filename, encoding="utf8") as notebook_file: - notebook = nbformat.read(notebook_file, nbformat.NO_CONVERT) - 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 AssertionError(output["text"]) - - def test_jetbrains_bug_py_66491(notebook_filename): """checks if all notebooks have the execution_count key for each cell in JSON what is required by GitHub renderer and what happens not to be the case if generating the notebook @@ -111,119 +80,3 @@ def test_jetbrains_bug_py_66491(notebook_filename): + " (could be due to a bug in PyCharm," + " see https://youtrack.jetbrains.com/issue/PY-66491 )" ) - - -def _preview_badge_markdown(absolute_path): - svg_badge_url = ( - "https://img.shields.io/static/v1?" - + "label=render%20on&logo=github&color=87ce3e&message=GitHub" - ) - link = ( - f"https://github.com/open-atmos/{_repo_path().name}/blob/main/" - + f"{_relative_path(absolute_path)}" - ) - return f"[![preview notebook]({svg_badge_url})]({link})" - - -def _mybinder_badge_markdown(abslute_path): - svg_badge_url = "https://mybinder.org/badge_logo.svg" - link = ( - f"https://mybinder.org/v2/gh/open-atmos/{_repo_path().name}.git/main?urlpath=lab/tree/" - + f"{_relative_path(abslute_path)}" - ) - return f"[![launch on mybinder.org]({svg_badge_url})]({link})" - - -def _colab_badge_markdown(absolute_path): - svg_badge_url = "https://colab.research.google.com/assets/colab-badge.svg" - link = ( - f"https://colab.research.google.com/github/open-atmos/{_repo_path().name}/blob/main/" - + f"{_relative_path(absolute_path)}" - ) - return f"[![launch on Colab]({svg_badge_url})]({link})" - - -def test_first_cell_contains_three_badges(notebook_filename): - """checks if all notebooks feature Github preview, mybinder and Colab badges - (in the first cell)""" - with open(notebook_filename, encoding="utf8") as fp: - nb = nbformat.read(fp, nbformat.NO_CONVERT) - assert len(nb.cells) > 0 - assert nb.cells[0].cell_type == "markdown" - lines = nb.cells[0].source.split("\n") - assert len(lines) == 3 - assert lines[0] == _preview_badge_markdown(notebook_filename) - assert lines[1] == _mybinder_badge_markdown(notebook_filename) - assert lines[2] == _colab_badge_markdown(notebook_filename) - - -def test_second_cell_is_a_markdown_cell(notebook_filename): - """checks if all notebooks have their second cell with some markdown - (hopefully clarifying what the example is about)""" - with open(notebook_filename, encoding="utf8") as fp: - nb = nbformat.read(fp, nbformat.NO_CONVERT) - assert len(nb.cells) > 1 - assert nb.cells[1].cell_type == "markdown" - - -def test_third_cell_contains_colab_header(notebook_filename): - """checks if all notebooks feature a Colab-magic cell""" - with open(notebook_filename, encoding="utf8") as fp: - nb = nbformat.read(fp, nbformat.NO_CONVERT) - assert len(nb.cells) > 2 - assert nb.cells[2].cell_type == "code" - assert nb.cells[2].source == COLAB_HEADER - - -def test_cell_contains_output(notebook_filename): - """checks if all notebook cells have an output present""" - with open(notebook_filename, encoding="utf8") as fp: - nb = nbformat.read(fp, nbformat.NO_CONVERT) - for cell in nb.cells: - if cell.cell_type == "code" and cell.source != "": - assert cell.execution_count is not None - - -def test_show_plot_used_instead_of_matplotlib(notebook_filename): - """checks if plotting is done with open_atmos_jupyter_utils show_plot()""" - with open(notebook_filename, encoding="utf8") as fp: - nb = nbformat.read(fp, nbformat.NO_CONVERT) - matplot_used = False - show_plot_used = False - for cell in nb.cells: - if cell.cell_type == "code": - if ( - "pyplot.show()" in cell.source - or "plt.show()" in cell.source - or "from matplotlib import pyplot" in cell.source - ): - matplot_used = True - if "show_plot()" in cell.source: - show_plot_used = True - if matplot_used and not show_plot_used: - raise AssertionError( - "if using matplotlib, please use open_atmos_jupyter_utils.show_plot()" - ) - - -def test_show_anim_used_instead_of_matplotlib(notebook_filename): - """checks if animation generation is done with open_atmos_jupyter_utils show_anim()""" - with open(notebook_filename, encoding="utf8") as fp: - nb = nbformat.read(fp, nbformat.NO_CONVERT) - matplot_used = False - show_anim_used = False - for cell in nb.cells: - if cell.cell_type == "code": - if ( - "funcAnimation" in cell.source - or "matplotlib.animation" in cell.source - or "from matplotlib import animation" in cell.source - ): - matplot_used = True - if "show_anim()" in cell.source: - show_anim_used = True - if matplot_used and not show_anim_used: - raise AssertionError( - """if using matplotlib for animations, - please use open_atmos_jupyter_utils.show_anim()""" - ) From 9902a892325b5a64a82f1621972715829c8917f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agnieszka=20=C5=BBaba?= Date: Sun, 24 Aug 2025 17:55:18 +0200 Subject: [PATCH 03/17] cleanup --- pre_commit_hooks/check_badges.py | 2 +- test_notebooks.py | 9 --------- 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/pre_commit_hooks/check_badges.py b/pre_commit_hooks/check_badges.py index 4ff6c76..021deb4 100644 --- a/pre_commit_hooks/check_badges.py +++ b/pre_commit_hooks/check_badges.py @@ -5,7 +5,7 @@ import nbformat -from utils import find_files, relative_path, repo_path +from utils import relative_path, repo_path COLAB_HEADER = f"""import sys if 'google.colab' in sys.modules: diff --git a/test_notebooks.py b/test_notebooks.py index 1b3eadb..1f02ff9 100644 --- a/test_notebooks.py +++ b/test_notebooks.py @@ -11,13 +11,11 @@ import gc import os -import pathlib import warnings import nbformat import pint import pytest -from git.cmd import Git from .utils import find_files @@ -28,13 +26,6 @@ SI = pint.UnitRegistry() -COLAB_HEADER = f"""import sys -if 'google.colab' in sys.modules: - !pip --quiet install open-atmos-jupyter-utils - from open_atmos_jupyter_utils import pip_install_on_colab - pip_install_on_colab('{_repo_path().name}-examples')""" - - @pytest.fixture( params=find_files(file_extension=".ipynb"), name="notebook_filename", From 80a3baf95cc03004591a60ed355c972fbe4495ec Mon Sep 17 00:00:00 2001 From: sfonxu Date: Mon, 15 Sep 2025 10:42:47 +0200 Subject: [PATCH 04/17] squash! cleanup --- pre_commit_hooks/check_notebooks.py | 20 ++++++++++++++++++++ test_notebooks.py | 20 -------------------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/pre_commit_hooks/check_notebooks.py b/pre_commit_hooks/check_notebooks.py index 5afa44f..8607a3f 100644 --- a/pre_commit_hooks/check_notebooks.py +++ b/pre_commit_hooks/check_notebooks.py @@ -1,9 +1,13 @@ from __future__ import annotations import argparse +import os from collections.abc import Sequence import nbformat +import pint + +SI = pint.UnitRegistry() def test_cell_contains_output(notebook): @@ -66,6 +70,20 @@ def test_show_anim_used_instead_of_matplotlib(notebook): ) +def test_jetbrains_bug_py_66491(notebook): + """checks if all notebook have the execution_count key for each cell in JSON, which is required by GitHub renderer and may not be generated by some buggy PyCharm versions: https://youtrack.jetbrains.com/issue/PY-66491""" + for cell in notebook.cells: + if cell.cell_type == "code" and not hasattr(cell, "execution_count"): + raise AssertionError( + "Notebook cell missing execution_count attribute. (May be due to PyCharm bug, see: https://youtrack.jetbrains.com/issue/PY-66491 )" + ) + + +def test_file_size(notebook_filename): + """Test if notebook is smaller than an arbitrary size limit""" + assert os.stat(notebook_filename).st_size * SI.byte < 2 * SI.megabyte + + def main(argv: Sequence[str] | None = None) -> int: parser = argparse.ArgumentParser() parser.add_argument("filenames", nargs="*", help="Filenames to check.") @@ -76,6 +94,8 @@ def main(argv: Sequence[str] | None = None) -> int: with open(filename, encoding="utf8") as notebook_file: notebook = nbformat.read(notebook_file, nbformat.NO_CONVERT) try: + test_file_size(filename) + test_jetbrains_bug_py_66491(notebook) test_cell_contains_output(notebook) test_no_errors_or_warnings_in_output(notebook) except ValueError as exc: diff --git a/test_notebooks.py b/test_notebooks.py index 1f02ff9..fb3977e 100644 --- a/test_notebooks.py +++ b/test_notebooks.py @@ -51,23 +51,3 @@ def test_run_notebooks(notebook_filename, tmp_path): # so that nbconvert perplexities are reported here, and not at some dtor test later on gc.collect() - - -def test_file_size(notebook_filename): - """checks if all example Jupyter notebooks have file size less than an arbitrary limit""" - assert os.stat(notebook_filename).st_size * SI.byte < 2 * SI.megabyte - - -def test_jetbrains_bug_py_66491(notebook_filename): - """checks if all notebooks have the execution_count key for each cell in JSON what is - required by GitHub renderer and what happens not to be the case if generating the notebook - using buggy versions of PyCharm: https://youtrack.jetbrains.com/issue/PY-66491""" - with open(notebook_filename, encoding="utf8") as notebook_file: - notebook = nbformat.read(notebook_file, nbformat.NO_CONVERT) - for cell in notebook.cells: - if cell.cell_type == "code" and not hasattr(cell, "execution_count"): - raise AssertionError( - "notebook cell is missing the execution_count attribute" - + " (could be due to a bug in PyCharm," - + " see https://youtrack.jetbrains.com/issue/PY-66491 )" - ) From 0952a84d175af4a5456eff9ad96b944adfbc9303 Mon Sep 17 00:00:00 2001 From: Sfonxu Date: Mon, 6 Oct 2025 11:43:59 +0200 Subject: [PATCH 05/17] Refactor main --- pre_commit_hooks/check_notebooks.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/pre_commit_hooks/check_notebooks.py b/pre_commit_hooks/check_notebooks.py index 8607a3f..dd4a227 100644 --- a/pre_commit_hooks/check_notebooks.py +++ b/pre_commit_hooks/check_notebooks.py @@ -77,11 +77,18 @@ def test_jetbrains_bug_py_66491(notebook): raise AssertionError( "Notebook cell missing execution_count attribute. (May be due to PyCharm bug, see: https://youtrack.jetbrains.com/issue/PY-66491 )" ) + return 1 + return 0 def test_file_size(notebook_filename): """Test if notebook is smaller than an arbitrary size limit""" - assert os.stat(notebook_filename).st_size * SI.byte < 2 * SI.megabyte + if not os.stat(notebook_filename).st_size * SI.byte < 2 * SI.megabyte: + raise AssertionError( + f"Notebook '{notebook_filename}' has size larger than {2 * SI.megabyte}." + ) + return 1 + return 0 def main(argv: Sequence[str] | None = None) -> int: @@ -93,13 +100,10 @@ def main(argv: Sequence[str] | None = None) -> int: for filename in args.filenames: with open(filename, encoding="utf8") as notebook_file: notebook = nbformat.read(notebook_file, nbformat.NO_CONVERT) - try: - test_file_size(filename) - test_jetbrains_bug_py_66491(notebook) - test_cell_contains_output(notebook) - test_no_errors_or_warnings_in_output(notebook) - except ValueError as exc: - retval = 1 + retval |= test_file_size(filename) + retval |= test_jetbrains_bug_py_66491(notebook) + retval |= test_cell_contains_output(notebook) + retval |= test_no_errors_or_warnings_in_output(notebook) return retval From 92d1f8946950908507e0bd0d5d5e44624bde2a23 Mon Sep 17 00:00:00 2001 From: Sfonxu Date: Mon, 6 Oct 2025 14:25:01 +0200 Subject: [PATCH 06/17] Restructure the repo, add pre-commit hooks and pyproject.toml --- .pre-commit-config.yaml | 12 +- .pre-commit-hooks.yaml | 6 + __init__.py | 0 pre_commit_hooks/check_badges.py | 96 ------------- pre_commit_hooks/check_notebooks.py | 111 --------------- pre_commit_hooks/utils.py | 45 ------ pyproject.toml | 29 ++++ requirements.txt | 10 -- test_files/template2.ipynb | 60 ++++++++ test_notebooks.py | 205 ---------------------------- test_todos_annotated.py | 72 ---------- utils.py | 27 ---- 12 files changed, 98 insertions(+), 575 deletions(-) create mode 100644 .pre-commit-hooks.yaml delete mode 100644 __init__.py delete mode 100644 pre_commit_hooks/check_badges.py delete mode 100644 pre_commit_hooks/check_notebooks.py delete mode 100644 pre_commit_hooks/utils.py create mode 100644 pyproject.toml delete mode 100644 requirements.txt create mode 100644 test_files/template2.ipynb delete mode 100644 test_notebooks.py delete mode 100644 test_todos_annotated.py delete mode 100644 utils.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a756a2f..1dec1e0 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,4 +1,3 @@ -files: '.py|.ipynb' exclude: '.git' default_stages: [pre-commit] @@ -27,13 +26,8 @@ repos: - repo: local hooks: - - id: check_notebooks + - id: check-notebooks name: check notebooks - entry: python ./pre_commit_hooks/check_notebooks.py - language: system + entry: check_notebooks + language: python files: '.ipynb' - - id: check_badges - name: check badges - entry: python ./pre_commit_hooks/check_badges.py - language: system - files: '.ipynb' \ No newline at end of file diff --git a/.pre-commit-hooks.yaml b/.pre-commit-hooks.yaml new file mode 100644 index 0000000..f0bd68b --- /dev/null +++ b/.pre-commit-hooks.yaml @@ -0,0 +1,6 @@ +- id: check-notebooks + name: check notebooks + description: check Jupyter Notebook contents + entry: check_notebooks + language: python + stages: [pre-commit] diff --git a/__init__.py b/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/pre_commit_hooks/check_badges.py b/pre_commit_hooks/check_badges.py deleted file mode 100644 index 021deb4..0000000 --- a/pre_commit_hooks/check_badges.py +++ /dev/null @@ -1,96 +0,0 @@ -from __future__ import annotations - -import argparse -from collections.abc import Sequence - -import nbformat - -from utils import relative_path, repo_path - -COLAB_HEADER = f"""import sys -if 'google.colab' in sys.modules: - !pip --quiet install open-atmos-jupyter-utils - from open_atmos_jupyter_utils import pip_install_on_colab - pip_install_on_colab('{repo_path().name}-examples')""" - - -def _preview_badge_markdown(absolute_path): - svg_badge_url = ( - "https://img.shields.io/static/v1?" - + "label=render%20on&logo=github&color=87ce3e&message=GitHub" - ) - link = ( - f"https://github.com/open-atmos/{repo_path().name}/blob/main/" - + f"{relative_path(absolute_path)}" - ) - return f"[![preview notebook]({svg_badge_url})]({link})" - - -def _mybinder_badge_markdown(absolute_path): - svg_badge_url = "https://mybinder.org/badge_logo.svg" - link = ( - f"https://mybinder.org/v2/gh/open-atmos/{repo_path().name}.git/main?urlpath=lab/tree/" - + f"{relative_path(absolute_path)}" - ) - return f"[![launch on mybinder.org]({svg_badge_url})]({link})" - - -def _colab_badge_markdown(absolute_path): - svg_badge_url = "https://colab.research.google.com/assets/colab-badge.svg" - link = ( - f"https://colab.research.google.com/github/open-atmos/{repo_path().name}/blob/main/" - + f"{relative_path(absolute_path)}" - ) - return f"[![launch on Colab]({svg_badge_url})]({link})" - - -def test_first_cell_contains_three_badges(notebook_filename): - """checks if all notebooks feature Github preview, mybinder and Colab badges - (in the first cell)""" - with open(notebook_filename, encoding="utf8") as fp: - nb = nbformat.read(fp, nbformat.NO_CONVERT) - assert len(nb.cells) > 0 - assert nb.cells[0].cell_type == "markdown" - lines = nb.cells[0].source.split("\n") - assert len(lines) == 3 - assert lines[0] == _preview_badge_markdown(notebook_filename) - assert lines[1] == _mybinder_badge_markdown(notebook_filename) - assert lines[2] == _colab_badge_markdown(notebook_filename) - - -def test_second_cell_is_a_markdown_cell(notebook_filename): - """checks if all notebooks have their second cell with some markdown - (hopefully clarifying what the example is about)""" - with open(notebook_filename, encoding="utf8") as fp: - nb = nbformat.read(fp, nbformat.NO_CONVERT) - assert len(nb.cells) > 1 - assert nb.cells[1].cell_type == "markdown" - - -def test_third_cell_contains_colab_header(notebook_filename): - """checks if all notebooks feature a Colab-magic cell""" - with open(notebook_filename, encoding="utf8") as fp: - nb = nbformat.read(fp, nbformat.NO_CONVERT) - assert len(nb.cells) > 2 - assert nb.cells[2].cell_type == "code" - assert nb.cells[2].source == COLAB_HEADER - - -def main(argv: Sequence[str] | None = None) -> int: - parser = argparse.ArgumentParser() - parser.add_argument("filenames", nargs="*", help="Filenames to check.") - args = parser.parse_args(argv) - - retval = 0 - for filename in args.filenames: - try: - test_first_cell_contains_three_badges(filename) - test_second_cell_is_a_markdown_cell(filename) - test_third_cell_contains_colab_header(filename) - except ValueError as exc: - retval = 1 - return retval - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/pre_commit_hooks/check_notebooks.py b/pre_commit_hooks/check_notebooks.py deleted file mode 100644 index dd4a227..0000000 --- a/pre_commit_hooks/check_notebooks.py +++ /dev/null @@ -1,111 +0,0 @@ -from __future__ import annotations - -import argparse -import os -from collections.abc import Sequence - -import nbformat -import pint - -SI = pint.UnitRegistry() - - -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 != "": - assert cell.execution_count is not None - - -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 AssertionError(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 - show_plot_used = False - for cell in notebook.cells: - if cell.cell_type == "code": - if ( - "pyplot.show()" in cell.source - or "plt.show()" in cell.source - or "from matplotlib import pyplot" in cell.source - ): - matplot_used = True - if "show_plot()" in cell.source: - show_plot_used = True - if matplot_used and not show_plot_used: - raise AssertionError( - "if using matplotlib, please use open_atmos_jupyter_utils.show_plot()" - ) - - -def test_show_anim_used_instead_of_matplotlib(notebook): - """checks if animation generation is done with open_atmos_jupyter_utils show_anim()""" - matplot_used = False - show_anim_used = False - for cell in notebook.cells: - if cell.cell_type == "code": - if ( - "funcAnimation" in cell.source - or "matplotlib.animation" in cell.source - or "from matplotlib import animation" in cell.source - ): - matplot_used = True - if "show_anim()" in cell.source: - show_anim_used = True - if matplot_used and not show_anim_used: - raise AssertionError( - """if using matplotlib for animations, - please use open_atmos_jupyter_utils.show_anim()""" - ) - - -def test_jetbrains_bug_py_66491(notebook): - """checks if all notebook have the execution_count key for each cell in JSON, which is required by GitHub renderer and may not be generated by some buggy PyCharm versions: https://youtrack.jetbrains.com/issue/PY-66491""" - for cell in notebook.cells: - if cell.cell_type == "code" and not hasattr(cell, "execution_count"): - raise AssertionError( - "Notebook cell missing execution_count attribute. (May be due to PyCharm bug, see: https://youtrack.jetbrains.com/issue/PY-66491 )" - ) - return 1 - return 0 - - -def test_file_size(notebook_filename): - """Test if notebook is smaller than an arbitrary size limit""" - if not os.stat(notebook_filename).st_size * SI.byte < 2 * SI.megabyte: - raise AssertionError( - f"Notebook '{notebook_filename}' has size larger than {2 * SI.megabyte}." - ) - return 1 - return 0 - - -def main(argv: Sequence[str] | None = None) -> int: - 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) - retval |= test_file_size(filename) - retval |= test_jetbrains_bug_py_66491(notebook) - retval |= test_cell_contains_output(notebook) - retval |= test_no_errors_or_warnings_in_output(notebook) - return retval - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/pre_commit_hooks/utils.py b/pre_commit_hooks/utils.py deleted file mode 100644 index 948be01..0000000 --- a/pre_commit_hooks/utils.py +++ /dev/null @@ -1,45 +0,0 @@ -""" -Utils functions to reuse in different parts of the codebase -""" - -import os -import pathlib - -from git.cmd import Git - - -def find_files(path_to_folder_from_project_root=".", file_extension=None): - """ - Returns all files in a current git repo. - The list of returned files may be filtered with `file_extension` param. - """ - all_files = [ - path - for path in Git( - Git(path_to_folder_from_project_root).rev_parse("--show-toplevel") - ) - .ls_files() - .split("\n") - if os.path.isfile(path) - ] - if file_extension is not None: - return list(filter(lambda path: path.endswith(file_extension), all_files)) - - return all_files - - -def relative_path(absolute_path): - """returns a path relative to the repo base (converting backslashes to slashes on Windows)""" - relpath = os.path.relpath(absolute_path, repo_path().absolute()) - posixpath_to_make_it_usable_in_urls_even_on_windows = pathlib.Path( - relpath - ).as_posix() - return posixpath_to_make_it_usable_in_urls_even_on_windows - - -def repo_path(): - """returns absolute path to the repo base (ignoring .git location if in a submodule)""" - path = pathlib.Path(__file__) - while not (path.is_dir() and Git(path).rev_parse("--git-dir") == ".git"): - path = path.parent - return path diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..2873f70 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,29 @@ +[build-system] +requires = ["setuptools >= 77.0.3"] +build-backend = "setuptools.build_meta" + +[tool.setuptools] +packages = ['devops_tests'] + +[project] +name = "devops_tests" +dependencies = [ + "pytest", + "ghapi", + "gitpython", + "binaryornot", + "nbconvert", + "jsonschema", + "jupyter-client>=8.0.2", + "ipython", + "ipykernel", +] +readme = "README.md" +license = "GPL-3.0-only" +authors = [ + {name = "https://github.com/open-atmos/devops_tests/graphs/contributors", email = "sylwester.arabas@agh.edu.pl"} +] +dynamic = ['version'] + +[project.scripts] +check_notebooks = "devops_tests.pre_commit_hooks.check_notebooks:main" diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 1cbac3c..0000000 --- a/requirements.txt +++ /dev/null @@ -1,10 +0,0 @@ -pytest -ghapi -gitpython -binaryornot -nbconvert -jsonschema -jupyter-client>=8.0.2 -ipython -ipykernel -pint diff --git a/test_files/template2.ipynb b/test_files/template2.ipynb new file mode 100644 index 0000000..01ec238 --- /dev/null +++ b/test_files/template2.ipynb @@ -0,0 +1,60 @@ +{ + "cells": [ + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "[![preview notebook](https://img.shields.io/static/v1?label=render%20on&logo=github&color=87ce3e&message=GitHub)](https://github.com/open-atmos/devops_tests/blob/main/test_files/template.ipynb)\n", + "[![launch on mybinder.org](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/open-atmos/devops_tests.git/main?urlpath=lab/tree/test_files/template.ipynb)\n", + "[![launch on Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/open-atmos/devops_tests/blob/main/test_files/template.ipynb)" + ], + "id": "29186ad9e7311ae0" + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "", + "id": "7a729b2624b70eae" + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2024-10-26T12:29:32.925592Z", + "start_time": "2024-10-26T12:29:32.919920Z" + } + }, + "cell_type": "code", + "source": [ + "import sys\n", + "if 'google.colab' in sys.modules:\n", + " !pip --quiet install open-atmos-jupyter-utils\n", + " from open_atmos_jupyter_utils import pip_install_on_colab\n", + " pip_install_on_colab('devops_tests-examples')" + ], + "id": "72ccd23c0ab9f08e", + "outputs": [], + "execution_count": 1 + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.6" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/test_notebooks.py b/test_notebooks.py deleted file mode 100644 index 62923fd..0000000 --- a/test_notebooks.py +++ /dev/null @@ -1,205 +0,0 @@ -"""executes all Jupyter notebooks tracked by git""" - -# pylint: disable=wrong-import-position -# https://bugs.python.org/issue37373 -import sys - -if sys.platform == "win32" and sys.version_info[:2] >= (3, 7): - import asyncio - - asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) - -import gc -import os -import warnings - -import nbformat -import pint -import pytest - -from .utils import find_files - -with warnings.catch_warnings(): - warnings.filterwarnings("ignore") - from nbconvert.preprocessors import ExecutePreprocessor - -SI = pint.UnitRegistry() - - -@pytest.fixture( - params=find_files(file_extension=".ipynb"), - name="notebook_filename", -) -def _notebook_filename(request): - return request.param - - -def test_run_notebooks(notebook_filename, tmp_path): - """executes a given notebook""" - os.environ["JUPYTER_PLATFORM_DIRS"] = "1" - - executor = ExecutePreprocessor(timeout=15 * 60, kernel_name="python3") - - with open(notebook_filename, encoding="utf8") as notebook_file: - # https://github.com/pytest-dev/pytest-asyncio/issues/212 - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", message="There is no current event loop") - executor.preprocess( - nbformat.read(notebook_file, as_version=4), - {"metadata": {"path": tmp_path}}, - ) - - # so that nbconvert perplexities are reported here, and not at some dtor test later on - gc.collect() - -def test_file_size(notebook_filename): - """checks if all example Jupyter notebooks have file size less than an arbitrary limit""" - assert os.stat(notebook_filename).st_size * SI.byte < 2 * SI.megabyte - - -def test_no_errors_or_warnings_in_output(notebook_filename): - """checks if all example Jupyter notebooks have clear std-err output - (i.e., no errors or warnings) visible; with exception of acceptable - diagnostics from the joblib package""" - with open(notebook_filename, encoding="utf8") as notebook_file: - notebook = nbformat.read(notebook_file, nbformat.NO_CONVERT) - for cell in notebook.cells: - if cell.cell_type == "code": - for output in cell.outputs: - if (output.get("name") == "stderr") or ( - output.get("output_type") in ("error", "pyerr") - ): - if not output["text"].startswith("[Parallel(n_jobs="): - raise AssertionError(output["text"]) - - -def test_jetbrains_bug_py_66491(notebook_filename): - """checks if all notebooks have the execution_count key for each cell in JSON what is - required by GitHub renderer and what happens not to be the case if generating the notebook - using buggy versions of PyCharm: https://youtrack.jetbrains.com/issue/PY-66491""" - with open(notebook_filename, encoding="utf8") as notebook_file: - notebook = nbformat.read(notebook_file, nbformat.NO_CONVERT) - for cell in notebook.cells: - if cell.cell_type == "code" and not hasattr(cell, "execution_count"): - raise AssertionError( - "notebook cell is missing the execution_count attribute" - + " (could be due to a bug in PyCharm," - + " see https://youtrack.jetbrains.com/issue/PY-66491 )" - ) - - -def _preview_badge_markdown(absolute_path): - svg_badge_url = ( - "https://img.shields.io/static/v1?" - + "label=render%20on&logo=github&color=87ce3e&message=GitHub" - ) - link = ( - f"https://github.com/open-atmos/{_repo_path().name}/blob/main/" - + f"{_relative_path(absolute_path)}" - ) - return f"[![preview notebook]({svg_badge_url})]({link})" - - -def _mybinder_badge_markdown(abslute_path): - svg_badge_url = "https://mybinder.org/badge_logo.svg" - link = ( - f"https://mybinder.org/v2/gh/open-atmos/{_repo_path().name}.git/main?urlpath=lab/tree/" - + f"{_relative_path(abslute_path)}" - ) - return f"[![launch on mybinder.org]({svg_badge_url})]({link})" - - -def _colab_badge_markdown(absolute_path): - svg_badge_url = "https://colab.research.google.com/assets/colab-badge.svg" - link = ( - f"https://colab.research.google.com/github/open-atmos/{_repo_path().name}/blob/main/" - + f"{_relative_path(absolute_path)}" - ) - return f"[![launch on Colab]({svg_badge_url})]({link})" - - -def test_first_cell_contains_three_badges(notebook_filename): - """checks if all notebooks feature Github preview, mybinder and Colab badges - (in the first cell)""" - with open(notebook_filename, encoding="utf8") as fp: - nb = nbformat.read(fp, nbformat.NO_CONVERT) - assert len(nb.cells) > 0 - assert nb.cells[0].cell_type == "markdown" - lines = nb.cells[0].source.split("\n") - assert len(lines) == 3 - assert lines[0] == _preview_badge_markdown(notebook_filename) - assert lines[1] == _mybinder_badge_markdown(notebook_filename) - assert lines[2] == _colab_badge_markdown(notebook_filename) - - -def test_second_cell_is_a_markdown_cell(notebook_filename): - """checks if all notebooks have their second cell with some markdown - (hopefully clarifying what the example is about)""" - with open(notebook_filename, encoding="utf8") as fp: - nb = nbformat.read(fp, nbformat.NO_CONVERT) - assert len(nb.cells) > 1 - assert nb.cells[1].cell_type == "markdown" - - -def test_third_cell_contains_colab_header(notebook_filename): - """checks if all notebooks feature a Colab-magic cell""" - with open(notebook_filename, encoding="utf8") as fp: - nb = nbformat.read(fp, nbformat.NO_CONVERT) - assert len(nb.cells) > 2 - assert nb.cells[2].cell_type == "code" - assert nb.cells[2].source == COLAB_HEADER - - -def test_cell_contains_output(notebook_filename): - """checks if all notebook cells have an output present""" - with open(notebook_filename, encoding="utf8") as fp: - nb = nbformat.read(fp, nbformat.NO_CONVERT) - for cell in nb.cells: - if cell.cell_type == "code" and cell.source != "": - assert cell.execution_count is not None - - -def test_show_plot_used_instead_of_matplotlib(notebook_filename): - """checks if plotting is done with open_atmos_jupyter_utils show_plot()""" - with open(notebook_filename, encoding="utf8") as fp: - nb = nbformat.read(fp, nbformat.NO_CONVERT) - matplot_used = False - show_plot_used = False - for cell in nb.cells: - if cell.cell_type == "code": - if ( - "pyplot.show()" in cell.source - or "plt.show()" in cell.source - or "from matplotlib import pyplot" in cell.source - ): - matplot_used = True - if "show_plot()" in cell.source: - show_plot_used = True - if matplot_used and not show_plot_used: - raise AssertionError( - "if using matplotlib, please use open_atmos_jupyter_utils.show_plot()" - ) - - -def test_show_anim_used_instead_of_matplotlib(notebook_filename): - """checks if animation generation is done with open_atmos_jupyter_utils show_anim()""" - with open(notebook_filename, encoding="utf8") as fp: - nb = nbformat.read(fp, nbformat.NO_CONVERT) - matplot_used = False - show_anim_used = False - for cell in nb.cells: - if cell.cell_type == "code": - if ( - "funcAnimation" in cell.source - or "matplotlib.animation" in cell.source - or "from matplotlib import animation" in cell.source - ): - matplot_used = True - if "show_anim()" in cell.source: - show_anim_used = True - if matplot_used and not show_anim_used: - raise AssertionError( - """if using matplotlib for animations, - please use open_atmos_jupyter_utils.show_anim()""" - ) - diff --git a/test_todos_annotated.py b/test_todos_annotated.py deleted file mode 100644 index bfd2684..0000000 --- a/test_todos_annotated.py +++ /dev/null @@ -1,72 +0,0 @@ -"""utilities to ensure all TO-DO comments in the code are annotated -with an id of an open GitHub issue""" - -import os -import re - -import pytest -from binaryornot.check import is_binary -from ghapi.all import GhApi, paged -from git.cmd import Git - -from .utils import find_files - - -def _grep(filepath, regex): - reg_obj = re.compile(regex) - res = [] - with open(filepath, encoding="utf8") as file_lines: - for line in file_lines: - if reg_obj.match(line): - res.append(line) - return res - - -@pytest.fixture( - params=find_files(), - name="git_tracked_file", -) -def _git_tracked_file(request): - return request.param - - -@pytest.fixture(scope="session", name="gh_issues") -def _gh_issues(): - res = {} - repo = os.path.basename(Git(".").rev_parse("--show-toplevel")) - api = GhApi(owner="open-atmos", repo=repo) - pages = paged( - api.issues.list_for_repo, - owner="open-atmos", - repo=repo, - state="all", - per_page=100, - ) - for page in pages: - for item in page.items: - res[item.number] = item.state - return res - - -def test_todos_annotated(git_tracked_file, gh_issues): - """raises assertion errors if a (TODO|FIXME) is not annotated or if the annotation - does not point to an open issue""" - if is_binary(git_tracked_file): - pytest.skip("binary file") - for line in _grep(git_tracked_file, r".*(TODO|FIXME).*"): - if "(TODO|FIXME)" in line: - continue - match = re.search(r"(TODO|FIXME) #(\d+)", line) - if match is None: - raise AssertionError(f"(TODO|FIXME) not annotated with issue id ({line})") - giving_up_with_hope_other_builds_did_it = len(gh_issues) == 0 - if not giving_up_with_hope_other_builds_did_it: - number = int(match.group(2)) - if number not in gh_issues.keys(): - raise AssertionError( - f"(TODO|FIXME) annotated with non-existent id ({line})" - ) - if gh_issues[number] != "open": - raise AssertionError( - f"(TODO|FIXME) remains for a non-open issue ({line})" - ) diff --git a/utils.py b/utils.py deleted file mode 100644 index 50e0cf6..0000000 --- a/utils.py +++ /dev/null @@ -1,27 +0,0 @@ -""" -Utils functions to reuse in different parts of the codebase -""" - -import os - -from git.cmd import Git - - -def find_files(path_to_folder_from_project_root=".", file_extension=None): - """ - Returns all files in a current git repo. - The list of returned files may be filtered with `file_extension` param. - """ - all_files = [ - path - for path in Git( - Git(path_to_folder_from_project_root).rev_parse("--show-toplevel") - ) - .ls_files() - .split("\n") - if os.path.isfile(path) - ] - if file_extension is not None: - return list(filter(lambda path: path.endswith(file_extension), all_files)) - - return all_files From 0f714fdb09bdfaa41d0c6ea191df61a4ed110295 Mon Sep 17 00:00:00 2001 From: Sfonxu Date: Mon, 6 Oct 2025 14:35:10 +0200 Subject: [PATCH 07/17] update hook versions --- .pre-commit-config.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1dec1e0..74a1565 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -3,17 +3,17 @@ default_stages: [pre-commit] repos: - repo: https://github.com/psf/black - rev: 25.1.0 + rev: 25.9.0 hooks: - id: black - repo: https://github.com/timothycrosley/isort - rev: 6.0.1 + rev: 6.1.0 hooks: - id: isort - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v5.0.0 + rev: v6.0.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer From d9a4c2878e85f1de4730a932126c60f247a9cbb0 Mon Sep 17 00:00:00 2001 From: Sfonxu Date: Mon, 6 Oct 2025 14:44:17 +0200 Subject: [PATCH 08/17] Add missing folder in tree --- .github/workflows/checks.yml | 1 - .pre-commit-config.yaml | 3 +- devops_tests/__init__.py | 0 devops_tests/pre_commit_hooks/__init__.py | 0 devops_tests/pre_commit_hooks/check_badges.py | 95 ++++++++++++++++ .../pre_commit_hooks/check_notebooks.py | 104 ++++++++++++++++++ devops_tests/test_notebooks.py | 46 ++++++++ devops_tests/utils.py | 45 ++++++++ 8 files changed, 291 insertions(+), 3 deletions(-) create mode 100644 devops_tests/__init__.py create mode 100644 devops_tests/pre_commit_hooks/__init__.py create mode 100644 devops_tests/pre_commit_hooks/check_badges.py create mode 100755 devops_tests/pre_commit_hooks/check_notebooks.py create mode 100644 devops_tests/test_notebooks.py create mode 100644 devops_tests/utils.py diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 9d7d9c9..01946ca 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -51,4 +51,3 @@ jobs: - env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: pytest -vv -rP -We . - diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 74a1565..3153766 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,8 +1,7 @@ -exclude: '.git' default_stages: [pre-commit] repos: - - repo: https://github.com/psf/black + - repo: https://github.com/psf/black-pre-commit-mirror rev: 25.9.0 hooks: - id: black diff --git a/devops_tests/__init__.py b/devops_tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/devops_tests/pre_commit_hooks/__init__.py b/devops_tests/pre_commit_hooks/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/devops_tests/pre_commit_hooks/check_badges.py b/devops_tests/pre_commit_hooks/check_badges.py new file mode 100644 index 0000000..4bce726 --- /dev/null +++ b/devops_tests/pre_commit_hooks/check_badges.py @@ -0,0 +1,95 @@ +from __future__ import annotations + +import argparse +from collections.abc import Sequence + +import nbformat +from utils import relative_path, repo_path + +COLAB_HEADER = f"""import sys +if 'google.colab' in sys.modules: + !pip --quiet install open-atmos-jupyter-utils + from open_atmos_jupyter_utils import pip_install_on_colab + pip_install_on_colab('{repo_path().name}-examples')""" + + +def _preview_badge_markdown(absolute_path): + svg_badge_url = ( + "https://img.shields.io/static/v1?" + + "label=render%20on&logo=github&color=87ce3e&message=GitHub" + ) + link = ( + f"https://github.com/open-atmos/{repo_path().name}/blob/main/" + + f"{relative_path(absolute_path)}" + ) + return f"[![preview notebook]({svg_badge_url})]({link})" + + +def _mybinder_badge_markdown(absolute_path): + svg_badge_url = "https://mybinder.org/badge_logo.svg" + link = ( + f"https://mybinder.org/v2/gh/open-atmos/{repo_path().name}.git/main?urlpath=lab/tree/" + + f"{relative_path(absolute_path)}" + ) + return f"[![launch on mybinder.org]({svg_badge_url})]({link})" + + +def _colab_badge_markdown(absolute_path): + svg_badge_url = "https://colab.research.google.com/assets/colab-badge.svg" + link = ( + f"https://colab.research.google.com/github/open-atmos/{repo_path().name}/blob/main/" + + f"{relative_path(absolute_path)}" + ) + return f"[![launch on Colab]({svg_badge_url})]({link})" + + +def test_first_cell_contains_three_badges(notebook_filename): + """checks if all notebooks feature Github preview, mybinder and Colab badges + (in the first cell)""" + with open(notebook_filename, encoding="utf8") as fp: + nb = nbformat.read(fp, nbformat.NO_CONVERT) + assert len(nb.cells) > 0 + assert nb.cells[0].cell_type == "markdown" + lines = nb.cells[0].source.split("\n") + assert len(lines) == 3 + assert lines[0] == _preview_badge_markdown(notebook_filename) + assert lines[1] == _mybinder_badge_markdown(notebook_filename) + assert lines[2] == _colab_badge_markdown(notebook_filename) + + +def test_second_cell_is_a_markdown_cell(notebook_filename): + """checks if all notebooks have their second cell with some markdown + (hopefully clarifying what the example is about)""" + with open(notebook_filename, encoding="utf8") as fp: + nb = nbformat.read(fp, nbformat.NO_CONVERT) + assert len(nb.cells) > 1 + assert nb.cells[1].cell_type == "markdown" + + +def test_third_cell_contains_colab_header(notebook_filename): + """checks if all notebooks feature a Colab-magic cell""" + with open(notebook_filename, encoding="utf8") as fp: + nb = nbformat.read(fp, nbformat.NO_CONVERT) + assert len(nb.cells) > 2 + assert nb.cells[2].cell_type == "code" + assert nb.cells[2].source == COLAB_HEADER + + +def main(argv: Sequence[str] | None = None) -> int: + parser = argparse.ArgumentParser() + parser.add_argument("filenames", nargs="*", help="Filenames to check.") + args = parser.parse_args(argv) + + retval = 0 + for filename in args.filenames: + try: + test_first_cell_contains_three_badges(filename) + test_second_cell_is_a_markdown_cell(filename) + test_third_cell_contains_colab_header(filename) + except ValueError as exc: + retval = 1 + return retval + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/devops_tests/pre_commit_hooks/check_notebooks.py b/devops_tests/pre_commit_hooks/check_notebooks.py new file mode 100755 index 0000000..5f6a667 --- /dev/null +++ b/devops_tests/pre_commit_hooks/check_notebooks.py @@ -0,0 +1,104 @@ +#!/usr/bin/env python3 + +from __future__ import annotations + +import argparse +import os +from collections.abc import Sequence + +import nbformat + + +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 != "": + assert cell.execution_count is not None + + +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 AssertionError(output["text"]) + + +def test_jupyter_utils_used_intead_of_matplotlib(notebooks): + def test_show_plot_used_instead_of_matplotlib(notebook): + """checks if plotting is done with open_atmos_jupyter_utils show_plot()""" + matplot_used = False + show_plot_used = False + for cell in notebook.cells: + if cell.cell_type == "code": + if ( + "pyplot.show()" in cell.source + or "plt.show()" in cell.source + or "from matplotlib import pyplot" in cell.source + ): + matplot_used = True + if "show_plot()" in cell.source: + show_plot_used = True + if matplot_used and not show_plot_used: + raise AssertionError( + "if using matplotlib, please use open_atmos_jupyter_utils.show_plot()" + ) + + def test_show_anim_used_instead_of_matplotlib(notebook): + """checks if animation generation is done with open_atmos_jupyter_utils show_anim()""" + matplot_used = False + show_anim_used = False + for cell in notebook.cells: + if cell.cell_type == "code": + if ( + "funcAnimation" in cell.source + or "matplotlib.animation" in cell.source + or "from matplotlib import animation" in cell.source + ): + matplot_used = True + if "show_anim()" in cell.source: + show_anim_used = True + if matplot_used and not show_anim_used: + raise AssertionError( + """if using matplotlib for animations, + please use open_atmos_jupyter_utils.show_anim()""" + ) + + +def test_jetbrains_bug_py_66491(notebook): + """checks if all notebook have the execution_count key for each cell in JSON, which is required by GitHub renderer and may not be generated by some buggy PyCharm versions: https://youtrack.jetbrains.com/issue/PY-66491""" + for cell in notebook.cells: + if cell.cell_type == "code" and not hasattr(cell, "execution_count"): + raise AssertionError( + "Notebook cell missing execution_count attribute. (May be due to PyCharm bug, see: https://youtrack.jetbrains.com/issue/PY-66491 )" + ) + return 1 + return 0 + + +def main(argv: Sequence[str] | None = None) -> int: + parser = argparse.ArgumentParser() + parser.add_argument("filenames", nargs="*", help="Filenames to check.") + args = parser.parse_args(argv) + + retval = 0 + print(args.filenames) + for filename in args.filenames: + with open(filename, encoding="utf8") as notebook_file: + notebook = nbformat.read(notebook_file, nbformat.NO_CONVERT) + try: + test_jetbrains_bug_py_66491(notebook) + test_cell_contains_output(notebook) + # retval |= test_no_errors_or_warnings_in_output(notebook) + except Exception as e: + print(e) + retval = 1 + return retval + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/devops_tests/test_notebooks.py b/devops_tests/test_notebooks.py new file mode 100644 index 0000000..abcd991 --- /dev/null +++ b/devops_tests/test_notebooks.py @@ -0,0 +1,46 @@ +import sys + +if sys.platform == "win32" and sys.version_info[:2] >= (3, 7): + import asyncio + + asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) + +import gc +import os +import warnings + +import nbformat +import pytest + +from .utils import find_files + +with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + from nbconvert.preprocessors import ExecutePreprocessor + + +@pytest.fixture( + params=find_files(file_extension=".ipynb"), + name="notebook_filename", +) +def _notebook_filename(request): + return request.param + + +def test_run_notebooks(notebook_filename, tmp_path): + """executes a given notebook""" + os.environ["JUPYTER_PLATFORM_DIRS"] = "1" + + executor = ExecutePreprocessor(timeout=15 * 60, kernel_name="python3") + + with open(notebook_filename, encoding="utf8") as notebook_file: + # https://github.com/pytest-dev/pytest-asyncio/issues/212 + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", message="There is no current event loop") + executor.preprocess( + nbformat.read(notebook_file, as_version=4), + {"metadata": {"path": tmp_path}}, + ) + + # so that nbconvert perplexities are reported here, and not at some dtor test later on + gc.collect() diff --git a/devops_tests/utils.py b/devops_tests/utils.py new file mode 100644 index 0000000..948be01 --- /dev/null +++ b/devops_tests/utils.py @@ -0,0 +1,45 @@ +""" +Utils functions to reuse in different parts of the codebase +""" + +import os +import pathlib + +from git.cmd import Git + + +def find_files(path_to_folder_from_project_root=".", file_extension=None): + """ + Returns all files in a current git repo. + The list of returned files may be filtered with `file_extension` param. + """ + all_files = [ + path + for path in Git( + Git(path_to_folder_from_project_root).rev_parse("--show-toplevel") + ) + .ls_files() + .split("\n") + if os.path.isfile(path) + ] + if file_extension is not None: + return list(filter(lambda path: path.endswith(file_extension), all_files)) + + return all_files + + +def relative_path(absolute_path): + """returns a path relative to the repo base (converting backslashes to slashes on Windows)""" + relpath = os.path.relpath(absolute_path, repo_path().absolute()) + posixpath_to_make_it_usable_in_urls_even_on_windows = pathlib.Path( + relpath + ).as_posix() + return posixpath_to_make_it_usable_in_urls_even_on_windows + + +def repo_path(): + """returns absolute path to the repo base (ignoring .git location if in a submodule)""" + path = pathlib.Path(__file__) + while not (path.is_dir() and Git(path).rev_parse("--git-dir") == ".git"): + path = path.parent + return path From 6bc1506d2effbbb5f13e600554efe120b6a88b30 Mon Sep 17 00:00:00 2001 From: Sfonxu Date: Mon, 6 Oct 2025 14:58:53 +0200 Subject: [PATCH 09/17] Refactor try-catch blocks --- .../pre_commit_hooks/check_notebooks.py | 96 ++++++++++--------- 1 file changed, 50 insertions(+), 46 deletions(-) diff --git a/devops_tests/pre_commit_hooks/check_notebooks.py b/devops_tests/pre_commit_hooks/check_notebooks.py index 5f6a667..6230005 100755 --- a/devops_tests/pre_commit_hooks/check_notebooks.py +++ b/devops_tests/pre_commit_hooks/check_notebooks.py @@ -28,45 +28,45 @@ def test_no_errors_or_warnings_in_output(notebook): raise AssertionError(output["text"]) -def test_jupyter_utils_used_intead_of_matplotlib(notebooks): - def test_show_plot_used_instead_of_matplotlib(notebook): - """checks if plotting is done with open_atmos_jupyter_utils show_plot()""" - matplot_used = False - show_plot_used = False - for cell in notebook.cells: - if cell.cell_type == "code": - if ( - "pyplot.show()" in cell.source - or "plt.show()" in cell.source - or "from matplotlib import pyplot" in cell.source - ): - matplot_used = True - if "show_plot()" in cell.source: - show_plot_used = True - if matplot_used and not show_plot_used: - raise AssertionError( - "if using matplotlib, please use open_atmos_jupyter_utils.show_plot()" - ) - - def test_show_anim_used_instead_of_matplotlib(notebook): - """checks if animation generation is done with open_atmos_jupyter_utils show_anim()""" - matplot_used = False - show_anim_used = False - for cell in notebook.cells: - if cell.cell_type == "code": - if ( - "funcAnimation" in cell.source - or "matplotlib.animation" in cell.source - or "from matplotlib import animation" in cell.source - ): - matplot_used = True - if "show_anim()" in cell.source: - show_anim_used = True - if matplot_used and not show_anim_used: - raise AssertionError( - """if using matplotlib for animations, - please use open_atmos_jupyter_utils.show_anim()""" - ) +def test_show_plot_used_instead_of_matplotlib(notebook): + """checks if plotting is done with open_atmos_jupyter_utils show_plot()""" + matplot_used = False + show_plot_used = False + for cell in notebook.cells: + if cell.cell_type == "code": + if ( + "pyplot.show()" in cell.source + or "plt.show()" in cell.source + or "from matplotlib import pyplot" in cell.source + ): + matplot_used = True + if "show_plot()" in cell.source: + show_plot_used = True + if matplot_used and not show_plot_used: + raise AssertionError( + "if using matplotlib, please use open_atmos_jupyter_utils.show_plot()" + ) + + +def test_show_anim_used_instead_of_matplotlib(notebook): + """checks if animation generation is done with open_atmos_jupyter_utils show_anim()""" + matplot_used = False + show_anim_used = False + for cell in notebook.cells: + if cell.cell_type == "code": + if ( + "funcAnimation" in cell.source + or "matplotlib.animation" in cell.source + or "from matplotlib import animation" in cell.source + ): + matplot_used = True + if "show_anim()" in cell.source: + show_anim_used = True + if matplot_used and not show_anim_used: + raise AssertionError( + """if using matplotlib for animations, + please use open_atmos_jupyter_utils.show_anim()""" + ) def test_jetbrains_bug_py_66491(notebook): @@ -87,16 +87,20 @@ def main(argv: Sequence[str] | None = None) -> int: retval = 0 print(args.filenames) + test_functions = [ + test_cell_contains_output, + test_no_errors_or_warnings_in_output, + test_jetbrains_bug_py_66491, + ] for filename in args.filenames: with open(filename, encoding="utf8") as notebook_file: notebook = nbformat.read(notebook_file, nbformat.NO_CONVERT) - try: - test_jetbrains_bug_py_66491(notebook) - test_cell_contains_output(notebook) - # retval |= test_no_errors_or_warnings_in_output(notebook) - except Exception as e: - print(e) - retval = 1 + for func in test_functions: + try: + func(notebook) + except Exception as e: + print(e) + retval = 1 return retval From 8bfb9dc5c09311451b05457e6a5560135abf6e44 Mon Sep 17 00:00:00 2001 From: Sfonxu Date: Mon, 6 Oct 2025 15:19:09 +0200 Subject: [PATCH 10/17] Fix import errors --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 2873f70..69530f3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ requires = ["setuptools >= 77.0.3"] build-backend = "setuptools.build_meta" [tool.setuptools] -packages = ['devops_tests'] +packages = ['devops_tests', 'devops_tests.pre_commit_hooks'] [project] name = "devops_tests" From 7913984e5c63835d1b0280e6c60239438a672f7e Mon Sep 17 00:00:00 2001 From: Sfonxu Date: Mon, 6 Oct 2025 15:45:11 +0200 Subject: [PATCH 11/17] Add filter for chec-notebooks hook --- .pre-commit-config.yaml | 8 -------- .pre-commit-hooks.yaml | 1 + 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3153766..27cefb6 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -22,11 +22,3 @@ repos: rev: 2.0.1 hooks: - id: enforce-notebook-run-order - - - repo: local - hooks: - - id: check-notebooks - name: check notebooks - entry: check_notebooks - language: python - files: '.ipynb' diff --git a/.pre-commit-hooks.yaml b/.pre-commit-hooks.yaml index f0bd68b..c49acc1 100644 --- a/.pre-commit-hooks.yaml +++ b/.pre-commit-hooks.yaml @@ -4,3 +4,4 @@ entry: check_notebooks language: python stages: [pre-commit] + types: [jupyter] From d98a364e60406ef889eb632a108b2b2514388168 Mon Sep 17 00:00:00 2001 From: Sfonxu Date: Mon, 6 Oct 2025 15:46:29 +0200 Subject: [PATCH 12/17] Remove debug print --- devops_tests/pre_commit_hooks/check_notebooks.py | 1 - 1 file changed, 1 deletion(-) diff --git a/devops_tests/pre_commit_hooks/check_notebooks.py b/devops_tests/pre_commit_hooks/check_notebooks.py index 6230005..2da97e7 100755 --- a/devops_tests/pre_commit_hooks/check_notebooks.py +++ b/devops_tests/pre_commit_hooks/check_notebooks.py @@ -86,7 +86,6 @@ def main(argv: Sequence[str] | None = None) -> int: args = parser.parse_args(argv) retval = 0 - print(args.filenames) test_functions = [ test_cell_contains_output, test_no_errors_or_warnings_in_output, From fc890b33f44cd8f7b26ca33e862ef834d1351e9c Mon Sep 17 00:00:00 2001 From: Sfonxu Date: Mon, 6 Oct 2025 15:49:54 +0200 Subject: [PATCH 13/17] Add exception messages --- devops_tests/pre_commit_hooks/check_notebooks.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/devops_tests/pre_commit_hooks/check_notebooks.py b/devops_tests/pre_commit_hooks/check_notebooks.py index 2da97e7..696ee6c 100755 --- a/devops_tests/pre_commit_hooks/check_notebooks.py +++ b/devops_tests/pre_commit_hooks/check_notebooks.py @@ -13,7 +13,8 @@ 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 != "": - assert cell.execution_count is not None + if cell.execution_count is None: + raise Exception(f"{notebook}: Cell does not contain output!") def test_no_errors_or_warnings_in_output(notebook): @@ -25,7 +26,7 @@ def test_no_errors_or_warnings_in_output(notebook): for output in cell.outputs: if "name" in output and output["name"] == "stderr": if not output["text"].startswith("[Parallel(n_jobs="): - raise AssertionError(output["text"]) + raise Exception(output["text"]) def test_show_plot_used_instead_of_matplotlib(notebook): @@ -43,7 +44,7 @@ def test_show_plot_used_instead_of_matplotlib(notebook): if "show_plot()" in cell.source: show_plot_used = True if matplot_used and not show_plot_used: - raise AssertionError( + raise Exception( "if using matplotlib, please use open_atmos_jupyter_utils.show_plot()" ) @@ -73,11 +74,9 @@ def test_jetbrains_bug_py_66491(notebook): """checks if all notebook have the execution_count key for each cell in JSON, which is required by GitHub renderer and may not be generated by some buggy PyCharm versions: https://youtrack.jetbrains.com/issue/PY-66491""" for cell in notebook.cells: if cell.cell_type == "code" and not hasattr(cell, "execution_count"): - raise AssertionError( + raise Exception( "Notebook cell missing execution_count attribute. (May be due to PyCharm bug, see: https://youtrack.jetbrains.com/issue/PY-66491 )" ) - return 1 - return 0 def main(argv: Sequence[str] | None = None) -> int: From b097c259401d5d4daa4705d3d3cbb350abda47d7 Mon Sep 17 00:00:00 2001 From: Sfonxu Date: Mon, 6 Oct 2025 15:51:53 +0200 Subject: [PATCH 14/17] Update error messages to avoid printing the whole notebook --- devops_tests/pre_commit_hooks/check_notebooks.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/devops_tests/pre_commit_hooks/check_notebooks.py b/devops_tests/pre_commit_hooks/check_notebooks.py index 696ee6c..860de73 100755 --- a/devops_tests/pre_commit_hooks/check_notebooks.py +++ b/devops_tests/pre_commit_hooks/check_notebooks.py @@ -14,7 +14,7 @@ def test_cell_contains_output(notebook): for cell in notebook.cells: if cell.cell_type == "code" and cell.source != "": if cell.execution_count is None: - raise Exception(f"{notebook}: Cell does not contain output!") + raise Exception("Cell does not contain output!") def test_no_errors_or_warnings_in_output(notebook): @@ -97,7 +97,7 @@ def main(argv: Sequence[str] | None = None) -> int: try: func(notebook) except Exception as e: - print(e) + print(f"{filename} : {e}") retval = 1 return retval From df0020da110da50196b7b4b57304f2541e166cdc Mon Sep 17 00:00:00 2001 From: Sfonxu Date: Mon, 6 Oct 2025 15:55:12 +0200 Subject: [PATCH 15/17] Add show_anim/show_plot tests --- devops_tests/pre_commit_hooks/check_notebooks.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/devops_tests/pre_commit_hooks/check_notebooks.py b/devops_tests/pre_commit_hooks/check_notebooks.py index 860de73..0101a8d 100755 --- a/devops_tests/pre_commit_hooks/check_notebooks.py +++ b/devops_tests/pre_commit_hooks/check_notebooks.py @@ -89,6 +89,8 @@ def main(argv: Sequence[str] | None = None) -> int: 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: From 837ae14fd945b49daa901443feec121807789135 Mon Sep 17 00:00:00 2001 From: Sfonxu Date: Mon, 6 Oct 2025 15:57:56 +0200 Subject: [PATCH 16/17] Change logic in show_anim/show_plot --- devops_tests/pre_commit_hooks/check_notebooks.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/devops_tests/pre_commit_hooks/check_notebooks.py b/devops_tests/pre_commit_hooks/check_notebooks.py index 0101a8d..0176638 100755 --- a/devops_tests/pre_commit_hooks/check_notebooks.py +++ b/devops_tests/pre_commit_hooks/check_notebooks.py @@ -41,7 +41,7 @@ def test_show_plot_used_instead_of_matplotlib(notebook): or "from matplotlib import pyplot" in cell.source ): matplot_used = True - if "show_plot()" in cell.source: + if "show_plot(" in cell.source: show_plot_used = True if matplot_used and not show_plot_used: raise Exception( @@ -61,7 +61,7 @@ def test_show_anim_used_instead_of_matplotlib(notebook): or "from matplotlib import animation" in cell.source ): matplot_used = True - if "show_anim()" in cell.source: + if "show_anim(" in cell.source: show_anim_used = True if matplot_used and not show_anim_used: raise AssertionError( From 798a43fde51864f51f26725642dd2176b10dbe83 Mon Sep 17 00:00:00 2001 From: Sfonxu Date: Mon, 6 Oct 2025 16:07:26 +0200 Subject: [PATCH 17/17] More logic fixes --- devops_tests/pre_commit_hooks/check_notebooks.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/devops_tests/pre_commit_hooks/check_notebooks.py b/devops_tests/pre_commit_hooks/check_notebooks.py index 0176638..7b01e5f 100755 --- a/devops_tests/pre_commit_hooks/check_notebooks.py +++ b/devops_tests/pre_commit_hooks/check_notebooks.py @@ -35,11 +35,7 @@ def test_show_plot_used_instead_of_matplotlib(notebook): show_plot_used = False for cell in notebook.cells: if cell.cell_type == "code": - if ( - "pyplot.show()" in cell.source - or "plt.show()" in cell.source - or "from matplotlib import pyplot" in cell.source - ): + if "pyplot.show(" in cell.source or "plt.show(" in cell.source: matplot_used = True if "show_plot(" in cell.source: show_plot_used = True