diff --git a/.github/workflows/test-notebooks.yml b/.github/workflows/test-notebooks.yml new file mode 100644 index 000000000..e18ded7c8 --- /dev/null +++ b/.github/workflows/test-notebooks.yml @@ -0,0 +1,46 @@ +name: Test notebooks + +on: + push: + branches: [main] + pull_request: + branches: [main] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + test: + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest] + python: ["3.11"] + + steps: + - uses: actions/checkout@v3 + with: + submodules: recursive + - name: Set up Python ${{ matrix.python }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python }} + + - uses: astral-sh/setup-uv@v5 + with: + enable-cache: true + python-version: ${{ matrix.python }} + cache-dependency-glob: pyproject.toml + - name: Create notebooks environment + run: uvx hatch -v env create notebooks + - name: Test notebooks + env: + MPLBACKEND: agg + PLATFORM: ${{ matrix.os }} + DISPLAY: :42 + + run: | + uvx hatch run notebooks:setup-squidpy-kernel + uvx hatch run notebooks:run-notebooks diff --git a/.scripts/ci/run_notebooks.py b/.scripts/ci/run_notebooks.py new file mode 100644 index 000000000..8ab221a32 --- /dev/null +++ b/.scripts/ci/run_notebooks.py @@ -0,0 +1,96 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import argparse +import glob +import os +import subprocess +import sys + +EPILOG = """ +Examples: + python run_notebooks.py docs/notebooks + python run_notebooks.py /path/to/notebooks --kernel my-kernel +""" + + +def main() -> None: + # Set up argument parser + parser = argparse.ArgumentParser( + description="Run Jupyter notebooks in specified directories using jupytext", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=EPILOG, + ) + + parser.add_argument("base_directory", help="Base directory containing notebook subdirectories") + + parser.add_argument( + "-k", "--kernel", default="squidpy", help="Jupyter kernel to use for execution (default: squidpy)" + ) + + parser.add_argument( + "--dry-run", action="store_true", help="Show which notebooks would be run without executing them" + ) + + args = parser.parse_args() + + # Base directory for notebooks + base_dir = args.base_directory + + # Define notebook directories or patterns + notebook_patterns = [ + f"{base_dir}/examples/tools/*.ipynb", + f"{base_dir}/examples/plotting/*.ipynb", + f"{base_dir}/examples/image/*.ipynb", + f"{base_dir}/examples/graph/*.ipynb", + # f"{base_dir}/tutorials/*.ipynb" # don't include because it contains many external modules + ] + + # Initialize a list to hold valid notebook paths + valid_notebooks = [] + + # Gather all valid notebook files from the patterns + print("Gathering notebooks...") + for pattern in notebook_patterns: + for nb_path in glob.glob(pattern): + if os.path.isfile(nb_path): # Check if the file exists + valid_notebooks.append(nb_path) # Add to the list of valid notebooks + + # Check if we have any notebooks to run + if len(valid_notebooks) == 0: + print("No notebooks found to run.") + sys.exit(1) + + # Echo the notebooks that will be run for clarity + print("Preparing to run the following notebooks:") + for nb in valid_notebooks: + print(f" {nb}") + + # If dry run, exit here + if args.dry_run: + print(f"\nDry run complete. Would execute {len(valid_notebooks)} notebooks with kernel '{args.kernel}'.") + return + + # Initialize a flag to track the success of all commands + all_success = True + + # Execute all valid notebooks + print(f"\nExecuting notebooks with kernel '{args.kernel}'...") + for nb in valid_notebooks: + print(f"Running {nb}") + try: + subprocess.run(["jupytext", "-k", args.kernel, "--execute", nb], check=True) + except subprocess.CalledProcessError: + print(f"Failed to run {nb}") + all_success = False + + # Check if any executions failed + if not all_success: + print("One or more notebooks failed to execute.") + sys.exit(1) + + print("All notebooks executed successfully.") + + +if __name__ == "__main__": + main() diff --git a/docs/index.rst b/docs/index.rst index ebc70d665..8f97ee5f9 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -77,6 +77,7 @@ We are happy about any contributions! Before you start, check out our `contribut notebooks/tutorials/index notebooks/examples/index + notebooks/deprecated_features/index .. |PyPI| image:: https://img.shields.io/pypi/v/squidpy.svg :target: https://pypi.org/project/squidpy/ diff --git a/docs/notebooks b/docs/notebooks index 4f47e47bf..98d97da49 160000 --- a/docs/notebooks +++ b/docs/notebooks @@ -1 +1 @@ -Subproject commit 4f47e47bf66da170963a2252c5c699b77bb050d9 +Subproject commit 98d97da49528e4229548a315a88569ebfc618942 diff --git a/hatch.toml b/hatch.toml index e9a942f9a..388594bf6 100644 --- a/hatch.toml +++ b/hatch.toml @@ -56,4 +56,20 @@ python = [ "3.13" ] # set the environment variable `UV_PRERELEASE` to "allow". matrix.deps.env-vars = [ { key = "UV_PRERELEASE", value = "allow", if = [ "pre" ] }, -] \ No newline at end of file +] + +[envs.notebooks] +extra-dependencies = [ + "ipykernel", + "jupytext", + "nbconvert", + "leidenalg", + "watermark", + "napari-spatialdata", +] +extras = ["docs"] + +[envs.notebooks.scripts] + +setup-squidpy-kernel = "python -m ipykernel install --user --name=squidpy" +run-notebooks = "python ./.scripts/ci/run_notebooks.py docs/notebooks" \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index e8968966c..d65304b4f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -281,4 +281,3 @@ exclude_lines = [ show_missing = true precision = 2 skip_empty = true -sort = "Miss" \ No newline at end of file