NexusCoder / nexus /tools /code_dependency.py
AdminReal's picture
Import NexusCoder from github.com/mhieuhonda/NexusCoder
eca5751 verified
Raw
History Blame Contribute Delete
4.52 kB
"""
Code Dependency Tool - Trích xuất import graph từ Python file/thư mục.
Author: Hieu Louis (2026)
Trả về danh sách (file, imported_module) pairs. Hỗ trợ recursive scan
thư mục. Module string chứa dotted path đầy đủ (vd `os.path`, `.foo.bar`
cho relative imports).
"""
from __future__ import annotations
import ast
import json
import os
from typing import Any, Dict, List, Optional
from .base import Tool, ToolResult, ToolContext, ToolCategory, ToolSafety
def _extract_imports(source: str) -> List[str]:
"""Trích danh sách module imported từ source code (dùng ast)."""
try:
tree = ast.parse(source)
except SyntaxError:
return []
modules: List[str] = []
for n in ast.walk(tree):
if isinstance(n, ast.Import):
for alias in n.names:
modules.append(alias.name)
elif isinstance(n, ast.ImportFrom):
mod = "." * (n.level or 0) + (n.module or "")
if mod:
modules.append(mod)
return modules
def _walk_python_files(path: str, recursive: bool) -> List[str]:
"""Tìm tất cả file .py trong path (file hoặc dir)."""
if os.path.isfile(path):
return [path]
files: List[str] = []
if not os.path.isdir(path):
return files
if recursive:
for root, _dirs, names in os.walk(path):
for name in sorted(names):
if name.endswith(".py"):
files.append(os.path.join(root, name))
else:
for name in sorted(os.listdir(path)):
full = os.path.join(path, name)
if os.path.isfile(full) and name.endswith(".py"):
files.append(full)
return files
class CodeDependencyTool(Tool):
"""Extract import graph từ Python file/thư mục."""
category = ToolCategory.CODE
safety = ToolSafety.SAFE # read-only analysis
@property
def name(self) -> str:
return "code_dependency"
@property
def description(self) -> str:
return (
"Extract import graph từ Python file/dir. Trả về list (file, imported_module). "
"Hỗ trợ recursive scan và top-level package ranking."
)
@property
def parameters(self) -> Dict[str, Any]:
return {
"type": "object",
"properties": {
"path": {"type": "string", "description": "File hoặc thư mục Python"},
"recursive": {
"type": "boolean",
"description": "Scan đệ quy nếu là thư mục (default true)",
},
},
"required": ["path"],
}
def validate_args(self, args: Dict[str, Any]) -> Optional[str]:
if not args.get("path"):
return "Missing required arg: path"
return None
def execute(self, args: Dict[str, Any], context: ToolContext) -> ToolResult:
path: str = args["path"]
recursive: bool = bool(args.get("recursive", True))
if not os.path.exists(path):
return ToolResult(
success=False,
error=f"Path không tồn tại: {path}",
return_code=1,
)
files = _walk_python_files(path, recursive)
if not files:
return ToolResult(
success=True,
output="[]",
metadata={"path": path, "file_count": 0, "edge_count": 0},
)
edges: List[Dict[str, str]] = []
for fp in files:
try:
with open(fp, "r", encoding="utf-8") as f:
src = f.read()
except Exception:
continue
for mod in _extract_imports(src):
edges.append({"file": fp, "module": mod})
# Top-level package ranking
top_packages: Dict[str, int] = {}
for e in edges:
top = e["module"].lstrip(".").split(".")[0]
if top:
top_packages[top] = top_packages.get(top, 0) + 1
ranked = dict(sorted(top_packages.items(), key=lambda kv: -kv[1])[:20])
return ToolResult(
success=True,
output=json.dumps(edges, indent=2, ensure_ascii=False),
metadata={
"path": path,
"recursive": recursive,
"file_count": len(files),
"edge_count": len(edges),
"top_packages": ranked,
},
)