Skip to content

Commit afed9a6

Browse files
committed
deps-analyser: renforcer la détection interne/externe des imports
- Améliore la normalisation de chemins et la classification stdlib/internal/third_party/unknown. - Ajoute des heuristiques robustes: chaînes __init__.py, layouts src/lib/python, pyproject.toml et setup.cfg. - Corrige le filtrage des fichiers analysés (venv, caches, artefacts) et prend mieux en charge certains imports relatifs.
1 parent 27e40d4 commit afed9a6

1 file changed

Lines changed: 140 additions & 48 deletions

File tree

Core/deps_analyser/analyser.py

Lines changed: 140 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
# limitations under the License.
1515

1616
import functools
17+
import configparser
1718
import json
1819
import os
1920
import platform
@@ -185,7 +186,7 @@ def _normalize_realpath(path: str | None) -> str:
185186
if not path:
186187
return ""
187188
try:
188-
return os.path.realpath(path)
189+
return os.path.realpath(os.path.abspath(path))
189190
except Exception:
190191
return path
191192

@@ -194,7 +195,9 @@ def _is_path_under(path: str, root: str) -> bool:
194195
if not path or not root:
195196
return False
196197
try:
197-
return os.path.commonpath([path, root]) == root
198+
p = _normalize_realpath(path)
199+
r = _normalize_realpath(root)
200+
return os.path.commonpath([p, r]) == r
198201
except Exception:
199202
return False
200203

@@ -203,33 +206,116 @@ def _collect_workspace_module_roots(
203206
filtered_files: list[str], workspace_dir: str | None
204207
) -> set[str]:
205208
"""
206-
Build a conservative set of internal module roots from workspace files.
207-
Handles common src-layout aliases (src/, lib/, python/).
209+
Build internal module roots from workspace files.
210+
Uses __init__.py package chains, src-layout aliases and project metadata.
208211
"""
209212
roots: set[str] = set()
210213
ws = _normalize_realpath(workspace_dir)
211-
src_aliases = {"src", "lib", "python"}
214+
if not ws:
215+
return roots
216+
217+
def _sanitize_module_name(name: str) -> str:
218+
if not name:
219+
return ""
220+
token = str(name).strip().replace("-", "_").split(".")[0]
221+
return token if token.isidentifier() else ""
222+
223+
def _add_declared_project_names() -> None:
224+
pyproject_path = os.path.join(ws, "pyproject.toml")
225+
if os.path.isfile(pyproject_path):
226+
try:
227+
import tomllib
228+
229+
with open(pyproject_path, "rb") as f:
230+
data = tomllib.load(f)
231+
candidates = []
232+
project = data.get("project", {})
233+
if isinstance(project, dict):
234+
candidates.append(project.get("name", ""))
235+
tool = data.get("tool", {})
236+
if isinstance(tool, dict):
237+
poetry = tool.get("poetry", {})
238+
if isinstance(poetry, dict):
239+
candidates.append(poetry.get("name", ""))
240+
for candidate in candidates:
241+
module_name = _sanitize_module_name(candidate)
242+
if module_name:
243+
roots.add(module_name)
244+
except Exception:
245+
pass
246+
247+
setup_cfg_path = os.path.join(ws, "setup.cfg")
248+
if os.path.isfile(setup_cfg_path):
249+
try:
250+
parser = configparser.ConfigParser()
251+
parser.read(setup_cfg_path, encoding="utf-8")
252+
if parser.has_section("metadata"):
253+
module_name = _sanitize_module_name(
254+
parser.get("metadata", "name", fallback="")
255+
)
256+
if module_name:
257+
roots.add(module_name)
258+
except Exception:
259+
pass
260+
261+
def _add_chain_from_init(module_dir: str) -> None:
262+
current_dir = module_dir
263+
top_package_name = ""
264+
while _is_path_under(current_dir, ws):
265+
init_py = os.path.join(current_dir, "__init__.py")
266+
if not os.path.isfile(init_py):
267+
break
268+
rel_dir = os.path.relpath(current_dir, ws)
269+
parts = [p for p in rel_dir.split(os.sep) if p]
270+
if parts:
271+
if parts[0] in {"src", "lib", "python"} and len(parts) > 1:
272+
top_package_name = parts[1]
273+
else:
274+
top_package_name = parts[0]
275+
parent = os.path.dirname(current_dir)
276+
if parent == current_dir:
277+
break
278+
current_dir = parent
279+
if top_package_name:
280+
roots.add(top_package_name)
281+
212282
for f in filtered_files:
213283
try:
214284
abs_f = _normalize_realpath(f)
215-
base = os.path.splitext(os.path.basename(abs_f))[0]
216-
if base and base != "__init__":
217-
roots.add(base)
218-
if not ws or not _is_path_under(abs_f, ws):
285+
if not _is_path_under(abs_f, ws):
219286
continue
220287
rel = os.path.relpath(abs_f, ws)
221-
parts = rel.split(os.sep)
288+
rel_no_ext = os.path.splitext(rel)[0]
289+
parts = [p for p in rel_no_ext.split(os.sep) if p]
222290
if not parts:
223291
continue
224-
first = parts[0]
225-
if first and first not in {"", ".", "__pycache__"}:
226-
roots.add(first)
227-
if first in src_aliases and len(parts) > 1:
228-
second = parts[1]
229-
if second and second not in {"", ".", "__pycache__"}:
230-
roots.add(second)
292+
if parts[0] in {"src", "lib", "python"} and len(parts) > 1:
293+
roots.add(parts[1])
294+
roots.add(parts[0])
295+
if len(parts) > 1 and parts[-1] == "__init__":
296+
roots.add(parts[-2])
297+
_add_chain_from_init(os.path.dirname(abs_f))
231298
except Exception:
232299
pass
300+
301+
# Extra scan for top-level packages that may not be in the selected file list.
302+
for layout in ("", "src", "lib", "python"):
303+
base_dir = ws if not layout else os.path.join(ws, layout)
304+
if not os.path.isdir(base_dir):
305+
continue
306+
try:
307+
for entry in os.listdir(base_dir):
308+
if entry.startswith("."):
309+
continue
310+
entry_path = os.path.join(base_dir, entry)
311+
if not os.path.isdir(entry_path):
312+
continue
313+
if os.path.isfile(os.path.join(entry_path, "__init__.py")) and entry.isidentifier():
314+
roots.add(entry)
315+
except Exception:
316+
pass
317+
318+
_add_declared_project_names()
233319
return roots
234320

