5252from mypy .version import __version__
5353from mypy .plugin import Plugin , DefaultPlugin , ChainedPlugin
5454from mypy .defaults import PYTHON3_VERSION_MIN
55+ from mypy .server .deps import get_dependencies
56+
57+
58+ # Switch to True to produce debug output related to fine-grained incremental
59+ # mode only that is useful during development. This produces only a subset of
60+ # output compared to --verbose output. We use a global flag to enable this so
61+ # that it's easy to enable this when running tests.
62+ DEBUG_FINE_GRAINED = False
5563
5664
5765PYTHON_EXTENSIONS = ['.pyi' , '.py' ]
@@ -392,6 +400,7 @@ def default_lib_path(data_dir: str,
392400 ('child_modules' , List [str ]), # all submodules of the given module
393401 ('options' , Optional [Dict [str , object ]]), # build options
394402 ('dep_prios' , List [int ]),
403+ ('dep_lines' , List [int ]),
395404 ('interface_hash' , str ), # hash representing the public interface
396405 ('version_id' , str ), # mypy version for cache invalidation
397406 ('ignore_all' , bool ), # if errors were ignored
@@ -417,6 +426,7 @@ def cache_meta_from_dict(meta: Dict[str, Any], data_json: str) -> CacheMeta:
417426 meta .get ('child_modules' , []),
418427 meta .get ('options' ),
419428 meta .get ('dep_prios' , []),
429+ meta .get ('dep_lines' , []),
420430 meta .get ('interface_hash' , '' ),
421431 meta .get ('version_id' , sentinel ),
422432 meta .get ('ignore_all' , True ),
@@ -731,6 +741,17 @@ def log(self, *message: str) -> None:
731741 print (file = sys .stderr )
732742 sys .stderr .flush ()
733743
744+ def log_fine_grained (self , * message : str ) -> None :
745+ if self .options .verbosity >= 1 :
746+ self .log ('fine-grained:' , * message )
747+ elif DEBUG_FINE_GRAINED :
748+ # Output log in a simplified format that is quick to browse.
749+ if message :
750+ print (* message , file = sys .stderr )
751+ else :
752+ print (file = sys .stderr )
753+ sys .stderr .flush ()
754+
734755 def trace (self , * message : str ) -> None :
735756 if self .options .verbosity >= 2 :
736757 print ('TRACE:' , * message , file = sys .stderr )
@@ -1039,7 +1060,8 @@ def find_cache_meta(id: str, path: str, manager: BuildManager) -> Optional[Cache
10391060 # Ignore cache if generated by an older mypy version.
10401061 if ((m .version_id != manager .version_id and not manager .options .skip_version_check )
10411062 or m .options is None
1042- or len (m .dependencies ) != len (m .dep_prios )):
1063+ or len (m .dependencies ) != len (m .dep_prios )
1064+ or len (m .dependencies ) != len (m .dep_lines )):
10431065 manager .log ('Metadata abandoned for {}: new attributes are missing' .format (id ))
10441066 return None
10451067
@@ -1127,6 +1149,17 @@ def validate_meta(meta: Optional[CacheMeta], id: str, path: Optional[str],
11271149 if not stat .S_ISREG (st .st_mode ):
11281150 manager .log ('Metadata abandoned for {}: file {} does not exist' .format (id , path ))
11291151 return None
1152+
1153+ # When we are using a fine-grained cache, we want our initial
1154+ # build() to load all of the cache information and then do a
1155+ # fine-grained incremental update to catch anything that has
1156+ # changed since the cache was generated. We *don't* want to do a
1157+ # coarse-grained incremental rebuild, so we accept the cache
1158+ # metadata even if it doesn't match the source file.
1159+ if manager .options .use_fine_grained_cache :
1160+ manager .log ('Using potentially stale metadata for {}' .format (id ))
1161+ return meta
1162+
11301163 size = st .st_size
11311164 if size != meta .size :
11321165 manager .log ('Metadata abandoned for {}: file {} has different size' .format (id , path ))
@@ -1156,6 +1189,7 @@ def validate_meta(meta: Optional[CacheMeta], id: str, path: Optional[str],
11561189 'options' : (manager .options .clone_for_module (id )
11571190 .select_options_affecting_cache ()),
11581191 'dep_prios' : meta .dep_prios ,
1192+ 'dep_lines' : meta .dep_lines ,
11591193 'interface_hash' : meta .interface_hash ,
11601194 'version_id' : manager .version_id ,
11611195 'ignore_all' : meta .ignore_all ,
@@ -1183,8 +1217,9 @@ def compute_hash(text: str) -> str:
11831217
11841218
11851219def write_cache (id : str , path : str , tree : MypyFile ,
1220+ serialized_fine_grained_deps : Dict [str , List [str ]],
11861221 dependencies : List [str ], suppressed : List [str ],
1187- child_modules : List [str ], dep_prios : List [int ],
1222+ child_modules : List [str ], dep_prios : List [int ], dep_lines : List [ int ],
11881223 old_interface_hash : str , source_hash : str ,
11891224 ignore_all : bool , manager : BuildManager ) -> Tuple [str , Optional [CacheMeta ]]:
11901225 """Write cache files for a module.
@@ -1201,6 +1236,7 @@ def write_cache(id: str, path: str, tree: MypyFile,
12011236 suppressed: module IDs which were suppressed as dependencies
12021237 child_modules: module IDs which are this package's direct submodules
12031238 dep_prios: priorities (parallel array to dependencies)
1239+ dep_lines: import line locations (parallel array to dependencies)
12041240 old_interface_hash: the hash from the previous version of the data cache file
12051241 source_hash: the hash of the source code
12061242 ignore_all: the ignore_all flag for this module
@@ -1221,7 +1257,9 @@ def write_cache(id: str, path: str, tree: MypyFile,
12211257 assert os .path .dirname (meta_json ) == parent
12221258
12231259 # Serialize data and analyze interface
1224- data = tree .serialize ()
1260+ data = {'tree' : tree .serialize (),
1261+ 'fine_grained_deps' : serialized_fine_grained_deps ,
1262+ }
12251263 if manager .options .debug_cache :
12261264 data_str = json .dumps (data , indent = 2 , sort_keys = True )
12271265 else :
@@ -1282,6 +1320,7 @@ def write_cache(id: str, path: str, tree: MypyFile,
12821320 'child_modules' : child_modules ,
12831321 'options' : options .select_options_affecting_cache (),
12841322 'dep_prios' : dep_prios ,
1323+ 'dep_lines' : dep_lines ,
12851324 'interface_hash' : interface_hash ,
12861325 'version_id' : manager .version_id ,
12871326 'ignore_all' : ignore_all ,
@@ -1523,6 +1562,8 @@ class State:
15231562 # Whether the module has an error or any of its dependencies have one.
15241563 transitive_error = False
15251564
1565+ fine_grained_deps = None # type: Dict[str, Set[str]]
1566+
15261567 # Type checker used for checking this file. Use type_checker() for
15271568 # access and to construct this on demand.
15281569 _type_checker = None # type: Optional[TypeChecker]
@@ -1551,6 +1592,7 @@ def __init__(self,
15511592 self .id = id or '__main__'
15521593 self .options = manager .options .clone_for_module (self .id )
15531594 self ._type_checker = None
1595+ self .fine_grained_deps = {}
15541596 if not path and source is None :
15551597 assert id is not None
15561598 file_id = id
@@ -1626,8 +1668,10 @@ def __init__(self,
16261668 assert len (self .meta .dependencies ) == len (self .meta .dep_prios )
16271669 self .priorities = {id : pri
16281670 for id , pri in zip (self .meta .dependencies , self .meta .dep_prios )}
1671+ assert len (self .meta .dependencies ) == len (self .meta .dep_lines )
1672+ self .dep_line_map = {id : line
1673+ for id , line in zip (self .meta .dependencies , self .meta .dep_lines )}
16291674 self .child_modules = set (self .meta .child_modules )
1630- self .dep_line_map = {}
16311675 else :
16321676 # Parse the file (and then some) to get the dependencies.
16331677 self .parse_file ()
@@ -1734,7 +1778,9 @@ def load_tree(self) -> None:
17341778 with open (self .meta .data_json ) as f :
17351779 data = json .load (f )
17361780 # TODO: Assert data file wasn't changed.
1737- self .tree = MypyFile .deserialize (data )
1781+ self .tree = MypyFile .deserialize (data ['tree' ])
1782+ self .fine_grained_deps = {k : set (v ) for k , v in data ['fine_grained_deps' ].items ()}
1783+
17381784 self .manager .modules [self .id ] = self .tree
17391785 self .manager .add_stats (fresh_trees = 1 )
17401786
@@ -1977,6 +2023,19 @@ def _patch_indirect_dependencies(self,
19772023 elif dep not in self .suppressed and dep in self .manager .missing_modules :
19782024 self .suppressed .append (dep )
19792025
2026+ def compute_fine_grained_deps (self ) -> None :
2027+ assert self .tree is not None
2028+ if '/typeshed/' in self .xpath or self .xpath .startswith ('typeshed/' ):
2029+ # We don't track changes to typeshed -- the assumption is that they are only changed
2030+ # as part of mypy updates, which will invalidate everything anyway.
2031+ #
2032+ # TODO: Not a reliable test, as we could have a package named typeshed.
2033+ # TODO: Consider relaxing this -- maybe allow some typeshed changes to be tracked.
2034+ return
2035+ self .fine_grained_deps = get_dependencies (target = self .tree ,
2036+ type_map = self .type_map (),
2037+ python_version = self .options .python_version )
2038+
19802039 def valid_references (self ) -> Set [str ]:
19812040 assert self .ancestors is not None
19822041 valid_refs = set (self .dependencies + self .suppressed + self .ancestors )
@@ -2001,10 +2060,12 @@ def write_cache(self) -> None:
20012060 self .mark_interface_stale (on_errors = True )
20022061 return
20032062 dep_prios = self .dependency_priorities ()
2063+ dep_lines = self .dependency_lines ()
20042064 new_interface_hash , self .meta = write_cache (
20052065 self .id , self .path , self .tree ,
2066+ {k : list (v ) for k , v in self .fine_grained_deps .items ()},
20062067 list (self .dependencies ), list (self .suppressed ), list (self .child_modules ),
2007- dep_prios , self .interface_hash , self .source_hash , self .ignore_all ,
2068+ dep_prios , dep_lines , self .interface_hash , self .source_hash , self .ignore_all ,
20082069 self .manager )
20092070 if new_interface_hash == self .interface_hash :
20102071 self .manager .log ("Cached module {} has same interface" .format (self .id ))
@@ -2016,6 +2077,9 @@ def write_cache(self) -> None:
20162077 def dependency_priorities (self ) -> List [int ]:
20172078 return [self .priorities .get (dep , PRI_HIGH ) for dep in self .dependencies ]
20182079
2080+ def dependency_lines (self ) -> List [int ]:
2081+ return [self .dep_line_map .get (dep , 1 ) for dep in self .dependencies ]
2082+
20192083 def generate_unused_ignore_notes (self ) -> None :
20202084 if self .options .warn_unused_ignores :
20212085 self .manager .errors .generate_unused_ignore_notes (self .xpath )
@@ -2348,6 +2412,14 @@ def process_graph(graph: Graph, manager: BuildManager) -> None:
23482412 manager .log ("Processing SCC of size %d (%s) as %s" % (size , scc_str , fresh_msg ))
23492413 process_stale_scc (graph , scc , manager )
23502414
2415+ # If we are running in fine-grained incremental mode with caching,
2416+ # we always process fresh SCCs so that we have all of the symbol
2417+ # tables and fine-grained dependencies available.
2418+ if manager .options .use_fine_grained_cache :
2419+ for prev_scc in fresh_scc_queue :
2420+ process_fresh_scc (graph , prev_scc , manager )
2421+ fresh_scc_queue = []
2422+
23512423 sccs_left = len (fresh_scc_queue )
23522424 nodes_left = sum (len (scc ) for scc in fresh_scc_queue )
23532425 manager .add_stats (sccs_left = sccs_left , nodes_left = nodes_left )
@@ -2534,6 +2606,8 @@ def process_stale_scc(graph: Graph, scc: List[str], manager: BuildManager) -> No
25342606 graph [id ].transitive_error = True
25352607 for id in stale :
25362608 graph [id ].finish_passes ()
2609+ if manager .options .cache_fine_grained or manager .options .fine_grained_incremental :
2610+ graph [id ].compute_fine_grained_deps ()
25372611 graph [id ].generate_unused_ignore_notes ()
25382612 manager .flush_errors (manager .errors .file_messages (graph [id ].xpath ), False )
25392613 graph [id ].write_cache ()
0 commit comments