fix: resolve CI lint failures - removed unused imports and variables
Some checks failed
CI / test (push) Failing after 5s
CI / build (push) Has been skipped

This commit is contained in:
2026-01-30 17:08:24 +00:00
parent 93dcfd1491
commit b940df494f

View File

@@ -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)