From f4a617a4ee7fa2c92c3129be5a042c09ec5cb6be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5kon=20H=C3=A6gland?= Date: Tue, 22 Sep 2026 20:31:37 +0200 Subject: [PATCH 1/3] Render field lists in template-format docstrings Docstrings coming from the template format were passed to docutils as a single line, so reStructuredText field lists in them were never parsed. The published API page showed the markup itself: :param init: Whether to call MPI_Init() or not. instead of a parameter table. The cause is the separator in for line in doc.split('\\n'): # Handle escaped newlines In Python source '\\n' is a two-character string, a backslash followed by an n. json.load has already turned the \n escapes in the JSON file into real newline characters, so nothing in the docstring matches and split returns one piece: the whole docstring, newlines and all, appended to the ViewList as a single line. docutils parses line by line, so the ":param" never starts a line and is treated as ordinary text. Split on a real newline instead, as the flat-format path a few lines below already does. That path was unaffected, which is why the opm-common page has always rendered its parameter tables correctly while the opm-simulators page did not. Both the constructor loop and the method loop had the same separator. --- .../sphinx_docs/src/opm_python_docs/sphinx_ext_docstrings.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/python/sphinx_docs/src/opm_python_docs/sphinx_ext_docstrings.py b/python/sphinx_docs/src/opm_python_docs/sphinx_ext_docstrings.py index 26311c5..f27ecfa 100644 --- a/python/sphinx_docs/src/opm_python_docs/sphinx_ext_docstrings.py +++ b/python/sphinx_docs/src/opm_python_docs/sphinx_ext_docstrings.py @@ -70,7 +70,7 @@ def process_template_docstrings(directive, config): rst.append("", source="") doc = expanded.get("doc", "") if doc: - for line in doc.split('\\n'): # Handle escaped newlines + for line in doc.split('\n'): rst.append(f" {line}", source="") rst.append("", source="") @@ -83,7 +83,7 @@ def process_template_docstrings(directive, config): rst.append("", source="") doc = expanded.get("doc", "") if doc: - for line in doc.split('\\n'): # Handle escaped newlines + for line in doc.split('\n'): rst.append(f" {line}", source="") rst.append("", source="") From 004d35163b55ca6478771debed8018b72b7aeac3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5kon=20H=C3=A6gland?= Date: Tue, 22 Sep 2026 20:31:37 +0200 Subject: [PATCH 2/3] Add a regression test for template-format field lists Builds a minimal Sphinx project from a small template-format docstrings file and asserts that a ":param" in the JSON becomes a parameter table rather than visible text, for both a method and a constructor since they go through separate loops. The existing tests/files/docstrings_simulators.json is in the older flat format, which uses the code path that was already correct, so it cannot exercise this. A small template-format file is added alongside it. Both tests fail on the previous commit's parent and pass on it. --- .../files/docstrings_simulators_template.json | 21 ++++++ .../tests/test_sphinx_ext_docstrings.py | 72 +++++++++++++++++++ 2 files changed, 93 insertions(+) create mode 100644 python/sphinx_docs/tests/files/docstrings_simulators_template.json create mode 100644 python/sphinx_docs/tests/test_sphinx_ext_docstrings.py diff --git a/python/sphinx_docs/tests/files/docstrings_simulators_template.json b/python/sphinx_docs/tests/files/docstrings_simulators_template.json new file mode 100644 index 0000000..686f5b5 --- /dev/null +++ b/python/sphinx_docs/tests/files/docstrings_simulators_template.json @@ -0,0 +1,21 @@ +{ + "simulators": { + "PyBlackOilSimulator": { + "name": "BlackOilSimulator", + "class": "PyBlackOilSimulator", + "doc": "Simulator for black oil cases." + } + }, + "constructors": { + "filename_constructor": { + "signature_template": "opm.simulators.{{name}}.__init__(filename: str) -> None", + "doc": "Constructor from a deck file name.\n\n:param filename: Path to the deck file.\n:type filename: str" + } + }, + "common_methods": { + "advance": { + "signature_template": "opm.simulators.{{name}}.advance(report_step: int) -> None", + "doc": "Advances the simulation to a specific report step.\n\n:param report_step: Target report step to advance to.\n:type report_step: int" + } + } +} diff --git a/python/sphinx_docs/tests/test_sphinx_ext_docstrings.py b/python/sphinx_docs/tests/test_sphinx_ext_docstrings.py new file mode 100644 index 0000000..8183d3f --- /dev/null +++ b/python/sphinx_docs/tests/test_sphinx_ext_docstrings.py @@ -0,0 +1,72 @@ +"""Tests for the JSON -> Sphinx documentation extension. + +The template format stores a method's docstring as a single JSON string with +embedded newlines. The extension has to hand those to docutils one line at a +time; feeding it the whole docstring as a single line makes docutils treat +reStructuredText field lists such as ``:param x:`` as ordinary text, so the +published page shows the markup instead of a parameter table. +""" + +import shutil +from pathlib import Path + +from sphinx.application import Sphinx + + +def build_docs(tmp_path: Path, test_file_path: Path, json_name: str) -> str: + """Build a minimal Sphinx project that renders one docstrings JSON file. + + Returns the generated HTML. + """ + srcdir = tmp_path / "src" + srcdir.mkdir() + shutil.copy(test_file_path / json_name, srcdir / json_name) + + (srcdir / "conf.py").write_text( + "extensions = ['opm_python_docs.sphinx_ext_docstrings']\n" + f"opm_simulators_docstrings_path = r'{srcdir / json_name}'\n" + f"opm_common_docstrings_path = r'{srcdir / json_name}'\n" + ) + (srcdir / "index.rst").write_text( + "Test\n" + "====\n" + "\n" + ".. opm_simulators_docstrings::\n" + ) + + outdir = tmp_path / "out" + app = Sphinx( + srcdir=str(srcdir), + confdir=str(srcdir), + outdir=str(outdir), + doctreedir=str(tmp_path / "doctrees"), + buildername="html", + ) + app.build() + return (outdir / "index.html").read_text() + + +def test_template_format_renders_field_lists( + tmp_path: Path, test_file_path: Path +) -> None: + """A ``:param:`` in a template-format docstring becomes a parameter table.""" + html = build_docs(tmp_path, test_file_path, "docstrings_simulators_template.json") + + # The rendered page must not contain the field-list markup as visible text. + assert ":param report_step:" not in html + assert ":type report_step:" not in html + + # It must contain a real parameter table instead. + assert "field-list" in html + assert "report_step" in html + assert "Target report step to advance to." in html + + +def test_template_format_renders_constructor_field_lists( + tmp_path: Path, test_file_path: Path +) -> None: + """Constructors go through a separate code path and need the same handling.""" + html = build_docs(tmp_path, test_file_path, "docstrings_simulators_template.json") + + assert ":param filename:" not in html + assert "Path to the deck file." in html From 43c05d299ed18cfdf92fb3bb35ec82dd47402f29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5kon=20H=C3=A6gland?= Date: Tue, 22 Sep 2026 20:49:00 +0200 Subject: [PATCH 3/3] Remove a debug print from read_doc_strings The function printed the JSON path it was about to open, on every call, to the build's standard output. With two directives across three branch builds that is six bare absolute paths in the log, interleaved with sphinx-versioned's own output and with no indication of what they are. Removed rather than converted to a logger call, since the path is already determined by conf.py and visible there. If it is wanted as a diagnostic, sphinx.util.logging's logger.verbose() would put it behind sphinx-build -v instead of printing it unconditionally. --- python/sphinx_docs/src/opm_python_docs/sphinx_ext_docstrings.py | 1 - 1 file changed, 1 deletion(-) diff --git a/python/sphinx_docs/src/opm_python_docs/sphinx_ext_docstrings.py b/python/sphinx_docs/src/opm_python_docs/sphinx_ext_docstrings.py index f27ecfa..c71065c 100644 --- a/python/sphinx_docs/src/opm_python_docs/sphinx_ext_docstrings.py +++ b/python/sphinx_docs/src/opm_python_docs/sphinx_ext_docstrings.py @@ -96,7 +96,6 @@ def process_template_docstrings(directive, config): return result def read_doc_strings(directive, docstrings_path): - print(docstrings_path) with open(docstrings_path, 'r') as file: docstrings = json.load(file)