Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Examples page #7

Merged
merged 113 commits into from
Jul 5, 2023
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
113 commits
Select commit Hold shift + click to select a range
b1d05ba
Add openmm cookbook
Yoshanuikabundi Apr 20, 2023
6ee79d7
Examples progress
Yoshanuikabundi Apr 27, 2023
413c1a7
Remove sphinx_gallery
Yoshanuikabundi May 31, 2023
75f45dc
Add gitpython to lint environment
Yoshanuikabundi May 31, 2023
45e6bd3
Remove nbsphinx_prolog
Yoshanuikabundi May 31, 2023
d8f197e
Download zip files with notebooks and files, not just notebooks
Yoshanuikabundi May 31, 2023
cd59e3e
Pre-processing notebooks basically works
Yoshanuikabundi Jun 6, 2023
7479768
Execute notebooks in parallel and only update changed notebooks
Yoshanuikabundi Jun 6, 2023
08dfb53
Set INTERCHANGE_EXPERIMENTAL and simplify delay code
Yoshanuikabundi Jun 6, 2023
2bacdde
Notes to self
Yoshanuikabundi Jun 6, 2023
d773f31
Interim fixes for extension
Yoshanuikabundi Jun 6, 2023
7926fb4
Fix module stuff
Yoshanuikabundi Jun 7, 2023
96dbb8f
Clean up removed notebooks and download latest release examples
Yoshanuikabundi Jun 7, 2023
a214387
Get the sphinx extension building the processed stuff
Yoshanuikabundi Jun 8, 2023
2c92744
Proof of concept gallery
Yoshanuikabundi Jun 8, 2023
975fa72
Simplify code and fix warnings
Yoshanuikabundi Jun 8, 2023
ac0a263
Fix css in cookbook
Yoshanuikabundi Jun 8, 2023
79f2e8a
Finish transition to MyST-NB
Yoshanuikabundi Jun 8, 2023
97a2705
Temporary fix for NGLView widgets.
Yoshanuikabundi Jun 20, 2023
7215221
Process all notebooks in single folder
Yoshanuikabundi Jun 20, 2023
b7a46e1
Refactor towards caching notebooks on Git branch
Yoshanuikabundi Jun 20, 2023
7fbaac3
Avoid unnecessary API requests AKA Generators are cool & I am a nerd
Yoshanuikabundi Jun 20, 2023
fa6b6b9
Order Sphinx hooks chronologically
Yoshanuikabundi Jun 20, 2023
0c956e7
Application -> Sphinx
Yoshanuikabundi Jun 20, 2023
23912d3
Typos
Yoshanuikabundi Jun 20, 2023
7071026
Merge branch 'cookbook_gha' into examples
Yoshanuikabundi Jun 21, 2023
3a644c8
Merge branch 'main' into examples
Yoshanuikabundi Jun 21, 2023
f8d3a90
Fix colab link generation
Yoshanuikabundi Jun 21, 2023
1b50d0f
Download cache if pre-processed notebooks are missing
Yoshanuikabundi Jun 21, 2023
69f9d21
Print out git version
Yoshanuikabundi Jun 21, 2023
a851793
Troubleshooting improvements for broken examples
Yoshanuikabundi Jun 21, 2023
eea7e04
Remove unnecessary + redundant remove_notebooks function
Yoshanuikabundi Jun 21, 2023
9d15655
Ping github in sphinx extension to test RTD internet access
Yoshanuikabundi Jun 21, 2023
512e8bd
Try multiping
Yoshanuikabundi Jun 21, 2023
4ed8dea
Remove ping test
Yoshanuikabundi Jun 21, 2023
3492931
Try cloning in RTD hook
Yoshanuikabundi Jun 21, 2023
8975f9c
Simplify git command
Yoshanuikabundi Jun 21, 2023
86a5b81
mv -> rsync
Yoshanuikabundi Jun 21, 2023
c09b28a
Install rsync + git
Yoshanuikabundi Jun 21, 2023
870fb42
Remove ls command
Yoshanuikabundi Jun 21, 2023
1746529
Don't print git version
Yoshanuikabundi Jun 21, 2023
5fde2ab
Fix include_css_files()
Yoshanuikabundi Jun 21, 2023
d3a993c
Remove unused imports
Yoshanuikabundi Jun 21, 2023
625736d
Move notebook grid CSS out of extension
Yoshanuikabundi Jun 21, 2023
5c8be5a
Update lint environment
Yoshanuikabundi Jun 21, 2023
0b8f567
Merge branch 'main' into examples
Yoshanuikabundi Jun 21, 2023
26f49bf
Exclude examples from link checker
Yoshanuikabundi Jun 21, 2023
fe95db1
Remove annoying print statement
Yoshanuikabundi Jun 21, 2023
7c3604b
Debug link check ignore conf
Yoshanuikabundi Jun 21, 2023
23cb681
Move linkcheck exclusions to sphinx extension
Yoshanuikabundi Jun 21, 2023
d3bdbce
Fix linkcheck exclusions
Yoshanuikabundi Jun 21, 2023
ff4b45b
Tell MyST-NB about codelinter
Yoshanuikabundi Jun 21, 2023
bf28b5c
Merge branch 'main' into examples
Yoshanuikabundi Jun 26, 2023
fd77c1e
Parse tag out of repo list
Yoshanuikabundi Jun 26, 2023
25c9e3e
Copy thumbnails correctly
Yoshanuikabundi Jun 27, 2023
d3e1308
Get thumbnails into gallery
Yoshanuikabundi Jun 27, 2023
6adbb09
Break up _cookbook.py
Yoshanuikabundi Jun 27, 2023
ba7ac0a
Formatting
Yoshanuikabundi Jun 27, 2023
a26c0a0
Pin nglview>=3.0.6 and remove JS included therein
Yoshanuikabundi Jun 27, 2023
42ba5b3
Initial category support
Yoshanuikabundi Jun 27, 2023
618337f
Rejib cache paths
Yoshanuikabundi Jun 27, 2023
ebdadbe
Add fragmenter and reminder for QCSubmit
Yoshanuikabundi Jun 27, 2023
5a9423d
Disable fragmenter for now
Yoshanuikabundi Jun 27, 2023
5c3bf09
Move zipped notebooks to source directory
Yoshanuikabundi Jun 28, 2023
1a822b1
Support categories in cookbook
Yoshanuikabundi Jun 28, 2023
9f17df0
Tweak categories and switch to new interchange branch
Yoshanuikabundi Jun 28, 2023
9576cd6
Print env in rtd and update TK/Interchange
Yoshanuikabundi Jul 3, 2023
4d30f76
Try and enable custom widgets in colab
Yoshanuikabundi Jul 3, 2023
189bbd9
Support branch/pr caches and update main every night
Yoshanuikabundi Jul 3, 2023
d76f3aa
Fix inputs
Yoshanuikabundi Jul 3, 2023
4087b98
Ergonomics
Yoshanuikabundi Jul 3, 2023
0b2b619
Ensure deploy subdirectory exists
Yoshanuikabundi Jul 3, 2023
532e455
Fix ergonomics
Yoshanuikabundi Jul 3, 2023
1b4b9bb
Add --ignore-unmatch
Yoshanuikabundi Jul 3, 2023
6969dff
Use multiline string in .readthedocs.yaml
Yoshanuikabundi Jul 3, 2023
1de772a
Use old-style test in readthedocs.yaml
Yoshanuikabundi Jul 3, 2023
d86b2d6
String equality in .readthedocs.yaml
Yoshanuikabundi Jul 3, 2023
6336d79
Debug rtd with ls
Yoshanuikabundi Jul 3, 2023
0823436
Try ls --tree
Yoshanuikabundi Jul 3, 2023
0c423ea
Work around absence of --tree
Yoshanuikabundi Jul 3, 2023
c1b4b81
Fix cache download path
Yoshanuikabundi Jul 3, 2023
222a143
Debug rtd
Yoshanuikabundi Jul 3, 2023
553b56b
Try apt-get tree
Yoshanuikabundi Jul 3, 2023
30ff339
Fix source path?
Yoshanuikabundi Jul 3, 2023
f610d71
Move glob outside of quotes
Yoshanuikabundi Jul 3, 2023
b1cdd08
Simplify .readthedocs.yaml
Yoshanuikabundi Jul 3, 2023
620964c
Include branch in colab link
Yoshanuikabundi Jul 3, 2023
03d1d40
Update colab links in Sphinx extension
Yoshanuikabundi Jul 3, 2023
d565d10
Consolidate code between .readthedocs.yaml and python
Yoshanuikabundi Jul 3, 2023
90231a6
Fix argument handling in proc_examples.py
Yoshanuikabundi Jul 3, 2023
5b13759
Fix import
Yoshanuikabundi Jul 3, 2023
c17a1c7
Swap quotes
Yoshanuikabundi Jul 3, 2023
b155ab9
Try backtick shell expansion
Yoshanuikabundi Jul 3, 2023
56fad31
Use shell logic in .readthedocs.yaml
Yoshanuikabundi Jul 3, 2023
139b9a4
Clean up PR cache when closed
Yoshanuikabundi Jul 4, 2023
7d43834
Trigger RTD build when cache is updated (except in PRs)
Yoshanuikabundi Jul 4, 2023
0c28089
Merge branch 'main' into examples
Yoshanuikabundi Jul 4, 2023
f60ed11
Clarify description
Yoshanuikabundi Jul 4, 2023
7143205
Single quotes
Yoshanuikabundi Jul 4, 2023
fa10eee
Show source repository in gallery
Yoshanuikabundi Jul 4, 2023
b45fdb2
Style top-of-notebook links
Yoshanuikabundi Jul 4, 2023
90effe8
Add experimental warnings and improve styling
Yoshanuikabundi Jul 4, 2023
3ed8f85
Newline in css
Yoshanuikabundi Jul 4, 2023
33b77bb
globals -> globals_
Yoshanuikabundi Jul 4, 2023
8ed4661
Switch run_notebook.sh to use jupyterlab
Yoshanuikabundi Jul 4, 2023
0689914
Add installation instructions
Yoshanuikabundi Jul 4, 2023
a2b33a9
zip -> tgz
Yoshanuikabundi Jul 4, 2023
3c53bfe
Add CLI example for run_notebook.sh
Yoshanuikabundi Jul 4, 2023
52d05dc
Simplify
Yoshanuikabundi Jul 4, 2023
457ae4b
Fix old imports
Yoshanuikabundi Jul 4, 2023
0350a44
More robust run_notebook.sh
Yoshanuikabundi Jul 4, 2023
dfcc8de
Migrate to setup-micromamba
Yoshanuikabundi Jul 4, 2023
708dc2e
ZIP -> .TGZ in install instructions
Yoshanuikabundi Jul 4, 2023
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
10 changes: 10 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,12 @@
# Files generated by Sphinx
build/
source/_static/examples/
source/examples/

