Coverage for little_loops / cli / loop / diagram_modes.py: 44%

39 statements  

« prev     ^ index     » next       coverage.py v7.12.0, created at 2026-06-15 17:30 -0500

1"""DiagramFacets: orthogonal diagram display axes + named presets for ll-loop. 

2 

3Replaces the old ``--show-diagrams=main|full|mini`` single-enum with: 

4 

5 --show-diagrams=<topology-or-preset> 

6 topology: layered | neighborhood | inline 

7 preset: detailed | summary | clean | local | oneline 

8 

9 --diagram-edge-labels=on|off (default on) 

10 --diagram-state-detail=title|full (default full) 

11 --diagram-scope=main|full (default full) 

12""" 

13 

14from __future__ import annotations 

15 

16import argparse 

17from dataclasses import dataclass, replace 

18 

19TOPOLOGY_VALUES: frozenset[str] = frozenset({"layered", "neighborhood", "inline"}) 

20PRESET_VALUES: frozenset[str] = frozenset( 

21 {"detailed", "summary", "clean", "local", "oneline", "slim"} 

22) 

23 

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} 

39 

40 

41@dataclass(frozen=True) 

42class DiagramFacets: 

43 """Resolved, orthogonal diagram display settings. 

44 

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 """ 

50 

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 

56 

57 

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} 

66 

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} 

73 

74 

75def _parse_show_diagrams(value: str) -> str: 

76 """argparse ``type=`` validator for ``--show-diagrams``. 

77 

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 ) 

91 

92 

93def resolve_facets(args: argparse.Namespace) -> DiagramFacets | None: 

94 """Resolve argparse Namespace into a ``DiagramFacets``, or ``None`` if the flag was absent. 

95 

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 

103 

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}") 

112 

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 

116 

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 

119 

120 scope_raw = getattr(args, "diagram_scope", None) 

121 scope = scope_raw if scope_raw is not None else base.scope 

122 

123 return replace(base, edge_labels=edge_labels, state_detail=state_detail, scope=scope)