Files
Aether/tests/test_architecture_import_rules.py

79 lines
2.5 KiB
Python
Raw Normal View History

from __future__ import annotations
import ast
from dataclasses import dataclass
from pathlib import Path
@dataclass(frozen=True, slots=True)
class _Violation:
file: Path
line: int
imported: str
def _repo_root() -> Path:
return Path(__file__).resolve().parents[1]
def _iter_python_files(root: Path) -> list[Path]:
files: list[Path] = []
for path in root.rglob("*.py"):
if "__pycache__" in path.parts:
continue
files.append(path)
return files
def _scan_imports(py_file: Path) -> list[tuple[int, str]]:
"""返回 (lineno, module_name) 列表。"""
text = py_file.read_text(encoding="utf-8").lstrip("\ufeff")
tree = ast.parse(text, filename=str(py_file))
imports: list[tuple[int, str]] = []
for node in ast.walk(tree):
if isinstance(node, ast.Import):
for alias in node.names:
imports.append((int(getattr(node, "lineno", 0) or 0), str(alias.name)))
elif isinstance(node, ast.ImportFrom):
if node.level:
continue
if node.module:
imports.append((int(getattr(node, "lineno", 0) or 0), str(node.module)))
return imports
def _scan_forbidden_imports(*, scope_dir: Path, forbidden_prefix: str) -> list[_Violation]:
violations: list[_Violation] = []
for py_file in _iter_python_files(scope_dir):
for line, imported in _scan_imports(py_file):
if imported == forbidden_prefix or imported.startswith(f"{forbidden_prefix}."):
violations.append(_Violation(file=py_file, line=line, imported=imported))
return violations
def _format_violations(title: str, violations: list[_Violation]) -> str:
lines = [title, ""]
for v in sorted(violations, key=lambda x: (str(x.file), x.line, x.imported)):
rel = v.file.resolve().relative_to(_repo_root())
lines.append(f"- {rel}:{v.line} -> {v.imported}")
return "\n".join(lines)
def test_services_should_not_import_api() -> None:
repo = _repo_root()
violations = _scan_forbidden_imports(
scope_dir=repo / "src" / "services",
forbidden_prefix="src.api",
)
assert not violations, _format_violations("services 层禁止 import api 层:", violations)
def test_core_should_not_import_services() -> None:
repo = _repo_root()
violations = _scan_forbidden_imports(
scope_dir=repo / "src" / "core",
forbidden_prefix="src.services",
)
assert not violations, _format_violations("core 层禁止 import services 层:", violations)