Skip to content

docs: the Python API page renders every public name from its docstring - #1766

Merged
FBumann merged 5 commits into
mainfrom
claude/api-reference-from-docstrings
Sep 25, 2026
Merged

FBumann merged 5 commits into
mainfrom
claude/api-reference-from-docstrings

Conversation

@FBumann

@FBumann FBumann commented Sep 25, 2026 •

Copy link
Copy Markdown
Collaborator

Prompt: Use the docstrings. What is api.md then for anymore?

After "Do it", on the plan to render the docstrings first and move api.md's tasks into how-tos next. Later: "How can we ensure future code changes use this format for links?", answered by a test for every docstring link and a rule in AGENTS.md.

Note

The following content was generated by AI.

docs/reference/api.md now renders all 27 exported names from their docstrings with mkdocstrings, plus the four row and frame types a caller reads back. The 483 Sphinx roles in src/ are now links, and a test holds every docstring link in src/ to landing on a specsolve object, rendered or not.

What changed, what the build caught, guards, gates, not done

The page

  • A ## Reference section, grouped as the architecture's surface is: run a spec (check, build, solve, write, evaluate); run it many times (solve_over, EachCoordinate, EachWindow); what comes back (Model, Result, Sweep, and Diagnostics, Record, Metrics, SliceMetrics, which readers return but __all__ does not name); carry an answer (the two archives and six readers); errors and warnings.
  • It replaces "The verbs" table and "Errors and warnings". No page linked to either.
  • The task sections stay as they are, from "The spec argument" through "Choosing a solver". About 30 links on other pages point into them. Moving them to how-to guides is the stacked follow-up.

The tooling

  • The docs group adds mkdocstrings==1.0.6, mkdocstrings-python==2.0.9 and griffe-sphinx==0.3.0, pinned exactly like the rest of the group. uv.lock is relocked.
  • griffe-sphinx reads the #: attribute comments the tree already uses (AGENTS.md names them). Without it, the fields of Sweep, Diagnostics, Record, Metrics and SliceMetrics render with no text.
  • load_external_modules: true renders SpecsolveError, LanguageError, SchemaError and DimensionError, which specsolve re-exports from mathspec, from mathspec's installed source.

The roles. All 483 Sphinx roles in src/ are converted, as energy-models/mathspec#720 did:

  • A specsolve target becomes [`name`][], or [`name`][dotted.path] where it was written ~dotted.path. It resolves in the docstring's own scope.
  • A name from another package becomes plain code, because the site loads no object inventories.

Keeping future links in this form

  • tests/test_docs_site.py::test_no_docstring_links_with_a_sphinx_role refuses a Sphinx role anywhere in src/. It runs in the main suite.
  • tests/test_docstring_links.py (new) loads src/ with griffe and the same griffe_sphinx extension the site uses. It resolves each link the way the site does: in the docstring's scope, walking outward. It refuses one that lands nowhere, or on another package. It reads 440 links, docstrings and #: comments together, and asserts it read over 400, so a walk that reaches nothing fails too. It runs under pixi run docs-test, which CI's docs job already calls, and skips where griffe is absent.
  • It found 8 links in modules the site never renders that pointed at nothing. _entries no longer exists (now [_splice][]); placed is Grouping's, not _Order's; row_major, spanned, PolarsCompiler.expression and Assembly._build_objective needed their full path. A local function and mathspec's Program became plain code.
  • AGENTS.md, Docstrings: a name is linked as [`name`][] where the module imports it and [`name`][dotted.path] where it does not; a name from another package is plain code.

What the strict build caught, and the fixes

  • Nine links to objects the page does not render. Four are now rendered (Diagnostics, Record, Metrics, SliceMetrics). A private method, the KEEPS constant, concurrent.futures.Executor, LanguageError inside errors.py, and two internal digests are now plain code. The keep= docstrings now name the three values instead of pointing at KEEPS.
  • An indented line in Model's docstring rendered as a code block with the backticks showing; it is now one line of code spans.
  • A page mkdocstrings cannot render is dropped silently. zensical printed no error. The only symptom was 72 "page does not exist" warnings from inbound links. That is how the missing load_external_modules showed up. The strict build still fails, but the message points at the links, not at the cause.

