32
32
if MYPY :
33
33
from typing import Deque
34
34
35
- from mypy .nodes import (MypyFile , Node , ImportBase , Import , ImportFrom , ImportAll )
35
+ from mypy .nodes import (MODULE_REF , MypyFile , Node , ImportBase , Import , ImportFrom , ImportAll )
36
36
from mypy .semanal_pass1 import SemanticAnalyzerPass1
37
37
from mypy .semanal import SemanticAnalyzerPass2
38
38
from mypy .semanal_pass3 import SemanticAnalyzerPass3
@@ -179,6 +179,8 @@ def build(sources: List[BuildSource],
179
179
# multiple builds, there could be a mix of files/modules, so its easier
180
180
# to just define the semantics that we always add the current director
181
181
# to the lib_path
182
+ # TODO: Don't do this in some cases; for motivation see see
183
+ # https://github.com/python/mypy/issues/4195#issuecomment-341915031
182
184
lib_path .insert (0 , os .getcwd ())
183
185
184
186
# Prepend a config-defined mypy path.
@@ -1621,7 +1623,6 @@ def mark_as_rechecked(self) -> None:
1621
1623
1622
1624
def mark_interface_stale (self , * , on_errors : bool = False ) -> None :
1623
1625
"""Marks this module as having a stale public interface, and discards the cache data."""
1624
- self .meta = None
1625
1626
self .externally_same = False
1626
1627
if not on_errors :
1627
1628
self .manager .stale_modules .add (self .id )
@@ -1903,6 +1904,7 @@ def write_cache(self) -> None:
1903
1904
is_errors = self .manager .errors .is_errors ()
1904
1905
if is_errors :
1905
1906
delete_cache (self .id , self .path , self .manager )
1907
+ self .meta = None
1906
1908
self .mark_interface_stale (on_errors = True )
1907
1909
return
1908
1910
dep_prios = [self .priorities .get (dep , PRI_HIGH ) for dep in self .dependencies ]
@@ -1920,6 +1922,7 @@ def write_cache(self) -> None:
1920
1922
1921
1923
1922
1924
def dispatch (sources : List [BuildSource ], manager : BuildManager ) -> Graph :
1925
+ set_orig = set (manager .saved_cache )
1923
1926
manager .log ()
1924
1927
manager .log ("Mypy version %s" % __version__ )
1925
1928
t0 = time .time ()
@@ -1940,13 +1943,23 @@ def dispatch(sources: List[BuildSource], manager: BuildManager) -> Graph:
1940
1943
if manager .options .warn_unused_ignores :
1941
1944
# TODO: This could also be a per-module option.
1942
1945
manager .errors .generate_unused_ignore_notes ()
1946
+ updated = preserve_cache (graph )
1947
+ set_updated = set (updated )
1948
+ manager .saved_cache .clear ()
1949
+ manager .saved_cache .update (updated )
1950
+ set_final = set (manager .saved_cache )
1951
+ # These keys have numbers in them to force a sort order.
1952
+ manager .add_stats (saved_cache_1orig = len (set_orig ),
1953
+ saved_cache_2updated = len (set_updated & set_orig ),
1954
+ saved_cache_3added = len (set_final - set_orig ),
1955
+ saved_cache_4removed = len (set_orig - set_final ),
1956
+ saved_cache_5final = len (set_final ))
1943
1957
if manager .options .warn_unused_strictness_exceptions :
1944
1958
for option in manager .options .unused_strictness_whitelist :
1945
1959
for file in manager .options .unused_strictness_whitelist [option ]:
1946
1960
message = "Flag {} can be enabled for module '{}'" .format (option , file )
1947
1961
manager .errors .report (- 1 , - 1 , message , blocker = False , severity = 'warning' ,
1948
1962
file = file )
1949
- manager .saved_cache .update (preserve_cache (graph ))
1950
1963
if manager .options .dump_deps :
1951
1964
# This speeds up startup a little when not using the daemon mode.
1952
1965
from mypy .server .deps import dump_all_dependencies
@@ -2045,7 +2058,19 @@ def load_graph(sources: List[BuildSource], manager: BuildManager) -> Graph:
2045
2058
while new :
2046
2059
st = new .popleft ()
2047
2060
assert st .ancestors is not None
2048
- for dep in st .ancestors + st .dependencies + st .suppressed :
2061
+ # Strip out indirect dependencies. These will be dealt with
2062
+ # when they show up as direct dependencies, and there's a
2063
+ # scenario where they hurt:
2064
+ # - Suppose A imports B and B imports C.
2065
+ # - Suppose on the next round:
2066
+ # - C is deleted;
2067
+ # - B is updated to remove the dependency on C;
2068
+ # - A is unchanged.
2069
+ # - In this case A's cached *direct* dependencies are still valid
2070
+ # (since direct dependencies reflect the imports found in the source)
2071
+ # but A's cached *indirect* dependency on C is wrong.
2072
+ dependencies = [dep for dep in st .dependencies if st .priorities .get (dep ) != PRI_INDIRECT ]
2073
+ for dep in st .ancestors + dependencies + st .suppressed :
2049
2074
# We don't want to recheck imports marked with '# type: ignore'
2050
2075
# so we ignore any suppressed module not explicitly re-included
2051
2076
# from the command line.
@@ -2127,7 +2152,7 @@ def process_graph(graph: Graph, manager: BuildManager) -> None:
2127
2152
for id in scc :
2128
2153
deps .update (graph [id ].dependencies )
2129
2154
deps -= ascc
2130
- stale_deps = {id for id in deps if not graph [id ].is_interface_fresh ()}
2155
+ stale_deps = {id for id in deps if id in graph and not graph [id ].is_interface_fresh ()}
2131
2156
if not manager .options .quick_and_dirty :
2132
2157
fresh = fresh and not stale_deps
2133
2158
undeps = set ()
@@ -2184,8 +2209,9 @@ def process_graph(graph: Graph, manager: BuildManager) -> None:
2184
2209
2185
2210
scc_str = " " .join (scc )
2186
2211
if fresh :
2187
- manager .trace ("Queuing %s SCC (%s)" % (fresh_msg , scc_str ))
2188
- fresh_scc_queue .append (scc )
2212
+ if not maybe_reuse_in_memory_tree (graph , scc , manager ):
2213
+ manager .trace ("Queuing %s SCC (%s)" % (fresh_msg , scc_str ))
2214
+ fresh_scc_queue .append (scc )
2189
2215
else :
2190
2216
if len (fresh_scc_queue ) > 0 :
2191
2217
manager .log ("Processing {} queued fresh SCCs" .format (len (fresh_scc_queue )))
@@ -2273,23 +2299,6 @@ def process_fresh_scc(graph: Graph, scc: List[str], manager: BuildManager) -> No
2273
2299
2274
2300
If the tree is loaded from memory ('saved_cache') it's even quicker.
2275
2301
"""
2276
- saved_cache = manager .saved_cache
2277
- # Check that all nodes are available for loading from memory.
2278
- if all (id in saved_cache for id in scc ):
2279
- deps = set (dep for id in scc for dep in graph [id ].dependencies if dep in graph )
2280
- # Check that all dependencies were loaded from memory.
2281
- # If not, some dependency was reparsed but the interface hash
2282
- # wasn't changed -- in that case we can't reuse the tree.
2283
- if all (graph [dep ].is_from_saved_cache for dep in deps ):
2284
- trees = {id : saved_cache [id ][1 ] for id in scc }
2285
- for id , tree in trees .items ():
2286
- manager .add_stats (reused_trees = 1 )
2287
- manager .trace ("Reusing saved tree %s" % id )
2288
- st = graph [id ]
2289
- st .tree = tree # This is never overwritten.
2290
- st .is_from_saved_cache = True
2291
- manager .modules [id ] = tree
2292
- return
2293
2302
for id in scc :
2294
2303
graph [id ].load_tree ()
2295
2304
for id in scc :
@@ -2300,6 +2309,70 @@ def process_fresh_scc(graph: Graph, scc: List[str], manager: BuildManager) -> No
2300
2309
graph [id ].patch_dependency_parents ()
2301
2310
2302
2311
2312
+ def maybe_reuse_in_memory_tree (graph : Graph , scc : List [str ], manager : BuildManager ) -> bool :
2313
+ """Set the trees for the given SCC from the in-memory cache, if all valid.
2314
+
2315
+ If any saved tree for this SCC is invalid, set the trees for all
2316
+ SCC members to None and mark as not-from-cache.
2317
+ """
2318
+ if not can_reuse_in_memory_tree (graph , scc , manager ):
2319
+ for id in scc :
2320
+ manager .add_stats (cleared_trees = 1 )
2321
+ manager .trace ("Clearing tree %s" % id )
2322
+ st = graph [id ]
2323
+ st .tree = None
2324
+ st .is_from_saved_cache = False
2325
+ if id in manager .modules :
2326
+ del manager .modules [id ]
2327
+ return False
2328
+ trees = {id : manager .saved_cache [id ][1 ] for id in scc }
2329
+ for id , tree in trees .items ():
2330
+ manager .add_stats (reused_trees = 1 )
2331
+ manager .trace ("Reusing saved tree %s" % id )
2332
+ st = graph [id ]
2333
+ st .tree = tree
2334
+ st .is_from_saved_cache = True
2335
+ manager .modules [id ] = tree
2336
+ # Delete any submodules from the module that aren't
2337
+ # dependencies of the module; they will be re-added once
2338
+ # imported. It's possible that the parent module is reused
2339
+ # but a submodule isn't; we don't want to accidentally link
2340
+ # into the old submodule's tree. See also
2341
+ # patch_dependency_parents() above. The exception for subname
2342
+ # in st.dependencies handles the case where 'import m'
2343
+ # guarantees that some submodule of m is also available
2344
+ # (e.g. 'os.path'); in those cases the submodule is an
2345
+ # explicit dependency of the parent.
2346
+ for name in list (tree .names ):
2347
+ sym = tree .names [name ]
2348
+ subname = id + '.' + name
2349
+ if (sym .kind == MODULE_REF
2350
+ and sym .node is not None
2351
+ and sym .node .fullname () == subname
2352
+ and subname not in st .dependencies ):
2353
+ manager .trace ("Purging %s" % subname )
2354
+ del tree .names [name ]
2355
+ return True
2356
+
2357
+
2358
+ def can_reuse_in_memory_tree (graph : Graph , scc : List [str ], manager : BuildManager ) -> bool :
2359
+ """Check whether the given SCC can safely reuse the trees from saved_cache.
2360
+
2361
+ Assumes the SCC is already considered fresh.
2362
+ """
2363
+ saved_cache = manager .saved_cache
2364
+ # Check that all nodes are available for loading from memory.
2365
+ if all (id in saved_cache for id in scc ):
2366
+ # Check that all dependencies were loaded from memory.
2367
+ # If not, some dependency was reparsed but the interface hash
2368
+ # wasn't changed -- in that case we can't reuse the tree.
2369
+ deps = set (dep for id in scc for dep in graph [id ].dependencies if dep in graph )
2370
+ deps -= set (scc ) # Subtract the SCC itself (else nothing will be safe)
2371
+ if all (graph [dep ].is_from_saved_cache for dep in deps ):
2372
+ return True
2373
+ return False
2374
+
2375
+
2303
2376
def process_stale_scc (graph : Graph , scc : List [str ], manager : BuildManager ) -> None :
2304
2377
"""Process the modules in one SCC from source code.
2305
2378
0 commit comments