tools: command records - NOTES always bullets, examples held by tests (#142)
CI / verify (push) Successful in 1m12s
Release / release (push) Successful in 35s

Files changed:
- CHANGES.md
- VERSION
- tools/chemenu/cli_contract.py
- tools/chemenu/tests/test_cli.py
- tools/chemenu/tests/test_cli_contract.py
- tools/chemenu/tests/test_docs_verify.py
This commit is contained in:
torben committed 2026-09-26 09:33:46 +02:00
1 parent 5bfbb49d74
commit 5aae7fed1b
6 files changed
+54 -23

No files matched your search

+10 -1
View File
@@ -59,7 +59,7 @@ concern - readable here, never shipped as something to parse.
--- ---
## 7.1.0-beta.20 - 2026-09-26 - Command records, Instance health group: one bullet per check, examples ## 7.1.0-beta.21 - 2026-09-26 - Command records: NOTES is always a tuple of bullets; every record's examples are tested
**Author:** Torben Nehmer **Author:** Torben Nehmer
@@ -89,6 +89,7 @@ concern - readable here, never shipped as something to parse.
- Command records, Content migrations group: one line per cause, examples, prohibitions - Command records, Content migrations group: one line per cause, examples, prohibitions
- Command records, Private instances group: one line per cause, examples, prohibitions - Command records, Private instances group: one line per cause, examples, prohibitions
- Command records, Instance health group: one bullet per check, examples - Command records, Instance health group: one bullet per check, examples
- Command records: NOTES is always a tuple of bullets; every record's examples are tested
<!-- /wikitool:bumps --> <!-- /wikitool:bumps -->
### CalDAV task-tracker provider (Nextcloud Tasks, iOS Reminders); review reports unknown-value findings instead of skipping them ### CalDAV task-tracker provider (Nextcloud Tasks, iOS Reminders); review reports unknown-value findings instead of skipping them
@@ -333,6 +334,14 @@ dropped rather than moved: the record justified the conventions `FAIL` by `xref`
out of the section headings, while the check's own docstring calls those headings cosmetic and out of the section headings, while the check's own docstring calls those headings cosmetic and
grounds the `FAIL` in the file binding every page. grounds the `FAIL` in the file binding every page.
### Command records: NOTES is always a tuple of bullets; every record's examples are tested
With every group rewritten, `CommandRecord.notes` no longer accepts the single-paragraph string
it carried over from the first pass; a record that passes one is refused at import. Two tests
over the real registry hold what the rewrite established: every command has at least one
example, and every command with a gate shows how its clearance is passed back in (`--confirm`,
`--confirm-rebase` or `--resume`).
--- ---
## 7.0.0 - 2026-09-22 - Task-Tracker-Anbindung: Vorhaben als Seitenart, Verpflichtungsschicht, Weekly Review als Read-Time-Join ## 7.0.0 - 2026-09-22 - Task-Tracker-Anbindung: Vorhaben als Seitenart, Verpflichtungsschicht, Weekly Review als Read-Time-Join
+1 -1
View File
@@ -1 +1 @@
7.1.0-beta.20 7.1.0-beta.21
+14 -16
View File
@@ -11,11 +11,12 @@ the `###`-level grouping and rendering order, unchanged from what
Phase 1 (Gitea #121) filled every record mechanically and word-for-word from Phase 1 (Gitea #121) filled every record mechanically and word-for-word from
the two tables `tools/CONTRACT.md` used to carry. Phase 2 (Gitea #142) the two tables `tools/CONTRACT.md` used to carry. Phase 2 (Gitea #142)
rewrites them group by group: NOTES into present-tense bullets, one rewrote them: NOTES as present-tense bullets, one `Failure` per cause,
`Failure` per cause, EXAMPLES/NEVER/SEE ALSO filled, and "why" moved out to EXAMPLES/NEVER/SEE ALSO filled, and "why" moved out to a code comment where
a code comment where the behaviour is implemented. A sentence several the behaviour is implemented. How a record is written is the `CommandRecord`
records carry verbatim lives here once (`token_gate_reaction`, ...), so the docstring's job. A sentence several records carry verbatim lives here once
output repeats it and the source does not. (`token_gate_reaction`, ...), so the output repeats it and the source does
not.
""" """
from __future__ import annotations from __future__ import annotations
@@ -120,22 +121,22 @@ class CommandRecord:
- `see_also`: related commands and the instruction that uses this one. - `see_also`: related commands and the instruction that uses this one.
It is the only place another command may be named for context - a It is the only place another command may be named for context - a
behaviour this command shares with another is stated here in full, behaviour this command shares with another is stated here in full,
not as "same as `X`". not as "same as `X`"."""
A plain `str` for `notes` is still accepted and renders as one paragraph:
the phase-1 form, kept only until every group has been rewritten into
bullets (Gitea #142)."""
path: str path: str
summary: str summary: str
synopsis: tuple[Variant, ...] synopsis: tuple[Variant, ...]
properties: Properties properties: Properties
notes: tuple[str, ...] | str notes: tuple[str, ...]
failures: tuple[Failure, ...] failures: tuple[Failure, ...]
examples: tuple[str, ...] = () examples: tuple[str, ...] = ()
never: tuple[str, ...] = () never: tuple[str, ...] = ()
see_also: tuple[str, ...] = () see_also: tuple[str, ...] = ()
def __post_init__(self) -> None: def __post_init__(self) -> None:
if isinstance(self.notes, str):
raise ValueError(
f"cli_contract: {self.path!r} notes must be a tuple of bullets, not one string"
)
if not self.properties.gates and any(f.code == 42 for f in self.failures): if not self.properties.gates and any(f.code == 42 for f in self.failures):
raise ValueError( raise ValueError(
f"cli_contract: {self.path!r} lists an exit-42 cause but declares no gate" f"cli_contract: {self.path!r} lists an exit-42 cause but declares no gate"
@@ -294,11 +295,8 @@ def _labelled(failure: Failure, text: str) -> str:
return f"{failure.label}: {text}" if failure.label else text return f"{failure.label}: {text}" if failure.label else text
def _notes_lines(notes: tuple[str, ...] | str) -> list[str]: def _notes_lines(notes: tuple[str, ...]) -> list[str]:
"""NOTES as rendered lines: one `- ` bullet per entry, or the phase-1 """NOTES as rendered lines: one `- ` bullet per entry."""
paragraph unchanged."""
if isinstance(notes, str):
return [notes]
return [f"- {note}" for note in notes] return [f"- {note}" for note in notes]
+19
View File
@@ -235,3 +235,22 @@ def test_a_usage_error_is_unframed_and_names_wikitool(monkeypatch):
text = out + err text = out + err
assert "Usage: wikitool publish" in text assert "Usage: wikitool publish" in text
assert not FRAME_CHARS & set(text) assert not FRAME_CHARS & set(text)
def test_every_record_shows_at_least_one_example():
"""Every real command record carries a copyable example (Gitea #142)."""
missing = [path for path, rec in cli_contract.all_records().items() if not rec.examples]
assert missing == []
def test_every_gated_record_shows_its_re_run_after_exit_42():
"""A command with a gate shows how the clearance is passed back in -
`--confirm`, `--confirm-rebase`, or `new project`'s `--resume`."""
clearing_flags = ("--confirm", "--confirm-rebase", "--resume")
missing = [
path
for path, rec in cli_contract.all_records().items()
if rec.properties.gates
and not any(flag in example for example in rec.examples for flag in clearing_flags)
]
assert missing == []
+7 -2
View File
@@ -23,7 +23,7 @@ def _fixture_record(**overrides) -> cc.CommandRecord:
atomic="Yes - single file write", atomic="Yes - single file write",
budget=cc.Budget.COUNTED, budget=cc.Budget.COUNTED,
), ),
notes="Frobnicates the named widget in place.", notes=("Frobnicates the named widget in place.",),
failures=(cc.Failure(label="", cause="Widget not found", reaction="Fix the name and retry once"),), failures=(cc.Failure(label="", cause="Widget not found", reaction="Fix the name and retry once"),),
) )
defaults.update(overrides) defaults.update(overrides)
@@ -218,7 +218,7 @@ def test_render_markdown_section_has_no_options_heading():
section = cc.render_markdown_section(rec) section = cc.render_markdown_section(rec)
assert "OPTIONS" not in section assert "OPTIONS" not in section
assert "#### `frobnicate`" in section assert "#### `frobnicate`" in section
assert rec.notes in section assert f"- {rec.notes[0]}" in section
def test_notes_tuple_renders_one_bullet_per_entry(): def test_notes_tuple_renders_one_bullet_per_entry():
@@ -297,3 +297,8 @@ def test_exit_42_cause_without_a_gate_is_refused():
def test_failure_code_outside_0_1_42_is_refused(): def test_failure_code_outside_0_1_42_is_refused():
with pytest.raises(ValueError): with pytest.raises(ValueError):
cc.Failure(cause="Crash", reaction="Report", code=2) cc.Failure(cause="Crash", reaction="Report", code=2)
def test_notes_as_one_string_is_refused():
with pytest.raises(ValueError, match="tuple of bullets"):
_fixture_record(notes="One paragraph, the phase-1 form.")
+3 -3
View File
@@ -52,7 +52,7 @@ def _fixture_record(path: str) -> cli_contract.CommandRecord:
atomic="Yes", atomic="Yes",
budget=cli_contract.Budget.COUNTED, budget=cli_contract.Budget.COUNTED,
), ),
notes="Does a thing, mechanically.", notes=("Does a thing, mechanically.",),
failures=(cli_contract.Failure(label="", cause="It broke", reaction="Fix and retry"),), failures=(cli_contract.Failure(label="", cause="It broke", reaction="Fix and retry"),),
) )
@@ -150,7 +150,7 @@ def test_a_phantom_synopsis_flag_is_reported(monkeypatch):
summary="Does a thing.", summary="Does a thing.",
synopsis=(cli_contract.Variant(usage="frobnicate --flag <v> --invented <v>"),), synopsis=(cli_contract.Variant(usage="frobnicate --flag <v> --invented <v>"),),
properties=_fixture_record("frobnicate").properties, properties=_fixture_record("frobnicate").properties,
notes="Does a thing.", notes=("Does a thing.",),
failures=(), failures=(),
) )
param = click.Option(["--flag"]) param = click.Option(["--flag"])
@@ -188,7 +188,7 @@ def test_a_boolean_flag_pair_is_satisfied_by_either_spelling(monkeypatch):
summary="Does a thing.", summary="Does a thing.",
synopsis=(cli_contract.Variant(usage="frobnicate [--no-push]"),), synopsis=(cli_contract.Variant(usage="frobnicate [--no-push]"),),
properties=_fixture_record("frobnicate").properties, properties=_fixture_record("frobnicate").properties,
notes="Does a thing.", notes=("Does a thing.",),
failures=(), failures=(),
) )
param = click.Option(["--push/--no-push"], default=True) param = click.Option(["--push/--no-push"], default=True)