Spaces:
Sleeping
Sleeping
| """ | |
| resolver.py — Resolve Python import statements to filesystem paths. | |
| """ | |
| import ast | |
| import logging | |
| import os | |
| from typing import Dict, Optional | |
| from ._warn_once import warn_syntax_error_once, check_and_warn_encoding | |
| logger = logging.getLogger(__name__) | |
| def build_import_map(filename: str, repo_path: str) -> Dict[str, str]: | |
| """ | |
| Parse imports in a file and resolve them to absolute paths. | |
| Returns: | |
| dict: local_name -> absolute_path_of_source_file | |
| e.g. {"helper": "/repo/utils.py", "Session": "/repo/sessions.py"} | |
| __init__.py transparency: | |
| When an import resolves to a package __init__.py, we scan that | |
| __init__.py for re-export statements (`from .sub import Name`) and | |
| follow them to the actual definition file. This means: | |
| from flask import Flask | |
| # resolves to src/flask/__init__.py | |
| # __init__.py has: from .app import Flask | |
| # final result: src/flask/app.py ← correct | |
| """ | |
| with open(filename, "rb") as f: | |
| raw = f.read() | |
| check_and_warn_encoding(logger, filename, raw) | |
| source = raw.decode("utf-8", errors="ignore") | |
| try: | |
| tree = ast.parse(source) | |
| except SyntaxError as e: | |
| warn_syntax_error_once(logger, filename, e) | |
| return {} | |
| imports: Dict[str, str] = {} | |
| file_dir = os.path.dirname(os.path.abspath(filename)) | |
| repo_abs = os.path.abspath(repo_path) | |
| for node in ast.walk(tree): | |
| if isinstance(node, ast.ImportFrom): | |
| module = node.module or "" | |
| level = node.level | |
| if level > 0: | |
| # Relative import: from .utils import helper | |
| base = file_dir | |
| for _ in range(level - 1): | |
| base = os.path.dirname(base) | |
| module_path = os.path.join(base, module.replace(".", os.sep)) | |
| else: | |
| # Absolute import: from requests.utils import helper | |
| module_path = os.path.join(repo_abs, module.replace(".", os.sep)) | |
| resolved = _resolve_module_path(module_path) | |
| for alias in node.names: | |
| local_name = alias.asname or alias.name | |
| # Check if this is a submodule import (from package import module) | |
| submodule_path = os.path.join(module_path, alias.name) | |
| submodule_resolved = _resolve_module_path(submodule_path) | |
| if submodule_resolved: | |
| imports[local_name] = submodule_resolved | |
| elif resolved: | |
| # __init__.py transparency: follow re-exports one level | |
| if resolved.endswith("__init__.py"): | |
| real = _follow_init_reexport(resolved, alias.name, repo_abs) | |
| imports[local_name] = real if real else resolved | |
| else: | |
| imports[local_name] = resolved | |
| elif isinstance(node, ast.Import): | |
| for alias in node.names: | |
| top_module = alias.name.split(".")[0] | |
| local_name = alias.asname or top_module | |
| module_path = os.path.join(repo_abs, alias.name.replace(".", os.sep)) | |
| resolved = _resolve_module_path(module_path) | |
| if not resolved: | |
| # Bare `import x` — try the importing file's own directory | |
| sibling_path = os.path.join(file_dir, alias.name.replace(".", os.sep)) | |
| resolved = _resolve_module_path(sibling_path) | |
| if resolved: | |
| imports[local_name] = resolved | |
| return imports | |
| def _follow_init_reexport(init_path: str, name: str, repo_abs: str) -> Optional[str]: | |
| """ | |
| Given a package __init__.py and a name imported from it, check whether | |
| __init__.py re-exports that name from a submodule. | |
| Example: | |
| __init__.py contains: from .app import Flask | |
| name = "Flask" | |
| → returns /abs/path/to/app.py | |
| Only follows one level (no recursive re-export chasing) to stay fast. | |
| Returns None if the name is not re-exported or the submodule can't be found. | |
| """ | |
| try: | |
| with open(init_path, "rb") as f: | |
| raw = f.read() | |
| source = raw.decode("utf-8", errors="ignore") | |
| tree = ast.parse(source) | |
| except (OSError, SyntaxError): | |
| return None | |
| init_dir = os.path.dirname(init_path) | |
| for node in ast.walk(tree): | |
| if not isinstance(node, ast.ImportFrom): | |
| continue | |
| if node.level == 0: | |
| continue # only relative re-exports (from .sub import X) | |
| for alias in node.names: | |
| exported_name = alias.asname or alias.name | |
| if exported_name != name: | |
| continue | |
| # Found the re-export — resolve the submodule | |
| module = node.module or "" | |
| base = init_dir | |
| for _ in range(node.level - 1): | |
| base = os.path.dirname(base) | |
| sub_path = os.path.join(base, module.replace(".", os.sep)) | |
| # Check if alias.name itself is a submodule | |
| submodule_path = os.path.join(sub_path, alias.name) | |
| resolved = _resolve_module_path(submodule_path) or _resolve_module_path(sub_path) | |
| if resolved: | |
| return resolved | |
| return None | |
| def _resolve_module_path(module_path: str) -> Optional[str]: | |
| """Try to find a .py file or package __init__.py for a given path.""" | |
| candidates = [ | |
| module_path + ".py", | |
| os.path.join(module_path, "__init__.py"), | |
| ] | |
| for candidate in candidates: | |
| if os.path.isfile(candidate): | |
| return os.path.normpath(candidate) | |
| return None |