16
16
import traceback
17
17
from contextlib import redirect_stderr , redirect_stdout
18
18
19
- from typing import AbstractSet , Any , Callable , Dict , List , Optional , Sequence , Tuple
19
+ from typing import AbstractSet , Any , Callable , Dict , List , Optional , Sequence , Tuple , Set
20
20
from typing_extensions import Final
21
21
22
22
import mypy .build
23
23
import mypy .errors
24
24
import mypy .main
25
25
from mypy .find_sources import create_source_list , InvalidSourceList
26
- from mypy .server .update import FineGrainedBuildManager
26
+ from mypy .server .update import FineGrainedBuildManager , refresh_suppressed_submodules
27
27
from mypy .dmypy_util import receive
28
28
from mypy .ipc import IPCServer
29
29
from mypy .fscache import FileSystemCache
30
30
from mypy .fswatcher import FileSystemWatcher , FileData
31
- from mypy .modulefinder import BuildSource , compute_search_paths
31
+ from mypy .modulefinder import BuildSource , compute_search_paths , FindModuleCache , SearchPaths
32
32
from mypy .options import Options
33
33
from mypy .suggestions import SuggestionFailure , SuggestionEngine
34
34
from mypy .typestate import reset_global_state
@@ -361,8 +361,12 @@ def cmd_recheck(self,
361
361
t1 = time .time ()
362
362
manager = self .fine_grained_manager .manager
363
363
manager .log ("fine-grained increment: cmd_recheck: {:.3f}s" .format (t1 - t0 ))
364
- res = self .fine_grained_increment (sources , is_tty , terminal_width ,
365
- remove , update )
364
+ if not self .following_imports ():
365
+ messages = self .fine_grained_increment (sources , remove , update )
366
+ else :
367
+ assert remove is None and update is None
368
+ messages = self .fine_grained_increment_follow_imports (sources )
369
+ res = self .increment_output (messages , sources , is_tty , terminal_width )
366
370
self .fscache .flush ()
367
371
self .update_stats (res )
368
372
return res
@@ -377,7 +381,11 @@ def check(self, sources: List[BuildSource],
377
381
if not self .fine_grained_manager :
378
382
res = self .initialize_fine_grained (sources , is_tty , terminal_width )
379
383
else :
380
- res = self .fine_grained_increment (sources , is_tty , terminal_width )
384
+ if not self .following_imports ():
385
+ messages = self .fine_grained_increment (sources )
386
+ else :
387
+ messages = self .fine_grained_increment_follow_imports (sources )
388
+ res = self .increment_output (messages , sources , is_tty , terminal_width )
381
389
self .fscache .flush ()
382
390
self .update_stats (res )
383
391
return res
@@ -389,6 +397,11 @@ def update_stats(self, res: Dict[str, Any]) -> None:
389
397
res ['stats' ] = manager .stats
390
398
manager .stats = {}
391
399
400
+ def following_imports (self ) -> bool :
401
+ """Are we following imports?"""
402
+ # TODO: What about silent?
403
+ return self .options .follow_imports == 'normal'
404
+
392
405
def initialize_fine_grained (self , sources : List [BuildSource ],
393
406
is_tty : bool , terminal_width : int ) -> Dict [str , Any ]:
394
407
self .fswatcher = FileSystemWatcher (self .fscache )
@@ -408,6 +421,11 @@ def initialize_fine_grained(self, sources: List[BuildSource],
408
421
return {'out' : out , 'err' : err , 'status' : 2 }
409
422
messages = result .errors
410
423
self .fine_grained_manager = FineGrainedBuildManager (result )
424
+
425
+ if self .following_imports ():
426
+ sources = find_all_sources_in_build (self .fine_grained_manager .graph )
427
+ self .update_sources (sources )
428
+
411
429
self .previous_sources = sources
412
430
413
431
# If we are using the fine-grained cache, build hasn't actually done
@@ -436,13 +454,19 @@ def initialize_fine_grained(self, sources: List[BuildSource],
436
454
t3 = time .time ()
437
455
# Run an update
438
456
messages = self .fine_grained_manager .update (changed , removed )
457
+
458
+ if self .following_imports ():
459
+ # We need to do another update to any new files found by following imports.
460
+ messages = self .fine_grained_increment_follow_imports (sources )
461
+
439
462
t4 = time .time ()
440
463
self .fine_grained_manager .manager .add_stats (
441
464
update_sources_time = t1 - t0 ,
442
465
build_time = t2 - t1 ,
443
466
find_changes_time = t3 - t2 ,
444
467
fg_update_time = t4 - t3 ,
445
468
files_changed = len (removed ) + len (changed ))
469
+
446
470
else :
447
471
# Stores the initial state of sources as a side effect.
448
472
self .fswatcher .find_changed ()
@@ -457,11 +481,19 @@ def initialize_fine_grained(self, sources: List[BuildSource],
457
481
458
482
def fine_grained_increment (self ,
459
483
sources : List [BuildSource ],
460
- is_tty : bool ,
461
- terminal_width : int ,
462
484
remove : Optional [List [str ]] = None ,
463
485
update : Optional [List [str ]] = None ,
464
- ) -> Dict [str , Any ]:
486
+ ) -> List [str ]:
487
+ """Perform a fine-grained type checking increment.
488
+
489
+ If remove and update are None, determine changed paths by using
490
+ fswatcher. Otherwise, assume that only these files have changes.
491
+
492
+ Args:
493
+ sources: sources passed on the command line
494
+ remove: paths of files that have been removed
495
+ update: paths of files that have been changed or created
496
+ """
465
497
assert self .fine_grained_manager is not None
466
498
manager = self .fine_grained_manager .manager
467
499
@@ -486,8 +518,180 @@ def fine_grained_increment(self,
486
518
fg_update_time = t2 - t1 ,
487
519
files_changed = len (removed ) + len (changed ))
488
520
489
- status = 1 if messages else 0
490
521
self .previous_sources = sources
522
+ return messages
523
+
524
+ def fine_grained_increment_follow_imports (self , sources : List [BuildSource ]) -> List [str ]:
525
+ """Like fine_grained_increment, but follow imports."""
526
+
527
+ # TODO:
528
+ # - file events
529
+ # - search path updates
530
+ # - logging
531
+
532
+ changed_paths = self .fswatcher .find_changed ()
533
+
534
+ assert self .fine_grained_manager is not None
535
+ fine_grained_manager = self .fine_grained_manager
536
+ graph = fine_grained_manager .graph
537
+ manager = fine_grained_manager .manager
538
+
539
+ orig_modules = list (graph .keys ())
540
+
541
+ # TODO: Are the differences from fine_grained_increment(), such as
542
+ # updating sources after finding changed, necessary?
543
+ self .update_sources (sources )
544
+
545
+ sources_set = {source .module for source in sources }
546
+
547
+ # Find changed modules reachable from roots (or in roots) already in graph.
548
+ seen = set () # type: Set[str]
549
+ changed , removed , new_files = self .follow_imports (
550
+ sources , graph , seen , changed_paths , sources_set
551
+ )
552
+ sources .extend (new_files )
553
+
554
+ # Process changes directly reachable from roots.
555
+ messages = fine_grained_manager .update (changed , removed )
556
+
557
+ # Follow deps from changed modules (still within graph).
558
+ worklist = changed [:]
559
+ while worklist :
560
+ module = worklist .pop ()
561
+ if module [0 ] not in graph :
562
+ continue
563
+ sources2 = self .direct_imports (module , graph )
564
+ changed , removed , new_files = self .follow_imports (
565
+ sources2 , graph , seen , changed_paths , sources_set
566
+ )
567
+ sources .extend (new_files )
568
+ self .update_sources (new_files )
569
+ messages = fine_grained_manager .update (changed , removed )
570
+ # TODO: Removed?
571
+ worklist .extend (changed )
572
+
573
+ for module_id , state in graph .items ():
574
+ refresh_suppressed_submodules (module_id , state .path , fine_grained_manager .deps , graph ,
575
+ self .fscache )
576
+
577
+ # There may be new files that became available, currently treated as
578
+ # suppressed imports. Process them.
579
+ seen_suppressed = set () # type: Set[str]
580
+ while True :
581
+ # TODO: Merge seen and seen_suppressed?
582
+ new_unsuppressed , seen_suppressed = self .find_added_suppressed (
583
+ graph , seen_suppressed , manager .search_paths
584
+ )
585
+ if not new_unsuppressed :
586
+ break
587
+ new_files = [BuildSource (mod [1 ], mod [0 ]) for mod in new_unsuppressed ]
588
+ sources .extend (new_files )
589
+ self .update_sources (new_files )
590
+ messages = fine_grained_manager .update (new_unsuppressed , [])
591
+
592
+ for module_id , path in new_unsuppressed :
593
+ refresh_suppressed_submodules (module_id , path , fine_grained_manager .deps , graph ,
594
+ self .fscache )
595
+
596
+ # Find all original modules in graph that were not reached -- they are deleted.
597
+ to_delete = []
598
+ for module_id in orig_modules :
599
+ if module_id not in graph :
600
+ continue
601
+ if module_id not in seen and module_id not in seen_suppressed :
602
+ module_path = graph [module_id ].path
603
+ assert module_path is not None
604
+ to_delete .append ((module_id , module_path ))
605
+ if to_delete :
606
+ messages = fine_grained_manager .update ([], to_delete )
607
+
608
+ fix_module_deps (graph )
609
+
610
+ # Store current file state as side effect
611
+ self .fswatcher .find_changed ()
612
+
613
+ self .previous_sources = find_all_sources_in_build (graph )
614
+ self .update_sources (self .previous_sources )
615
+ return messages
616
+
617
+ def follow_imports (self ,
618
+ sources : List [BuildSource ],
619
+ graph : mypy .build .Graph ,
620
+ seen : Set [str ],
621
+ changed_paths : AbstractSet [str ],
622
+ sources_set : Set [str ]) -> Tuple [List [Tuple [str , str ]],
623
+ List [Tuple [str , str ]],
624
+ List [BuildSource ]]:
625
+ """Follow imports within graph from given sources.
626
+
627
+ Args:
628
+ sources: roots of modules to search
629
+ graph: module graph to use for the search
630
+ seen: modules we've seen before that won't be visited (mutated here!)
631
+ changed_paths: which paths have changed (stop search here and return any found)
632
+ sources_set: set of sources (TODO: relationship with seen)
633
+
634
+ Return (reachable changed modules, removed modules, updated file list).
635
+ """
636
+ changed = []
637
+ new_files = []
638
+ worklist = sources [:]
639
+ seen .update (source .module for source in worklist )
640
+ while worklist :
641
+ nxt = worklist .pop ()
642
+ if nxt .module not in sources_set :
643
+ sources_set .add (nxt .module )
644
+ new_files .append (nxt )
645
+ if nxt .path in changed_paths :
646
+ assert nxt .path is not None # TODO
647
+ changed .append ((nxt .module , nxt .path ))
648
+ elif nxt .module in graph :
649
+ state = graph [nxt .module ]
650
+ for dep in state .dependencies :
651
+ if dep not in seen :
652
+ seen .add (dep )
653
+ worklist .append (BuildSource (graph [dep ].path ,
654
+ graph [dep ].id ))
655
+ return changed , [], new_files
656
+
657
+ def direct_imports (self ,
658
+ module : Tuple [str , str ],
659
+ graph : mypy .build .Graph ) -> List [BuildSource ]:
660
+ """Return the direct imports of module not included in seen."""
661
+ state = graph [module [0 ]]
662
+ return [BuildSource (graph [dep ].path , dep )
663
+ for dep in state .dependencies ]
664
+
665
+ def find_added_suppressed (self ,
666
+ graph : mypy .build .Graph ,
667
+ seen : Set [str ],
668
+ search_paths : SearchPaths ) -> Tuple [List [Tuple [str , str ]], Set [str ]]:
669
+ """Find suppressed modules that have been added (and not included in seen)."""
670
+ all_suppressed = set ()
671
+ for module , state in graph .items ():
672
+ all_suppressed |= state .suppressed_set
673
+
674
+ # TODO: Namespace packages
675
+ # TODO: Handle seen?
676
+
677
+ finder = FindModuleCache (search_paths , self .fscache , self .options )
678
+
679
+ found = []
680
+
681
+ for module in all_suppressed :
682
+ result = finder .find_module (module )
683
+ if isinstance (result , str ) and module not in seen :
684
+ found .append ((module , result ))
685
+ seen .add (module )
686
+
687
+ return found , seen
688
+
689
+ def increment_output (self ,
690
+ messages : List [str ],
691
+ sources : List [BuildSource ],
692
+ is_tty : bool ,
693
+ terminal_width : int ) -> Dict [str , Any ]:
694
+ status = 1 if messages else 0
491
695
messages = self .pretty_messages (messages , len (sources ), is_tty , terminal_width )
492
696
return {'out' : '' .join (s + '\n ' for s in messages ), 'err' : '' , 'status' : status }
493
697
@@ -622,3 +826,30 @@ def get_meminfo() -> Dict[str, Any]:
622
826
factor = 1024 # Linux
623
827
res ['memory_maxrss_mib' ] = rusage .ru_maxrss * factor / MiB
624
828
return res
829
+
830
+
831
+ def find_all_sources_in_build (graph : mypy .build .Graph ) -> List [BuildSource ]:
832
+ result = []
833
+ for module , state in graph .items ():
834
+ result .append (BuildSource (state .path , module ))
835
+ return result
836
+
837
+
838
+ def fix_module_deps (graph : mypy .build .Graph ) -> None :
839
+ """After an incremental update, update module dependencies to reflect the new state.
840
+
841
+ This can make some suppressed dependencies non-suppressed, and vice versa (if modules
842
+ have been added to or removed from the build).
843
+ """
844
+ for module , state in graph .items ():
845
+ new_suppressed = []
846
+ new_dependencies = []
847
+ for dep in state .dependencies + state .suppressed :
848
+ if dep in graph :
849
+ new_dependencies .append (dep )
850
+ else :
851
+ new_suppressed .append (dep )
852
+ state .dependencies = new_dependencies
853
+ state .dependencies_set = set (new_dependencies )
854
+ state .suppressed = new_suppressed
855
+ state .suppressed_set = set (new_suppressed )
0 commit comments