| from __future__ import annotations |
|
|
| import ast |
| import importlib.util |
| import logging |
| from typing import TYPE_CHECKING |
|
|
| from codeforge.sandbox.models import ImportReport |
|
|
| if TYPE_CHECKING: |
| from pathlib import Path |
|
|
| _log = logging.getLogger(__name__) |
|
|
|
|
| def _top_level(name: str) -> str: |
| return name.split(".", maxsplit=1)[0] |
|
|
|
|
| def _extract_imports(tree: ast.AST) -> set[str]: |
| out: set[str] = set() |
| for node in ast.walk(tree): |
| if isinstance(node, ast.Import): |
| for alias in node.names: |
| out.add(_top_level(alias.name)) |
| elif isinstance(node, ast.ImportFrom): |
| if node.level != 0 or node.module is None: |
| continue |
| out.add(_top_level(node.module)) |
| return out |
|
|
|
|
| def _local_modules(project_dir: Path) -> set[str]: |
| local: set[str] = set() |
| for py in project_dir.rglob("*.py"): |
| if py.name == "__init__.py": |
| local.add(py.parent.name) |
| else: |
| local.add(py.stem) |
| return local |
|
|
|
|
| def scan_imports(project_dir: Path) -> ImportReport: |
| by_file: dict[str, tuple[str, ...]] = {} |
| all_pkgs: set[str] = set() |
| total = 0 |
|
|
| for py in sorted(project_dir.rglob("*.py")): |
| try: |
| tree = ast.parse(py.read_text(encoding="utf-8")) |
| except (SyntaxError, UnicodeDecodeError) as e: |
| _log.warning("imports: parse error %s: %s", py, e) |
| by_file[str(py.relative_to(project_dir))] = ("__parse_error__",) |
| continue |
| pkgs = _extract_imports(tree) |
| total += len(pkgs) |
| by_file[str(py.relative_to(project_dir))] = tuple(sorted(pkgs)) |
| all_pkgs.update(pkgs) |
|
|
| local = _local_modules(project_dir) |
| unresolved = tuple( |
| sorted( |
| p for p in all_pkgs |
| if p not in local and importlib.util.find_spec(p) is None |
| ) |
| ) |
| return ImportReport(total=total, unresolved=unresolved, by_file=by_file) |
|
|