42
42
reverse_builtin_aliases ,
43
43
)
44
44
from mypy .server .update import FineGrainedBuildManager
45
- from mypy .util import module_prefix , split_target
45
+ from mypy .util import split_target
46
+ from mypy .find_sources import SourceFinder , InvalidSourceList
47
+ from mypy .modulefinder import PYTHON_EXTENSIONS
46
48
from mypy .plugin import Plugin , FunctionContext , MethodContext
47
49
from mypy .traverser import TraverserVisitor
48
50
from mypy .checkexpr import has_any_type
@@ -162,6 +164,7 @@ def __init__(self, fgmanager: FineGrainedBuildManager,
162
164
self .manager = fgmanager .manager
163
165
self .plugin = self .manager .plugin
164
166
self .graph = fgmanager .graph
167
+ self .finder = SourceFinder (self .manager .fscache )
165
168
166
169
self .give_json = json
167
170
self .no_errors = no_errors
@@ -174,19 +177,21 @@ def __init__(self, fgmanager: FineGrainedBuildManager,
174
177
175
178
def suggest (self , function : str ) -> str :
176
179
"""Suggest an inferred type for function."""
177
- with self .restore_after (function ):
180
+ mod , func_name , node = self .find_node (function )
181
+
182
+ with self .restore_after (mod ):
178
183
with self .with_export_types ():
179
- suggestion = self .get_suggestion (function )
184
+ suggestion = self .get_suggestion (mod , node )
180
185
181
186
if self .give_json :
182
- return self .json_suggestion (function , suggestion )
187
+ return self .json_suggestion (mod , func_name , node , suggestion )
183
188
else :
184
189
return self .format_signature (suggestion )
185
190
186
191
def suggest_callsites (self , function : str ) -> str :
187
192
"""Find a list of call sites of function."""
188
- with self .restore_after (function ):
189
- _ , _ , node = self .find_node ( function )
193
+ mod , _ , node = self .find_node (function )
194
+ with self .restore_after ( mod ):
190
195
callsites , _ = self .get_callsites (node )
191
196
192
197
return '\n ' .join (dedup (
@@ -195,17 +200,15 @@ def suggest_callsites(self, function: str) -> str:
195
200
))
196
201
197
202
@contextmanager
198
- def restore_after (self , target : str ) -> Iterator [None ]:
203
+ def restore_after (self , module : str ) -> Iterator [None ]:
199
204
"""Context manager that reloads a module after executing the body.
200
205
201
206
This should undo any damage done to the module state while mucking around.
202
207
"""
203
208
try :
204
209
yield
205
210
finally :
206
- module = module_prefix (self .graph , target )
207
- if module :
208
- self .reload (self .graph [module ])
211
+ self .reload (self .graph [module ])
209
212
210
213
@contextmanager
211
214
def with_export_types (self ) -> Iterator [None ]:
@@ -321,13 +324,12 @@ def find_best(self, func: FuncDef, guesses: List[CallableType]) -> Tuple[Callabl
321
324
key = lambda s : (count_errors (errors [s ]), self .score_callable (s )))
322
325
return best , count_errors (errors [best ])
323
326
324
- def get_suggestion (self , function : str ) -> PyAnnotateSignature :
327
+ def get_suggestion (self , mod : str , node : FuncDef ) -> PyAnnotateSignature :
325
328
"""Compute a suggestion for a function.
326
329
327
330
Return the type and whether the first argument should be ignored.
328
331
"""
329
332
graph = self .graph
330
- mod , _ , node = self .find_node (function )
331
333
callsites , orig_errors = self .get_callsites (node )
332
334
333
335
if self .no_errors and orig_errors :
@@ -386,15 +388,49 @@ def format_args(self,
386
388
return "(%s)" % (", " .join (args ))
387
389
388
390
def find_node (self , key : str ) -> Tuple [str , str , FuncDef ]:
389
- """From a target name, return module/target names and the func def."""
391
+ """From a target name, return module/target names and the func def.
392
+
393
+ The 'key' argument can be in one of two formats:
394
+ * As the function full name, e.g., package.module.Cls.method
395
+ * As the function location as file and line separated by column,
396
+ e.g., path/to/file.py:42
397
+ """
390
398
# TODO: Also return OverloadedFuncDef -- currently these are ignored.
391
- graph = self .fgmanager .graph
392
- target = split_target (graph , key )
393
- if not target :
394
- raise SuggestionFailure ("Cannot find module for %s" % (key ,))
395
- modname , tail = target
399
+ node = None # type: Optional[SymbolNode]
400
+ if ':' in key :
401
+ if key .count (':' ) > 1 :
402
+ raise SuggestionFailure (
403
+ 'Malformed location for function: {}. Must be either'
404
+ ' package.module.Class.method or path/to/file.py:line' .format (key ))
405
+ file , line = key .split (':' )
406
+ if not line .isdigit ():
407
+ raise SuggestionFailure ('Line number must be a number. Got {}' .format (line ))
408
+ line_number = int (line )
409
+ modname , node = self .find_node_by_file_and_line (file , line_number )
410
+ tail = node .fullname ()[len (modname ) + 1 :] # add one to account for '.'
411
+ else :
412
+ target = split_target (self .fgmanager .graph , key )
413
+ if not target :
414
+ raise SuggestionFailure ("Cannot find module for %s" % (key ,))
415
+ modname , tail = target
416
+ node = self .find_node_by_module_and_name (modname , tail )
396
417
397
- tree = self .ensure_loaded (graph [modname ])
418
+ if isinstance (node , Decorator ):
419
+ node = self .extract_from_decorator (node )
420
+ if not node :
421
+ raise SuggestionFailure ("Object %s is a decorator we can't handle" % key )
422
+
423
+ if not isinstance (node , FuncDef ):
424
+ raise SuggestionFailure ("Object %s is not a function" % key )
425
+
426
+ return modname , tail , node
427
+
428
+ def find_node_by_module_and_name (self , modname : str , tail : str ) -> Optional [SymbolNode ]:
429
+ """Find symbol node by module id and qualified name.
430
+
431
+ Raise SuggestionFailure if can't find one.
432
+ """
433
+ tree = self .ensure_loaded (self .fgmanager .graph [modname ])
398
434
399
435
# N.B. This is reimplemented from update's lookup_target
400
436
# basically just to produce better error messages.
@@ -416,18 +452,38 @@ def find_node(self, key: str) -> Tuple[str, str, FuncDef]:
416
452
# Look for the actual function/method
417
453
funcname = components [- 1 ]
418
454
if funcname not in names :
455
+ key = modname + '.' + tail
419
456
raise SuggestionFailure ("Unknown %s %s" %
420
457
("method" if len (components ) > 1 else "function" , key ))
421
- node = names [funcname ].node
422
- if isinstance (node , Decorator ):
423
- node = self .extract_from_decorator (node )
424
- if not node :
425
- raise SuggestionFailure ("Object %s is a decorator we can't handle" % key )
458
+ return names [funcname ].node
426
459
427
- if not isinstance ( node , FuncDef ) :
428
- raise SuggestionFailure ( "Object %s is not a function" % key )
460
+ def find_node_by_file_and_line ( self , file : str , line : int ) -> Tuple [ str , SymbolNode ] :
461
+ """Find symbol node by path to file and line number.
429
462
430
- return (modname , tail , node )
463
+ Return module id and the node found. Raise SuggestionFailure if can't find one.
464
+ """
465
+ if not any (file .endswith (ext ) for ext in PYTHON_EXTENSIONS ):
466
+ raise SuggestionFailure ('Source file is not a Python file' )
467
+ try :
468
+ modname , _ = self .finder .crawl_up (os .path .normpath (file ))
469
+ except InvalidSourceList :
470
+ raise SuggestionFailure ('Invalid source file name: ' + file )
471
+ if modname not in self .graph :
472
+ raise SuggestionFailure ('Unknown module: ' + modname )
473
+ # We must be sure about any edits in this file as this might affect the line numbers.
474
+ tree = self .ensure_loaded (self .fgmanager .graph [modname ], force = True )
475
+ node = None # type: Optional[SymbolNode]
476
+ for _ , sym , _ in tree .local_definitions ():
477
+ if isinstance (sym .node , FuncDef ) and sym .node .line == line :
478
+ node = sym .node
479
+ break
480
+ elif isinstance (sym .node , Decorator ) and sym .node .func .line == line :
481
+ node = sym .node
482
+ break
483
+ # TODO: add support for OverloadedFuncDef.
484
+ if not node :
485
+ raise SuggestionFailure ('Cannot find a function at line {}' .format (line ))
486
+ return modname , node
431
487
432
488
def extract_from_decorator (self , node : Decorator ) -> Optional [FuncDef ]:
433
489
for dec in node .decorators :
@@ -483,19 +539,19 @@ def reload(self, state: State, check_errors: bool = False) -> List[str]:
483
539
raise SuggestionFailure ("Error while trying to load %s" % state .id )
484
540
return res
485
541
486
- def ensure_loaded (self , state : State ) -> MypyFile :
542
+ def ensure_loaded (self , state : State , force : bool = False ) -> MypyFile :
487
543
"""Make sure that the module represented by state is fully loaded."""
488
- if not state .tree or state .tree .is_cache_skeleton :
544
+ if not state .tree or state .tree .is_cache_skeleton or force :
489
545
self .reload (state , check_errors = True )
490
546
assert state .tree is not None
491
547
return state .tree
492
548
493
549
def builtin_type (self , s : str ) -> Instance :
494
550
return self .manager .semantic_analyzer .builtin_type (s )
495
551
496
- def json_suggestion (self , function : str , suggestion : PyAnnotateSignature ) -> str :
552
+ def json_suggestion (self , mod : str , func_name : str , node : FuncDef ,
553
+ suggestion : PyAnnotateSignature ) -> str :
497
554
"""Produce a json blob for a suggestion suitable for application by pyannotate."""
498
- mod , func_name , node = self .find_node (function )
499
555
# pyannotate irritatingly drops class names for class and static methods
500
556
if node .is_class or node .is_static :
501
557
func_name = func_name .split ('.' , 1 )[- 1 ]
0 commit comments