@@ -231,30 +231,48 @@ def _extract_located_imports(file_path: str) -> list[_LocatedImport]:
231231 imports : list [_LocatedImport ] = []
232232 ignore_directives = _find_ignore_directives (source )
233233 type_checking_ranges = _find_type_checking_ranges (tree )
234+ conditional_import_ranges = _find_conditional_import_ranges (tree )
234235
235236 for node in ast .walk (tree ):
236237 if isinstance (node , ast .Import ):
237- is_type = _in_type_checking (node , type_checking_ranges )
238- kind = ImportKind .TYPE_IMPORT if is_type else ImportKind .IMPORT
238+ kind = _classify_import (
239+ node ,
240+ ImportKind .IMPORT ,
241+ type_checking_ranges ,
242+ conditional_import_ranges ,
243+ )
239244 for alias in node .names :
240245 imports .append (_LocatedImport (alias .name , kind , node .lineno ))
241246
242247 elif isinstance (node , ast .ImportFrom ):
243- is_type = _in_type_checking (node , type_checking_ranges )
244248 if node .level and node .level > 0 :
245249 # Relative import
246- kind = ImportKind .TYPE_IMPORT if is_type else ImportKind .RELATIVE_IMPORT
250+ kind = _classify_import (
251+ node ,
252+ ImportKind .RELATIVE_IMPORT ,
253+ type_checking_ranges ,
254+ conditional_import_ranges ,
255+ )
247256 module = node .module or ""
248257 dots = "." * node .level
249258 imports .append (_LocatedImport (f"{ dots } { module } " , kind , node .lineno ))
250259 else :
251- kind = ImportKind .TYPE_IMPORT if is_type else ImportKind .FROM_IMPORT
260+ kind = _classify_import (
261+ node ,
262+ ImportKind .FROM_IMPORT ,
263+ type_checking_ranges ,
264+ conditional_import_ranges ,
265+ )
252266 if node .module :
253267 imports .append (_LocatedImport (node .module , kind , node .lineno ))
254268
255269 elif isinstance (node , ast .Call ):
256- is_type = _in_type_checking (node , type_checking_ranges )
257- kind = ImportKind .TYPE_IMPORT if is_type else ImportKind .DYNAMIC_IMPORT
270+ kind = _classify_import (
271+ node ,
272+ ImportKind .DYNAMIC_IMPORT ,
273+ type_checking_ranges ,
274+ conditional_import_ranges ,
275+ )
258276 for module_name in _extract_dynamic_import_names (node ):
259277 imports .append (_LocatedImport (module_name , kind , node .lineno ))
260278
@@ -321,6 +339,20 @@ def _extract_dynamic_import_names(node: ast.Call) -> list[str]:
321339 return []
322340
323341
342+ def _classify_import (
343+ node : ast .AST ,
344+ default_kind : ImportKind ,
345+ type_checking_ranges : list [tuple [int , int ]],
346+ conditional_import_ranges : list [tuple [int , int ]],
347+ ) -> ImportKind :
348+ """Classify an import node by special context before syntax kind."""
349+ if _in_type_checking (node , type_checking_ranges ):
350+ return ImportKind .TYPE_IMPORT
351+ if _in_conditional_import (node , conditional_import_ranges ):
352+ return ImportKind .CONDITIONAL_IMPORT
353+ return default_kind
354+
355+
324356def _find_type_checking_ranges (tree : ast .Module ) -> list [tuple [int , int ]]:
325357 """Find line ranges of TYPE_CHECKING blocks."""
326358 ranges : list [tuple [int , int ]] = []
@@ -346,6 +378,46 @@ def _find_type_checking_ranges(tree: ast.Module) -> list[tuple[int, int]]:
346378 return sorted (ranges , key = lambda ele : ele [0 ])
347379
348380
381+ def _find_conditional_import_ranges (tree : ast .Module ) -> list [tuple [int , int ]]:
382+ """Find try/except ImportError ranges that contain optional imports."""
383+ ranges : list [tuple [int , int ]] = []
384+
385+ for node in ast .walk (tree ):
386+ if not isinstance (node , ast .Try ):
387+ continue
388+ if not any (_handles_import_error (handler .type ) for handler in node .handlers ):
389+ continue
390+
391+ ranges .extend (_statement_ranges (node .body ))
392+ for handler in node .handlers :
393+ if _handles_import_error (handler .type ):
394+ ranges .extend (_statement_ranges (handler .body ))
395+
396+ return sorted (ranges , key = lambda ele : ele [0 ])
397+
398+
399+ def _handles_import_error (node : ast .expr | None ) -> bool :
400+ """Return True if an except handler catches import-related errors."""
401+ if node is None :
402+ return False
403+ if isinstance (node , ast .Name ):
404+ return node .id in {"ImportError" , "ModuleNotFoundError" }
405+ if isinstance (node , ast .Attribute ):
406+ return node .attr in {"ImportError" , "ModuleNotFoundError" }
407+ if isinstance (node , ast .Tuple ):
408+ return any (_handles_import_error (elt ) for elt in node .elts )
409+ return False
410+
411+
412+ def _statement_ranges (statements : list [ast .stmt ]) -> list [tuple [int , int ]]:
413+ """Return line ranges covered by statement blocks."""
414+ if not statements :
415+ return []
416+ start = statements [0 ].lineno
417+ end = max (getattr (statement , "end_lineno" , statement .lineno ) for statement in statements )
418+ return [(start , end )]
419+
420+
349421def _in_type_checking (node : ast .AST , ranges : list [tuple [int , int ]]) -> bool :
350422 """Check if a node is inside a TYPE_CHECKING block."""
351423 if not hasattr (node , "lineno" ):
@@ -354,6 +426,14 @@ def _in_type_checking(node: ast.AST, ranges: list[tuple[int, int]]) -> bool:
354426 return any (start <= lineno <= end for start , end in ranges )
355427
356428
429+ def _in_conditional_import (node : ast .AST , ranges : list [tuple [int , int ]]) -> bool :
430+ """Check if a node is inside a try/except ImportError block."""
431+ if not hasattr (node , "lineno" ):
432+ return False
433+ lineno = node .lineno
434+ return any (start <= lineno <= end for start , end in ranges )
435+
436+
357437def _resolve_import (
358438 import_name : str ,
359439 source_file : str ,
@@ -365,7 +445,15 @@ def _resolve_import(
365445 Returns (resolved_path, is_external).
366446 The path is normalized with forward slashes.
367447 """
368- if kind in (ImportKind .RELATIVE_IMPORT , ImportKind .TYPE_IMPORT ) and import_name .startswith ("." ):
448+ if (
449+ kind
450+ in (
451+ ImportKind .RELATIVE_IMPORT ,
452+ ImportKind .TYPE_IMPORT ,
453+ ImportKind .CONDITIONAL_IMPORT ,
454+ )
455+ and import_name .startswith ("." )
456+ ):
369457 # Relative import
370458 return _resolve_relative_import (import_name , source_file , project_root )
371459
0 commit comments