Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 20 additions & 23 deletions .github/workflows/execute.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: Execute notebooks

on:
schedule:
# Weekly: Mondays 04:00 UTC re-executes ALL notebooks against latest releases.
# Weekly: Mondays 04:00 UTC; re-executes ALL notebooks against latest releases.
- cron: "0 4 * * 1"
pull_request:
branches: [main]
Expand All @@ -22,36 +22,45 @@ jobs:
timeout-minutes: 60
steps:
- uses: actions/checkout@v5
with:
# Need full history so we can diff PR HEAD against the merge base.
fetch-depth: 0

- uses: actions/setup-python@v5
with:
python-version: "3.13"
cache: pip
cache-dependency-path: pyproject.toml

- name: Cache pooch datasets
uses: actions/cache@v4
with:
path: ~/.cache/pooch
key: pooch-${{ runner.os }}-${{ hashFiles('**/*.ipynb') }}
# Datasets are pinned by URL inside notebook code, not by the
# notebook's surrounding markdown — bump the v* suffix when an
# actually-new dataset URL lands.
key: pooch-${{ runner.os }}-v1
restore-keys: |
pooch-${{ runner.os }}-

- name: Install execution environment
run: |
pip install --upgrade pip
pip install -e ".[exec]" nbdime
pip install -e ".[exec]"

- name: Detect changed notebooks (PR only)
if: github.event_name == 'pull_request'
id: changed
uses: tj-actions/changed-files@v45
with:
files: |
tutorials/**/*.ipynb
examples/**/*.ipynb

- name: Determine notebooks to execute
id: pick
run: |
# On the weekly schedule and manual dispatch, run all notebooks.
# On PR, run only notebooks the PR touched, to keep CI fast as the
# gallery grows. (sklearn does the same.)
if [ "${{ github.event_name }}" = "pull_request" ]; then
base="${{ github.event.pull_request.base.sha }}"
nbs=$(git diff --name-only --diff-filter=AMR "$base"...HEAD -- '*.ipynb' | grep -E '^(tutorials|examples)/' || true)
nbs="${{ steps.changed.outputs.all_changed_files }}"
# tj-actions emits space-separated; one-per-line for the loop below.
nbs=$(printf '%s\n' $nbs)
else
nbs=$(find tutorials examples -name "*.ipynb" -not -path "*/.ipynb_checkpoints/*" 2>/dev/null || true)
fi
Expand All @@ -75,15 +84,3 @@ jobs:
echo "Executing $nb"
jupyter nbconvert --to notebook --execute --inplace "$nb"
done <<< "${{ steps.pick.outputs.files }}"

- name: Diff outputs against committed
if: steps.pick.outputs.files != ''
run: |
# If outputs drift from what's committed, fail the job. Authors are
# expected to commit re-executed notebooks; CI catches drift between
# commits (e.g., upstream lib changes).
if ! git diff --quiet -- '*.ipynb'; then
echo "Notebook outputs drifted from committed state:"
nbdime diff
exit 1
fi
6 changes: 4 additions & 2 deletions .github/workflows/preview.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ jobs:
with:
repository: scverse/spatialdata-plot
path: lib
fetch-depth: 0

- name: Mount PR notebooks into lib's submodule path
run: |
Expand Down Expand Up @@ -69,7 +68,10 @@ jobs:
const pr = context.issue.number;
const owner = context.repo.owner;
const repo = context.repo.repo;
const url = `https://${owner}.github.io/${repo}/pr-${pr}/gallery.html`;
// Use the canonical scverse.org URL directly. The github.io URL
// 301-redirects there because of the org-wide CNAME, which makes
// the displayed link confusingly different from the destination.
const url = `https://scverse.org/${repo}/pr-${pr}/gallery.html`;
const marker = '<!-- preview-link -->';
const body = `${marker}\n📖 **Docs preview**: ${url}\n\n_Built from ${context.sha.substring(0, 7)}; redeployed on every push._`;
const { data: comments } = await github.rest.issues.listComments({ owner, repo, issue_number: pr });
Expand Down
36 changes: 32 additions & 4 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,11 +1,27 @@
# pre-commit config. Auto-run on PRs by pre-commit.ci (see ci: block below);
# run locally with `pre-commit run --all-files` after `pip install pre-commit`.

