diff --git a/README.md b/README.md index ee64d11..d740e57 100644 --- a/README.md +++ b/README.md @@ -5,9 +5,47 @@ The documentation of the current master can found [here](https://opm.github.io/opm-python-documentation/master/index.html) ## Building the documentation locally -Follow the commands in `.github/workflows/python_sphinx_docs.yml` for your local setup! -See also the script [opmdoc-download-files](https://github.com/OPM/opm-python-documentation/blob/master/python/sphinx_docs/README.md) for more information. +Requires Python 3.10 or newer and [poetry](https://python-poetry.org/docs/). + +1. **Check out the branch you want to build, and commit your changes.** + `sphinx-versioned` builds from git history rather than from the working + tree, so uncommitted edits are invisible to it. + +2. **Install the helper scripts and fetch the docstring files.** + + ``` + cd python/sphinx_docs + poetry install + poetry run opmdoc-download-files + ``` + + The API pages are generated from `docstrings_common.json` and + `docstrings_simulators.json`, which live in `opm-common` and `opm-simulators` + rather than in this repository. `opmdoc-download-files` fetches the current + master copies. To build against a pull request in one of those repositories + instead, pass its number: `opmdoc-download-files --opm-simulators 1234`. + + On a release branch (`release-*`), skip this download. Release branches build + from snapshots of these files committed under `python/`, and + `opmdoc-download-files` refuses to run there rather than overwrite them. + +3. **Build, and open the result.** + + ``` + poetry run make docs + poetry run opmdoc-view-doc + ``` + + `make docs` builds the branch you are on; `opmdoc-view-doc` opens it in your + default browser, on Linux, macOS and Windows alike. Add `--branch=master` to + open a different branch. The generated pages are written to + `python/sphinx_docs/docs/_build//` and open correctly straight from + disk, so no web server is needed. + +See [python/sphinx_docs/README.md](python/sphinx_docs/README.md) for the +individual scripts, and `.github/workflows/python_sphinx_docs.yml` for how the +published site is built. ## Building the documentation online on your fork - Turn on github actions at `https://github.com//opm-python-documentation/actions` diff --git a/python/sphinx_docs/src/opm_python_docs/download_files.py b/python/sphinx_docs/src/opm_python_docs/download_files.py index 1c0925b..694a0df 100755 --- a/python/sphinx_docs/src/opm_python_docs/download_files.py +++ b/python/sphinx_docs/src/opm_python_docs/download_files.py @@ -1,6 +1,8 @@ #! /usr/bin/env python3 import logging +from pathlib import Path + import requests import click @@ -12,6 +14,17 @@ URL_DUNE_MODULE = "https://raw.githubusercontent.com/OPM/opm-simulators/master/dune.module" +def docstrings_dir() -> Path: + """Return the directory the documentation build reads downloaded files from. + + docs/conf.py reads python/master-tmp/ on every branch except release + branches, which use snapshots committed under python/ instead. Nothing is + downloaded on a release branch; see main(). + """ + target = helpers.get_git_root() / "python" / "master-tmp" + target.mkdir(parents=True, exist_ok=True) + return target + def convert_pr_to_commit_hash(repo: str, pr_number: int) -> str: """Convert a PR number to a commit hash.""" url = f"https://api.github.com/repos/OPM/{repo}/pulls/{pr_number}" @@ -34,8 +47,7 @@ def download_docstring_file(url: str, pr_number: int|None) -> None: logging.info(f"Downloading docstrings file from {url}") response = requests.get(url) response.raise_for_status() # Raises 404 if the file is not found - git_root_dir = helpers.get_git_root() - save_path = git_root_dir / "python" / filename + save_path = docstrings_dir() / filename with open(str(save_path), "wb") as file: file.write(response.content) logging.info(f"Saved docstrings file to {save_path}") @@ -45,8 +57,7 @@ def download_dune_module() -> None: logging.info("Downloading dune.module file") response = requests.get(URL_DUNE_MODULE) response.raise_for_status() - git_root_dir = helpers.get_git_root() - save_path = git_root_dir / "dune.module" + save_path = docstrings_dir() / "dune.module" with open(save_path, "wb") as file: file.write(response.content) logging.info(f"Saved dune.module file to {save_path}") @@ -78,6 +89,15 @@ def download_dune_module() -> None: @click.option("--opm-common", type=int, help="PR number for opm-common") def main(opm_simulators: int|None, opm_common: int|None) -> None: logging.basicConfig(level=logging.INFO) + branch = helpers.get_current_branch() + if branch.startswith("release-"): + # The committed snapshot in python/ must not be replaced by master's files. + raise click.ClickException( + f"'{branch}' is a release branch. Release branches build from the " + "docstring snapshots committed in python/, so there is nothing to " + "download. To update a release snapshot, take the files from the " + "release's own branch or tag in opm-common and opm-simulators." + ) download_docstring_file(URL_SIMULATORS, pr_number=opm_simulators) download_docstring_file(URL_COMMON, pr_number=opm_common) download_dune_module() diff --git a/python/sphinx_docs/tests/test_download_files.py b/python/sphinx_docs/tests/test_download_files.py new file mode 100644 index 0000000..f5be5cd --- /dev/null +++ b/python/sphinx_docs/tests/test_download_files.py @@ -0,0 +1,79 @@ +"""Tests for the docstrings download command. + +docs/conf.py reads the docstring JSON files from python/master-tmp/ on every +branch except release branches, so opmdoc-download-files has to write there, +or the build fails with a FileNotFoundError on a file the user has just +downloaded. + +Release branches are different: they build from snapshots committed under +python/, taken from the release's own sources. Downloading master's files +there would overwrite that snapshot, so on a release branch the command must +refuse and write nothing. +""" + +from pathlib import Path + +import pytest +from click.testing import CliRunner +from pytest_mock.plugin import MockerFixture + +from opm_python_docs import download_files + + +def _fake_repo(tmp_path: Path, mocker: MockerFixture, branch: str) -> Path: + (tmp_path / "python").mkdir() + mocker.patch.object(download_files.helpers, "get_git_root", return_value=tmp_path) + mocker.patch.object( + download_files.helpers, "get_current_branch", return_value=branch + ) + return tmp_path + + +@pytest.mark.parametrize("branch", ["master", "some-feature-branch"]) +def test_docstrings_dir_is_master_tmp( + tmp_path: Path, mocker: MockerFixture, branch: str +) -> None: + root = _fake_repo(tmp_path, mocker, branch) + assert download_files.docstrings_dir() == root / "python" / "master-tmp" + + +def test_docstrings_dir_creates_master_tmp( + tmp_path: Path, mocker: MockerFixture +) -> None: + """A fresh clone has no python/master-tmp, so it must be created.""" + root = _fake_repo(tmp_path, mocker, "master") + assert not (root / "python" / "master-tmp").exists() + assert download_files.docstrings_dir().is_dir() + + +def test_main_downloads_into_master_tmp( + tmp_path: Path, mocker: MockerFixture +) -> None: + root = _fake_repo(tmp_path, mocker, "master") + response = mocker.Mock(content=b"{}") + get = mocker.patch.object(download_files.requests, "get", return_value=response) + + result = CliRunner().invoke(download_files.main, []) + + assert result.exit_code == 0, result.output + assert get.call_count == 3 + written = sorted(p.name for p in (root / "python" / "master-tmp").iterdir()) + assert written == ["docstrings_common.json", "docstrings_simulators.json", "dune.module"] + + +def test_main_refuses_on_a_release_branch( + tmp_path: Path, mocker: MockerFixture +) -> None: + """The committed release snapshot must not be replaced by master's files.""" + root = _fake_repo(tmp_path, mocker, "release-2026.04") + snapshot = root / "python" / "docstrings_simulators.json" + snapshot.write_text("release snapshot") + get = mocker.patch.object(download_files.requests, "get") + + result = CliRunner().invoke(download_files.main, []) + + assert result.exit_code != 0 + assert "release branch" in result.output + get.assert_not_called() + assert snapshot.read_text() == "release snapshot" + assert not (root / "python" / "master-tmp").exists()