Diffcontext / diffcontext /resolver.py
trakshan-mishra
Deploy FastAPI & MCP server over SSE
036a2db
Raw
History Blame Contribute Delete
5.72 kB
"""
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