Skip to content

Commit 0aa9adf

Browse files
authored
Merge branch 'main' into codex/fix-mcp-cve-2026-59951
2 parents 83e9e47 + 78adb96 commit 0aa9adf

8 files changed

Lines changed: 224 additions & 6 deletions

File tree

‎.github/workflows/build-fern-docs.yml‎

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,6 @@ jobs:
152152
- name: Install docs dependencies
153153
working-directory: website
154154
# Publishing only converts or restores notebooks; it does not execute them.
155-
# Avoid building the Git-pinned nbconvert, which downloads CSS from Jupyter's CDN.
156155
run: uv sync --frozen --python 3.13 --all-packages --group docs
157156

158157
- name: Check Fern docs
@@ -246,7 +245,6 @@ jobs:
246245
- name: Install docs dependencies
247246
working-directory: website
248247
# Publishing only converts or restores notebooks; it does not execute them.
249-
# Avoid building the Git-pinned nbconvert, which downloads CSS from Jupyter's CDN.
250248
run: uv sync --frozen --python 3.13 --all-packages --group docs
251249

252250
- name: Download executed notebooks

‎.github/workflows/build-notebooks.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ jobs:
5050
- name: Set up Python
5151
run: uv python install 3.11
5252
- name: Install notebook dependencies
53-
run: uv sync --all-packages --group notebooks --group docs
53+
run: make install-docs-deps DOCS_PYTHON_VERSION=3.11 NOTEBOOK_CSS=0
5454
- name: Restore notebook cache
5555
if: inputs.use_cache
5656
id: cache

‎.github/workflows/publish-fern-devnotes.yml‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,6 @@ jobs:
105105
- name: Install docs dependencies
106106
working-directory: website
107107
# Publishing only converts or restores notebooks; it does not execute them.
108-
# Avoid building the Git-pinned nbconvert, which downloads CSS from Jupyter's CDN.
109108
run: uv sync --frozen --python 3.13 --all-packages --group docs
110109

111110
- name: Restore published notebook snapshot

‎DEVELOPMENT.md‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,11 @@ make install-dev
2929
make install-dev-notebooks
3030
```
3131

32+
Notebook setup restores nbconvert's HTML stylesheets from a checksum-pinned PyPI wheel without installing its
33+
Python code. The nh3 source pin remains in use. Existing stylesheets are reused without another download.
34+
Use `NOTEBOOK_CSS=0` with the notebook setup targets for execution-only environments, which do not support
35+
nbconvert HTML export. After a direct `uv sync --group notebooks`, run `make install-notebook-css` if you need HTML export.
36+
3237
### Verify Your Setup
3338

3439
```bash