# Python stuff
__pycache__/
.pytest_cache
.ipynb_checkpoints

# SOAP
.soap/
16 changes: 16 additions & 0 deletions devtools/conda-envs/examples_env.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
channels:
- conda-forge
- bioconda
dependencies:
- pip
- python=3.10
# Cookbook
- gitpython
- nbconvert
- nbformat
# Examples
- openff-toolkit-examples
- gromacs
- lammps
- rich
- jax
2 changes: 2 additions & 0 deletions devtools/conda-envs/lint_env.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ dependencies:
- sphinx>=5,<7
- myst-parser>=1,<2
- sphinx-notfound-page
- nbsphinx
- gitpython
# Code example deps
- openff-toolkit-base
- openff-interchange-base
Expand Down
6 changes: 6 additions & 0 deletions devtools/conda-envs/rtd_env.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ dependencies:
- sphinx>=5,<7
- myst-parser>=1,<2
- sphinx-notfound-page
- nbsphinx
- ipython >=8.8
# Examples
- gitpython
- nbconvert
- nbformat
# Theme
- pip:
# Theme
Expand Down
60 changes: 60 additions & 0 deletions source/_ext/cookbook/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
from ._cookbook import (
collect_notebooks,
remove_old_notebooks,
process_notebook,
find_notebook_docnames,
CookbookDirective,
)
from sphinx.application import Sphinx as Application


