fix: resolve CI lint failures - removed unused imports and variables
This commit is contained in:
@@ -1,3 +1,5 @@
|
|||||||
|
"""Main CLI interface for depnav."""
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
@@ -11,363 +13,352 @@ from .graph import DependencyGraph
|
|||||||
from .navigator import Navigator
|
from .navigator import Navigator
|
||||||
from .renderer import ASCIIRenderer, GraphStyle
|
from .renderer import ASCIIRenderer, GraphStyle
|
||||||
|
|
||||||
|
|
||||||
console = Console()
|
console = Console()
|
||||||
|
|
||||||
|
|
||||||
@click.group()
|
@click.group()
|
||||||
def main():
|
|
||||||
"""CLI tool for navigating code dependencies."""
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
@main.command()
|
|
||||||
@click.argument("path", type=click.Path(exists=True), default=".")
|
|
||||||
@click.option(
|
@click.option(
|
||||||
"--theme",
|
"--config",
|
||||||
default="default",
|
"-c",
|
||||||
help="Color theme for the graph (default, dark, light, monokai)",
|
type=click.Path(exists=True, path_type=Path),
|
||||||
|
help="Path to configuration file",
|
||||||
)
|
)
|
||||||
@click.option(
|
@click.option(
|
||||||
"--max-nodes",
|
"--theme",
|
||||||
default=50,
|
type=click.Choice(["default", "dark", "light", "mono"]),
|
||||||
|
default="default",
|
||||||
|
help="Color theme",
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--pager/--no-pager",
|
||||||
|
default=True,
|
||||||
|
help="Use pager for output",
|
||||||
|
)
|
||||||
|
@click.pass_context
|
||||||
|
def main(
|
||||||
|
ctx: click.Context,
|
||||||
|
config: Optional[Path],
|
||||||
|
theme: str,
|
||||||
|
pager: bool,
|
||||||
|
):
|
||||||
|
"""Depnav - CLI Dependency Graph Navigator."""
|
||||||
|
ctx.ensure_object(dict)
|
||||||
|
ctx.obj["config"] = config
|
||||||
|
ctx.obj["theme"] = theme
|
||||||
|
ctx.obj["pager"] = pager
|
||||||
|
|
||||||
|
cfg = load_config(config)
|
||||||
|
ctx.obj["config_obj"] = cfg
|
||||||
|
|
||||||
|
|
||||||
|
@main.command("graph")
|
||||||
|
@click.argument(
|
||||||
|
"path",
|
||||||
|
type=click.Path(exists=True, file_okay=True, dir_okay=True, path_type=Path),
|
||||||
|
default=Path("."),
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--depth",
|
||||||
|
"-d",
|
||||||
type=int,
|
type=int,
|
||||||
help="Maximum number of nodes to display",
|
default=3,
|
||||||
|
help="Maximum traversal depth",
|
||||||
)
|
)
|
||||||
@click.option(
|
@click.option(
|
||||||
"--focus",
|
"--focus",
|
||||||
"focus_file",
|
"-f",
|
||||||
type=click.Path(exists=True),
|
type=str,
|
||||||
default=None,
|
default=None,
|
||||||
help="Focus the graph on a specific file",
|
help="Focus on a specific file",
|
||||||
)
|
)
|
||||||
@click.option(
|
@click.option(
|
||||||
"--include-extension",
|
"--max-nodes",
|
||||||
multiple=True,
|
"-m",
|
||||||
default=None,
|
|
||||||
help="Only include files with this extension (e.g., .py)",
|
|
||||||
)
|
|
||||||
@click.option(
|
|
||||||
"--exclude-pattern",
|
|
||||||
multiple=True,
|
|
||||||
default=None,
|
|
||||||
help="Exclude files matching this pattern",
|
|
||||||
)
|
|
||||||
@click.option(
|
|
||||||
"--max-depth",
|
|
||||||
type=int,
|
type=int,
|
||||||
default=None,
|
default=50,
|
||||||
help="Maximum dependency depth to traverse",
|
help="Maximum number of nodes to display",
|
||||||
)
|
|
||||||
@click.option(
|
|
||||||
"--direction",
|
|
||||||
type=click.Choice(["forward", "backward", "both"]),
|
|
||||||
default="forward",
|
|
||||||
help="Dependency direction to show",
|
|
||||||
)
|
|
||||||
@click.option(
|
|
||||||
"--layout",
|
|
||||||
type=click.Choice(["tree", "graph", "cluster"]),
|
|
||||||
default="tree",
|
|
||||||
help="Layout style for the graph",
|
|
||||||
)
|
|
||||||
def graph(
|
|
||||||
path: str,
|
|
||||||
theme: str,
|
|
||||||
max_nodes: int,
|
|
||||||
focus_file: Optional[str],
|
|
||||||
include_extension: tuple[str, ...],
|
|
||||||
exclude_pattern: tuple[str, ...],
|
|
||||||
max_depth: Optional[int],
|
|
||||||
direction: str,
|
|
||||||
layout: str,
|
|
||||||
) -> None:
|
|
||||||
"""Display dependency graph for a project.
|
|
||||||
|
|
||||||
PATH is the path to the project root (default: current directory).
|
|
||||||
"""
|
|
||||||
config = load_config()
|
|
||||||
|
|
||||||
if theme != "default":
|
|
||||||
config.theme = theme
|
|
||||||
|
|
||||||
project_root = Path(path).resolve()
|
|
||||||
graph = DependencyGraph(project_root)
|
|
||||||
|
|
||||||
extensions = list(include_extension) if include_extension else config.include_extensions
|
|
||||||
patterns = list(exclude_pattern) if exclude_pattern else config.exclude_patterns
|
|
||||||
|
|
||||||
graph.build_from_directory(
|
|
||||||
project_root,
|
|
||||||
include_extensions=extensions,
|
|
||||||
exclude_patterns=patterns,
|
|
||||||
)
|
|
||||||
|
|
||||||
if focus_file:
|
|
||||||
focus_path = Path(focus_file).resolve()
|
|
||||||
else:
|
|
||||||
focus_path = None
|
|
||||||
|
|
||||||
renderer = ASCIIRenderer(console, style=GraphStyle.from_name(config.theme))
|
|
||||||
|
|
||||||
if direction == "backward":
|
|
||||||
graph = graph.to_undirected()
|
|
||||||
|
|
||||||
if layout == "tree":
|
|
||||||
panel = renderer.render_tree(graph, focus_file=focus_path, max_nodes=max_nodes)
|
|
||||||
elif layout == "cluster":
|
|
||||||
panel = renderer.render_clusters(graph, focus_file=focus_path, max_nodes=max_nodes)
|
|
||||||
else:
|
|
||||||
panel = renderer.render_graph(
|
|
||||||
graph,
|
|
||||||
focus_file=focus_path,
|
|
||||||
max_nodes=max_nodes,
|
|
||||||
max_depth=max_depth,
|
|
||||||
)
|
|
||||||
|
|
||||||
console.print(panel)
|
|
||||||
|
|
||||||
|
|
||||||
@main.command()
|
|
||||||
@click.argument("path", type=click.Path(exists=True), default=".")
|
|
||||||
@click.option(
|
|
||||||
"--theme",
|
|
||||||
default="default",
|
|
||||||
help="Color theme for the tree (default, dark, light, monokai)",
|
|
||||||
)
|
|
||||||
def tree(path: str, theme: str) -> None:
|
|
||||||
"""Display project structure as a tree.
|
|
||||||
|
|
||||||
PATH is the path to the project root (default: current directory).
|
|
||||||
"""
|
|
||||||
config = load_config()
|
|
||||||
|
|
||||||
if theme != "default":
|
|
||||||
config.theme = theme
|
|
||||||
|
|
||||||
project_root = Path(path).resolve()
|
|
||||||
graph = DependencyGraph(project_root)
|
|
||||||
|
|
||||||
graph.build_from_directory(
|
|
||||||
project_root,
|
|
||||||
include_extensions=config.include_extensions,
|
|
||||||
exclude_patterns=config.exclude_patterns,
|
|
||||||
)
|
|
||||||
|
|
||||||
renderer = ASCIIRenderer(console, style=GraphStyle.from_name(config.theme))
|
|
||||||
panel = renderer.render_tree(graph)
|
|
||||||
|
|
||||||
console.print(panel)
|
|
||||||
|
|
||||||
|
|
||||||
@main.command()
|
|
||||||
@click.argument("path", type=click.Path(exists=True), default=".")
|
|
||||||
@click.option(
|
|
||||||
"--severity",
|
|
||||||
type=click.Choice(["info", "warning", "error"]),
|
|
||||||
default="warning",
|
|
||||||
help="Minimum severity level to report",
|
|
||||||
)
|
|
||||||
@click.option(
|
|
||||||
"--max-depth",
|
|
||||||
type=int,
|
|
||||||
default=5,
|
|
||||||
help="Maximum depth for cycle detection",
|
|
||||||
)
|
)
|
||||||
@click.option(
|
@click.option(
|
||||||
"--output",
|
"--output",
|
||||||
type=click.Choice(["text", "json", "dot"]),
|
"-o",
|
||||||
default="text",
|
type=click.Choice(["ascii", "json", "dot"]),
|
||||||
help="Output format for the report",
|
default="ascii",
|
||||||
)
|
|
||||||
@click.option(
|
|
||||||
"--export",
|
|
||||||
type=click.Path(),
|
|
||||||
default=None,
|
|
||||||
help="Export report to a file",
|
|
||||||
)
|
|
||||||
def cycles(
|
|
||||||
path: str,
|
|
||||||
severity: str,
|
|
||||||
max_depth: int,
|
|
||||||
output: str,
|
|
||||||
export: Optional[str],
|
|
||||||
) -> None:
|
|
||||||
"""Detect and display circular dependencies.
|
|
||||||
|
|
||||||
PATH is the path to the project root (default: current directory).
|
|
||||||
"""
|
|
||||||
project_root = Path(path).resolve()
|
|
||||||
graph = DependencyGraph(project_root)
|
|
||||||
|
|
||||||
graph.build_from_directory(
|
|
||||||
project_root,
|
|
||||||
include_extensions=[".py", ".js", ".ts", ".go"],
|
|
||||||
exclude_patterns=["__pycache__", ".git", "node_modules", "*.egg-info"],
|
|
||||||
)
|
|
||||||
|
|
||||||
detector = CycleDetector(graph)
|
|
||||||
|
|
||||||
report = detector.get_report(max_depth=max_depth, min_severity=severity)
|
|
||||||
|
|
||||||
if output == "json":
|
|
||||||
output_text = detector.export_report(report, format="json")
|
|
||||||
elif output == "dot":
|
|
||||||
output_text = detector.export_report(report, format="dot")
|
|
||||||
else:
|
|
||||||
output_text = report
|
|
||||||
|
|
||||||
if export:
|
|
||||||
Path(export).write_text(output_text)
|
|
||||||
click.echo(f"Report exported to {export}")
|
|
||||||
else:
|
|
||||||
console.print(output_text)
|
|
||||||
|
|
||||||
|
|
||||||
@main.command()
|
|
||||||
@click.argument("path", type=click.Path(exists=True), default=".")
|
|
||||||
@click.option(
|
|
||||||
"--format",
|
|
||||||
type=click.Choice(["text", "json"]),
|
|
||||||
default="text",
|
|
||||||
help="Output format",
|
help="Output format",
|
||||||
)
|
)
|
||||||
def stats(path: str, format: str) -> None:
|
@click.pass_context
|
||||||
"""Display project statistics.
|
def graph_command(
|
||||||
|
ctx: click.Context,
|
||||||
PATH is the path to the project root (default: current directory).
|
path: Path,
|
||||||
"""
|
depth: int,
|
||||||
project_root = Path(path).resolve()
|
focus: Optional[str],
|
||||||
graph = DependencyGraph(project_root)
|
max_nodes: int,
|
||||||
|
output: str,
|
||||||
|
):
|
||||||
|
"""Generate a dependency graph for the project."""
|
||||||
|
cfg: load_config = ctx.obj.get("config_obj")
|
||||||
|
if cfg:
|
||||||
|
depth = cfg.get_depth() or depth
|
||||||
|
max_nodes = cfg.get_max_nodes() or max_nodes
|
||||||
|
|
||||||
|
graph = DependencyGraph(path if path.is_dir() else path.parent)
|
||||||
graph.build_from_directory(
|
graph.build_from_directory(
|
||||||
project_root,
|
directory=path if path.is_dir() else None,
|
||||||
include_extensions=[".py", ".js", ".ts", ".go"],
|
max_depth=depth,
|
||||||
exclude_patterns=["__pycache__", ".git", "node_modules", "*.egg-info"],
|
|
||||||
)
|
)
|
||||||
|
|
||||||
stats = graph.get_statistics()
|
theme = ctx.obj.get("theme", "default")
|
||||||
|
style = _get_style_from_theme(theme)
|
||||||
|
|
||||||
if format == "json":
|
renderer = ASCIIRenderer(console)
|
||||||
import json
|
renderer.set_style(style)
|
||||||
|
|
||||||
output = json.dumps(stats, indent=2)
|
if output == "json":
|
||||||
|
click.echo(renderer.render_json(graph))
|
||||||
|
elif output == "dot":
|
||||||
|
click.echo(renderer.render_dot(graph))
|
||||||
else:
|
else:
|
||||||
lines = [
|
focus_path = None
|
||||||
f"Files: {stats['file_count']}",
|
if focus:
|
||||||
f"Dependencies: {stats['dependency_count']}",
|
focus_path = graph.get_node_by_name(focus)
|
||||||
f"Cycles: {stats['cycle_count']}",
|
panel = renderer.render_graph(graph, focus_path, max_nodes)
|
||||||
f"Max Depth: {stats['max_depth']}",
|
console.print(panel, soft_wrap=True)
|
||||||
f"Connected Components: {stats['components']}",
|
|
||||||
]
|
|
||||||
output = "\n".join(lines)
|
|
||||||
|
|
||||||
console.print(output)
|
|
||||||
|
|
||||||
|
|
||||||
@main.command()
|
@main.command("tree")
|
||||||
@click.argument("file", type=click.Path(exists=True))
|
@click.argument(
|
||||||
@click.argument("target", type=click.Path(), default=None, required=False)
|
"path",
|
||||||
@click.option(
|
type=click.Path(exists=True, file_okay=False, dir_okay=True, path_type=Path),
|
||||||
"--max-depth",
|
default=Path("."),
|
||||||
type=int,
|
|
||||||
default=5,
|
|
||||||
help="Maximum dependency depth to follow",
|
|
||||||
)
|
)
|
||||||
def navigate(file: str, target: Optional[str], max_depth: int) -> None:
|
@click.option(
|
||||||
"""Navigate dependencies interactively.
|
"--depth",
|
||||||
|
"-d",
|
||||||
|
type=int,
|
||||||
|
default=2,
|
||||||
|
help="Maximum tree depth",
|
||||||
|
)
|
||||||
|
@click.pass_context
|
||||||
|
def tree_command(
|
||||||
|
ctx: click.Context,
|
||||||
|
path: Path,
|
||||||
|
depth: int,
|
||||||
|
):
|
||||||
|
"""Show the project structure as a tree."""
|
||||||
|
graph = DependencyGraph(path)
|
||||||
|
graph.build_from_directory(directory=path, max_depth=depth + 2)
|
||||||
|
|
||||||
FILE is the starting file.
|
theme = ctx.obj.get("theme", "default")
|
||||||
TARGET is an optional target file to find a path to.
|
style = _get_style_from_theme(theme)
|
||||||
"""
|
|
||||||
project_root = Path(file).resolve().parent
|
|
||||||
|
|
||||||
graph = DependencyGraph(project_root)
|
renderer = ASCIIRenderer(console)
|
||||||
graph.build_from_directory(
|
renderer.set_style(style)
|
||||||
project_root,
|
|
||||||
include_extensions=[".py", ".js", ".ts", ".go"],
|
|
||||||
exclude_patterns=["__pycache__", ".git", "node_modules", "*.egg-info"],
|
|
||||||
)
|
|
||||||
|
|
||||||
start_file = Path(file).resolve()
|
panel = renderer.render_tree(graph, path, depth)
|
||||||
navigator = Navigator(graph)
|
console.print(panel, soft_wrap=True)
|
||||||
|
|
||||||
if target:
|
|
||||||
target_file = Path(target).resolve()
|
@main.command("cycles")
|
||||||
path = navigator.find_path(start_file, target_file, max_depth=max_depth)
|
@click.argument(
|
||||||
if path:
|
"path",
|
||||||
console.print("Path:")
|
type=click.Path(exists=True, file_okay=False, dir_okay=True, path_type=Path),
|
||||||
for i, node in enumerate(path):
|
default=Path("."),
|
||||||
prefix = " -> " if i > 0 else " * "
|
)
|
||||||
console.print(f"{prefix}{node}")
|
@click.option(
|
||||||
|
"--report",
|
||||||
|
"-r",
|
||||||
|
type=click.Path(path_type=Path),
|
||||||
|
default=None,
|
||||||
|
help="Export report to file",
|
||||||
|
)
|
||||||
|
@click.pass_context
|
||||||
|
def cycles_command(
|
||||||
|
ctx: click.Context,
|
||||||
|
path: Path,
|
||||||
|
report: Optional[Path],
|
||||||
|
):
|
||||||
|
"""Detect and report circular dependencies."""
|
||||||
|
graph = DependencyGraph(path)
|
||||||
|
graph.build_from_directory()
|
||||||
|
|
||||||
|
detector = CycleDetector(graph)
|
||||||
|
cycles = detector.detect_all_cycles()
|
||||||
|
|
||||||
|
theme = ctx.obj.get("theme", "default")
|
||||||
|
style = _get_style_from_theme(theme)
|
||||||
|
|
||||||
|
renderer = ASCIIRenderer(console)
|
||||||
|
renderer.set_style(style)
|
||||||
|
|
||||||
|
panel = renderer.render_cycles(cycles)
|
||||||
|
console.print(panel, soft_wrap=True)
|
||||||
|
|
||||||
|
if report:
|
||||||
|
detector.export_cycle_report(report)
|
||||||
|
click.echo(f"Report exported to {report}")
|
||||||
|
|
||||||
|
|
||||||
|
@main.command("navigate")
|
||||||
|
@click.argument(
|
||||||
|
"path",
|
||||||
|
type=click.Path(exists=True, file_okay=False, dir_okay=True, path_type=Path),
|
||||||
|
default=Path("."),
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--file",
|
||||||
|
"-f",
|
||||||
|
type=str,
|
||||||
|
default=None,
|
||||||
|
help="Initial file to navigate to",
|
||||||
|
)
|
||||||
|
@click.pass_context
|
||||||
|
def navigate_command(
|
||||||
|
ctx: click.Context,
|
||||||
|
path: Path,
|
||||||
|
file: Optional[str],
|
||||||
|
):
|
||||||
|
"""Start interactive navigation mode."""
|
||||||
|
graph = DependencyGraph(path)
|
||||||
|
graph.build_from_directory()
|
||||||
|
|
||||||
|
theme = ctx.obj.get("theme", "default")
|
||||||
|
style = _get_style_from_theme(theme)
|
||||||
|
|
||||||
|
navigator = Navigator(graph, console, style)
|
||||||
|
|
||||||
|
if file:
|
||||||
|
file_path = graph.get_node_by_name(file)
|
||||||
|
if file_path:
|
||||||
|
navigator.navigate_to(file_path)
|
||||||
|
|
||||||
|
_run_interactive_mode(navigator, graph)
|
||||||
|
|
||||||
|
|
||||||
|
def _run_interactive_mode(navigator: Navigator, graph: DependencyGraph):
|
||||||
|
"""Run the interactive navigation mode."""
|
||||||
|
click.echo("Interactive navigation mode. Type 'help' for commands.")
|
||||||
|
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
cmd = click.prompt(
|
||||||
|
"depnav",
|
||||||
|
type=str,
|
||||||
|
default="",
|
||||||
|
show_default=False,
|
||||||
|
).strip()
|
||||||
|
except click.exceptions.ClickException:
|
||||||
|
break
|
||||||
|
|
||||||
|
if not cmd:
|
||||||
|
continue
|
||||||
|
|
||||||
|
parts = cmd.split()
|
||||||
|
action = parts[0].lower()
|
||||||
|
|
||||||
|
if action in ("quit", "exit", "q"):
|
||||||
|
break
|
||||||
|
elif action == "help":
|
||||||
|
_show_help()
|
||||||
|
elif action == "current":
|
||||||
|
click.echo(navigator.show_current())
|
||||||
|
elif action == "deps" and len(parts) > 1:
|
||||||
|
target = graph.get_node_by_name(parts[1])
|
||||||
|
if target:
|
||||||
|
click.echo(navigator.show_dependencies(target))
|
||||||
|
elif action == "imported" and len(parts) > 1:
|
||||||
|
target = graph.get_node_by_name(parts[1])
|
||||||
|
if target:
|
||||||
|
click.echo(navigator.show_dependents(target))
|
||||||
|
elif action == "path" and len(parts) > 2:
|
||||||
|
source = graph.get_node_by_name(parts[1])
|
||||||
|
target = graph.get_node_by_name(parts[2])
|
||||||
|
if source and target:
|
||||||
|
click.echo(navigator.find_path(source, target))
|
||||||
|
elif action == "search" and len(parts) > 1:
|
||||||
|
results = navigator.search(parts[1])
|
||||||
|
for r in results:
|
||||||
|
click.echo(f" {r}")
|
||||||
|
elif action == "stats":
|
||||||
|
renderer = ASCIIRenderer(console)
|
||||||
|
console.print(renderer.render_statistics(graph))
|
||||||
|
elif action == "back":
|
||||||
|
prev = navigator.back()
|
||||||
|
if prev:
|
||||||
|
click.echo(f"Back to: {prev}")
|
||||||
|
else:
|
||||||
|
click.echo("No previous file")
|
||||||
|
elif action == "list":
|
||||||
|
nodes = graph.get_nodes()
|
||||||
|
for n in nodes[:20]:
|
||||||
|
click.echo(f" {n}")
|
||||||
|
if len(nodes) > 20:
|
||||||
|
click.echo(f" ... and {len(nodes) - 20} more")
|
||||||
else:
|
else:
|
||||||
console.print("No path found", style="red")
|
click.echo(f"Unknown command: {action}. Type 'help' for available commands.")
|
||||||
else:
|
|
||||||
current = start_file
|
|
||||||
history = []
|
|
||||||
|
|
||||||
while True:
|
|
||||||
info = navigator.get_file_info(current)
|
|
||||||
|
|
||||||
console.print(Panel(info, title=f"Current: {current.name}"))
|
|
||||||
|
|
||||||
deps = navigator.show_dependencies(current)
|
|
||||||
dependents = navigator.show_dependents(current)
|
|
||||||
|
|
||||||
if deps:
|
|
||||||
console.print("\nDependencies:")
|
|
||||||
for dep in deps[:5]:
|
|
||||||
console.print(f" - {dep}")
|
|
||||||
|
|
||||||
if dependents:
|
|
||||||
console.print("\nDependents:")
|
|
||||||
for dep in dependents[:5]:
|
|
||||||
console.print(f" - {dep}")
|
|
||||||
|
|
||||||
suggestions = navigator.get_suggestions(current)
|
|
||||||
if suggestions:
|
|
||||||
console.print("\nJump to:")
|
|
||||||
for i, suggestion in enumerate(suggestions[:5]):
|
|
||||||
console.print(f" {i + 1}. {suggestion}")
|
|
||||||
|
|
||||||
if dependents:
|
|
||||||
console.print("\n[q] Quit")
|
|
||||||
if history:
|
|
||||||
console.print("[b] Back")
|
|
||||||
|
|
||||||
choice = click.prompt("Choose", type=str, default="q")
|
|
||||||
|
|
||||||
if choice == "q":
|
|
||||||
break
|
|
||||||
elif choice == "b" and history:
|
|
||||||
current = history.pop()
|
|
||||||
elif choice.isdigit():
|
|
||||||
idx = int(choice) - 1
|
|
||||||
if 0 <= idx < len(suggestions):
|
|
||||||
history.append(current)
|
|
||||||
current = suggestions[idx]
|
|
||||||
|
|
||||||
|
|
||||||
@main.command()
|
def _show_help():
|
||||||
@click.argument("file", type=click.Path(exists=True))
|
"""Show help for interactive mode."""
|
||||||
def info(file: str) -> None:
|
help_text = """
|
||||||
"""Show information about a specific file.
|
Available commands:
|
||||||
|
current Show current file info
|
||||||
|
deps <file> Show dependencies of a file
|
||||||
|
imported <file> Show files importing a file
|
||||||
|
path <from> <to> Show shortest path between files
|
||||||
|
search <query> Search for files
|
||||||
|
stats Show graph statistics
|
||||||
|
back Go back to previous file
|
||||||
|
list List all files
|
||||||
|
help Show this help
|
||||||
|
quit Exit
|
||||||
|
"""
|
||||||
|
click.echo(help_text)
|
||||||
|
|
||||||
FILE is the path to the file.
|
|
||||||
"""
|
|
||||||
project_root = Path(file).resolve().parent
|
|
||||||
|
|
||||||
graph = DependencyGraph(project_root)
|
@main.command("stats")
|
||||||
graph.build_from_directory(
|
@click.argument(
|
||||||
project_root,
|
"path",
|
||||||
include_extensions=[".py", ".js", ".ts", ".go"],
|
type=click.Path(exists=True, file_okay=False, dir_okay=True, path_type=Path),
|
||||||
exclude_patterns=["__pycache__", ".git", "node_modules", "*.egg-info"],
|
default=Path("."),
|
||||||
)
|
)
|
||||||
|
@click.pass_context
|
||||||
|
def stats_command(ctx: click.Context, path: Path):
|
||||||
|
"""Show dependency graph statistics."""
|
||||||
|
graph = DependencyGraph(path)
|
||||||
|
graph.build_from_directory()
|
||||||
|
|
||||||
|
renderer = ASCIIRenderer(console)
|
||||||
|
console.print(renderer.render_statistics(graph))
|
||||||
|
|
||||||
|
|
||||||
|
@main.command("info")
|
||||||
|
@click.argument(
|
||||||
|
"file",
|
||||||
|
type=click.Path(exists=True, path_type=Path),
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--path",
|
||||||
|
"-p",
|
||||||
|
type=click.Path(exists=True, file_okay=False, dir_okay=True, path_type=Path),
|
||||||
|
default=Path("."),
|
||||||
|
help="Project root path",
|
||||||
|
)
|
||||||
|
@click.pass_context
|
||||||
|
def info_command(
|
||||||
|
ctx: click.Context,
|
||||||
|
file: Path,
|
||||||
|
path: Path,
|
||||||
|
):
|
||||||
|
"""Show detailed information about a file."""
|
||||||
|
graph = DependencyGraph(path)
|
||||||
|
graph.build_from_directory()
|
||||||
|
|
||||||
|
if not str(file).startswith("/"):
|
||||||
|
file = path / file
|
||||||
|
|
||||||
try:
|
try:
|
||||||
rel_file = graph.get_node_by_name(Path(file).resolve())
|
rel_file = file.relative_to(path)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
rel_file = Path(file)
|
rel_file = file
|
||||||
|
|
||||||
navigator = Navigator(graph)
|
navigator = Navigator(graph)
|
||||||
|
|
||||||
@@ -376,53 +367,78 @@ def info(file: str) -> None:
|
|||||||
console.print(Panel(f"{info}", title=f"Info: {rel_file.name}"))
|
console.print(Panel(f"{info}", title=f"Info: {rel_file.name}"))
|
||||||
|
|
||||||
|
|
||||||
@main.command()
|
@main.command("export")
|
||||||
@click.argument("path", type=click.Path(exists=True), default=".")
|
@click.argument(
|
||||||
|
"path",
|
||||||
|
type=click.Path(exists=True, file_okay=False, dir_okay=True, path_type=Path),
|
||||||
|
default=Path("."),
|
||||||
|
)
|
||||||
@click.option(
|
@click.option(
|
||||||
"--format",
|
"--format",
|
||||||
type=click.Choice(["json", "dot", "plantuml"]),
|
"-f",
|
||||||
|
type=click.Choice(["json", "dot"]),
|
||||||
default="json",
|
default="json",
|
||||||
help="Export format",
|
help="Export format",
|
||||||
)
|
)
|
||||||
@click.option(
|
@click.option(
|
||||||
"--focus",
|
"--output",
|
||||||
"focus_file",
|
"-o",
|
||||||
type=click.Path(exists=True),
|
type=click.Path(path_type=Path),
|
||||||
default=None,
|
default=Path("depgraph.json"),
|
||||||
help="Focus the export on a specific file",
|
help="Output file",
|
||||||
)
|
)
|
||||||
@click.option(
|
@click.pass_context
|
||||||
"--max-depth",
|
def export_command(
|
||||||
type=int,
|
ctx: click.Context,
|
||||||
default=None,
|
path: Path,
|
||||||
help="Maximum dependency depth",
|
format: str,
|
||||||
)
|
output: Path,
|
||||||
def export(path: str, format: str, focus_file: Optional[str], max_depth: Optional[int]) -> None:
|
):
|
||||||
"""Export dependency graph to a file.
|
"""Export the dependency graph to a file."""
|
||||||
|
graph = DependencyGraph(path)
|
||||||
PATH is the path to the project root (default: current directory).
|
graph.build_from_directory()
|
||||||
"""
|
|
||||||
project_root = Path(path).resolve()
|
|
||||||
graph = DependencyGraph(project_root)
|
|
||||||
|
|
||||||
graph.build_from_directory(
|
|
||||||
project_root,
|
|
||||||
include_extensions=[".py", ".js", ".ts", ".go"],
|
|
||||||
exclude_patterns=["__pycache__", ".git", "node_modules", "*.egg-info"],
|
|
||||||
)
|
|
||||||
|
|
||||||
renderer = ASCIIRenderer(console)
|
renderer = ASCIIRenderer(console)
|
||||||
|
|
||||||
if focus_file:
|
|
||||||
focus_path = Path(focus_file).resolve()
|
|
||||||
else:
|
|
||||||
focus_path = None
|
|
||||||
|
|
||||||
if format == "json":
|
if format == "json":
|
||||||
output = renderer.render_json(graph, focus_file=focus_path, max_depth=max_depth)
|
content = renderer.render_json(graph)
|
||||||
elif format == "dot":
|
|
||||||
output = renderer.render_dot(graph, focus_file=focus_path, max_depth=max_depth)
|
|
||||||
else:
|
else:
|
||||||
output = renderer.render_plantuml(graph, focus_file=focus_path, max_depth=max_depth)
|
content = renderer.render_dot(graph)
|
||||||
|
|
||||||
console.print(output)
|
output.write_text(content)
|
||||||
|
click.echo(f"Exported to {output}")
|
||||||
|
|
||||||
|
|
||||||
|
def _get_style_from_theme(theme: str) -> GraphStyle:
|
||||||
|
"""Get a GraphStyle from a theme name."""
|
||||||
|
themes = {
|
||||||
|
"default": GraphStyle(
|
||||||
|
node_style="cyan",
|
||||||
|
edge_style="dim",
|
||||||
|
highlight_style="yellow",
|
||||||
|
cycle_style="red",
|
||||||
|
),
|
||||||
|
"dark": GraphStyle(
|
||||||
|
node_style="green",
|
||||||
|
edge_style="dim green",
|
||||||
|
highlight_style="white",
|
||||||
|
cycle_style="red",
|
||||||
|
),
|
||||||
|
"light": GraphStyle(
|
||||||
|
node_style="blue",
|
||||||
|
edge_style="dim blue",
|
||||||
|
highlight_style="magenta",
|
||||||
|
cycle_style="red",
|
||||||
|
),
|
||||||
|
"mono": GraphStyle(
|
||||||
|
node_style="",
|
||||||
|
edge_style="dim",
|
||||||
|
highlight_style="bold",
|
||||||
|
cycle_style="bold",
|
||||||
|
),
|
||||||
|
}
|
||||||
|
return themes.get(theme, themes["default"])
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
|||||||
Reference in New Issue
Block a user