Building the Docs#

This site is built with Jupyter Book 1.x.

pixi install -e docs
pixi run -e docs docs_build

Then open docs/_build/html/index.html. Other commands:

pixi run -e docs docs_convert  # just regenerate the notebooks
pixi run -e docs docs_clean    # remove docs/_build

Without pixi:

pip install ".[docs]"
python docs/examples/_convert_examples_to_notebooks.py
jupyter-book build docs/

Warning

Pin Jupyter Book to >=1.0,<2.0. Version 2 is a ground-up rewrite on the MyST engine with a different configuration schema — no _toc.yml, no Sphinx autodoc — so an unpinned install produces a completely different and broken build.

How examples become pages#

Examples are plain Python scripts in docs/examples/, not notebooks. A converter creates an .ipynb file for each script during the build. Jupyter Book runs each notebook.

This design has two benefits. You can review example changes as Python code. CI can run each file as a script. A broken example fails tests before documentation deployment.

The convention, enforced by docs/examples/_convert_examples_to_notebooks.py:

"""
# Page Title

The module docstring becomes the first markdown cell, so its heading titles the page.
"""


def main():
    import numpy as np

    """
    ## A section

    Triple-quoted strings inside `main()` become markdown cells. Code between
    them becomes code cells.
    """

    x = np.arange(10)
    print(x.sum())


if __name__ == "__main__":
    main()

Rules:

  • The filename must end with _example.py. This is the converter glob. Files that start with _ are helpers and are skipped.

  • Put all code inside def main():. The converter removes the def line, the if __name__ == "__main__": guard, and the main() call. It removes four spaces from the body.

  • Do not use return in main(). After the converter removes the wrapper, a bare return is a syntax error at notebook top level.

  • Put imports inside main(). This avoids ruff E402 errors. Imports occur in the first code cell.

  • To display a value, end a cell with the bare expression, as in a notebook.

Generated .ipynb files are gitignored. The .py file is the source.

Add a new page by writing the file and adding it to docs/_toc.yml without the extension:

- file: examples/06_my_example

Notebooks are executed on every build#

_config.yml sets execute_notebooks: force and nb_execution_raise_on_error: true. The build runs code for every number and figure. A failing example fails the build.

Examples are offline and deterministic. They read small DEM crops from docs/data/. They do not use the network. Do not call USGSApi from an example. Even though it batches its requests, it still depends on a public service, which would make the build unreliable. Document it in a prose page.

Warning

Do not set MPLBACKEND=Agg when you build documentation. The notebook kernel inherits this setting. It disables inline figure capture. The build succeeds, but published matplotlib plots do not appear. Use Agg only for CI that runs examples as scripts.

The sample data#

Examples use corridor crops from two real USGS tiles in docs/data/. They need 460 KB instead of 900 MB. Values in these corridors are identical to source data. See docs/data/README.md.

Regenerating requires the full source tiles, which are not in the repository:

pixi run -e dev python scripts/make_docs_data.py --source-dir /path/to/tiles

The script checks each crop against the full tile before it writes data. It refuses to write if they differ. The DEMOS table at the top of the script has trace slice bounds. Regenerate data after you change a bound.

The API reference#

docs/api_docs.rst is hand-written. It has one automodule block for each public module. Sphinx autodoc gets docstrings. Package docstrings are published documentation. Update incorrect paths and defaults in docstrings.

Adding a new public module means adding a block to that file; it is not automatic.

Deployment#

.github/workflows/deploy-docs.yml builds the book for each pull request that changes docs/, gradeit/, or pyproject.toml. A push to main publishes to gh-pages. Pull requests build but do not publish. This finds broken notebooks before merge.

The API reference uses docstrings. Therefore, this workflow watches gradeit/** and docs/**.