def setup(app: Application):
# TODO: Remove these config values and replace with simpler versions
app.add_config_value(
"cookbook_default_conda_forge_deps",
default=[],
rebuild="env",
)
app.add_config_value(
"cookbook_default_required_files",
default=None,
rebuild="env",
)
app.add_config_value(
"cookbook_required_files_base_uri",
default="",
rebuild="env",
)

app.add_config_value(
"cookbook_example_repos",
default=[],
rebuild="env",
)
app.add_config_value(
"cookbook_examples_path",
default="_static/examples/",
rebuild="env",
)
app.add_config_value(
"cookbook_dont_fetch",
default=False,
rebuild="env",
)
# TODO: Implement this config value
app.add_config_value(
"cookbook_ignore_notebook_pattern",
default=["deprecated/**"],
rebuild="env",
)

app.connect("config-inited", collect_notebooks)
app.connect("env-purge-doc", remove_old_notebooks)
app.connect("source-read", process_notebook)
app.connect("env-before-read-docs", find_notebook_docnames)
app.add_directive("cookbook", CookbookDirective)

return {
"parallel_read_safe": True,
"parallel_write_safe": True,
}
113 changes: 113 additions & 0 deletions source/_ext/cookbook/_cookbook.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
"""Implementation of the cookbook Sphinx extension"""

