52
52
from mypy .version import __version__
53
53
from mypy .plugin import Plugin , DefaultPlugin , ChainedPlugin
54
54
from 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
55
63
56
64
57
65
PYTHON_EXTENSIONS = ['.pyi' , '.py' ]
@@ -392,6 +400,7 @@ def default_lib_path(data_dir: str,
392
400
('child_modules' , List [str ]), # all submodules of the given module
393
401
('options' , Optional [Dict [str , object ]]), # build options
394
402
('dep_prios' , List [int ]),
403
+ ('dep_lines' , List [int ]),
395
404
('interface_hash' , str ), # hash representing the public interface
396
405
('version_id' , str ), # mypy version for cache invalidation
397
406
('ignore_all' , bool ), # if errors were ignored
@@ -417,6 +426,7 @@ def cache_meta_from_dict(meta: Dict[str, Any], data_json: str) -> CacheMeta:
417
426
meta .get ('child_modules' , []),
418
427
meta .get ('options' ),
419
428
meta .get ('dep_prios' , []),
429
+ meta .get ('dep_lines' , []),
420
430
meta .get ('interface_hash' , '' ),
421
431
meta .get ('version_id' , sentinel ),
422
432
meta .get ('ignore_all' , True ),
@@ -731,6 +741,17 @@ def log(self, *message: str) -> None:
731
741
print (file = sys .stderr )
732
742
sys .stderr .flush ()
733
743
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
+
734
755
def trace (self , * message : str ) -> None :
735
756
if self .options .verbosity >= 2 :
736
757
print ('TRACE:' , * message , file = sys .stderr )
@@ -1039,7 +1060,8 @@ def find_cache_meta(id: str, path: str, manager: BuildManager) -> Optional[Cache
1039
1060
# Ignore cache if generated by an older mypy version.
1040
1061
if ((m .version_id != manager .version_id and not manager .options .skip_version_check )
1041
1062
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 )):
1043
1065
manager .log ('Metadata abandoned for {}: new attributes are missing' .format (id ))
1044
1066
return None
1045
1067
@@ -1127,6 +1149,17 @@ def validate_meta(meta: Optional[CacheMeta], id: str, path: Optional[str],
1127
1149
if not stat .S_ISREG (st .st_mode ):
1128
1150
manager .log ('Metadata abandoned for {}: file {} does not exist' .format (id , path ))
1129
1151
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
+
1130
1163
size = st .st_size
1131
1164
if size != meta .size :
1132
1165
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],
1156
1189
'options' : (manager .options .clone_for_module (id )
1157
1190
.select_options_affecting_cache ()),
1158
1191
'dep_prios' : meta .dep_prios ,
1192
+ 'dep_lines' : meta .dep_lines ,
1159
1193
'interface_hash' : meta .interface_hash ,
1160
1194
'version_id' : manager .version_id ,
1161
1195
'ignore_all' : meta .ignore_all ,
@@ -1183,8 +1217,9 @@ def compute_hash(text: str) -> str:
1183
1217
1184
1218
1185
1219
def write_cache (id : str , path : str , tree : MypyFile ,
1220
+ serialized_fine_grained_deps : Dict [str , List [str ]],
1186
1221
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 ],
1188
1223
old_interface_hash : str , source_hash : str ,
1189
1224
ignore_all : bool , manager : BuildManager ) -> Tuple [str , Optional [CacheMeta ]]:
1190
1225
"""Write cache files for a module.
@@ -1201,6 +1236,7 @@ def write_cache(id: str, path: str, tree: MypyFile,
1201
1236
suppressed: module IDs which were suppressed as dependencies
1202
1237
child_modules: module IDs which are this package's direct submodules
1203
1238
dep_prios: priorities (parallel array to dependencies)
1239
+ dep_lines: import line locations (parallel array to dependencies)
1204
1240
old_interface_hash: the hash from the previous version of the data cache file
1205
1241
source_hash: the hash of the source code
1206
1242
ignore_all: the ignore_all flag for this module
@@ -1221,7 +1257,9 @@ def write_cache(id: str, path: str, tree: MypyFile,
1221
1257
assert os .path .dirname (meta_json ) == parent
1222
1258
1223
1259
# Serialize data and analyze interface
1224
- data = tree .serialize ()
1260
+ data = {'tree' : tree .serialize (),
1261
+ 'fine_grained_deps' : serialized_fine_grained_deps ,
1262
+ }
1225
1263
if manager .options .debug_cache :
1226
1264
data_str = json .dumps (data , indent = 2 , sort_keys = True )
1227
1265
else :
@@ -1282,6 +1320,7 @@ def write_cache(id: str, path: str, tree: MypyFile,
1282
1320
'child_modules' : child_modules ,
1283
1321
'options' : options .select_options_affecting_cache (),
1284
1322
'dep_prios' : dep_prios ,
1323
+ 'dep_lines' : dep_lines ,
1285
1324
'interface_hash' : interface_hash ,
1286
1325
'version_id' : manager .version_id ,
1287
1326
'ignore_all' : ignore_all ,
@@ -1523,6 +1562,8 @@ class State:
1523
1562
# Whether the module has an error or any of its dependencies have one.
1524
1563
transitive_error = False
1525
1564
1565
+ fine_grained_deps = None # type: Dict[str, Set[str]]
1566
+
1526
1567
# Type checker used for checking this file. Use type_checker() for
1527
1568
# access and to construct this on demand.
1528
1569
_type_checker = None # type: Optional[TypeChecker]
@@ -1551,6 +1592,7 @@ def __init__(self,
1551
1592
self .id = id or '__main__'
1552
1593
self .options = manager .options .clone_for_module (self .id )
1553
1594
self ._type_checker = None
1595
+ self .fine_grained_deps = {}
1554
1596
if not path and source is None :
1555
1597
assert id is not None
1556
1598
file_id = id
@@ -1626,8 +1668,10 @@ def __init__(self,
1626
1668
assert len (self .meta .dependencies ) == len (self .meta .dep_prios )
1627
1669
self .priorities = {id : pri
1628
1670
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 )}
1629
1674
self .child_modules = set (self .meta .child_modules )
1630
- self .dep_line_map = {}
1631
1675
else :
1632
1676
# Parse the file (and then some) to get the dependencies.
1633
1677
self .parse_file ()
@@ -1734,7 +1778,9 @@ def load_tree(self) -> None:
1734
1778
with open (self .meta .data_json ) as f :
1735
1779
data = json .load (f )
1736
1780
# 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
+
1738
1784
self .manager .modules [self .id ] = self .tree
1739
1785
self .manager .add_stats (fresh_trees = 1 )
1740
1786
@@ -1977,6 +2023,19 @@ def _patch_indirect_dependencies(self,
1977
2023
elif dep not in self .suppressed and dep in self .manager .missing_modules :
1978
2024
self .suppressed .append (dep )
1979
2025
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
+
1980
2039
def valid_references (self ) -> Set [str ]:
1981
2040
assert self .ancestors is not None
1982
2041
valid_refs = set (self .dependencies + self .suppressed + self .ancestors )
@@ -2001,10 +2060,12 @@ def write_cache(self) -> None:
2001
2060
self .mark_interface_stale (on_errors = True )
2002
2061
return
2003
2062
dep_prios = self .dependency_priorities ()
2063
+ dep_lines = self .dependency_lines ()
2004
2064
new_interface_hash , self .meta = write_cache (
2005
2065
self .id , self .path , self .tree ,
2066
+ {k : list (v ) for k , v in self .fine_grained_deps .items ()},
2006
2067
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 ,
2008
2069
self .manager )
2009
2070
if new_interface_hash == self .interface_hash :
2010
2071
self .manager .log ("Cached module {} has same interface" .format (self .id ))
@@ -2016,6 +2077,9 @@ def write_cache(self) -> None:
2016
2077
def dependency_priorities (self ) -> List [int ]:
2017
2078
return [self .priorities .get (dep , PRI_HIGH ) for dep in self .dependencies ]
2018
2079
2080
+ def dependency_lines (self ) -> List [int ]:
2081
+ return [self .dep_line_map .get (dep , 1 ) for dep in self .dependencies ]
2082
+
2019
2083
def generate_unused_ignore_notes (self ) -> None :
2020
2084
if self .options .warn_unused_ignores :
2021
2085
self .manager .errors .generate_unused_ignore_notes (self .xpath )
@@ -2348,6 +2412,14 @@ def process_graph(graph: Graph, manager: BuildManager) -> None:
2348
2412
manager .log ("Processing SCC of size %d (%s) as %s" % (size , scc_str , fresh_msg ))
2349
2413
process_stale_scc (graph , scc , manager )
2350
2414
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
+
2351
2423
sccs_left = len (fresh_scc_queue )
2352
2424
nodes_left = sum (len (scc ) for scc in fresh_scc_queue )
2353
2425
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
2534
2606
graph [id ].transitive_error = True
2535
2607
for id in stale :
2536
2608
graph [id ].finish_passes ()
2609
+ if manager .options .cache_fine_grained or manager .options .fine_grained_incremental :
2610
+ graph [id ].compute_fine_grained_deps ()
2537
2611
graph [id ].generate_unused_ignore_notes ()
2538
2612
manager .flush_errors (manager .errors .file_messages (graph [id ].xpath ), False )
2539
2613
graph [id ].write_cache ()
0 commit comments