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
« 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
6from ..graph import Graph
7from ..graphable import Graphable
8from ..registry import register_view
10logger = getLogger(__name__)
13@dataclass
14class TextTreeStylingConfig:
15 """
16 Configuration for text tree representation of the graph.
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 """
23 initial_indent: str = ""
24 node_text_fnc: Callable[[Graphable[Any]], str] = lambda n: n.reference
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.
33 Args:
34 graph (Graph): The graph to convert.
35 config (TextTreeStylingConfig | None): Styling configuration.
37 Returns:
38 str: The text tree representation.
39 """
40 if config is None:
41 config = TextTreeStylingConfig()
43 logger.debug("Creating topology tree text.")
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.
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.
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
69 subtree: list[str] = []
70 if is_root:
71 subtree.append(f"{indent}{config.node_text_fnc(node)}")
73 next_indent: str = indent
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}")
80 next_indent: str = indent + (" " if is_last else "│ ")
82 if already_seen:
83 return "\n".join(subtree)
84 visited.add(node)
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 )
98 return "\n".join(subtree)
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)
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.
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))