import json
from pathlib import Path
from typing import Generator

from sphinx.application import Sphinx as Application
from sphinx.config import Config
from sphinx.environment import BuildEnvironment
from nbsphinx import NbGallery

from ._notebook import insert_cell, get_metadata, find_notebooks, notebook_zip
from ._github import download_dir


def inject_tags_index(notebook: dict) -> dict:
"""Inject an `index` directive containing the notebook's metadata tags"""

tags = get_metadata(notebook, "tags", ["untagged"])

return insert_cell(
notebook,
cell_type="raw",
metadata={"raw_mimetype": "text/restructuredtext"},
source=[
f".. index:: {', '.join(tags)}",
],
)


def inject_links(notebook: dict, docpath: Path, cookbook_examples_path: Path) -> dict:
user, repo, *path = str(docpath.relative_to(cookbook_examples_path)).split("/")
path = "/".join(path)

github_url = f"https://github.com/{user}/{repo}/blob/main/{path}"
# TODO: Figure out how to get the conda colab install cell into this
colab_url = (
f"https://colab.research.google.com/github/{user}/{repo}/blob/main/{path}"
)

return insert_cell(
notebook,
cell_type="raw",
metadata={"raw_mimetype": "text/restructuredtext"},
source=[
f":download:`Download Notebook </{notebook_zip(docpath)}>`",
f"`View in GitHub <{github_url}>`_",
f"`Open in Google Colab <{colab_url}>`_",
],
)


def process_notebook(app: Application, docname: str, source: list[str]):
docpath = Path(app.env.doc2path(docname, False))
if docpath.suffix != ".ipynb":
return

notebook = json.loads(source[0])

notebook = inject_links(notebook, docpath, app.config.cookbook_examples_path)
notebook = inject_tags_index(notebook)

source[0] = json.dumps(notebook)


def remove_old_notebooks(app: Application, env: BuildEnvironment, docname: str):
"""Clean up processed notebooks from outdir"""
# outdir = Path(app.outdir)
# colab_path = outdir / "colab" / docname
# colab_path = colab_path.with_suffix(".ipynb")
# # We check is_relative_to just in case docname is absolute
# if colab_path.exists() and colab_path.is_relative_to(outdir / "colab"):
# colab_path.unlink()


def collect_notebooks(app: Application, config: Config):
"""Download examples directories from example repositories"""
app.cookbook_examples_path = Path(app.srcdir) / config.cookbook_examples_path # type: ignore
if config.cookbook_dont_fetch:
return
for repo in config.cookbook_example_repos:
print("Collecting examples from", repo)

download_dir(repo, "examples", app.cookbook_examples_path) # type: ignore


def find_notebook_docnames(app, env, docnames):
"""Find the downloaded notebooks and make sure Sphinx sees them"""
if not hasattr(app, "cookbook_notebooks"):
app.cookbook_notebooks = [] # type: ignore

