1919import json
2020import os
2121import platform
22+ import configparser
2223import re
2324import subprocess
2425import sys
@@ -166,6 +167,73 @@ def _collect_workspace_module_roots(filtered_files: list[str], workspace_dir: st
166167 ws_root = _normalize_realpath (workspace_dir )
167168 if not ws_root :
168169 return roots
170+
171+ def _sanitize_module_name (name : str ) -> str :
172+ if not name :
173+ return ""
174+ token = str (name ).strip ().replace ("-" , "_" ).split ("." )[0 ]
175+ return token if token .isidentifier () else ""
176+
177+ def _add_declared_project_names () -> None :
178+ pyproject_path = os .path .join (ws_root , "pyproject.toml" )
179+ if os .path .isfile (pyproject_path ):
180+ try :
181+ import tomllib
182+
183+ with open (pyproject_path , "rb" ) as f :
184+ data = tomllib .load (f )
185+ candidates = []
186+ project = data .get ("project" , {})
187+ if isinstance (project , dict ):
188+ candidates .append (project .get ("name" , "" ))
189+ tool = data .get ("tool" , {})
190+ if isinstance (tool , dict ):
191+ poetry = tool .get ("poetry" , {})
192+ if isinstance (poetry , dict ):
193+ candidates .append (poetry .get ("name" , "" ))
194+ for candidate in candidates :
195+ module_name = _sanitize_module_name (candidate )
196+ if module_name :
197+ roots .add (module_name )
198+ except Exception :
199+ pass
200+
201+ setup_cfg_path = os .path .join (ws_root , "setup.cfg" )
202+ if os .path .isfile (setup_cfg_path ):
203+ try :
204+ parser = configparser .ConfigParser ()
205+ parser .read (setup_cfg_path , encoding = "utf-8" )
206+ if parser .has_section ("metadata" ):
207+ module_name = _sanitize_module_name (
208+ parser .get ("metadata" , "name" , fallback = "" )
209+ )
210+ if module_name :
211+ roots .add (module_name )
212+ except Exception :
213+ pass
214+
215+ def _add_chain_from_init (module_dir : str ) -> None :
216+ # Si pkg/subpkg/__init__.py existe, on ajoute pkg comme racine interne.
217+ current_dir = module_dir
218+ top_package_name = ""
219+ while _is_path_under (current_dir , ws_root ):
220+ init_py = os .path .join (current_dir , "__init__.py" )
221+ if not os .path .isfile (init_py ):
222+ break
223+ rel_dir = os .path .relpath (current_dir , ws_root )
224+ parts = [p for p in rel_dir .split (os .sep ) if p ]
225+ if parts :
226+ if parts [0 ] in {"src" , "lib" , "python" } and len (parts ) > 1 :
227+ top_package_name = parts [1 ]
228+ else :
229+ top_package_name = parts [0 ]
230+ parent = os .path .dirname (current_dir )
231+ if parent == current_dir :
232+ break
233+ current_dir = parent
234+ if top_package_name :
235+ roots .add (top_package_name )
236+
169237 for file_path in filtered_files :
170238 norm = _normalize_realpath (file_path )
171239 if not _is_path_under (norm , ws_root ):
@@ -181,6 +249,26 @@ def _collect_workspace_module_roots(filtered_files: list[str], workspace_dir: st
181249 roots .add (parts [0 ])
182250 if len (parts ) > 1 and parts [- 1 ] == "__init__" :
183251 roots .add (parts [- 2 ])
252+ _add_chain_from_init (os .path .dirname (norm ))
253+
254+ # Complément pour les layouts où certains packages ne sont pas dans la sélection.
255+ for layout in ("" , "src" , "lib" , "python" ):
256+ base_dir = ws_root if not layout else os .path .join (ws_root , layout )
257+ if not os .path .isdir (base_dir ):
258+ continue
259+ try :
260+ for entry in os .listdir (base_dir ):
261+ if entry .startswith ("." ):
262+ continue
263+ entry_path = os .path .join (base_dir , entry )
264+ if not os .path .isdir (entry_path ):
265+ continue
266+ if os .path .isfile (os .path .join (entry_path , "__init__.py" )) and entry .isidentifier ():
267+ roots .add (entry )
268+ except Exception :
269+ pass
270+
271+ _add_declared_project_names ()
184272 return roots
185273
186274
0 commit comments