‎Makefile‎

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ help:
8787
@echo " generate-fern-notebooks - Convert docs/notebook_source/*.py → fern/components/notebooks/{json,ts}"
8888
@echo " generate-fern-notebooks-with-outputs - Full pipeline: execute notebooks (needs API key), colabify, convert to Fern"
8989
@echo " install-docs-deps - Install docs and notebook dependencies"
90+
@echo " install-notebook-css - Restore nbconvert HTML export stylesheets"
9091
@echo " prepare-fern-release VERSION=X.Y.Z - Add or refresh Fern version files for release preview"
9192
@echo " check-fern-release-version VERSION=X.Y.Z - Verify Fern has a version entry for release publishing"
9293
@echo " prepare-fern-docs - Generate local Fern artifacts"
@@ -177,6 +178,7 @@ install-dev-notebooks:
177178
@echo " Packages: data-designer-config → data-designer-engine → data-designer"
178179
@echo " Groups: dev + docs + notebooks (Jupyter, jupytext, etc.)"
179180
uv sync --all-packages --group dev --group docs --group notebooks
181+
@$(MAKE) install-notebook-css
180182
$(call install-pre-commit-hooks)
181183
@echo "✅ Dev + docs + notebooks installation complete!"
182184
@echo ""
@@ -495,6 +497,7 @@ update-license-headers:
495497
DOCS_PYTHON_VERSION ?= 3.13
496498
DOCS_PYTHON ?= .venv/bin/python
497499
DOCS_JUPYTEXT ?= .venv/bin/jupytext
500+
NOTEBOOK_CSS ?= 1
498501
FERN_VERSION ?= $(shell jq -r .version fern/fern.config.json)
499502
FERN ?= npx -y fern-api@$(FERN_VERSION)
500503
FERN_ORG ?= $(shell jq -r .organization fern/fern.config.json)
@@ -509,6 +512,16 @@ DOCS_CERTS = SSL_CERT_FILE=$$($(DOCS_PYTHON) -c "import certifi; print(certifi.w
509512
install-docs-deps:
510513
@echo "📦 Installing docs dependencies (Python $(DOCS_PYTHON_VERSION))..."
511514
uv sync --python $(DOCS_PYTHON_VERSION) --all-packages --group docs --group notebooks
515+
@$(MAKE) install-notebook-css
516+
517+
install-notebook-css:
518+
ifeq ($(NOTEBOOK_CSS),1)
519+
$(DOCS_CERTS) $(DOCS_PYTHON) docs/scripts/prepare_nbconvert_css.py
520+
else ifeq ($(NOTEBOOK_CSS),0)
521+
@echo "Skipping nbconvert CSS (notebook execution only)."
522+
else
523+
$(error NOTEBOOK_CSS must be 0 or 1)
524+
endif
512525

513526
prepare-fern-release:
514527
ifndef VERSION
@@ -791,7 +804,7 @@ clean-test-coverage:
791804
format format-check format-check-config format-check-engine format-check-interface \
792805
format-config format-engine format-interface \
793806
generate-colab-notebooks generate-fern-notebooks generate-fern-notebooks-with-outputs help \
794-
install install-dev install-dev-notebooks install-dev-recipes install-docs-deps install-graphify \
807+
install install-dev install-dev-notebooks install-dev-recipes install-docs-deps install-graphify install-notebook-css \
795808
lint lint-config lint-engine lint-fix lint-fix-config lint-fix-engine lint-fix-interface lint-interface lock-graphify \
796809
perf-import perf-import-runtime prepare-fern-docs prepare-fern-release publish serve-fern-docs-dev serve-fern-docs-local-theme serve-fern-docs-locally show-versions \
797810
health-checks \
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
from __future__ import annotations
5+
6+
import base64
7+
import csv
8+
import hashlib
9+
import io
10+
import os
11+
import sys
12+
from importlib.metadata import distribution
13+
from pathlib import Path
14+
from urllib.request import urlopen
15+
from zipfile import ZipFile
16+
17+
WHEEL_URL = (
18+
"https://files.pythonhosted.org/packages/81/fd/a8d457af68848a8e5c12dd1d84ff35c415db471af78280e0703db4efbb12/"
19+
"nbconvert-7.17.2-py3-none-any.whl"
20+
)
21+
WHEEL_SHA256 = "6a143c11c00bf831604da4b1be4347d36ea52367057eb920135d69bc2862fed2"
22+
WHEEL_TEMPLATES = "nbconvert-7.17.2.data/data/share/jupyter/nbconvert/templates"
23+
CSS_SHA256 = {
24+
"classic/static/style.css": "5865a609f4437b0464bc121cd567b619074e540a0515a3b82f222f764eb51e01",
25+
"lab/static/index.css": "917ff47850a7cc08fd0658026fda7672a85220aaab258e8849e891b37426f947",
26+
"lab/static/theme-dark.css": "795f2d5069737cbeb5cba01e6b5c7cadbde227c909e43004c5a60f58d5160aec",
27+
"lab/static/theme-light.css": "11bf3558fd3ed353a4c1401ac0c1730d01df073f6436d357c5bbf02a03bd6962",
28+
}
29+
30+
31+
def prepare_nbconvert_css() -> None:
32+
"""Restore the pinned nbconvert build's HTML assets without replacing its Python code."""
33+
installed = distribution("nbconvert")
34+
templates = Path(sys.prefix) / "share" / "jupyter" / "nbconvert" / "templates"
35+
if all(
36+
(templates / name).is_file() and hashlib.sha256((templates / name).read_bytes()).hexdigest() == digest
37+
for name, digest in CSS_SHA256.items()
38+
):
39+
print("nbconvert HTML stylesheets already present.")
40+
return
41+
42+
record_entry = next(
43+
entry for entry in installed.files or () if entry.name == "RECORD" and entry.parent.name.endswith(".dist-info")
44+
)
45+
record_path = Path(installed.locate_file(record_entry))
46+
print("Downloading nbconvert HTML stylesheets from PyPI (256 KiB wheel)...")
47+
with urlopen(WHEEL_URL, timeout=30) as response:
48+
wheel = response.read()
49+
if hashlib.sha256(wheel).hexdigest() != WHEEL_SHA256:
50+
raise ValueError("nbconvert CSS wheel checksum mismatch")
51+
with ZipFile(io.BytesIO(wheel)) as archive:
52+
stylesheets = {name: archive.read(f"{WHEEL_TEMPLATES}/{name}") for name in CSS_SHA256}
53+
for name, content in stylesheets.items():
54+
if hashlib.sha256(content).hexdigest() != CSS_SHA256[name]:
55+
raise ValueError(f"nbconvert stylesheet checksum mismatch: {name}")
56+
57+
paths = {
58+
name: os.path.relpath(templates / name, record_path.parent.parent).replace(os.sep, "/") for name in stylesheets
59+
}
60+
records = [
61+
row for row in csv.reader(io.StringIO(record_path.read_text(encoding="utf-8"))) if row[0] not in paths.values()
62+
]
63+
for name, content in stylesheets.items():
64+
destination = templates / name
65+
destination.parent.mkdir(parents=True, exist_ok=True)
66+
destination.write_bytes(content)
67+
digest = base64.urlsafe_b64encode(hashlib.sha256(content).digest()).rstrip(b"=").decode()
68+
records.append([paths[name], f"sha256={digest}", str(len(content))])
69+
# Track restored assets so reinstalling or uninstalling nbconvert also removes them.
70+
with record_path.open("w", newline="", encoding="utf-8") as record:
71+
csv.writer(record).writerows(records)
72+
print("Restored nbconvert HTML stylesheets; installed Python code is unchanged.")
73+
74+
75+
if __name__ == "__main__":
76+
prepare_nbconvert_css()
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
from __future__ import annotations
5+
6+
import csv
7+
import hashlib
8+
import importlib.util
9+
import io
10+
from importlib.metadata import PathDistribution
11+
from pathlib import Path
12+
from types import ModuleType
13+
from zipfile import ZipFile
14+
15+
import pytest
16+
17+
SCRIPT_PATH = Path(__file__).resolve().parents[4] / "docs" / "scripts" / "prepare_nbconvert_css.py"
18+
19+
20+
@pytest.fixture
21+
def css_environment(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> tuple[ModuleType, Path, Path]:
22+
spec = importlib.util.spec_from_file_location("prepare_nbconvert_css", SCRIPT_PATH)
23+
assert spec is not None and spec.loader is not None
24+
module = importlib.util.module_from_spec(spec)
25+
spec.loader.exec_module(module)
26+
27+
prefix = tmp_path / "venv"
28+
info = prefix / "lib" / "site-packages" / "nbconvert-7.17.2.dist-info"
29+
info.mkdir(parents=True)
30+
(info / "METADATA").write_text("Name: nbconvert\nVersion: 7.17.2\n")
31+
record = info / "RECORD"
32+
record.write_text("nbconvert-7.17.2.dist-info/RECORD,,\n")
33+
installed = PathDistribution(info)
34+
monkeypatch.setattr(module, "distribution", lambda name: installed)
35+
monkeypatch.setattr(module.sys, "prefix", str(prefix))
36+
37+
stylesheets = {name: f"/* {name} */".encode() for name in module.CSS_SHA256}
38+
payload = io.BytesIO()
39+
with ZipFile(payload, "w") as wheel:
40+
for name, content in stylesheets.items():
41+
wheel.writestr(f"{module.WHEEL_TEMPLATES}/{name}", content)
42+
wheel.writestr("nbconvert/__init__.py", "raise RuntimeError('must not install wheel code')")
43+
wheel_bytes = payload.getvalue()
44+
monkeypatch.setattr(module, "WHEEL_SHA256", hashlib.sha256(wheel_bytes).hexdigest())
45+
monkeypatch.setattr(
46+
module, "CSS_SHA256", {name: hashlib.sha256(data).hexdigest() for name, data in stylesheets.items()}
47+
)
48+
monkeypatch.setattr(module, "urlopen", lambda *args, **kwargs: io.BytesIO(wheel_bytes))
49+
return module, prefix, record
50+
51+
52+
def test_restores_only_css_and_tracks_assets_for_uninstall(css_environment: tuple[ModuleType, Path, Path]) -> None:
53+
module, prefix, record = css_environment
54+
55+
module.prepare_nbconvert_css()
56+
57+
templates = prefix / "share" / "jupyter" / "nbconvert" / "templates"
58+
assert {str(path.relative_to(templates)) for path in templates.rglob("*.css")} == module.CSS_SHA256.keys()
59+
assert not (record.parent.parent / "nbconvert").exists()
60+
with record.open(newline="") as stream:
61+
records = list(csv.reader(stream))
62+
assert len(records) == 5
63+
for name, digest, size in records[1:]:
64+
restored = record.parent.parent / name
65+
assert restored.is_file()
66+
assert digest.startswith("sha256=")
67+
assert len(restored.read_bytes()) == int(size)
68+
69+
70+
def test_complete_installation_needs_no_network(
71+
css_environment: tuple[ModuleType, Path, Path], monkeypatch: pytest.MonkeyPatch
72+
) -> None:
73+
module, _, record = css_environment
74+
module.prepare_nbconvert_css()
75+
previous = record.read_bytes()
76+
77+
def offline(*args: object, **kwargs: object) -> None:
78+
raise AssertionError("must not download existing assets")
79+
80+
monkeypatch.setattr(module, "urlopen", offline)
81+
module.prepare_nbconvert_css()
82+
83+
assert record.read_bytes() == previous
84+
85+
86+
@pytest.mark.parametrize("corruption", ["wheel", "stylesheet", "missing-member"])
87+
def test_rejects_invalid_assets_before_writing(
88+
css_environment: tuple[ModuleType, Path, Path], monkeypatch: pytest.MonkeyPatch, corruption: str
89+
) -> None:
90+
module, prefix, record = css_environment
91+
previous = record.read_bytes()
92+
if corruption == "wheel":
93+
monkeypatch.setattr(module, "WHEEL_SHA256", "0" * 64)
94+
elif corruption == "stylesheet":
95+
monkeypatch.setitem(module.CSS_SHA256, "classic/static/style.css", "0" * 64)
96+
else:
97+
monkeypatch.setitem(module.CSS_SHA256, "classic/static/missing.css", "0" * 64)
98+
99+
with pytest.raises((ValueError, KeyError), match="checksum mismatch|missing.css"):
100+
module.prepare_nbconvert_css()
101+
102+
assert not (prefix / "share").exists()
103+
assert record.read_bytes() == previous
104+
105+
106+
@pytest.mark.parametrize("corruption", ["missing", "modified"])
107+
def test_repairs_incomplete_installation_without_duplicate_records(
108+
css_environment: tuple[ModuleType, Path, Path], corruption: str
109+
) -> None:
110+
module, prefix, record = css_environment
111+
module.prepare_nbconvert_css()
112+
stylesheet = prefix / "share" / "jupyter" / "nbconvert" / "templates" / "classic" / "static" / "style.css"
113+
expected = stylesheet.read_bytes()
114+
if corruption == "missing":
115+
stylesheet.unlink()
116+
else:
117+
stylesheet.write_bytes(b"modified")
118+
119+
module.prepare_nbconvert_css()
120+
121+
assert stylesheet.read_bytes() == expected
122+
with record.open(newline="") as stream:
123+
assert len(list(csv.reader(stream))) == 5

‎pyproject.toml‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,17 @@ members = [
3030
[tool.uv]
3131
# IMPORTANT: This root is NOT a package. It only defines workspace configuration.
3232
package = false
33-
required-version = ">=0.7.10"
33+
required-version = ">=0.9.5"
3434

3535
[tool.uv.sources]
3636
# Temporary immutable pin to jupyter/nbconvert#2300, which replaces the EOL
3737
# Bleach dependency with nh3. Return to PyPI after the upstream fix is released.
3838
nbconvert = { git = "https://github.com/jupyter/nbconvert.git", rev = "1fab6813f44f89017e7bcad27578571447b5b9c2" }
3939

40+
[tool.uv.extra-build-variables]
41+
# Execution does not need CSS. The notebook setup targets restore HTML assets from a verified PyPI wheel.
42+
nbconvert = { HATCH_BUILD_NO_HOOKS = "true" }
43+
4044
[dependency-groups]
4145
dev = [
4246
"jsonpath-ng>=1.5.3,<2",

0 commit comments

Comments
 (0)