ci:
# Auto-fix PRs where possible; tag the commit so it's clearly bot-authored.
autofix_commit_msg: |
[pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci
autofix_prs: true
autoupdate_branch: ""
autoupdate_commit_msg: "[pre-commit.ci] pre-commit autoupdate"
autoupdate_schedule: monthly
skip: []
submodules: false

fail_fast: false
default_language_version:
python: python3
minimum_pre_commit_version: 3.0.0

repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
rev: v6.0.0
hooks:
- id: check-yaml
- id: check-toml
Expand All @@ -20,14 +36,26 @@ repos:
exclude: \.ipynb$

- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.7.4
rev: v0.15.12
hooks:
- id: ruff
- id: ruff-check
args: [--fix]
- id: ruff-format
# Notebook cells use a chained fluent API; ruff-format collapses
# short chains onto one line which hurts readability. Lint notebooks
# via nbqa-ruff (below) instead.
exclude: \.ipynb$

- repo: https://github.com/nbQA-dev/nbQA
rev: 1.9.0
rev: 1.9.1
hooks:
- id: nbqa-ruff
args: [--fix]

- repo: local
hooks:
- id: strip-widget-metadata
name: Strip Jupyter widget metadata from notebooks
entry: python scripts/strip_widget_metadata.py
language: system
files: \.ipynb$
13 changes: 6 additions & 7 deletions examples/index.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
# Examples

Short, focused notebooks demonstrating individual `spatialdata-plot` features.
Use these as a quick reference when you know what you want to do and need to
see the call shape.
Worked examples showing `spatialdata-plot` on real spatial-omics datasets.
Use these to see how the API composes on data you'd actually analyse.

```{note}
No examples yet — contributions welcome! See
[CONTRIBUTING](https://github.com/scverse/spatialdata-plot-notebooks/blob/main/CONTRIBUTING.md)
for how to add one.
```{toctree}
:maxdepth: 1

visium_mouse_brain
```
429 changes: 429 additions & 0 deletions examples/visium_mouse_brain.ipynb

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ exec = [
"squidpy",
"jupyter",
"ipykernel",
"watermark",
]
dev = [
"pre-commit>=3.0",
Expand Down
56 changes: 56 additions & 0 deletions scripts/strip_widget_metadata.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
#!/usr/bin/env python3
"""Strip Jupyter widget metadata + outputs from .ipynb files.

Anything using `tqdm.notebook` (pooch downloads, scanpy progress bars in a
Jupyter kernel) emits widget output blobs whose UUIDs regenerate on every
execution. Stripping them keeps committed notebooks small and produces
clean PR diffs that reflect real source changes only.

Usage: strip_widget_metadata.py <notebook> [<notebook> ...]
Exits 0 if no changes, 1 if files were modified (so pre-commit reports the
fix the way other auto-fixers do).
"""

from __future__ import annotations

import json
import sys
from pathlib import Path

WIDGET_MIME = "application/vnd.jupyter.widget-view+json"


def strip(path: Path) -> bool:
nb = json.loads(path.read_text())
changed = False

if "widgets" in nb.get("metadata", {}):
nb["metadata"].pop("widgets")
changed = True

for cell in nb.get("cells", []):
for output in cell.get("outputs", []):
data = output.get("data", {})
if WIDGET_MIME in data:
data.pop(WIDGET_MIME)
changed = True

if changed:
path.write_text(json.dumps(nb, indent=1) + "\n")
return changed


def main() -> int:
if len(sys.argv) < 2:
print("usage: strip_widget_metadata.py <notebook> [<notebook> ...]", file=sys.stderr)
return 2
any_changed = False
for arg in sys.argv[1:]:
if strip(Path(arg)):
print(f"stripped widgets: {arg}")
any_changed = True
return 1 if any_changed else 0


if __name__ == "__main__":
sys.exit(main())
172 changes: 113 additions & 59 deletions tutorials/getting_started.ipynb

Large diffs are not rendered by default.

Loading