Spaces:
Sleeping
Sleeping
File size: 5,720 Bytes
036a2db | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 | """
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 |