Guards. Mutation table, taken by hand because tools/mutate.py mutates Python only. Each run started from a committed tree, restored through git checkout --, and dropped __pycache__ on both sides; the tree was clean after each set:

mutation caught by result
the ::: specsolve.check entry deleted from api.md the entry test caught
a :func: role added to solve's docstring the Sphinx-role test caught
a stale name restored ([_entries][]) the link test caught
a link to a name that exists nowhere the link test caught
a link to another package ([to_spec][mathspec.to_spec]) the link test caught

Gates, in a uv environment (dev + docs groups, gurobi and xpress extras), because pixi cannot be installed here:

  • ruff check . and ruff format --check . pass.
  • pytest -n auto: 4143 passed, 250 skipped, 1 xfailed.
  • pyrefly check: 0 errors, on the first commit. The second commit edits docstrings and tests only.
  • zensical build --strict: builds, with no issues on the first commit. The rendered page holds 130 entries, no Sphinx role, and no stray doubled backticks.
  • The docs-test task's command, pytest tests/test_docs_math.py tests/test_docstring_links.py: 70 passed.

Not done:

  • Moving the task sections into how-tos. That is the stacked follow-up, as agreed.
  • A read of every public docstring as a caller now sees it. Only what the build and the link test flagged is fixed here.
  • Hard rule 5 is unchanged. "The public interface is a declared model, not a Python API" is about constructing math, and still holds. architecture.md's surface section now says the docstrings are the reference.

🤖 Generated with Claude Code

https://claude.ai/code/session_016Pv7LzSgzt7Yn2K3ioyJXw

The reference on docs/reference/api.md is now mkdocstrings, one entry per
name in specsolve.__all__ plus the rows and frames a caller reads back
(Diagnostics, Record, Metrics, SliceMetrics). It replaces the
hand-written verb table and error list, which repeated the docstrings.

- The 483 Sphinx roles in src/ are mkdocstrings cross-references, which
  the site resolves; a name from another package is plain code.
- griffe-sphinx reads the `#:` attribute comments the tree already uses.
- load_external_modules renders the language's errors that specsolve
  re-exports from mathspec.
- tests/test_docs_site.py holds every exported name to an entry, and
  refuses a Sphinx role in src/.

The task sections of api.md stay where they are; moving them into the
how-to guides is the next change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016Pv7LzSgzt7Yn2K3ioyJXw
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016Pv7LzSgzt7Yn2K3ioyJXw
@read-the-docs-community

read-the-docs-community Bot commented Sep 25, 2026 •

Copy link
Copy Markdown

@codspeed

codspeed Bot commented Sep 25, 2026 •

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 24 untouched benchmarks
⏩ 82 skipped benchmarks1


Comparing claude/api-reference-from-docstrings (8e79d37) with main (8c783ca)2

Open in CodSpeed

Footnotes

  1. 82 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports. ↩

  2. No successful run was found on main (a8c9fbe) during the generation of this report, so 8c783ca was used instead as the comparison base. There might be some changes unrelated to this pull request in this report. ↩

tests/test_docstring_links.py walks every docstring in src/, the `#:`
comments included, resolves each link the way the site does, and refuses
one that lands nowhere or on another package. The strict build only
checks the docstrings it renders, and eight links in modules it never
renders pointed at nothing; they are fixed. It runs in `pixi run
docs-test`, where griffe is installed.

