Skip to content

Commit b10b521

Browse files
committed
Wire pre-commit.ci, fix lint/execute drift, use canonical preview URL
Lint: ruff-format was collapsing the fluent .pl chains onto single lines because they fit under 120 chars. Exclude *.ipynb from ruff-format; lint notebooks via nbqa-ruff only. Execute drift: pooch's tqdm.notebook spawns Jupyter widgets whose UUIDs regenerate on every execution, breaking the diff-against-committed check. Add scripts/strip_widget_metadata.py and run it as both a pre-commit hook and a post-execute step in execute.yaml so committed and re-executed notebooks stay symmetric. pre-commit.ci: add the ci: config block so PRs are auto-fixed by the bot (monthly autoupdate, autofix on every PR). Requires installing the pre-commit.ci GitHub App on the repo (one-time, repo settings -> Apps). Preview URL: the github.io URL 301-redirected through the scverse-org-wide scverse.org CNAME, which made the displayed link confusingly different from the destination. Switch the comment body to the canonical https://scverse.org/<repo>/pr-N/gallery.html URL — same content, no redirect.
1 parent ba19765 commit b10b521

5 files changed

Lines changed: 117 additions & 14 deletions

File tree

.github/workflows/execute.yaml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,19 @@ jobs:
7676
jupyter nbconvert --to notebook --execute --inplace "$nb"
7777
done <<< "${{ steps.pick.outputs.files }}"
7878
79+
- name: Strip widget metadata
80+
if: steps.pick.outputs.files != ''
81+
# `pooch` and other libs spawn `tqdm.notebook` widgets when running
82+
# in a Jupyter kernel; widget UUIDs regenerate on every execution and
83+
# would cause spurious diff failures below. Same hook runs locally
84+
# via .pre-commit-config.yaml so committed and re-executed notebooks
85+
# stay symmetric.
86+
run: |
87+
while IFS= read -r nb; do
88+
[ -z "$nb" ] && continue
89+
python scripts/strip_widget_metadata.py "$nb" || true
90+
done <<< "${{ steps.pick.outputs.files }}"
91+
7992
- name: Diff outputs against committed
8093
if: steps.pick.outputs.files != ''
8194
run: |

.github/workflows/preview.yaml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,10 @@ jobs:
6969
const pr = context.issue.number;
7070
const owner = context.repo.owner;
7171
const repo = context.repo.repo;
72-
const url = `https://${owner}.github.io/${repo}/pr-${pr}/gallery.html`;
72+
// Use the canonical scverse.org URL directly. The github.io URL
73+
// 301-redirects there because of the org-wide CNAME, which makes
74+
// the displayed link confusingly different from the destination.
75+
const url = `https://scverse.org/${repo}/pr-${pr}/gallery.html`;
7376
const marker = '<!-- preview-link -->';
7477
const body = `${marker}\n📖 **Docs preview**: ${url}\n\n_Built from ${context.sha.substring(0, 7)}; redeployed on every push._`;
7578
const { data: comments } = await github.rest.issues.listComments({ owner, repo, issue_number: pr });

.pre-commit-config.yaml

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,19 @@
1+
# pre-commit config. Auto-run on PRs by pre-commit.ci (see ci: block below);
2+
# run locally with `pre-commit run --all-files` after `pip install pre-commit`.
3+
4+
ci:
5+
# Auto-fix PRs where possible; tag the commit so it's clearly bot-authored.
6+
autofix_commit_msg: |
7+
[pre-commit.ci] auto fixes from pre-commit.com hooks
8+
9+
for more information, see https://pre-commit.ci
10+
autofix_prs: true
11+
autoupdate_branch: ""
12+
autoupdate_commit_msg: "[pre-commit.ci] pre-commit autoupdate"
13+
autoupdate_schedule: monthly
14+
skip: []
15+
submodules: false
16+
117
fail_fast: false
218
default_language_version:
319
python: python3
@@ -25,9 +41,22 @@ repos:
2541
- id: ruff
2642
args: [--fix]
2743
- id: ruff-format
44+
# Notebook cells use a chained fluent API; ruff-format collapses
45+
# short chains onto one line which hurts readability. Lint notebooks
46+
# via nbqa-ruff (below) instead.
47+
exclude: \.ipynb$
2848

2949
- repo: https://github.com/nbQA-dev/nbQA
3050
rev: 1.9.0
3151
hooks:
3252
- id: nbqa-ruff
3353
args: [--fix]
54+
55+
- repo: local
56+
hooks:
57+
- id: strip-widget-metadata
58+
name: Strip Jupyter widget metadata from notebooks
59+
entry: python scripts/strip_widget_metadata.py
60+
language: system
61+
files: \.ipynb$
62+
require_serial: false