for repo in env.config.cookbook_example_repos:
for path in find_notebooks(app.cookbook_examples_path / repo / "examples"):
docname = env.project.path2doc(str(path.relative_to(app.srcdir)))
app.cookbook_notebooks.append(docname)
docnames.append(docname)


class CookbookDirective(NbGallery):
"""
Directive to draw thumbnails of the cookbook.
"""

def run(self):
nodes = super().run()
toctree = nodes[0][0][0]

toctree["entries"].extend(
[(None, docname) for docname in self.env.app.cookbook_notebooks]
)
toctree["includefiles"].extend(self.env.app.cookbook_notebooks)

return nodes
124 changes: 124 additions & 0 deletions source/_ext/cookbook/_github.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
"""Code for working with GitHub in both the Sphinx extension and proc_examples.py"""
import shutil
from pathlib import Path
from typing import Generator, Iterable, Literal, Optional

from git.repo import Repo
from git.diff import DiffIndex


class UpdateSpec:
"""Stores paths that must be reprocessed or cleaned up"""

def __init__(
self,
reprocess: Optional[Iterable[Path]] = None,
cleanup: Optional[Iterable[Path]] = None,
start_over=False,
):
# Find the notebooks associated with each change, and remove duplicates
reprocess = set(self._find_all_notebooks(reprocess))
cleanup = set(self._find_all_notebooks(cleanup))

# We don't need to cleanup notebooks we're reprocessing, so remove them
cleanup = cleanup - reprocess

self.reprocess = list(reprocess)
"""List of paths to reprocess"""
self.cleanup = list(cleanup)
"""List of paths to clean up"""
self.start_over = start_over
"""If True, all notebooks should be cleaned up and reprocessed"""

@classmethod
def from_diff(cls, diffs) -> "UpdateSpec":
reprocess = []
cleanup = []

for diff in diffs:
# Files we need to re-process (from additions, copies, renames,
# modifications)
if diff.change_type in "ACRM":
reprocess.append(Path(diff.b_path))
# Files we need to clean up (from deletions, renames)
if diff.change_type == "DR":
cleanup.append(Path(diff.a_path))

return cls(reprocess=reprocess, cleanup=cleanup)

@classmethod
def update_all(cls) -> "UpdateSpec":
return cls(start_over=True)

@classmethod
def _find_all_notebooks(
cls, paths: Optional[Iterable[Path]]
) -> Generator[Path, None, None]:
"""Find the notebooks that could correspond to each file"""
if paths is None:
return

for path in paths:
yield from cls._find_notebooks(path)

@staticmethod
def _find_notebooks(path: Path) -> list[Path]:
"""Find the notebooks that could correspond to a file"""
if path.suffix == ".ipynb":
return [path]

for parent in path.parents:
notebooks = [*parent.glob("*.ipynb")]
if notebooks:
return notebooks
return []

def __repr__(self) -> str:
return (
f"{self.__class__.__name__}(reprocess={list(self.reprocess)},"
+ f" cleanup={list(self.cleanup)}, start_over={self.start_over})"
)


def download_dir(src_repo: str, src_path: str, examples_path: Path) -> UpdateSpec:
"""Download src_path from GitHub src_repo

The folder is downloaded to to ``<examples_path>/<src_repo>/<src_path>``.
Git is used to only download new or updated files. The names of
newly-downloaded or updated files are returned."""
local_repo_path = examples_path / src_repo

# Try a simple pull
try:
repo = Repo(local_repo_path)

previous = repo.head.commit
repo.remotes.origin.pull(depth=1)
new = repo.head.commit
except Exception:
# Clean up whatever's there
if local_repo_path.exists():
shutil.rmtree(local_repo_path)
else:
if previous == new:
return UpdateSpec()
else:
return UpdateSpec.from_diff(previous.diff(new))

# If the repo doesn't exist, or a pull fails for some other reason, clone
local_repo_path.mkdir(parents=True, exist_ok=True)
# Clone without downloading anything
repo = Repo.clone_from(
url=f"https://github.com/{src_repo}.git",
to_path=local_repo_path,
multi_options=[
"--depth=1",
"--filter=tree:0",
"--no-checkout",
],
)
# Just checkout the examples directory
repo.git.sparse_checkout("set", src_path)
repo.git.checkout()

return UpdateSpec.update_all()
Loading