AGENTS.md says how a docstring links a name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016Pv7LzSgzt7Yn2K3ioyJXw
@FBumann
FBumann added this pull request to stack #1770 September 25, 2026 13:13
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016Pv7LzSgzt7Yn2K3ioyJXw
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016Pv7LzSgzt7Yn2K3ioyJXw
@FBumann
FBumann merged commit a011020 into main Sep 25, 2026
13 checks passed
@FBumann
FBumann deleted the claude/api-reference-from-docstrings branch September 25, 2026 16:29
FBumann added a commit that referenced this pull request Sep 25, 2026
… of the name it belongs to (#1769)

> **Prompt:** Is there even more duplication in the docs now? Especially
in Reference? As a stacked PR

> [!NOTE]
> The following content was generated by AI.

The API page is now the rendered reference plus two rules that every
verb shares. Each task-section fact that no docstring held moved into
the docstring of its name. The `keep=` guidance moved to the debugging
how-to. Four drifts are fixed.

<details><summary>What moved where</summary>

| api.md section (deleted) | Its unique facts now live in |
|---|---|
| The spec argument, Checking a spec | `check`: the spec shapes, "a
`Spec` is not read again", "no verb takes a `Program`", the
`expand('piecewise')` / `expand()` choice, and the CI-verb sentence |
| Checking against a sink | `check`: "`solve` and `write` read the same
table", and a refusal names the construct and the sinks that take it.
The intro of "What each sink takes" |
| The sources argument | `build` `sources`, which links the data
contract |
| Building a model | `Model` (already covered) |
| Reading one row | `Model.row` (a label its dimension cannot hold, no
reader for a column). `ConstraintRow`, now rendered (linopy's format,
`display_terms` summary) |
| Reading a result | `dual` (an expanded set, duals only where a solver
ran, no reduced costs or slacks). `dual_ray` (`InfUnbdInfo` /
`presolve`, live only). `Result.evaluate` (an undeclared expression is
not a kind, the archive model check, a new parameter is a build).
`to_pandas` (pandas is not installed) |
| Writing a file | `write` `out` |
| Re-solving with new numbers | `Model.update` (a raise releases the
model, the reason behind `DataError`, `solve_over` is the loop written
for you) |
| How much of the session a solve keeps | The contract was already in
`Model.solve` `keep` and `Result.kept`. The when-to-use guidance, the
#815 numbers and #382 moved to howto/debug §6 |
| Archiving a model, Loading or scanning | howto/archiving (already
covered), `Model.solve` `archive` (sources through the build door,
uncompressed), `solve_over` `archive` (a sliced source archived whole, a
hand-built axis refused), `SolveArchive` (spec as written, digest
caveats), `scan_result` (re-read at every collect) |
| Diagnostics | the `Diagnostics` and `Metrics` attributes (already
covered) |
| Choosing a solver | `Model.solve` `solver_name` / `solver_options`
(xpress, a time limit in three vocabularies, Gurobi environment
options). `solve` now says "As `Model.solve` takes it" |

Other duplicates:
- sweeps.md: "The axes" table became links to `EachCoordinate` and
`EachWindow` plus the hand-built row. The `SliceMetrics` and "asked
before it is sliced" rows point at the entries. `EachWindow` now states
the window checks rather than naming a private method. The remote-Gurobi
sentence that #1771 added now links `Model.solve`.
- glossary: "Row types" became one-liners that link the entries.

**Drifts fixed:**
- `solver_name` omitted xpress.
- `sweep.record` columns were listed in three forms (6, 8 and 6). The
real record has 9 columns. `Sweep.record`, sweeps.md and howto/parallel
now say "one `Record` per slice", so the column list has one home.
- `check`'s Returns said every verb takes the `Program` back. No verb
does.
- howto/data said index *levels* attach by name. A `MultiIndex` is
refused.
- The sink table said HiGHS rewrites SOS to binaries. It refuses and
names `Spec.expand()`.
- Two docstrings linked `../../docs/reference/sweeps.md`, which is
broken when rendered. They now use the site URL. `ConstraintRow` linked
a private helper; that is now plain text.

**Gates** on the merge of #1766 (which now carries main through #1771),
in a `uv` environment with the dev and docs groups and the `gurobi` and
`xpress` extras:
- `ruff check .` and `ruff format --check .`: clean.
- `pyrefly check`: 0 errors.
- `zensical build --strict`: no issues. Every `api.md#…` anchor used
under `docs/` exists in the rendered page.
- Docs tests (`test_docs_math`, `test_docstring_links`): 70 passed.
- Full suite: 4115 passed, 250 skipped, 1 xfailed. It collects fewer
tests than #1766 (4153) because `test_doc_examples` runs one test per
fence, and the deleted sections held fences.

**Not done:** the task sections were not rewritten as new how-tos. Their
examples already exist in howto/archiving, howto/debug and
interactive.md. #1765 edits the api.md archive table that this PR
deletes, so whichever PR merges second resolves that conflict.

</details>

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_016Pv7LzSgzt7Yn2K3ioyJXw

---------

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants