python.py•4.29 kB
import ast
from pathlib import Path
from typing import Optional, Set
from ..utils.path import search_in_path
from .base import BaseAnalyzer
class PythonAnalyzer(BaseAnalyzer):
    """Python用アナライザー
    Attributes:
        base_dir (Path): 基準となるディレクトリパス
        search_paths (list[Path]): モジュール検索パスのリスト
    """
    def __init__(self, base_dir: Path):
        super().__init__(base_dir)
        self.search_paths = [
            self.base_dir,
            self.base_dir / "src",
        ]
    @property
    def file_extensions(self) -> list[str]:
        """対象とするファイル拡張子のリストを返す
        Returns:
            list[str]: ファイル拡張子のリスト
        """
        return [".py"]
    def resolve_relative_path(
        self, import_name: str, current_file: Path, allow_init: bool = True
    ) -> Optional[Path]:
        """相対インポートのパス解決を行う
        Args:
            import_name (str): インポート名
            current_file (Path): 現在のファイルパス
            allow_init (bool): __init__.pyの許可フラグ
        Returns:
            Optional[Path]: 解決されたパス。見つからない場合はNone
        """
        try:
            # インポート名をパスに変換
            import_path = Path(import_name.replace(".", "/"))
            if allow_init:
                patterns = [f"{import_path}.py", f"{import_path}/__init__.py"]
            else:
                patterns = [f"{import_path}.py"]
            # 現在のファイルからの相対パスで検索
            current_dir = current_file.parent
            for pattern in patterns:
                potential_path = current_dir / pattern
                if potential_path.exists():
                    return potential_path
            return None
        except Exception:
            return None
    def analyze_imports(self, content: str, file_path: Path) -> Set[str]:
        """ファイル内のインポート文を解析する
        Args:
            content (str): ファイルの内容
            file_path (Path): ファイルパス
        Returns:
            Set[str]: インポートされているファイルパスの集合
        """
        imports = set()
        tree = ast.parse(content)
        for node in ast.walk(tree):
            if isinstance(node, (ast.Import, ast.ImportFrom)):
                if isinstance(node, ast.ImportFrom):
                    # from文の場合
                    if node.level > 0:  # 相対インポート
                        current = file_path.parent
                        for _ in range(node.level - 1):
                            current = current.parent
                        if node.module:
                            import_path = current / Path(node.module.replace(".", "/"))
                        else:
                            import_path = current
                    else:  # 絶対インポート
                        import_path = Path(
                            node.module.replace(".", "/") if node.module else ""
                        )
                else:
                    # import文の場合
                    for name in node.names:
                        import_path = Path(name.name.split(".")[0])
                # モジュールの解決を試みる
                if isinstance(import_path, Path):
                    # まず相対パスで解決を試みる
                    resolved_path = self.resolve_relative_path(
                        str(import_path), file_path, allow_init=True
                    )
                    # 相対パスで見つからない場合は検索パスから探す
                    if not resolved_path:
                        resolved_path = search_in_path(
                            str(import_path),
                            self.search_paths,
                            self.file_extensions,
                            allow_init=True,
                        )
                    if resolved_path:
                        normalized_path = self.normalize_path(resolved_path)
                        imports.add(normalized_path)
        return imports