Skip to content
Open
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
42 changes: 40 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/<branch>/` 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/<your-github-username>/opm-python-documentation/actions`
Expand Down
28 changes: 24 additions & 4 deletions python/sphinx_docs/src/opm_python_docs/download_files.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
#! /usr/bin/env python3

import logging
from pathlib import Path

import requests

import click
Expand All @@ -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}"
Expand All @@ -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}")
Expand All @@ -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}")
Expand Down Expand Up @@ -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()
Expand Down
79 changes: 79 additions & 0 deletions python/sphinx_docs/tests/test_download_files.py
Original file line number Diff line number Diff line change
@@ -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()
Loading