@@ -41,6 +41,7 @@ class _LocatedImport:
4141 module_name : str
4242 import_kind : ImportKind
4343 line_number : int
44+ aliases : tuple [str , ...] = ()
4445
4546
4647@dataclass (frozen = True )
@@ -147,29 +148,28 @@ def _extract_graph_uncached(
147148
148149 imports = _extract_located_imports (file_path )
149150 for located_import in imports :
150- module_name = located_import .module_name
151151 import_kind = located_import .import_kind
152152 if (
153153 ignore_type_checking_imports
154154 and import_kind == ImportKind .TYPE_IMPORT
155155 ):
156156 continue
157- resolved , is_external = _resolve_import (
158- module_name , file_path , project_path , import_kind
159- )
160- if resolved and resolved != _normalize (file_path ):
161- # Check if the resolved path is in our project
162- if not is_external and resolved not in normalized_py_file_set :
163- is_external = True
164-
165- edges .append (
166- Edge (
167- source = _normalize (file_path ),
168- target = resolved ,
169- external = is_external ,
170- import_kinds = (import_kind ,),
157+ for resolved , is_external in _resolve_import_targets (
158+ located_import , file_path , project_path
159+ ):
160+ if resolved and resolved != _normalize (file_path ):
161+ # Check if the resolved path is in our project
162+ if not is_external and resolved not in normalized_py_file_set :
163+ is_external = True
164+
165+ edges .append (
166+ Edge (
167+ source = _normalize (file_path ),
168+ target = resolved ,
169+ external = is_external ,
170+ import_kinds = (import_kind ,),
171+ )
171172 )
172- )
173173
174174 return _merge_edges (edges )
175175
@@ -246,11 +246,25 @@ def _extract_located_imports(file_path: str) -> list[_LocatedImport]:
246246 kind = ImportKind .TYPE_IMPORT if is_type else ImportKind .RELATIVE_IMPORT
247247 module = node .module or ""
248248 dots = "." * node .level
249- imports .append (_LocatedImport (f"{ dots } { module } " , kind , node .lineno ))
249+ imports .append (
250+ _LocatedImport (
251+ f"{ dots } { module } " ,
252+ kind ,
253+ node .lineno ,
254+ _module_aliases (node ),
255+ )
256+ )
250257 else :
251258 kind = ImportKind .TYPE_IMPORT if is_type else ImportKind .FROM_IMPORT
252259 if node .module :
253- imports .append (_LocatedImport (node .module , kind , node .lineno ))
260+ imports .append (
261+ _LocatedImport (
262+ node .module ,
263+ kind ,
264+ node .lineno ,
265+ _module_aliases (node ),
266+ )
267+ )
254268
255269 elif isinstance (node , ast .Call ):
256270 is_type = _in_type_checking (node , type_checking_ranges )
@@ -321,6 +335,11 @@ def _extract_dynamic_import_names(node: ast.Call) -> list[str]:
321335 return []
322336
323337
338+ def _module_aliases (node : ast .ImportFrom ) -> tuple [str , ...]:
339+ """Return aliases that may refer to imported submodules."""
340+ return tuple (alias .name for alias in node .names if alias .name != "*" )
341+
342+
324343def _find_type_checking_ranges (tree : ast .Module ) -> list [tuple [int , int ]]:
325344 """Find line ranges of TYPE_CHECKING blocks."""
326345 ranges : list [tuple [int , int ]] = []
@@ -373,6 +392,45 @@ def _resolve_import(
373392 return _resolve_absolute_import (import_name , project_root )
374393
375394
395+ def _resolve_import_targets (
396+ import_ : _LocatedImport ,
397+ source_file : str ,
398+ project_root : str ,
399+ ) -> list [tuple [str , bool ]]:
400+ """Resolve an import, including namespace-package submodule aliases."""
401+ resolved , is_external = _resolve_import (
402+ import_ .module_name ,
403+ source_file ,
404+ project_root ,
405+ import_ .import_kind ,
406+ )
407+ if not is_external or not import_ .aliases :
408+ return [(resolved , is_external )]
409+
410+ alias_targets : list [tuple [str , bool ]] = []
411+ for alias in import_ .aliases :
412+ alias_module = _join_import_alias (import_ .module_name , alias )
413+ alias_resolved , alias_is_external = _resolve_import (
414+ alias_module ,
415+ source_file ,
416+ project_root ,
417+ import_ .import_kind ,
418+ )
419+ if not alias_is_external :
420+ alias_targets .append ((alias_resolved , alias_is_external ))
421+
422+ return alias_targets or [(resolved , is_external )]
423+
424+
425+ def _join_import_alias (module_name : str , alias : str ) -> str :
426+ """Join a from-import module name with a candidate submodule alias."""
427+ if not module_name :
428+ return alias
429+ if set (module_name ) == {"." }:
430+ return f"{ module_name } { alias } "
431+ return f"{ module_name } .{ alias } "
432+
433+
376434def _resolve_relative_import (
377435 import_name : str ,
378436 source_file : str ,
0 commit comments