From 037dc04abc73998ccf51d55f200353f727a8a74e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5kon=20H=C3=A6gland?= Date: Wed, 23 Sep 2026 05:47:02 +0200 Subject: [PATCH 1/5] Download the docstring files where the build looks for them docs/conf.py picks the directory to read docstrings_common.json and docstrings_simulators.json from by branch name: release branches use the snapshots committed under python/, every other branch uses python/master-tmp/. opmdoc-download-files always wrote to python/, and wrote dune.module to the repository root, which matches neither layout. So on any branch other than a release branch, the documented sequence opmdoc-download-files make docs fails on a file the user has just downloaded: Exception occurred: FileNotFoundError: [Errno 2] No such file or directory: '.../python/master-tmp/docstrings_common.json' Pick the destination the same way conf.py does, and create python/master-tmp when it does not exist yet, as on a fresh clone. All three files now go to the same directory, which is also what the workflow does for the published build. --- .../src/opm_python_docs/download_files.py | 22 +++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) 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..a940d74 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,20 @@ URL_DUNE_MODULE = "https://raw.githubusercontent.com/OPM/opm-simulators/master/dune.module" +def docstrings_dir() -> Path: + """Return the directory the documentation build reads the JSON files from. + + docs/conf.py picks that directory by branch name: release branches use the + snapshots committed under python/, every other branch uses python/master-tmp/. + Downloading into the other one leaves the build unable to find the files. + """ + git_root_dir = helpers.get_git_root() + if helpers.get_current_branch().startswith("release-"): + return git_root_dir / "python" + target = git_root_dir / "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 +50,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 +60,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}") From af3a792eb821ad9c6718aa477e15e267ade90fd6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5kon=20H=C3=A6gland?= Date: Wed, 23 Sep 2026 05:47:02 +0200 Subject: [PATCH 2/5] Add tests for the download destination Checks that docstrings_dir() follows the same branch rule as docs/conf.py, and that it creates python/master-tmp when it is missing, which is the case on a fresh clone since that directory is gitignored. --- .../sphinx_docs/tests/test_download_files.py | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 python/sphinx_docs/tests/test_download_files.py 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..baa9e1d --- /dev/null +++ b/python/sphinx_docs/tests/test_download_files.py @@ -0,0 +1,49 @@ +"""Tests for the docstrings download command. + +The documentation build reads its JSON files from a directory that depends on +the branch name (see docs/conf.py): release branches use the snapshots +committed under python/, every other branch uses python/master-tmp/. +opmdoc-download-files has to write into the same directory, or the build fails +with a FileNotFoundError on a file the user has just downloaded. +""" + +from pathlib import Path + +import pytest +from pytest_mock.plugin import MockerFixture + +from opm_python_docs import download_files + + +@pytest.mark.parametrize( + "branch, expected", + [ + ("master", "python/master-tmp"), + ("some-feature-branch", "python/master-tmp"), + ("release-2026.04", "python"), + ], +) +def test_docstrings_dir_follows_the_branch( + tmp_path: Path, mocker: MockerFixture, branch: str, expected: str +) -> None: + (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 + ) + + assert download_files.docstrings_dir() == tmp_path / expected + + +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.""" + (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="master" + ) + + assert not (tmp_path / "python" / "master-tmp").exists() + assert download_files.docstrings_dir().is_dir() From b6db52d850712a78eb40d5fd101d2f2db6a45378 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5kon=20H=C3=A6gland?= Date: Wed, 23 Sep 2026 05:47:02 +0200 Subject: [PATCH 3/5] Give the README the steps for building the docs locally The section pointed at .github/workflows/python_sphinx_docs.yml, which interleaves the local build with the multi-branch machinery, the gh-pages deployment and the CI-only setup, and does not say which parts someone building locally needs. Replace it with the four commands that do the job, and state the two things that are not discoverable from the workflow: that sphinx-versioned builds from git history rather than the working tree, so changes have to be committed first, and that the docstring JSON files come from opm-common and opm-simulators rather than from this repository. Also mention make view-docs and opmdoc-view-doc, which already exist but were only reachable through a link to another README, and note that the built pages open straight from disk without a web server. --- README.md | 38 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ee64d11..4960eb7 100644 --- a/README.md +++ b/README.md @@ -5,9 +5,43 @@ 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`. + +3. **Build, and open the result.** + + ``` + poetry run make docs + poetry run make view-docs + ``` + + `make docs` builds the branch you are on; `make view-docs` opens it in your + default browser. Use `opmdoc-view-doc --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` From f02529ce942b0fc8886ab7dea30339afe171b202 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5kon=20H=C3=A6gland?= Date: Fri, 25 Sep 2026 05:19:56 +0200 Subject: [PATCH 4/5] Refuse to download docstring files on a release branch The previous commits made opmdoc-download-files write where the build reads, following docs/conf.py's branch rule: python/master-tmp/ on most branches, python/ on release branches. That rule is right for reading but wrong for writing. On a release branch, python/ holds the committed snapshot of docstrings_common.json, docstrings_simulators.json and dune.module, taken from the release's own sources. Downloading master's copies there replaces that snapshot and leaves the files modified in the working tree, ready to be committed by mistake. Reproduced in a worktree of release-2026.04: the command wrote all three files and git status showed them modified. The code before this branch already overwrote the two JSON files there; the previous commits added dune.module. A release branch needs nothing downloaded, so the command now stops with an explanation and a non-zero exit before any request is made. docstrings_dir() therefore only returns python/master-tmp. The tests check both paths through main(): three files written to python/master-tmp/ on master, and on a release branch no request, no change to the snapshot, and no python/master-tmp created. The README says the download step is not for release branches. --- README.md | 4 + .../src/opm_python_docs/download_files.py | 22 ++++-- .../sphinx_docs/tests/test_download_files.py | 78 +++++++++++++------ 3 files changed, 72 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index 4960eb7..c81acec 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,10 @@ Requires Python 3.10 or newer and [poetry](https://python-poetry.org/docs/). 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.** ``` 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 a940d74..694a0df 100755 --- a/python/sphinx_docs/src/opm_python_docs/download_files.py +++ b/python/sphinx_docs/src/opm_python_docs/download_files.py @@ -15,16 +15,13 @@ def docstrings_dir() -> Path: - """Return the directory the documentation build reads the JSON files from. + """Return the directory the documentation build reads downloaded files from. - docs/conf.py picks that directory by branch name: release branches use the - snapshots committed under python/, every other branch uses python/master-tmp/. - Downloading into the other one leaves the build unable to find the files. + 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(). """ - git_root_dir = helpers.get_git_root() - if helpers.get_current_branch().startswith("release-"): - return git_root_dir / "python" - target = git_root_dir / "python" / "master-tmp" + target = helpers.get_git_root() / "python" / "master-tmp" target.mkdir(parents=True, exist_ok=True) return target @@ -92,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 index baa9e1d..f5be5cd 100644 --- a/python/sphinx_docs/tests/test_download_files.py +++ b/python/sphinx_docs/tests/test_download_files.py @@ -1,49 +1,79 @@ """Tests for the docstrings download command. -The documentation build reads its JSON files from a directory that depends on -the branch name (see docs/conf.py): release branches use the snapshots -committed under python/, every other branch uses python/master-tmp/. -opmdoc-download-files has to write into the same directory, or the build fails -with a FileNotFoundError on a file the user has just downloaded. +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 -@pytest.mark.parametrize( - "branch, expected", - [ - ("master", "python/master-tmp"), - ("some-feature-branch", "python/master-tmp"), - ("release-2026.04", "python"), - ], -) -def test_docstrings_dir_follows_the_branch( - tmp_path: Path, mocker: MockerFixture, branch: str, expected: str -) -> None: +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 + - assert download_files.docstrings_dir() == tmp_path / expected +@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.""" - (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="master" - ) - - assert not (tmp_path / "python" / "master-tmp").exists() + 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() From 488448a685cfec9cdeeb6a27b1839bf8a7aa1d65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5kon=20H=C3=A6gland?= Date: Fri, 25 Sep 2026 05:20:06 +0200 Subject: [PATCH 5/5] Open the built docs with opmdoc-view-doc in the README The README told users to open the result with make view-docs, but that Makefile target runs xdg-open, which exists on Linux only; on macOS the documented step fails. opmdoc-view-doc opens the same page, python/sphinx_docs/docs/_build/ /index.html, through click.launch, which uses the platform's own opener. Without --branch it opens the current branch, as make view-docs does, so it replaces that step directly. The README already mentioned it for opening other branches. --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index c81acec..d740e57 100644 --- a/README.md +++ b/README.md @@ -34,12 +34,12 @@ Requires Python 3.10 or newer and [poetry](https://python-poetry.org/docs/). ``` poetry run make docs - poetry run make view-docs + poetry run opmdoc-view-doc ``` - `make docs` builds the branch you are on; `make view-docs` opens it in your - default browser. Use `opmdoc-view-doc --branch=master` to open a different - branch. The generated pages are written to + `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.