tools: command records - NOTES always bullets, examples held by tests (#142)
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:
1 parent
5bfbb49d74
commit
5aae7fed1b
6 files changed
+54
-23
No files matched your search
+10
-1
@@ -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
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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 == []
|
||||||
@@ -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.")
|
||||||
@@ -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)
|
||||||
|
|||||||
Reference in new issue
Block a user