@@ -195,8 +195,19 @@ def visit_SimpleStatementLine(self, node: cst.SimpleStatementLine) -> None:
195195 self .last_import_line = self .current_line
196196
197197
198- class ConditionalImportCollector (cst .CSTVisitor ):
199- """Collect imports inside top-level conditionals (e.g., if TYPE_CHECKING, try/except)."""
198+ class DottedImportCollector (cst .CSTVisitor ):
199+ """Collects all top-level imports from a Python module in normalized dotted format, including top-level conditional imports like `if TYPE_CHECKING:`.
200+
201+ Examples
202+ --------
203+ import os ==> "os"
204+ import dbt.adapters.factory ==> "dbt.adapters.factory"
205+ from pathlib import Path ==> "pathlib.Path"
206+ from recce.adapter.base import BaseAdapter ==> "recce.adapter.base.BaseAdapter"
207+ from typing import Any, List, Optional ==> "typing.Any", "typing.List", "typing.Optional"
208+ from recce.util.lineage import ( build_column_key, filter_dependency_maps) ==> "recce.util.lineage.build_column_key", "recce.util.lineage.filter_dependency_maps"
209+
210+ """
200211
201212 def __init__ (self ) -> None :
202213 self .imports : set [str ] = set ()
@@ -217,7 +228,10 @@ def _collect_imports_from_block(self, block: cst.IndentedBlock) -> None:
217228 for alias in child .names :
218229 module = self .get_full_dotted_name (alias .name )
219230 asname = alias .asname .name .value if alias .asname else alias .name .value
220- self .imports .add (module if module == asname else f"{ module } .{ asname } " )
231+ if isinstance (asname , cst .Attribute ):
232+ self .imports .add (module )
233+ else :
234+ self .imports .add (module if module == asname else f"{ module } .{ asname } " )
221235
222236 elif isinstance (child , cst .ImportFrom ):
223237 if child .module is None :
@@ -231,6 +245,7 @@ def _collect_imports_from_block(self, block: cst.IndentedBlock) -> None:
231245
232246 def visit_Module (self , node : cst .Module ) -> None :
233247 self .depth = 0
248+ self ._collect_imports_from_block (node )
234249
235250 def visit_FunctionDef (self , node : cst .FunctionDef ) -> None :
236251 self .depth += 1
@@ -388,45 +403,44 @@ def add_needed_imports_from_module(
388403 logger .error (f"Error parsing source module code: { e } " )
389404 return dst_module_code
390405
391- cond_import_collector = ConditionalImportCollector ()
406+ dotted_import_collector = DottedImportCollector ()
392407 try :
393408 parsed_dst_module = cst .parse_module (dst_module_code )
394- parsed_dst_module .visit (cond_import_collector )
409+ parsed_dst_module .visit (dotted_import_collector )
395410 except cst .ParserSyntaxError as e :
396411 logger .exception (f"Syntax error in destination module code: { e } " )
397412 return dst_module_code # Return the original code if there's a syntax error
398413
399414 try :
400415 for mod in gatherer .module_imports :
401- if mod in cond_import_collector .imports :
402- continue
403- AddImportsVisitor .add_needed_import (dst_context , mod )
416+ if mod not in dotted_import_collector .imports :
417+ AddImportsVisitor .add_needed_import (dst_context , mod )
404418 RemoveImportsVisitor .remove_unused_import (dst_context , mod )
405419 for mod , obj_seq in gatherer .object_mapping .items ():
406420 for obj in obj_seq :
407421 if (
408422 f"{ mod } .{ obj } " in helper_functions_fqn or dst_context .full_module_name == mod # avoid circular deps
409423 ):
410424 continue # Skip adding imports for helper functions already in the context
411- if f"{ mod } .{ obj } " in cond_import_collector .imports :
412- continue
413- AddImportsVisitor .add_needed_import (dst_context , mod , obj )
425+ if f"{ mod } .{ obj } " not in dotted_import_collector .imports :
426+ AddImportsVisitor .add_needed_import (dst_context , mod , obj )
414427 RemoveImportsVisitor .remove_unused_import (dst_context , mod , obj )
415428 except Exception as e :
416429 logger .exception (f"Error adding imports to destination module code: { e } " )
417430 return dst_module_code
431+
418432 for mod , asname in gatherer .module_aliases .items ():
419- if f"{ mod } .{ asname } " in cond_import_collector .imports :
420- continue
421- AddImportsVisitor .add_needed_import (dst_context , mod , asname = asname )
433+ if f"{ mod } .{ asname } " not in dotted_import_collector .imports :
434+ AddImportsVisitor .add_needed_import (dst_context , mod , asname = asname )
422435 RemoveImportsVisitor .remove_unused_import (dst_context , mod , asname = asname )
436+
423437 for mod , alias_pairs in gatherer .alias_mapping .items ():
424438 for alias_pair in alias_pairs :
425439 if f"{ mod } .{ alias_pair [0 ]} " in helper_functions_fqn :
426440 continue
427- if f" { mod } . { alias_pair [ 1 ] } " in cond_import_collector . imports :
428- continue
429- AddImportsVisitor .add_needed_import (dst_context , mod , alias_pair [0 ], asname = alias_pair [1 ])
441+
442+ if f" { mod } . { alias_pair [ 1 ] } " not in dotted_import_collector . imports :
443+ AddImportsVisitor .add_needed_import (dst_context , mod , alias_pair [0 ], asname = alias_pair [1 ])
430444 RemoveImportsVisitor .remove_unused_import (dst_context , mod , alias_pair [0 ], asname = alias_pair [1 ])
431445
432446 try :
0 commit comments