235321

@@ -447,37 +533,38 @@ def _t(_key: str, fr: str, en: str) -> str:
447533
venv_dir = os.path.abspath(self.venv_path_manuel)
448534
else:
449535
venv_dir = os.path.abspath(os.path.join(self.workspace_dir, "venv"))
450-
filtered_files = [
451-
f
452-
for f in files
453-
if not os.path.commonpath([os.path.abspath(f), venv_dir]) == venv_dir
454-
and not any(
455-
part.startswith(".")
456-
or part
457-
== (
458-
"**/__pycache__/**",
459-
"**/*.pyc",
460-
"**/*.pyo",
461-
"**/*.pyd",
462-
".git/**",
463-
".svn/**",
464-
".hg/**",
465-
"venv/**",
466-
".venv/**",
467-
"env/**",
468-
".env/**",
469-
"node_modules/**",
470-
"build/**",
471-
"dist/**",
472-
"*.egg-info/**",
473-
".pytest_cache/**",
474-
".mypy_cache/**",
475-
".tox/**",
476-
"site-packages/**",
477-
)
478-
for part in f.split(os.sep)
479-
)
480-
]
536+
excluded_dir_names = {
537+
"__pycache__",
538+
".git",
539+
".svn",
540+
".hg",
541+
"venv",
542+
".venv",
543+
"env",
544+
".env",
545+
"node_modules",
546+
"build",
547+
"dist",
548+
".pytest_cache",
549+
".mypy_cache",
550+
".tox",
551+
"site-packages",
552+
}
553+
filtered_files = []
554+
for file_path in files:
555+
abs_file = os.path.abspath(file_path)
556+
if _is_path_under(abs_file, venv_dir):
557+
continue
558+
rel_parts = [p for p in abs_file.split(os.sep) if p]
559+
if any(p.startswith(".") and p not in {".", ".."} for p in rel_parts):
560+
continue
561+
if any(p in excluded_dir_names for p in rel_parts):
562+
continue
563+
if abs_file.endswith((".pyc", ".pyo", ".pyd")):
564+
continue
565+
if ".egg-info" in abs_file:
566+
continue
567+
filtered_files.append(file_path)
481568

482569
# Créer une barre de progression pour l'analyse
483570
analysis_progress = None
@@ -521,6 +608,11 @@ def _t(_key: str, fr: str, en: str) -> str:
521608
elif isinstance(node, ast.ImportFrom):
522609
if node.module:
523610
modules.add(node.module.split(".")[0])
611+
elif getattr(node, "level", 0) > 0:
612+
for alias in getattr(node, "names", []) or []:
613+
name = getattr(alias, "name", "")
614+
if name:
615+
modules.add(name.split(".")[0])
524616
# Imports dynamiques via __import__ ou importlib.import_module
525617
dynamic_imports = re.findall(r"__import__\(['\"]([\w\.]+)['\"]\)", source)
526618
modules.update([mod.split(".")[0] for mod in dynamic_imports])

0 commit comments

Comments
 (0)