@@ -86,6 +86,58 @@ def get_reexports_from_tree(tree, module: str, package: str) -> dict:
8686 return reexports
8787
8888
89+ def get_module_aliases_from_tree (tree , package : str ) -> dict [str , tuple [str , ...]]:
90+ """Map runtime aliases such as fla.modules.l2norm to their canonical modules."""
91+ aliases = {}
92+ if not tree :
93+ return aliases
94+ for node in tree .body :
95+ if not isinstance (node , ast .Assign ):
96+ continue
97+ if not any (isinstance (target , ast .Name ) and target .id == '_MODULE_ALIASES' for target in node .targets ):
98+ continue
99+ if not isinstance (node .value , ast .Dict ):
100+ continue
101+ for key , value in zip (node .value .keys , node .value .values ):
102+ if not isinstance (key , ast .Constant ) or not isinstance (key .value , str ):
103+ continue
104+ if not isinstance (value , ast .Tuple | ast .List ):
105+ continue
106+ targets = [
107+ f'{ package } .{ item .value } '
108+ for item in value .elts
109+ if isinstance (item , ast .Constant ) and isinstance (item .value , str )
110+ ]
111+ if targets :
112+ aliases [f'{ package } .{ key .value } ' ] = tuple (targets )
113+ return aliases
114+
115+
116+ def expand_aliased_imports (imports : set , reexports : dict , module_aliases : dict ) -> set :
117+ """Follow symbol re-exports and single-target module aliases."""
118+ expanded = set (imports )
119+ pending = list (imports )
120+ visited = set ()
121+ while pending :
122+ imported = pending .pop ()
123+ if imported in visited :
124+ continue
125+ visited .add (imported )
126+ module , symbol = imported
127+ candidates = []
128+ reexport = reexports .get (imported )
129+ if reexport is not None :
130+ candidates .append (reexport )
131+ targets = module_aliases .get (module )
132+ if targets is not None and len (targets ) == 1 :
133+ candidates .append ((targets [0 ], symbol ))
134+ for candidate in candidates :
135+ if candidate not in expanded :
136+ expanded .add (candidate )
137+ pending .append (candidate )
138+ return expanded
139+
140+
89141def _get_dispatch_owners (tree ) -> set [str ]:
90142 """Read operation keys from the supported dispatch imports."""
91143 if not tree :
@@ -198,10 +250,13 @@ def __init__(self, search_dirs, test_dir, project_root=None):
198250 all_source_files = [p for s_dir in search_dirs for p in Path (s_dir ).resolve ().rglob ("*.py" )]
199251 source_files = [p for p in all_source_files if p .name != '__init__.py' ]
200252 reexports = {}
253+ module_aliases = {}
201254 for file_path in all_source_files :
202255 module = file_to_module_path (file_path , self .project_root )
203256 package = module if file_path .name == '__init__.py' else module .rpartition ('.' )[0 ]
204- reexports .update (get_reexports_from_tree (parse_file (file_path ), module , package ))
257+ tree = parse_file (file_path )
258+ reexports .update (get_reexports_from_tree (tree , module , package ))
259+ module_aliases .update (get_module_aliases_from_tree (tree , package ))
205260 test_scope = os .environ .get ("TEST_SCOPE" , "ALL" ).upper ()
206261 if test_scope == "MODELS_ONLY" :
207262 test_files = [p for p in models_test_dir .rglob ("*.py" ) if p .name != '__init__.py' ]
@@ -225,13 +280,7 @@ def __init__(self, search_dirs, test_dir, project_root=None):
225280 for file_path in self .all_project_files :
226281 tree = parse_file (file_path )
227282 definitions = get_definitions_from_tree (tree )
228- imports = get_imports_from_tree (tree )
229- for imported in list (imports ):
230- visited = set ()
231- while imported in reexports and imported not in visited :
232- visited .add (imported )
233- imported = reexports [imported ]
234- imports .add (imported )
283+ imports = expand_aliased_imports (get_imports_from_tree (tree ), reexports , module_aliases )
235284 self .file_to_definitions [file_path ] = definitions
236285 self .file_to_imports [file_path ] = imports
237286 for defn in definitions :
0 commit comments