Coverage for little_loops / cli / loop / diagram_modes.py: 0%
39 statements
« prev ^ index » next coverage.py v7.12.0, created at 2026-05-28 13:07 -0500
« prev ^ index » next coverage.py v7.12.0, created at 2026-05-28 13:07 -0500
1"""DiagramFacets: orthogonal diagram display axes + named presets for ll-loop.
3Replaces the old ``--show-diagrams=main|full|mini`` single-enum with:
5 --show-diagrams=<topology-or-preset>
6 topology: layered | neighborhood | inline
7 preset: detailed | summary | clean | local | oneline
9 --diagram-edge-labels=on|off (default on)
10 --diagram-state-detail=title|full (default full)
11 --diagram-scope=main|full (default full)
12"""
14from __future__ import annotations
16import argparse
17from dataclasses import dataclass, replace
19TOPOLOGY_VALUES: frozenset[str] = frozenset({"layered", "neighborhood", "inline"})
20PRESET_VALUES: frozenset[str] = frozenset(
21 {"detailed", "summary", "clean", "local", "oneline", "slim"}
22)
24_LEGACY_HINTS: dict[str, str] = {
25 "main": (
26 "main was renamed: use --show-diagrams=summary "
27 "(path scope is now controlled by --diagram-scope=main)."
28 ),
29 "full": (
30 "full was renamed: use --show-diagrams=detailed "
31 "(full graph is now controlled by --diagram-scope=full)."
32 ),
33 "mini": (
34 "mini was renamed: use --show-diagrams=clean "
35 "(or set --diagram-edge-labels=off --diagram-state-detail=title "
36 "for the underlying primitives)."
37 ),
38}
41@dataclass(frozen=True)
42class DiagramFacets:
43 """Resolved, orthogonal diagram display settings.
45 ``source`` drives fallback gating in ``_choose_pinned_layout``:
46 - ``"topology"`` — user passed an explicit topology; no auto-degradation.
47 - ``"preset"`` — a named preset was used; fall back through smaller topologies.
48 - ``"default"`` — bare ``--show-diagrams`` flag; same fallback as preset.
49 """
51 topology: str # layered | neighborhood | inline
52 edge_labels: bool # True = render edge labels
53 state_detail: str # title | full
54 scope: str # main | full (silently ignored for inline topology)
55 source: str # default | preset | topology
58PRESET_EXPANSIONS: dict[str, DiagramFacets] = {
59 "detailed": DiagramFacets("layered", True, "full", "full", "preset"),
60 "summary": DiagramFacets("layered", True, "full", "main", "preset"),
61 "clean": DiagramFacets("layered", False, "title", "main", "preset"),
62 "local": DiagramFacets("neighborhood", True, "title", "main", "preset"),
63 "slim": DiagramFacets("neighborhood", False, "title", "main", "preset"),
64 "oneline": DiagramFacets("inline", True, "title", "full", "preset"),
65}
67# Maps topology name to the detail string used by _build_pinned_pane
68TOPOLOGY_TO_DETAIL: dict[str, str] = {
69 "layered": "full",
70 "neighborhood": "neighborhood",
71 "inline": "single",
72}
75def _parse_show_diagrams(value: str) -> str:
76 """argparse ``type=`` validator for ``--show-diagrams``.
78 Accepts topology values and preset names; rejects legacy mode strings
79 with a migration hint. The ``const=True`` sentinel (bare flag) bypasses
80 this function entirely — argparse does not call ``type=`` on ``const``.
81 """
82 if value in TOPOLOGY_VALUES | PRESET_VALUES:
83 return value
84 if value in _LEGACY_HINTS:
85 raise argparse.ArgumentTypeError(_LEGACY_HINTS[value])
86 raise argparse.ArgumentTypeError(
87 f"unknown --show-diagrams value {value!r}; "
88 f"choose from topologies {sorted(TOPOLOGY_VALUES)} "
89 f"or presets {sorted(PRESET_VALUES)}"
90 )
93def resolve_facets(args: argparse.Namespace) -> DiagramFacets | None:
94 """Resolve argparse Namespace into a ``DiagramFacets``, or ``None`` if the flag was absent.
96 Modifier flags (``diagram_edge_labels``, ``diagram_state_detail``,
97 ``diagram_scope``) override the preset expansion only when explicitly set
98 (non-None), so preset defaults are preserved when modifiers are absent.
99 """
100 raw = getattr(args, "show_diagrams", None)
101 if raw is None:
102 return None
104 if raw is True: # bare --show-diagrams (const=True sentinel)
105 base = replace(PRESET_EXPANSIONS["summary"], source="default")
106 elif raw in PRESET_EXPANSIONS:
107 base = PRESET_EXPANSIONS[raw]
108 elif raw in TOPOLOGY_VALUES:
109 base = DiagramFacets(raw, True, "full", "full", "topology")
110 else:
111 raise ValueError(f"unreachable: argparse should have rejected {raw!r}")
113 # Apply modifier overrides — only when the user explicitly set them (not None).
114 edge_labels_raw = getattr(args, "diagram_edge_labels", None)
115 edge_labels = (edge_labels_raw != "off") if edge_labels_raw is not None else base.edge_labels
117 state_detail_raw = getattr(args, "diagram_state_detail", None)
118 state_detail = state_detail_raw if state_detail_raw is not None else base.state_detail
120 scope_raw = getattr(args, "diagram_scope", None)
121 scope = scope_raw if scope_raw is not None else base.scope
123 return replace(base, edge_labels=edge_labels, state_detail=state_detail, scope=scope)