Coverage for src / graphable / views / texttree.py: 100%

44 statements  

« prev     ^ index     » next       coverage.py v7.13.3, created at 2026-02-16 21:32 +0000

1from dataclasses import dataclass 

2from logging import getLogger 

3from pathlib import Path 

4from typing import Any, Callable 

5 

6from ..graph import Graph 

7from ..graphable import Graphable 

8from ..registry import register_view 

9 

10logger = getLogger(__name__) 

11 

12 

13@dataclass 

14class TextTreeStylingConfig: 

15 """ 

16 Configuration for text tree representation of the graph. 

17 

18 Attributes: 

19 initial_indent: String to use for initial indentation. 

20 node_text_fnc: Function to generate the text representation of a node. 

21 """ 

22 

23 initial_indent: str = "" 

24 node_text_fnc: Callable[[Graphable[Any]], str] = lambda n: n.reference 

25 

26 

27def create_topology_tree_txt( 

28 graph: Graph, config: TextTreeStylingConfig | None = None 

29) -> str: 

30 """ 

31 Create a text-based tree representation of the graph topology. 

32 

33 Args: 

34 graph (Graph): The graph to convert. 

35 config (TextTreeStylingConfig | None): Styling configuration. 

36 

37 Returns: 

38 str: The text tree representation. 

39 """ 

40 if config is None: 

41 config = TextTreeStylingConfig() 

42 

43 logger.debug("Creating topology tree text.") 

44 

45 def create_topology_subtree_txt( 

46 node: Graphable[Any], 

47 indent: str = "", 

48 is_last: bool = True, 

49 is_root: bool = True, 

50 visited: set[Graphable[Any]] | None = None, 

51 ) -> str: 

52 """ 

53 Recursively generate the text representation for a subtree. 

54 

55 Args: 

56 node (Graphable): The current node being processed. 

57 indent (str): The current indentation string. 

58 is_last (bool): Whether this node is the last sibling. 

59 is_root (bool): Whether this is the root of the (sub)tree. 

60 visited (set[Graphable] | None): Set of already visited nodes to detect cycles/redundancy. 

61 

62 Returns: 

63 str: The text representation of the subtree. 

64 """ 

65 if visited is None: 

66 visited = set[Graphable]() 

67 already_seen: bool = node in visited 

68 

69 subtree: list[str] = [] 

70 if is_root: 

71 subtree.append(f"{indent}{config.node_text_fnc(node)}") 

72 

73 next_indent: str = indent 

74 

75 else: 

76 marker: str = "└─ " if is_last else "├─ " 

77 suffix: str = " (see above)" if already_seen and node.depends_on else "" 

78 subtree.append(f"{indent}{marker}{config.node_text_fnc(node)}{suffix}") 

79 

80 next_indent: str = indent + (" " if is_last else "│ ") 

81 

82 if already_seen: 

83 return "\n".join(subtree) 

84 visited.add(node) 

85 

86 internal_deps = list(graph.internal_depends_on(node)) 

87 for i, (subnode, _) in enumerate(internal_deps, start=1): 

88 subtree.append( 

89 create_topology_subtree_txt( 

90 node=subnode, 

91 indent=next_indent, 

92 is_last=(i == len(internal_deps)), 

93 is_root=False, 

94 visited=visited, 

95 ) 

96 ) 

97 

98 return "\n".join(subtree) 

99 

100 tree: list[str] = [] 

101 for node in graph.sinks: 

102 tree.append( 

103 create_topology_subtree_txt( 

104 node=node, indent=config.initial_indent, is_root=True 

105 ) 

106 ) 

107 return "\n".join(tree) 

108 

109 

110@register_view(".txt", creator_fnc=create_topology_tree_txt) 

111def export_topology_tree_txt( 

112 graph: Graph, output: Path, config: TextTreeStylingConfig | None = None 

113) -> None: 

114 """ 

115 Export the graph to a text tree file. 

116 

117 Args: 

118 graph (Graph): The graph to export. 

119 output (Path): The output file path. 

120 config (TextTreeStylingConfig | None): Styling configuration. 

121 """ 

122 logger.info(f"Exporting topology tree text to: {output}") 

123 with open(output, "w+") as f: 

124 f.write(create_topology_tree_txt(graph, config))