scripts/strip_widget_metadata.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
#!/usr/bin/env python3
2+
"""Strip Jupyter widget metadata + outputs from .ipynb files.
3+
4+
Widget UUIDs are regenerated on every execution, so any notebook that runs
5+
something using `tqdm.notebook` (e.g. `pooch` downloads, `scanpy` progress
6+
bars in a Jupyter kernel) drifts on every re-execution. Stripping widgets
7+
makes the diff-against-committed check in `execute.yaml` deterministic, and
8+
keeps committed notebooks reproducible without losing the visible outputs
9+
(figures, repr cells, prints).
10+
11+
Usage: strip_widget_metadata.py <notebook> [<notebook> ...]
12+
Exits 0 if no changes, 1 if files were modified (so pre-commit reports the
13+
fix the way other auto-fixers do).
14+
"""
15+
16+
from __future__ import annotations
17+
18+
import json
19+
import sys
20+
from pathlib import Path
21+
22+
WIDGET_MIME = "application/vnd.jupyter.widget-view+json"
23+
24+
25+
def strip(path: Path) -> bool:
26+
nb = json.loads(path.read_text())
27+
changed = False
28+
29+
if "widgets" in nb.get("metadata", {}):
30+
nb["metadata"].pop("widgets")
31+
changed = True
32+
33+
for cell in nb.get("cells", []):
34+
for output in cell.get("outputs", []):
35+
data = output.get("data", {})
36+
if WIDGET_MIME in data:
37+
data.pop(WIDGET_MIME)
38+
changed = True
39+
40+
if changed:
41+
path.write_text(json.dumps(nb, indent=1) + "\n")
42+
return changed
43+
44+
45+
def main() -> int:
46+
if len(sys.argv) < 2:
47+
print("usage: strip_widget_metadata.py <notebook> [<notebook> ...]", file=sys.stderr)
48+
return 2
49+
any_changed = False
50+
for arg in sys.argv[1:]:
51+
if strip(Path(arg)):
52+
print(f"stripped widgets: {arg}")
53+
any_changed = True
54+
return 1 if any_changed else 0
55+
56+
57+
if __name__ == "__main__":
58+
sys.exit(main())

tutorials/visium_breast_cancer.ipynb

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@
99
"\n",
1010
"This tutorial walks through visualising a real 10x Genomics Visium experiment with `spatialdata-plot`: H&E tissue image, spot polygons, gene expression overlays, and publication-style styling.\n",
1111
"\n",
12-
"**Dataset**: [Human Breast Cancer (Block A Section 1)][10x] from 10x Genomics fetched once via `scanpy.datasets.visium_sge` and cached by `pooch` for subsequent runs.\n",
12+
"**Dataset**: [Human Breast Cancer (Block A Section 1)][10x] from 10x Genomics \u2014 fetched once via `scanpy.datasets.visium_sge` and cached by `pooch` for subsequent runs.\n",
1313
"\n",
14-
"**Credit**: the example progression in this tutorial H&E + spots, gene-expression overlays, outline styling was originally curated by [@asarigun](https://github.com/asarigun) in [scverse/spatialdata-plot#590](https://github.com/scverse/spatialdata-plot/pull/590).\n",
14+
"**Credit**: the example progression in this tutorial \u2014 H&E + spots, gene-expression overlays, outline styling \u2014 was originally curated by [@asarigun](https://github.com/asarigun) in [scverse/spatialdata-plot#590](https://github.com/scverse/spatialdata-plot/pull/590).\n",
1515
"\n",
1616
"[10x]: https://www.10xgenomics.com/datasets/human-breast-cancer-block-a-section-1-1-standard-1-1-0"
1717
]
@@ -43,14 +43,14 @@
4343
"data": {
4444
"text/plain": [
4545
"SpatialData object\n",
46-
"├── Images\n",
47-
" └── 'tissue': DataArray[cyx] (3, 2000, 2000)\n",
48-
"├── Shapes\n",
49-
" └── 'spots': GeoDataFrame shape: (3798, 2) (2D shapes)\n",
50-
"└── Tables\n",
51-
" └── 'table': AnnData (3798, 36601)\n",
46+
"\u251c\u2500\u2500 Images\n",
47+
"\u2502 \u2514\u2500\u2500 'tissue': DataArray[cyx] (3, 2000, 2000)\n",
48+
"\u251c\u2500\u2500 Shapes\n",
49+
"\u2502 \u2514\u2500\u2500 'spots': GeoDataFrame shape: (3798, 2) (2D shapes)\n",
50+
"\u2514\u2500\u2500 Tables\n",
51+
" \u2514\u2500\u2500 'table': AnnData (3798, 36601)\n",
5252
"with coordinate systems:\n",
53-
" 'global', with elements:\n",
53+
" \u25b8 'global', with elements:\n",
5454
" tissue (Images), spots (Shapes)"
5555
]
5656
},
@@ -283,7 +283,7 @@
283283
"source": [
284284
"## Coloring spots by a category\n",
285285
"\n",
286-
"`color=` also accepts categorical columns here, the `in_tissue` flag 10x sets to mark spots that fall on tissue."
286+
"`color=` also accepts categorical columns \u2014 here, the `in_tissue` flag 10x sets to mark spots that fall on tissue."
287287
]
288288
},
289289
{
@@ -375,9 +375,9 @@
375375
"source": [
376376
"## Where to next\n",
377377
"\n",
378-
"- **API reference** every parameter of `render_shapes`, `render_images`, and `show()` is documented in the [plotting API](https://spatialdata.scverse.org/projects/plot/en/latest/api.html).\n",
379-
"- **Getting started tutorial** if you skipped it, the [Getting started](./getting_started.ipynb) tutorial covers the same fluent API on the lightweight built-in `blobs` dataset.\n",
380-
"- **Contributing** found a missing example? Open a PR on [`spatialdata-plot-notebooks`](https://github.com/scverse/spatialdata-plot-notebooks)."
378+
"- **API reference** \u2014 every parameter of `render_shapes`, `render_images`, and `show()` is documented in the [plotting API](https://spatialdata.scverse.org/projects/plot/en/latest/api.html).\n",
379+
"- **Getting started tutorial** \u2014 if you skipped it, the [Getting started](./getting_started.ipynb) tutorial covers the same fluent API on the lightweight built-in `blobs` dataset.\n",
380+
"- **Contributing** \u2014 found a missing example? Open a PR on [`spatialdata-plot-notebooks`](https://github.com/scverse/spatialdata-plot-notebooks)."
381381
]
382382
}
383383
],

0 commit comments

Comments
 (0)