Skip to content

Commit c59939b

Browse files
committed
Add CLI relationship summary
1 parent 0f09aee commit c59939b

3 files changed

Lines changed: 121 additions & 0 deletions

File tree

docs/cli-reference/index.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,28 @@ This documentation is auto-generated from test cases.
1818
| ⚙️ [General Options](general-options.md) | 18 | Utilities and meta options |
1919
| 📝 [Utility Options](utility-options.md) | 10 | Help, version, debug options |
2020

21+
## 🔗 Option Relationships
22+
23+
These links are generated from CLI option metadata and summarize options that imply, require, or conflict with other options.
24+
25+
| Source | Kind | Condition | Target | Note |
26+
|--------|------|-----------|--------|------|
27+
| [`--use-annotated`](typing-customization.md#use-annotated) | Implies | Always | [`--field-constraints`](field-customization.md#field-constraints) enabled | - |
28+
| [`--use-specialized-enum`](typing-customization.md#use-specialized-enum) | Requires | Always | [`--target-python-version`](model-customization.md#target-python-version) = `3.11+` | `--use-specialized-enum` requires `--target-python-version` 3.11 or higher. |
29+
| [`--original-field-name-delimiter`](field-customization.md#original-field-name-delimiter) | Requires | Always | [`--snake-case-field`](field-customization.md#snake-case-field) enabled | `--original-field-name-delimiter` can not be used without `--snake-case-field`. |
30+
| [`--allow-extra-fields`](model-customization.md#allow-extra-fields) | Implies | Always | [`--extra-fields`](field-customization.md#extra-fields) = `allow` | - |
31+
| [`--collapse-root-models-name-strategy`](model-customization.md#collapse-root-models-name-strategy) | Requires | Always | [`--collapse-root-models`](model-customization.md#collapse-root-models) enabled | `--collapse-root-models-name-strategy` requires `--collapse-root-models`. |
32+
| [`--keyword-only`](model-customization.md#keyword-only) | Requires | Always | [`--target-python-version`](model-customization.md#target-python-version) = `3.10+` | `--keyword-only` requires `--target-python-version` 3.10 or higher for dataclasses. |
33+
| [`--output-model-type`](model-customization.md#output-model-type) | Implies | `--output-model-type` = `msgspec.Struct` | [`--use-annotated`](typing-customization.md#use-annotated) enabled | - |
34+
| [`--parent-scoped-naming`](model-customization.md#parent-scoped-naming) | Implies | Always | [`--naming-strategy`](model-customization.md#naming-strategy) = `parent-prefixed` | - |
35+
| [`--reuse-scope`](model-customization.md#reuse-scope) | Requires | `--reuse-scope` = `tree` | [`--reuse-model`](model-customization.md#reuse-model) enabled | `--reuse-scope=tree` has no effect without `--reuse-model`. |
36+
| [`--union-mode`](model-customization.md#union-mode) | Requires | Always | [`--output-model-type`](model-customization.md#output-model-type) = `pydantic_v2.BaseModel` | `--union-mode` is only supported for `--output-model-type pydantic_v2.BaseModel`. |
37+
| [`--use-missing-sentinel`](model-customization.md#use-missing-sentinel) | Implies | Always | [`--target-pydantic-version`](model-customization.md#target-pydantic-version) = `2.12` | - |
38+
| [`--use-missing-sentinel`](model-customization.md#use-missing-sentinel) | Requires | Always | [`--output-model-type`](model-customization.md#output-model-type) = `pydantic_v2.BaseModel` | `--use-missing-sentinel` is only supported for `--output-model-type pydantic_v2.BaseModel`. |
39+
| [`--custom-file-header`](template-customization.md#custom-file-header) | Conflicts | Always | [`--custom-file-header-path`](template-customization.md#custom-file-header-path) | `--custom-file-header` can not be used with `--custom-file-header-path`. |
40+
| [`--custom-file-header-path`](template-customization.md#custom-file-header-path) | Conflicts | Always | [`--custom-file-header`](template-customization.md#custom-file-header) | `--custom-file-header-path` can not be used with `--custom-file-header`. |
41+
| [`--all-exports-collision-strategy`](general-options.md#all-exports-collision-strategy) | Requires | Always | [`--all-exports-scope`](general-options.md#all-exports-scope) = `recursive` | `--all-exports-collision-strategy` can only be used with `--all-exports-scope=recursive`. |
42+
2143
## All Options
2244

2345
**Jump to:** [A](#a) · [B](#b) · [C](#c) · [D](#d) · [E](#e) · [F](#f) · [G](#g) · [H](#h) · [I](#i) · [K](#k) · [L](#l) · [M](#m) · [N](#n) · [O](#o) · [P](#p) · [R](#r) · [S](#s) · [T](#t) · [U](#u) · [V](#v) · [W](#w)

scripts/build_cli_docs.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -943,6 +943,61 @@ def _generate_option_relationships(
943943
return "**Option relationships:**\n\n" + "\n".join(relationship_lines) + "\n\n"
944944

945945

946+
def _escape_table_cell(value: str) -> str:
947+
"""Escape Markdown table delimiters in generated table content."""
948+
return value.replace("|", "\\|")
949+
950+
951+
def _format_relation_condition_cell(option: str, when: Any) -> str:
952+
"""Format a relationship condition for a summary table cell."""
953+
if when is True:
954+
return f"`{option}` enabled"
955+
if when is False:
956+
return f"`{option}` disabled"
957+
if when is None:
958+
return "Always"
959+
return f"`{option}` = `{when}`"
960+
961+
962+
def generate_relationship_summary(
963+
categories: dict[OptionCategory, dict[str, CLIDocOption]],
964+
documented_options: frozenset[str],
965+
) -> str:
966+
"""Generate an index-level summary of CLI option relationship metadata."""
967+
rows: list[tuple[str, str, str, str, str]] = []
968+
for category in OptionCategory:
969+
for option in sorted(categories.get(category, {})):
970+
if not (meta := get_option_meta(option)):
971+
continue
972+
source = _format_option_link(option, documented_options)
973+
for relation_kind in OPTION_RELATION_KINDS:
974+
for relation in getattr(meta, relation_kind):
975+
target = _format_option_link(relation.option, documented_options)
976+
if relation_value := _format_relation_value(relation.value):
977+
target = f"{target} {relation_value}"
978+
rows.append((
979+
source,
980+
relation_kind.capitalize(),
981+
_format_relation_condition_cell(option, relation.when),
982+
target,
983+
relation.message or "-",
984+
))
985+
986+
if not rows:
987+
return ""
988+
989+
md = "## 🔗 Option Relationships\n\n"
990+
md += (
991+
"These links are generated from CLI option metadata and summarize options that imply, require, "
992+
"or conflict with other options.\n\n"
993+
)
994+
md += "| Source | Kind | Condition | Target | Note |\n"
995+
md += "|--------|------|-----------|--------|------|\n"
996+
for row in rows:
997+
md += "| " + " | ".join(_escape_table_cell(cell) for cell in row) + " |\n"
998+
return md + "\n"
999+
1000+
9461001
def generate_category_page(
9471002
category: OptionCategory,
9481003
options: dict[str, CLIDocOption],
@@ -1095,6 +1150,10 @@ def generate_index_page(
10951150
md += f"| 📝 [Utility Options](utility-options.md) | {len(manual_docs)} | Help, version, debug options |\n"
10961151

10971152
md += "\n"
1153+
documented_options = frozenset(option for options in categories.values() for option in options)
1154+
if relationship_summary := generate_relationship_summary(categories, documented_options):
1155+
md += relationship_summary
1156+
10981157
md += "## All Options\n\n"
10991158
all_options: list[tuple[str, OptionCategory | None]] = []
11001159
for category, options in categories.items():

tests/cli_doc/test_cli_options_sync.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -417,6 +417,46 @@ def test_option_section_renders_conflicts_metadata() -> None:
417417
)
418418

419419

420+
def test_relationship_summary_lists_metadata_with_links() -> None:
421+
"""CLI index relationship summary should list cross-option metadata."""
422+
option = CLIDocOption(option_name="--use-missing-sentinel")
423+
summary = build_cli_docs.generate_relationship_summary(
424+
{OptionCategory.MODEL: {"--use-missing-sentinel": option}},
425+
documented_options=frozenset({
426+
"--output-model-type",
427+
"--target-pydantic-version",
428+
"--use-missing-sentinel",
429+
}),
430+
)
431+
432+
_fail_if_missing("## 🔗 Option Relationships", summary, "relationship summary heading")
433+
_fail_if_missing(
434+
"| [`--use-missing-sentinel`](model-customization.md#use-missing-sentinel) "
435+
"| Implies | Always | "
436+
"[`--target-pydantic-version`](model-customization.md#target-pydantic-version) = `2.12` | - |",
437+
summary,
438+
"--use-missing-sentinel implies row",
439+
)
440+
_fail_if_missing(
441+
"`--use-missing-sentinel` is only supported for `--output-model-type pydantic_v2.BaseModel`.",
442+
summary,
443+
"--use-missing-sentinel requires note",
444+
)
445+
446+
447+
def test_relationship_summary_omits_empty_metadata() -> None:
448+
"""CLI index relationship summary should not render without relationships."""
449+
option = CLIDocOption(option_name="--input")
450+
_fail_if_not_equal(
451+
build_cli_docs.generate_relationship_summary(
452+
{OptionCategory.BASE: {"--input": option}},
453+
documented_options=frozenset({"--input"}),
454+
),
455+
"",
456+
"empty relationship summary",
457+
)
458+
459+
420460
class TestCLIOptionMetaSync:
421461
"""Synchronization tests for CLI_OPTION_META."""
422462

0 commit comments

Comments
 (0)