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
1 change: 1 addition & 0 deletions .binder/environment.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ dependencies:
- xarray
- scipy
- pandas
- polars
- matplotlib
- cartopy
- cmocean
Expand Down
29 changes: 29 additions & 0 deletions .binder/patch_notebooks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import json
import re
import sys

# `html_str = pio.to_html(fig, ...)` followed by `display(HTML(html_str))`
PATTERN = re.compile(r"(\w+) = pio\.to_html\((\w+).*\)\ndisplay\(HTML\(\1\)\)")


def patch_notebook(nb: dict) -> int:
"""Patch a notebook dict in place, returning the number of replacements."""
n_total = 0
for cell in nb["cells"]:
if cell["cell_type"] == "code":
source, n = PATTERN.subn(r"\2.show()", "".join(cell["source"]))
if n:
cell["source"] = source.splitlines(keepends=True)
cell["outputs"] = [] # stale docs output
n_total += n
return n_total


if __name__ == "__main__":
for path in sys.argv[1:]:
with open(path, encoding="utf-8") as f:
nb = json.load(f)
if patch_notebook(nb):
with open(path, "w", encoding="utf-8") as f:
json.dump(nb, f, indent=1, ensure_ascii=False)
print(f"Patched plotly figures -> fig.show() in {path}")
5 changes: 5 additions & 0 deletions .binder/postBuild
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@ while IFS= read -r file_path || [ -n "$file_path" ]; do
fi
done < "$LIST_FILE"

# swap docs-only plotly HTML embedding for fig.show(), so figures render in Jupyter on Binder
shopt -s nullglob
python .binder/patch_notebooks.py "$TARGET_DIR"/*.ipynb
shopt -u nullglob

# landing page: copy to the workspace root
cp .binder/welcome.md welcome.md

Expand Down
6 changes: 4 additions & 2 deletions .github/workflows/check-binder-files.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ on:
paths:
- '.binder/files_to_fetch.txt'
- '.binder/config.yaml'
- '.binder/patch_notebooks.py'
- 'tests/**'
pull_request:
types: [ opened, synchronize, reopened ]
branches: [ main ]
Expand All @@ -31,6 +33,6 @@ jobs:
python -m pip install --upgrade pip
pip install pytest pyyaml

- name: Run notebook URL unit tests
- name: Run notebook URL and patch drift unit tests
run: |
pytest tests/test_binder_files.py -v
pytest tests/ -v
54 changes: 54 additions & 0 deletions tests/test_binder_patch_notebooks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import importlib.util
import json
import urllib.request
from pathlib import Path

import pytest

from test_binder_files import CONFIG, get_target_files

_spec = importlib.util.spec_from_file_location(
"patch_notebooks", Path(__file__).parent.parent / ".binder" / "patch_notebooks.py"
)
patch_notebooks = importlib.util.module_from_spec(_spec)
_spec.loader.exec_module(patch_notebooks)


def _code(nb):
return "\n".join(
"".join(c["source"]) for c in nb["cells"] if c["cell_type"] == "code"
)


def test_patch_notebook():
nb = {
"cells": [
{
"cell_type": "code",
"source": [
'html_str = pio.to_html(fig, include_plotlyjs="cdn")\n',
"display(HTML(html_str))",
],
"outputs": [{"output_type": "display_data"}],
}
]
}
assert patch_notebooks.patch_notebook(nb) == 1
assert nb["cells"][0]["source"] == ["fig.show()"]
assert nb["cells"][0]["outputs"] == []


@pytest.mark.parametrize("file_path", get_target_files())
def test_upstream_notebook_fully_patched(file_path):
"""Catch drift: any plotly HTML embedding upstream must be rewritten by the patch."""
url = (
f"https://raw.githubusercontent.com/{CONFIG['repo_owner']}/"
f"{CONFIG['repo_name']}/{CONFIG['branch']}/{file_path}"
)
with urllib.request.urlopen(url) as response:
nb = json.load(response)
patch_notebooks.patch_notebook(nb)
assert "to_html(" not in _code(nb), (
f"{file_path} embeds plotly HTML in a way .binder/patch_notebooks.py doesn't "
"rewrite to `fig.show()`, so it won't render on Binder. Update PATTERN."
)
Loading