@@ -239,6 +239,78 @@ def _has_signature_stub_body(
239239 )
240240
241241
242+ def _is_stub_value (node : ast .AST | None ) -> bool :
243+ """Recognize declaration values, not expressions that run an implementation."""
244+ if node is None or isinstance (node , ast .Constant ):
245+ return True
246+ return (
247+ isinstance (node , ast .UnaryOp )
248+ and isinstance (node .op , (ast .UAdd , ast .USub ))
249+ and isinstance (node .operand , ast .Constant )
250+ and isinstance (node .operand .value , (int , float , complex ))
251+ )
252+
253+
254+ def _is_stub_declaration (node : ast .AST ) -> bool :
255+ """Recognize non-concrete declarations in a Python type stub."""
256+ if isinstance (node , ast .AnnAssign ):
257+ return isinstance (node .target , ast .Name ) and _is_stub_value (node .value )
258+ if isinstance (node , ast .Assign ):
259+ return (
260+ all (isinstance (target , ast .Name ) for target in node .targets )
261+ and isinstance (node .value , ast .Constant )
262+ and node .value .value is Ellipsis
263+ )
264+ if isinstance (node , ast .If ):
265+ # Stubs may select declarations by version/platform or TYPE_CHECKING.
266+ # Calls, comprehensions and assignment expressions are not such guards.
267+ condition_nodes = (
268+ ast .Name ,
269+ ast .Attribute ,
270+ ast .Subscript ,
271+ ast .Slice ,
272+ ast .Tuple ,
273+ ast .List ,
274+ ast .Constant ,
275+ ast .Compare ,
276+ ast .BoolOp ,
277+ ast .UnaryOp ,
278+ ast .Load ,
279+ ast .cmpop ,
280+ ast .And ,
281+ ast .Or ,
282+ ast .Not ,
283+ ast .UAdd ,
284+ ast .USub ,
285+ )
286+ return all (
287+ isinstance (part , condition_nodes ) for part in ast .walk (node .test )
288+ ) and all (
289+ _is_stub_declaration (statement ) for statement in [* node .body , * node .orelse ]
290+ )
291+ if isinstance (node , (ast .FunctionDef , ast .AsyncFunctionDef , ast .ClassDef )):
292+ body = node .body
293+ if (
294+ body
295+ and isinstance (body [0 ], ast .Expr )
296+ and isinstance (body [0 ].value , ast .Constant )
297+ and isinstance (body [0 ].value .value , str )
298+ ):
299+ body = body [1 :]
300+ if isinstance (node , ast .ClassDef ):
301+ return all (_is_stub_declaration (statement ) for statement in body )
302+ return (
303+ len (body ) == 1
304+ and isinstance (body [0 ], ast .Expr )
305+ and _is_stub_declaration (body [0 ])
306+ )
307+ return (
308+ isinstance (node , ast .Expr )
309+ and isinstance (node .value , ast .Constant )
310+ and node .value .value is Ellipsis
311+ )
312+
313+
242314def _module_binds_name (module : ast .Module , target_name : str ) -> bool :
243315 class BindingProbe (ast .NodeVisitor ):
244316 def __init__ (self ) -> None :
@@ -462,6 +534,8 @@ class Visitor(ast.NodeVisitor):
462534 def __init__ (self , mod : str , file : Union [Path , str ]) -> None :
463535 self .mod = mod
464536 self .file = file
537+ self ._is_type_stub = Path (file ).suffix .lower () == ".pyi"
538+ self ._stub_binding_lines : dict [str , int ] = {}
465539 self .defs = []
466540 self .refs = []
467541 self .cls = None
@@ -533,6 +607,17 @@ def __init__(self, mod: str, file: Union[Path, str]) -> None:
533607 def add_def (
534608 self , name : str , t : str , line : int , node : Optional [ast .AST ] = None , ** extra : Any
535609 ) -> None :
610+ # Omit declaration candidates rather than adding synthetic references:
611+ # .py and .pyi siblings share qualified names in the merged symbol map.
612+ if (
613+ self ._is_type_stub
614+ and not self .current_function_scope
615+ and t in {"class" , "function" , "method" }
616+ and node is not None
617+ and _is_stub_declaration (node )
618+ ):
619+ self ._stub_binding_lines [name ] = line
620+ return
536621 found = False
537622 for d in self .defs :
538623 if d .name == name :
@@ -584,13 +669,11 @@ def visit_Module(self, node: ast.Module) -> None:
584669
585670 def qual (self , name : str ) -> str :
586671 if name in self .alias :
587- if self .mod :
588- local_name = f"{ self .mod } .{ name } "
589- if any (d .name == local_name for d in self .defs ):
590- return local_name
591- else :
592- if any (d .name == name for d in self .defs ):
593- return name
672+ local_name = f"{ self .mod } .{ name } " if self .mod else name
673+ if local_name in self ._stub_binding_lines or any (
674+ d .name == local_name for d in self .defs
675+ ):
676+ return local_name
594677 return self .alias [name ]
595678
596679 if name in PYTHON_BUILTINS :
@@ -880,6 +963,11 @@ def _local_binding_shadows_alias(
880963 candidates .append ("." .join (filter (None , [self .mod , self .cls , name ])))
881964 candidates .append (f"{ self .mod } .{ name } " if self .mod else name )
882965
966+ if any (
967+ candidate in self ._stub_binding_lines and candidate != current_definition
968+ for candidate in candidates
969+ ):
970+ return True
883971 return any (
884972 d .name in candidates
885973 and d .name != current_definition
@@ -930,6 +1018,17 @@ def _alias_binding_is_active(
9301018 ),
9311019 default = - 1 ,
9321020 )
1021+ latest_local_line = max (
1022+ latest_local_line ,
1023+ max (
1024+ (
1025+ self ._stub_binding_lines .get (candidate , - 1 )
1026+ for candidate in candidates
1027+ if candidate != current_definition
1028+ ),
1029+ default = - 1 ,
1030+ ),
1031+ )
9331032 return alias_line > latest_local_line
9341033
9351034 def _is_numba_overload_decorator (
@@ -1693,8 +1792,13 @@ def visit_Assign(self, node: ast.Assign) -> None:
16931792 if isinstance (t , ast .Name ):
16941793 self .pattern_tracker .f_string_patterns [t .id ] = pattern
16951794
1795+ declaration_only = (
1796+ self ._is_type_stub
1797+ and not self .current_function_scope
1798+ and _is_stub_declaration (node )
1799+ )
16961800 for target in node .targets :
1697- self ._process_target_for_def (target )
1801+ self ._process_target_for_def (target , declaration_only = declaration_only )
16981802
16991803 if isinstance (node .value , ast .Dict ):
17001804 self ._track_dict_dispatch (node )
@@ -1760,7 +1864,14 @@ def _define(t):
17601864 if in_typeddict and is_class_body and is_annotation_only :
17611865 return
17621866
1763- self .add_def (var_name , "variable" , t .lineno )
1867+ if (
1868+ self ._is_type_stub
1869+ and not self .current_function_scope
1870+ and _is_stub_declaration (node )
1871+ ):
1872+ self ._stub_binding_lines [var_name ] = t .lineno
1873+ else :
1874+ self .add_def (var_name , "variable" , t .lineno )
17641875
17651876 if (
17661877 self ._dataclass_stack
@@ -1815,7 +1926,11 @@ def visit_AugAssign(self, node: ast.AugAssign) -> None:
18151926 self .visit (node .value )
18161927
18171928 def _process_target_for_def (
1818- self , target_node : ast .expr , _in_tuple_unpack : bool = False
1929+ self ,
1930+ target_node : ast .expr ,
1931+ _in_tuple_unpack : bool = False ,
1932+ * ,
1933+ declaration_only : bool = False ,
18191934 ) -> None :
18201935 if isinstance (target_node , ast .Name ):
18211936 name_simple = target_node .id
@@ -1827,7 +1942,10 @@ def _process_target_for_def(
18271942 return
18281943
18291944 var_name = self ._compute_variable_name (name_simple )
1830- self .add_def (var_name , "variable" , target_node .lineno )
1945+ if declaration_only :
1946+ self ._stub_binding_lines [var_name ] = target_node .lineno
1947+ else :
1948+ self .add_def (var_name , "variable" , target_node .lineno )
18311949
18321950 if self .current_function_scope and self .local_var_maps :
18331951 self .local_var_maps [- 1 ][name_simple ] = var_name
@@ -1841,7 +1959,9 @@ def _process_target_for_def(
18411959
18421960 elif isinstance (target_node , (ast .Tuple , ast .List )):
18431961 for elt in target_node .elts :
1844- self ._process_target_for_def (elt , _in_tuple_unpack = True )
1962+ self ._process_target_for_def (
1963+ elt , _in_tuple_unpack = True , declaration_only = declaration_only
1964+ )
18451965
18461966 def _finalize_dunder_all_exports (self , statements : list [ast .stmt ]) -> None :
18471967 export_names : set [str ] | None = None
0 commit comments