docs: each fact about the Python API is stated once, in the docstring of the name it belongs to - #1769
Merged
FBumann merged 4 commits intoSep 25, 2026
Conversation
… of the name it belongs to The Python API page kept its task sections beside the rendered reference, so most rules were stated twice. Their facts that no docstring held move into the docstrings, the keep= guidance moves to the debugging how-to, and the page keeps only the two rules every verb shares: names that differ only by case, and what each sink takes. Inbound links point at the rendered entries. Four drifts fixed on the way: xpress missing from solver_name, sweep.record's columns listed three different ways, check's Program described as something a verb takes back, and a how-to promising that a MultiIndex attaches. 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
added this pull request to stack #1770
September 25, 2026 13:13
Merging this PR will not alter performance
Comparing Footnotes
|
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
added a commit
that referenced
this pull request
Sep 25, 2026
… version that wrote it, and one written by 0.1.0 or earlier is refused by name (#1765) > **Prompt:** we should add some sort of archive version or specsolve version to the archives to ensure backwards compat if we change sth later Later: "use layout: 1". > [!NOTE] > The following content was generated by AI. Every saved answer, archives included, carried `format.json` as `{"answer": 0}`, whatever build wrote it. It now reads `{"layout": 1, "specsolve": "<version>"}`: a layout number that covers everything specsolve writes, and the version that wrote it. **Breaking:** 0.1.0 shipped without this, so an answer 0.1.0 wrote is refused. <details><summary>What changes, why these choices, tests, gates, not done</summary> **What was there.** `relational/parquet.py` stamped every answer directory with `format.json`. Every reader refused any other number with a `LayoutError`: `load_result`, `scan_result`, a sweep, a spill resume, and an archive's `answer/`. The number was held at 0 "while the layout is still moving", so every answer since the stamp was added, 0.1.0 and the alphas included, read as current. A layout change from here on could not have been told apart from any of them. **What changes** - **The key is `layout` and the constant is `LAYOUT = 1`.** The number covers what a result, a sweep and an archive write. A release that changes any of them raises it, and its notes name the change. The constant's comment, `Result.save`'s docstring (which `LayoutError` points to) and `AGENTS.md` (under "Breaking changes are free") say so. - **`format.json` records `specsolve`**: the version from the installed metadata, or `null` from a source tree nothing installed. It is written for the refusal message and for a human reading the file; no reader branches on it. - **The refusal names what it found:** `… holds a saved answer in layout 0, written by specsolve 0.0.1a359, and this package reads layout 1. …` A stamp with no `layout` (what 0.1.0 and every earlier build wrote, `{"answer": 0}`) and a missing `format.json` both read `… with no layout stamp …`. Either way the message says to solve again and save. - **Answers written by 0.1.0 or earlier are refused by name**, instead of being read as though their layout were current. Before 1.0 that is the stated rule, and the `!` in the title marks the break for the release notes. An archive that 0.1.0 wrote still holds the spec and the data to solve again. **Why one stamp.** An archive's `answer/` is a saved answer and carries the same `format.json`, and reading an archive reads it through `load_result`. So one number covers results, sweeps and archives, and a second stamp at the archive root would be a second home for the same fact. The key is `layout`, not `answer`, because the number covers the archive's other members too. **Why the version comes from `importlib.metadata`:** `relational/` may import nothing from the package but `errors` (`test_engine_is_isolated`). This reads the same installed metadata as `specsolve.__version__`. **Tests** - `test_a_saved_answer_is_stamped_with_its_layout_and_the_specsolve_that_wrote_it` (new): a fresh save holds `{"layout": 1, "specsolve": sps.__version__}`. - `test_an_answer_in_another_layout_is_refused_by_name` covers three cases: layout 0 from `0.0.1a359` (names the layout and the writer), 0.1.0's `{"answer": 0}`, and no `format.json` (both "with no layout stamp"). - On the first commit, both tests failed against the previous `parquet.py` and passed on the new one. That was checked by swapping the file back and restoring it from git. **Mutation table**, from `tools/mutate.py` on fa8fc6a; the tree was clean after the run: | mutation | result | |---|---| | a stamp other than this layout is refused (`parquet.py:73-82`) | **caught** | **Gates** on the merge of main after #1769, in a `uv` environment (dev + docs groups, `gurobi` and `xpress` extras), because pixi cannot be installed here: - `ruff check .` and `ruff format --check .` pass. - `pyrefly check`: 0 errors. - `zensical build --strict` builds, and the rendered API page carries the stamp paragraph under `Result.save`. - `pytest tests/test_docs_math.py tests/test_docstring_links.py`: 70 passed. - `pytest -n auto`: 4114 passed, 250 skipped, 1 xfailed. **Not done** - **No reader for another layout.** Before 1.0 nothing reads another layout back, and an archive holds the spec and the data to solve again. - **No mathspec version in the stamp.** `spec.yaml` states the language version the file is written in. </details> 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_016Pv7LzSgzt7Yn2K3ioyJXw --------- Co-authored-by: Claude <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.What moved where
check: the spec shapes, "aSpecis not read again", "no verb takes aProgram", theexpand('piecewise')/expand()choice, and the CI-verb sentencecheck: "solveandwriteread the same table", and a refusal names the construct and the sinks that take it. The intro of "What each sink takes"buildsources, which links the data contractModel(already covered)Model.row(a label its dimension cannot hold, no reader for a column).ConstraintRow, now rendered (linopy's format,display_termssummary)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)writeoutModel.update(a raise releases the model, the reason behindDataError,solve_overis the loop written for you)Model.solvekeepandResult.kept. The when-to-use guidance, the #815 numbers and #382 moved to howto/debug §6Model.solvearchive(sources through the build door, uncompressed),solve_overarchive(a sliced source archived whole, a hand-built axis refused),SolveArchive(spec as written, digest caveats),scan_result(re-read at every collect)DiagnosticsandMetricsattributes (already covered)Model.solvesolver_name/solver_options(xpress, a time limit in three vocabularies, Gurobi environment options).solvenow says "AsModel.solvetakes it"Other duplicates:
EachCoordinateandEachWindowplus the hand-built row. TheSliceMetricsand "asked before it is sliced" rows point at the entries.EachWindownow states the window checks rather than naming a private method. The remote-Gurobi sentence that docs(sweeps): the sweep reference says how many sessions a sweep opens on a remote Gurobi #1771 added now linksModel.solve.Drifts fixed:
solver_nameomitted xpress.sweep.recordcolumns were listed in three forms (6, 8 and 6). The real record has 9 columns.Sweep.record, sweeps.md and howto/parallel now say "oneRecordper slice", so the column list has one home.check's Returns said every verb takes theProgramback. No verb does.MultiIndexis refused.Spec.expand().../../docs/reference/sweeps.md, which is broken when rendered. They now use the site URL.ConstraintRowlinked a private helper; that is now plain text.Gates on the merge of #1766 (which now carries main through #1771), in a
uvenvironment with the dev and docs groups and thegurobiandxpressextras:ruff check .andruff format --check .: clean.pyrefly check: 0 errors.zensical build --strict: no issues. Everyapi.md#…anchor used underdocs/exists in the rendered page.test_docs_math,test_docstring_links): 70 passed.test_doc_examplesruns 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.
🤖 Generated with Claude Code
https://claude.ai/code/session_016Pv7LzSgzt7Yn2K3ioyJXw