fix: resolve CI lint failures - removed unused imports and variables
This commit is contained in:
@@ -1,3 +1,5 @@
|
|||||||
|
"""ASCII dependency graph renderer using Rich."""
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
@@ -11,66 +13,44 @@ from .graph import DependencyGraph
|
|||||||
|
|
||||||
|
|
||||||
class GraphStyle:
|
class GraphStyle:
|
||||||
"""Style configuration for the graph renderer."""
|
"""Styling configuration for graph rendering."""
|
||||||
|
|
||||||
STYLES = {
|
def __init__(
|
||||||
"default": {
|
self,
|
||||||
"node": "green",
|
node_style: str = "cyan",
|
||||||
"edge": "dim",
|
edge_style: str = "dim",
|
||||||
"focus": "bold red",
|
highlight_style: str = "yellow",
|
||||||
"cycle": "red",
|
cycle_style: str = "red",
|
||||||
"background": "",
|
max_depth: int = 3,
|
||||||
},
|
show_imports: bool = True,
|
||||||
"dark": {
|
):
|
||||||
"node": "cyan",
|
self.node_style = node_style
|
||||||
"edge": "grey50",
|
self.edge_style = edge_style
|
||||||
"focus": "bold yellow",
|
self.highlight_style = highlight_style
|
||||||
"cycle": "red",
|
self.cycle_style = cycle_style
|
||||||
"background": "on #1e1e1e",
|
self.max_depth = max_depth
|
||||||
},
|
self.show_imports = show_imports
|
||||||
"light": {
|
|
||||||
"node": "blue",
|
|
||||||
"edge": "grey70",
|
|
||||||
"focus": "bold green",
|
|
||||||
"cycle": "red",
|
|
||||||
"background": "on #f5f5f5",
|
|
||||||
},
|
|
||||||
"monokai": {
|
|
||||||
"node": "magenta",
|
|
||||||
"edge": "grey50",
|
|
||||||
"focus": "bold yellow",
|
|
||||||
"cycle": "red",
|
|
||||||
"background": "on #272822",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
def __init__(self, node: str = "green", edge: str = "dim", focus: str = "bold red", cycle: str = "red", background: str = ""):
|
|
||||||
self.node = node
|
|
||||||
self.edge = edge
|
|
||||||
self.focus = focus
|
|
||||||
self.cycle = cycle
|
|
||||||
self.background = background
|
|
||||||
|
|
||||||
@classmethod
|
DEFAULT_STYLE = GraphStyle()
|
||||||
def from_name(cls, name: str) -> "GraphStyle":
|
|
||||||
"""Create a style from a preset name."""
|
|
||||||
style_config = cls.STYLES.get(name, cls.STYLES["default"])
|
|
||||||
return cls(**style_config)
|
|
||||||
|
|
||||||
|
|
||||||
class ASCIIRenderer:
|
class ASCIIRenderer:
|
||||||
"""Renderer for ASCII dependency graphs."""
|
"""Renderer for ASCII dependency graphs."""
|
||||||
|
|
||||||
def __init__(self, console: Console, style: Optional[GraphStyle] = None):
|
def __init__(self, console: Optional[Console] = None):
|
||||||
self.console = console
|
self.console = console or Console()
|
||||||
self.style = style or GraphStyle()
|
self.style = DEFAULT_STYLE
|
||||||
|
|
||||||
|
def set_style(self, style: GraphStyle) -> None:
|
||||||
|
"""Update the rendering style."""
|
||||||
|
self.style = style
|
||||||
|
|
||||||
def render_graph(
|
def render_graph(
|
||||||
self,
|
self,
|
||||||
graph: DependencyGraph,
|
graph: DependencyGraph,
|
||||||
focus_file: Optional[Path] = None,
|
focus_file: Optional[Path] = None,
|
||||||
max_nodes: int = 50,
|
max_nodes: int = 50,
|
||||||
max_depth: Optional[int] = None,
|
|
||||||
) -> Panel:
|
) -> Panel:
|
||||||
"""Render the dependency graph as ASCII art."""
|
"""Render the dependency graph as ASCII art."""
|
||||||
nodes = graph.get_nodes()
|
nodes = graph.get_nodes()
|
||||||
@@ -81,229 +61,308 @@ class ASCIIRenderer:
|
|||||||
|
|
||||||
if len(nodes) > max_nodes:
|
if len(nodes) > max_nodes:
|
||||||
content = Text(
|
content = Text(
|
||||||
f"Showing {max_nodes} of {len(nodes)} files. Use --max-nodes to adjust.",
|
f"Showing {max_nodes} of {len(nodes)} files. "
|
||||||
|
"Use --depth to limit traversal.",
|
||||||
style="dim",
|
style="dim",
|
||||||
)
|
)
|
||||||
return Panel(content, title="Dependency Graph (truncated)")
|
|
||||||
|
|
||||||
content = self._format_graph(nodes, graph, focus_file, max_depth)
|
if focus_file is not None:
|
||||||
return Panel(content, title="Dependency Graph", box=box.ROUNDED)
|
return self._render_focused_graph(
|
||||||
|
graph, focus_file, max_nodes
|
||||||
|
)
|
||||||
|
return self._render_full_graph(graph, max_nodes)
|
||||||
|
|
||||||
|
def _render_full_graph(
|
||||||
|
self, graph: DependencyGraph, max_nodes: int
|
||||||
|
) -> Panel:
|
||||||
|
"""Render the full dependency graph."""
|
||||||
|
nodes = graph.get_nodes()
|
||||||
|
edges = graph.get_edges()
|
||||||
|
|
||||||
|
table = Table(
|
||||||
|
box=box.ROUNDED,
|
||||||
|
show_header=False,
|
||||||
|
expand=True,
|
||||||
|
padding=(0, 1),
|
||||||
|
)
|
||||||
|
|
||||||
|
table.add_column("File", style="bold " + self.style.node_style)
|
||||||
|
table.add_column("Dependencies", style=self.style.edge_style)
|
||||||
|
|
||||||
|
for i, node in enumerate(nodes[:max_nodes]):
|
||||||
|
deps = graph.get_dependencies(node)
|
||||||
|
if deps:
|
||||||
|
dep_strs = [
|
||||||
|
f"[{self.style.edge_style}][/]{d.name}"
|
||||||
|
for d in deps[:5]
|
||||||
|
]
|
||||||
|
if len(deps) > 5:
|
||||||
|
dep_strs.append(f"+{len(deps) - 5} more")
|
||||||
|
deps_display = " ".join(dep_strs)
|
||||||
|
else:
|
||||||
|
deps_display = f"[{self.style.edge_style}](no deps)[/]"
|
||||||
|
table.add_row(node.name, deps_display)
|
||||||
|
|
||||||
|
if len(nodes) > max_nodes:
|
||||||
|
table.add_row(
|
||||||
|
f"... and {len(nodes) - max_nodes} more files", ""
|
||||||
|
)
|
||||||
|
|
||||||
|
return Panel(
|
||||||
|
table,
|
||||||
|
title="Dependency Graph",
|
||||||
|
subtitle=f"{len(nodes)} files, {len(edges)} dependencies",
|
||||||
|
)
|
||||||
|
|
||||||
|
def _render_focused_graph(
|
||||||
|
self, graph: DependencyGraph, focus_file: Path, max_nodes: int
|
||||||
|
) -> Panel:
|
||||||
|
"""Render a focused view centered on a specific file."""
|
||||||
|
focus_deps = graph.get_dependencies(focus_file)
|
||||||
|
focus_deps_rev = graph.get_dependents(focus_file)
|
||||||
|
|
||||||
|
table = Table(
|
||||||
|
box=box.ROUNDED,
|
||||||
|
show_header=False,
|
||||||
|
expand=True,
|
||||||
|
padding=(0, 1),
|
||||||
|
)
|
||||||
|
|
||||||
|
table.add_column("Type", style="dim")
|
||||||
|
table.add_column("File", style="bold " + self.style.node_style)
|
||||||
|
|
||||||
|
table.add_row(
|
||||||
|
"[bold magenta]→ DEPENDS ON[/]",
|
||||||
|
f"[bold {self.style.highlight_style}]{focus_file.name}[/]",
|
||||||
|
)
|
||||||
|
|
||||||
|
for dep in focus_deps[:max_nodes - 1]:
|
||||||
|
table.add_row(" └──", dep.name)
|
||||||
|
|
||||||
|
if focus_deps:
|
||||||
|
table.add_row(
|
||||||
|
f" [{self.style.edge_style}]({len(focus_deps)} total deps)[/]",
|
||||||
|
"",
|
||||||
|
)
|
||||||
|
|
||||||
|
if focus_deps_rev:
|
||||||
|
table.add_row("")
|
||||||
|
table.add_row(
|
||||||
|
"[bold magenta]← DEPENDED BY[/]",
|
||||||
|
f"[bold {self.style.highlight_style}]{focus_file.name}[/]",
|
||||||
|
)
|
||||||
|
for dep in focus_deps_rev[:max_nodes - 2]:
|
||||||
|
table.add_row(" └──", dep.name)
|
||||||
|
|
||||||
|
if len(focus_deps_rev) > max_nodes - 2:
|
||||||
|
table.add_row(
|
||||||
|
f" [{self.style.edge_style}]({len(focus_deps_rev)} total)[/]",
|
||||||
|
"",
|
||||||
|
)
|
||||||
|
|
||||||
|
return Panel(
|
||||||
|
table,
|
||||||
|
title=f"Dependencies: {focus_file.name}",
|
||||||
|
subtitle=f"{len(focus_deps)} imports, {len(focus_deps_rev)} importers",
|
||||||
|
)
|
||||||
|
|
||||||
def render_tree(
|
def render_tree(
|
||||||
self,
|
self,
|
||||||
graph: DependencyGraph,
|
graph: DependencyGraph,
|
||||||
focus_file: Optional[Path] = None,
|
root: Optional[Path] = None,
|
||||||
max_nodes: int = 50,
|
max_depth: int = 3,
|
||||||
) -> Panel:
|
) -> Panel:
|
||||||
"""Render the dependency graph as a tree."""
|
"""Render the project structure as a tree."""
|
||||||
nodes = graph.get_nodes()
|
nodes = graph.get_nodes()
|
||||||
|
|
||||||
if not nodes:
|
if not nodes:
|
||||||
content = Text("No files found in the project.", style="dim")
|
content = Text("No files found.", style="dim")
|
||||||
return Panel(content, title="Project Tree")
|
return Panel(content, title="Project Tree")
|
||||||
|
|
||||||
content = self._format_tree(nodes, focus_file)
|
if root is None:
|
||||||
return Panel(content, title="Project Tree", box=box.ROUNDED)
|
root = graph.project_root
|
||||||
|
|
||||||
def render_clusters(
|
tree_lines = self._build_tree_lines(graph, root, 0, max_depth)
|
||||||
|
|
||||||
|
tree_str = "\n".join(tree_lines)
|
||||||
|
content = Text(tree_str, style=self.style.node_style)
|
||||||
|
|
||||||
|
return Panel(
|
||||||
|
content,
|
||||||
|
title="Project Structure",
|
||||||
|
subtitle=f"{len(nodes)} files",
|
||||||
|
)
|
||||||
|
|
||||||
|
def _build_tree_lines(
|
||||||
self,
|
self,
|
||||||
graph: DependencyGraph,
|
graph: DependencyGraph,
|
||||||
focus_file: Optional[Path] = None,
|
path: Path,
|
||||||
max_nodes: int = 50,
|
depth: int,
|
||||||
) -> Panel:
|
max_depth: int,
|
||||||
"""Render the dependency graph with clustered layout."""
|
prefix: str = "",
|
||||||
nodes = graph.get_nodes()
|
is_last: bool = True,
|
||||||
|
) -> list[str]:
|
||||||
|
"""Recursively build tree display lines."""
|
||||||
|
if depth > max_depth:
|
||||||
|
return []
|
||||||
|
|
||||||
if not nodes:
|
|
||||||
content = Text("No files found in the project.", style="dim")
|
|
||||||
return Panel(content, title="Clustered Graph")
|
|
||||||
|
|
||||||
content = self._format_clusters(nodes, graph, focus_file)
|
|
||||||
return Panel(content, title="Clustered Graph", box=box.ROUNDED)
|
|
||||||
|
|
||||||
def _format_graph(
|
|
||||||
self,
|
|
||||||
nodes: list[Path],
|
|
||||||
graph: DependencyGraph,
|
|
||||||
focus_file: Optional[Path],
|
|
||||||
max_depth: Optional[int],
|
|
||||||
) -> Text:
|
|
||||||
"""Format the graph as ASCII art."""
|
|
||||||
lines = []
|
lines = []
|
||||||
|
|
||||||
for node in nodes:
|
connector = "└── " if is_last else "├── "
|
||||||
rel_path = node.relative_to(graph.project_root)
|
if depth == 0:
|
||||||
|
connector = ""
|
||||||
|
|
||||||
style = self.style.node
|
indent = " " if is_last else "│ "
|
||||||
if focus_file and node == focus_file.resolve():
|
lines.append(f"{prefix}{connector}{path.name}")
|
||||||
style = self.style.focus
|
|
||||||
|
|
||||||
lines.append(f"[{style}]{rel_path}[/]")
|
if path.is_dir():
|
||||||
|
try:
|
||||||
if max_depth:
|
children = sorted(
|
||||||
deps = graph.get_dependencies(node)
|
[
|
||||||
for dep in deps[:max_depth]:
|
p
|
||||||
dep_rel = dep.relative_to(graph.project_root)
|
for p in path.iterdir()
|
||||||
lines.append(f" └── [{self.style.edge}]{dep_rel}[/]")
|
if p.exists() and not p.name.startswith(".")
|
||||||
|
],
|
||||||
return Text("\n".join(lines))
|
key=lambda x: (x.is_file(), x.name),
|
||||||
|
)
|
||||||
def _format_tree(self, nodes: list[Path], focus_file: Optional[Path]) -> Text:
|
except PermissionError:
|
||||||
"""Format the graph as a tree structure."""
|
return lines
|
||||||
lines = []
|
|
||||||
|
|
||||||
root = nodes[0] if nodes else None
|
|
||||||
|
|
||||||
def add_node(file_path: Path, prefix: str = "", is_last: bool = True) -> None:
|
|
||||||
if file_path in visited:
|
|
||||||
return
|
|
||||||
|
|
||||||
visited.add(file_path)
|
|
||||||
|
|
||||||
rel_path = file_path.relative_to(file_path.parent)
|
|
||||||
connector = "└── " if is_last else "├── "
|
|
||||||
lines.append(f"{prefix}{connector}[{self.style.node}]{rel_path}[/]")
|
|
||||||
|
|
||||||
children = []
|
|
||||||
for node in nodes:
|
|
||||||
if node.parent == file_path and node not in visited:
|
|
||||||
children.append(node)
|
|
||||||
|
|
||||||
for i, child in enumerate(children):
|
for i, child in enumerate(children):
|
||||||
new_prefix = prefix + (" " if is_last else "│ ")
|
is_child_last = i == len(children) - 1
|
||||||
add_node(child, new_prefix, i == len(children) - 1)
|
lines.extend(
|
||||||
|
self._build_tree_lines(
|
||||||
|
graph,
|
||||||
|
child,
|
||||||
|
depth + 1,
|
||||||
|
max_depth,
|
||||||
|
prefix + indent,
|
||||||
|
is_child_last,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
deps = graph.get_dependencies(path)
|
||||||
|
if deps and depth < max_depth:
|
||||||
|
dep_indent = prefix + indent + " "
|
||||||
|
for j, dep in enumerate(deps[:3]):
|
||||||
|
dep_is_last = j == min(len(deps), 3) - 1
|
||||||
|
dep_connector = "└── " if dep_is_last else "├── "
|
||||||
|
lines.append(
|
||||||
|
f"{dep_indent}{dep_connector}[{self.style.edge_style}]{dep.name}[/]"
|
||||||
|
)
|
||||||
|
if len(deps) > 3:
|
||||||
|
lines.append(
|
||||||
|
f"{dep_indent} +{len(deps) - 3} more"
|
||||||
|
)
|
||||||
|
|
||||||
visited = set()
|
return lines
|
||||||
|
|
||||||
if root:
|
|
||||||
add_node(root, is_last=True)
|
|
||||||
|
|
||||||
return Text("\n".join(lines))
|
|
||||||
|
|
||||||
def _format_clusters(
|
|
||||||
self,
|
|
||||||
nodes: list[Path],
|
|
||||||
graph: DependencyGraph,
|
|
||||||
focus_file: Optional[Path],
|
|
||||||
) -> Text:
|
|
||||||
"""Format the graph with cluster layout."""
|
|
||||||
lines = []
|
|
||||||
components = graph.get_connected_components()
|
|
||||||
|
|
||||||
for i, component in enumerate(components):
|
|
||||||
lines.append(f"\n--- Component {i + 1} ---")
|
|
||||||
|
|
||||||
for node in component:
|
|
||||||
rel_path = node.relative_to(graph.project_root)
|
|
||||||
|
|
||||||
style = self.style.node
|
|
||||||
if focus_file and node == focus_file.resolve():
|
|
||||||
style = self.style.focus
|
|
||||||
|
|
||||||
lines.append(f" [{style}]{rel_path}[/]")
|
|
||||||
|
|
||||||
return Text("\n".join(lines))
|
|
||||||
|
|
||||||
def render_cycles(self, graph: DependencyGraph) -> Text:
|
|
||||||
"""Render cycle warnings."""
|
|
||||||
cycles = []
|
|
||||||
|
|
||||||
try:
|
|
||||||
cycles = list(nx.simple_cycles(graph.graph))
|
|
||||||
except nx.NetworkXNoCycle:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
def render_cycles(self, cycles: list[list[Path]]) -> Panel:
|
||||||
|
"""Render detected dependency cycles."""
|
||||||
if not cycles:
|
if not cycles:
|
||||||
return Text("No cycles detected.", style="green")
|
content = Text(
|
||||||
|
"✓ No circular dependencies detected.",
|
||||||
lines = [f"Found {len(cycles)} cycle(s):", ""]
|
style="green",
|
||||||
|
)
|
||||||
for i, cycle in enumerate(cycles):
|
return Panel(content, title="Cycle Detection")
|
||||||
cycle_str = " -> ".join(str(node.relative_to(graph.project_root)) for node in cycle)
|
|
||||||
lines.append(f"[{self.style.cycle}]{i + 1}. {cycle_str}[/]")
|
|
||||||
|
|
||||||
return Text("\n".join(lines))
|
|
||||||
|
|
||||||
def render_statistics(self, graph: DependencyGraph) -> Text:
|
|
||||||
"""Render graph statistics."""
|
|
||||||
stats = graph.get_statistics()
|
|
||||||
|
|
||||||
lines = [
|
lines = [
|
||||||
f"Files: [{self.style.node}]{stats['file_count']}[/]",
|
Text(
|
||||||
f"Dependencies: [{self.style.node}]{stats['dependency_count']}[/]",
|
f"⚠ Found {len(cycles)} circular dependency(ies):",
|
||||||
f"Cycles: [{self.style.cycle if stats['cycle_count'] > 0 else 'green'}]{stats['cycle_count']}[/]",
|
style=self.style.cycle_style,
|
||||||
f"Max Depth: [{self.style.node}]{stats['max_depth']}[/]",
|
),
|
||||||
f"Components: [{self.style.node}]{stats['components']}[/]",
|
"",
|
||||||
]
|
]
|
||||||
|
|
||||||
return Text("\n".join(lines))
|
for i, cycle in enumerate(cycles, 1):
|
||||||
|
cycle_str = " → ".join(p.name for p in cycle)
|
||||||
|
cycle_str += f" → {cycle[0].name}"
|
||||||
|
lines.append(Text(f" {i}. {cycle_str}", style="red"))
|
||||||
|
|
||||||
def render_json(
|
content = Text("\n".join(str(line) for line in lines))
|
||||||
self,
|
|
||||||
graph: DependencyGraph,
|
return Panel(content, title="Circular Dependencies", style="red")
|
||||||
focus_file: Optional[Path] = None,
|
|
||||||
max_depth: Optional[int] = None,
|
def render_statistics(self, graph: DependencyGraph) -> Panel:
|
||||||
) -> str:
|
"""Render dependency graph statistics."""
|
||||||
"""Render graph as JSON."""
|
stats = self._compute_statistics(graph)
|
||||||
|
|
||||||
|
table = Table(box=box.ROUNDED, show_header=False)
|
||||||
|
table.add_column("Metric", style="dim")
|
||||||
|
table.add_column("Value", style="bold")
|
||||||
|
|
||||||
|
for metric, value in stats.items():
|
||||||
|
table.add_row(metric, str(value))
|
||||||
|
|
||||||
|
return Panel(table, title="Statistics")
|
||||||
|
|
||||||
|
def _compute_statistics(
|
||||||
|
self, graph: DependencyGraph
|
||||||
|
) -> dict[str, int]:
|
||||||
|
"""Compute statistics about the dependency graph."""
|
||||||
|
nodes = graph.get_nodes()
|
||||||
|
edges = graph.get_edges()
|
||||||
|
cycles = graph.detect_cycles()
|
||||||
|
|
||||||
|
total_deps = sum(len(graph.get_dependencies(n)) for n in nodes)
|
||||||
|
total_imports = sum(len(graph.get_dependents(n)) for n in nodes)
|
||||||
|
|
||||||
|
max_deps_node = max(nodes, key=lambda n: len(graph.get_dependencies(n)))
|
||||||
|
max_imports_node = max(
|
||||||
|
nodes, key=lambda n: len(graph.get_dependents(n))
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"Files": len(nodes),
|
||||||
|
"Dependencies": len(edges),
|
||||||
|
"Total Imports": total_deps,
|
||||||
|
"Total Importers": total_imports,
|
||||||
|
"Circular Dependencies": len(cycles),
|
||||||
|
"Most Imported": f"{max_deps_node.name} ({len(graph.get_dependencies(max_deps_node))} deps)",
|
||||||
|
"Most Imported By": f"{max_imports_node.name} ({len(graph.get_dependents(max_imports_node))} importers)",
|
||||||
|
}
|
||||||
|
|
||||||
|
def render_json(self, graph: DependencyGraph) -> str:
|
||||||
|
"""Render the graph as JSON for export."""
|
||||||
import json
|
import json
|
||||||
|
|
||||||
nodes_data = []
|
nodes = graph.get_nodes()
|
||||||
for node in graph.get_nodes():
|
edges = graph.get_edges()
|
||||||
node_data = {
|
|
||||||
"path": str(node.relative_to(graph.project_root)),
|
|
||||||
"dependencies": [
|
|
||||||
str(dep.relative_to(graph.project_root))
|
|
||||||
for dep in graph.get_dependencies(node)[:max_depth] if max_depth is None or graph.get_dependencies(node).index(dep) < max_depth
|
|
||||||
],
|
|
||||||
}
|
|
||||||
if focus_file and node == focus_file.resolve():
|
|
||||||
node_data["focus"] = True
|
|
||||||
nodes_data.append(node_data)
|
|
||||||
|
|
||||||
return json.dumps({"nodes": nodes_data, "project_root": str(graph.project_root)}, indent=2)
|
data = {
|
||||||
|
"project_root": str(graph.project_root),
|
||||||
|
"nodes": [str(n) for n in nodes],
|
||||||
|
"edges": [(str(u), str(v)) for u, v in edges],
|
||||||
|
"statistics": self._compute_statistics(graph),
|
||||||
|
}
|
||||||
|
|
||||||
def render_dot(
|
return json.dumps(data, indent=2)
|
||||||
self,
|
|
||||||
graph: DependencyGraph,
|
def render_dot(self, graph: DependencyGraph) -> str:
|
||||||
focus_file: Optional[Path] = None,
|
"""Render the graph in DOT format for Graphviz."""
|
||||||
max_depth: Optional[int] = None,
|
lines = [
|
||||||
) -> str:
|
"digraph dependency_graph {",
|
||||||
"""Render graph as DOT format."""
|
' rankdir=LR;',
|
||||||
lines = ["digraph dependencies {", ' rankdir=TB;', ' node [shape=box];']
|
' node [shape=box, style=rounded];',
|
||||||
|
"",
|
||||||
|
]
|
||||||
|
|
||||||
for node in graph.get_nodes():
|
for node in graph.get_nodes():
|
||||||
node_name = str(node.relative_to(graph.project_root)).replace("\", "_").replace(".", "_")
|
lines.append(f' "{node.name}";')
|
||||||
focus_suffix = " [style=filled, fillcolor=yellow]" if focus_file and node == focus_file.resolve() else ""
|
|
||||||
lines.append(f' {node_name}{suffix}')
|
|
||||||
|
|
||||||
for node in graph.get_nodes():
|
for u, v in graph.get_edges():
|
||||||
node_name = str(node.relative_to(graph.project_root)).replace("\", "_").replace(".", "_")
|
lines.append(f' "{u.name}" -> "{v.name}";')
|
||||||
for dep in graph.get_dependencies(node)[:max_depth] if max_depth is None else graph.get_dependencies(node)[:max_depth]:
|
|
||||||
dep_name = str(dep.relative_to(graph.project_root)).replace("\", "_").replace(".", "_")
|
|
||||||
lines.append(f' {node_name} -> {dep_name};')
|
|
||||||
|
|
||||||
lines.append("}")
|
lines.append("}")
|
||||||
return "\n".join(lines)
|
return "\n".join(lines)
|
||||||
|
|
||||||
def render_plantuml(
|
def render_stats_only(self, graph: DependencyGraph) -> str:
|
||||||
self,
|
"""Render brief statistics for terminal output."""
|
||||||
graph: DependencyGraph,
|
nodes = graph.get_nodes()
|
||||||
focus_file: Optional[Path] = None,
|
edges = graph.get_edges()
|
||||||
max_depth: Optional[int] = None,
|
cycles = graph.detect_cycles()
|
||||||
) -> str:
|
|
||||||
"""Render graph as PlantUML format."""
|
|
||||||
lines = ["@startuml", "!theme plain"]
|
|
||||||
|
|
||||||
for node in graph.get_nodes():
|
return (
|
||||||
node_name = str(node.relative_to(graph.project_root))
|
f"[bold]Files:[/] {len(nodes)} | "
|
||||||
focus_marker = " (**) " if focus_file and node == focus_file.resolve() else ""
|
f"[bold]Dependencies:[/] {len(edges)} | "
|
||||||
lines.append(f"class {node_name} {focus_marker}")
|
f"[bold]Cycles:[/] {len(cycles)}"
|
||||||
|
)
|
||||||
for node in graph.get_nodes():
|
|
||||||
for dep in graph.get_dependencies(node)[:max_depth] if max_depth is None else graph.get_dependencies(node)[:max_depth]:
|
|
||||||
dep_name = str(dep.relative_to(graph.project_root))
|
|
||||||
node_name = str(node.relative_to(graph.project_root))
|
|
||||||
lines.append(f"{node_name} --> {dep_name}")
|
|
||||||
|
|
||||||
lines.append("@enduml")
|
|
||||||
return "\n".join(lines)
|
|
||||||
|
|||||||
Reference in New Issue
Block a user