Skip to content

Commit bb9ba27

Browse files
Merge branch 'master' into str-lower-upper
2 parents 2ae0353 + fd05265 commit bb9ba27

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

55 files changed

+1651
-833
lines changed

mypy/build.py

Lines changed: 80 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@
4747
from mypy.graph_utils import prepare_sccs, strongly_connected_components, topsort
4848
from mypy.indirection import TypeIndirectionVisitor
4949
from mypy.messages import MessageBuilder
50-
from mypy.nodes import Import, ImportAll, ImportBase, ImportFrom, MypyFile, SymbolTable, TypeInfo
50+
from mypy.nodes import Import, ImportAll, ImportBase, ImportFrom, MypyFile, SymbolTable
5151
from mypy.partially_defined import PossiblyUndefinedVariableVisitor
5252
from mypy.semanal import SemanticAnalyzer
5353
from mypy.semanal_pass1 import SemanticAnalyzerPreAnalysis
@@ -335,6 +335,7 @@ class CacheMeta(NamedTuple):
335335
# dep_prios and dep_lines are in parallel with dependencies + suppressed
336336
dep_prios: list[int]
337337
dep_lines: list[int]
338+
dep_hashes: dict[str, str]
338339
interface_hash: str # hash representing the public interface
339340
version_id: str # mypy version for cache invalidation
340341
ignore_all: bool # if errors were ignored
@@ -373,6 +374,7 @@ def cache_meta_from_dict(meta: dict[str, Any], data_json: str) -> CacheMeta:
373374
meta.get("options"),
374375
meta.get("dep_prios", []),
375376
meta.get("dep_lines", []),
377+
meta.get("dep_hashes", {}),
376378
meta.get("interface_hash", ""),
377379
meta.get("version_id", sentinel),
378380
meta.get("ignore_all", True),
@@ -890,8 +892,6 @@ def log(self, *message: str) -> None:
890892
self.stderr.flush()
891893

892894
def log_fine_grained(self, *message: str) -> None:
893-
import mypy.build
894-
895895
if self.verbosity() >= 1:
896896
self.log("fine-grained:", *message)
897897
elif mypy.build.DEBUG_FINE_GRAINED:
@@ -1500,6 +1500,7 @@ def validate_meta(
15001500
"options": (manager.options.clone_for_module(id).select_options_affecting_cache()),
15011501
"dep_prios": meta.dep_prios,
15021502
"dep_lines": meta.dep_lines,
1503+
"dep_hashes": meta.dep_hashes,
15031504
"interface_hash": meta.interface_hash,
15041505
"version_id": manager.version_id,
15051506
"ignore_all": meta.ignore_all,
@@ -1543,7 +1544,7 @@ def write_cache(
15431544
source_hash: str,
15441545
ignore_all: bool,
15451546
manager: BuildManager,
1546-
) -> tuple[str, CacheMeta | None]:
1547+
) -> tuple[str, tuple[dict[str, Any], str, str] | None]:
15471548
"""Write cache files for a module.
15481549
15491550
Note that this mypy's behavior is still correct when any given
@@ -1564,9 +1565,9 @@ def write_cache(
15641565
manager: the build manager (for pyversion, log/trace)
15651566
15661567
Returns:
1567-
A tuple containing the interface hash and CacheMeta
1568-
corresponding to the metadata that was written (the latter may
1569-
be None if the cache could not be written).
1568+
A tuple containing the interface hash and inner tuple with cache meta JSON
1569+
that should be written and paths to cache files (inner tuple may be None,
1570+
if the cache data could not be written).
15701571
"""
15711572
metastore = manager.metastore
15721573
# For Bazel we use relative paths and zero mtimes.
@@ -1581,6 +1582,8 @@ def write_cache(
15811582
if bazel:
15821583
tree.path = path
15831584

1585+
plugin_data = manager.plugin.report_config_data(ReportConfigContext(id, path, is_check=False))
1586+
15841587
# Serialize data and analyze interface
15851588
if manager.options.fixed_format_cache:
15861589
data_io = Buffer()
@@ -1589,9 +1592,7 @@ def write_cache(
15891592
else:
15901593
data = tree.serialize()
15911594
data_bytes = json_dumps(data, manager.options.debug_cache)
1592-
interface_hash = hash_digest(data_bytes)
1593-
1594-
plugin_data = manager.plugin.report_config_data(ReportConfigContext(id, path, is_check=False))
1595+
interface_hash = hash_digest(data_bytes + json_dumps(plugin_data))
15951596

15961597
# Obtain and set up metadata
15971598
st = manager.get_stat(path)
@@ -1659,16 +1660,22 @@ def write_cache(
16591660
"ignore_all": ignore_all,
16601661
"plugin_data": plugin_data,
16611662
}
1663+
return interface_hash, (meta, meta_json, data_json)
16621664

1665+
1666+
def write_cache_meta(
1667+
meta: dict[str, Any], manager: BuildManager, meta_json: str, data_json: str
1668+
) -> CacheMeta:
16631669
# Write meta cache file
1670+
metastore = manager.metastore
16641671
meta_str = json_dumps(meta, manager.options.debug_cache)
16651672
if not metastore.write(meta_json, meta_str):
16661673
# Most likely the error is the replace() call
16671674
# (see https://github.com/python/mypy/issues/3215).
16681675
# The next run will simply find the cache entry out of date.
16691676
manager.log(f"Error writing meta JSON file {meta_json}")
16701677

1671-
return interface_hash, cache_meta_from_dict(meta, data_json)
1678+
return cache_meta_from_dict(meta, data_json)
16721679

16731680

16741681
def delete_cache(id: str, path: str, manager: BuildManager) -> None:
@@ -1758,26 +1765,24 @@ def delete_cache(id: str, path: str, manager: BuildManager) -> None:
17581765
17591766
For single nodes, processing is simple. If the node was cached, we
17601767
deserialize the cache data and fix up cross-references. Otherwise, we
1761-
do semantic analysis followed by type checking. We also handle (c)
1762-
above; if a module has valid cache data *but* any of its
1763-
dependencies was processed from source, then the module should be
1764-
processed from source.
1765-
1766-
A relatively simple optimization (outside SCCs) we might do in the
1767-
future is as follows: if a node's cache data is valid, but one or more
1768-
of its dependencies are out of date so we have to re-parse the node
1769-
from source, once we have fully type-checked the node, we can decide
1770-
whether its symbol table actually changed compared to the cache data
1771-
(by reading the cache data and comparing it to the data we would be
1772-
writing). If there is no change we can declare the node up to date,
1773-
and any node that depends (and for which we have cached data, and
1774-
whose other dependencies are up to date) on it won't need to be
1775-
re-parsed from source.
1768+
do semantic analysis followed by type checking. Once we (re-)processed
1769+
an SCC we check whether its interface (symbol table) is still fresh
1770+
(matches previous cached value). If it is not, we consider dependent SCCs
1771+
stale so that they need to be re-parsed as well.
1772+
1773+
Note on indirect dependencies: normally dependencies are determined from
1774+
imports, but since our interfaces are "opaque" (i.e. symbol tables can
1775+
contain cross-references as well as types identified by name), these are not
1776+
enough. We *must* also add "indirect" dependencies from symbols and types to
1777+
their definitions. For this purpose, we record all accessed symbols during
1778+
semantic analysis, and after we finished processing a module, we traverse its
1779+
type map, and for each type we find (transitively) on which named types it
1780+
depends.
17761781
17771782
Import cycles
17781783
-------------
17791784
1780-
Finally we have to decide how to handle (c), import cycles. Here
1785+
Finally we have to decide how to handle (b), import cycles. Here
17811786
we'll need a modified version of the original state machine
17821787
(build.py), but we only need to do this per SCC, and we won't have to
17831788
deal with changes to the list of nodes while we're processing it.
@@ -1867,6 +1872,9 @@ class State:
18671872
# Map each dependency to the line number where it is first imported
18681873
dep_line_map: dict[str, int]
18691874

1875+
# Map from dependency id to its last observed interface hash
1876+
dep_hashes: dict[str, str] = {}
1877+
18701878
# Parent package, its parent, etc.
18711879
ancestors: list[str] | None = None
18721880

@@ -1879,9 +1887,6 @@ class State:
18791887
# If caller_state is set, the line number in the caller where the import occurred
18801888
caller_line = 0
18811889

1882-
# If True, indicate that the public interface of this module is unchanged
1883-
externally_same = True
1884-
18851890
# Contains a hash of the public interface in incremental mode
18861891
interface_hash: str = ""
18871892

@@ -1994,6 +1999,7 @@ def __init__(
19941999
self.priorities = {id: pri for id, pri in zip(all_deps, self.meta.dep_prios)}
19952000
assert len(all_deps) == len(self.meta.dep_lines)
19962001
self.dep_line_map = {id: line for id, line in zip(all_deps, self.meta.dep_lines)}
2002+
self.dep_hashes = self.meta.dep_hashes
19972003
if temporary:
19982004
self.load_tree(temporary=True)
19992005
if not manager.use_fine_grained_cache():
@@ -2046,26 +2052,17 @@ def is_fresh(self) -> bool:
20462052
"""Return whether the cache data for this file is fresh."""
20472053
# NOTE: self.dependencies may differ from
20482054
# self.meta.dependencies when a dependency is dropped due to
2049-
# suppression by silent mode. However when a suppressed
2055+
# suppression by silent mode. However, when a suppressed
20502056
# dependency is added back we find out later in the process.
2051-
return (
2052-
self.meta is not None
2053-
and self.is_interface_fresh()
2054-
and self.dependencies == self.meta.dependencies
2055-
)
2056-
2057-
def is_interface_fresh(self) -> bool:
2058-
return self.externally_same
2057+
return self.meta is not None and self.dependencies == self.meta.dependencies
20592058

20602059
def mark_as_rechecked(self) -> None:
20612060
"""Marks this module as having been fully re-analyzed by the type-checker."""
20622061
self.manager.rechecked_modules.add(self.id)
20632062

2064-
def mark_interface_stale(self, *, on_errors: bool = False) -> None:
2063+
def mark_interface_stale(self) -> None:
20652064
"""Marks this module as having a stale public interface, and discards the cache data."""
2066-
self.externally_same = False
2067-
if not on_errors:
2068-
self.manager.stale_modules.add(self.id)
2065+
self.manager.stale_modules.add(self.id)
20692066

20702067
def check_blockers(self) -> None:
20712068
"""Raise CompileError if a blocking error is detected."""
@@ -2410,21 +2407,15 @@ def finish_passes(self) -> None:
24102407

24112408
# We should always patch indirect dependencies, even in full (non-incremental) builds,
24122409
# because the cache still may be written, and it must be correct.
2413-
# TODO: find a more robust way to traverse *all* relevant types?
2414-
all_types = list(self.type_map().values())
2415-
for _, sym, _ in self.tree.local_definitions():
2416-
if sym.type is not None:
2417-
all_types.append(sym.type)
2418-
if isinstance(sym.node, TypeInfo):
2419-
# TypeInfo symbols have some extra relevant types.
2420-
all_types.extend(sym.node.bases)
2421-
if sym.node.metaclass_type:
2422-
all_types.append(sym.node.metaclass_type)
2423-
if sym.node.typeddict_type:
2424-
all_types.append(sym.node.typeddict_type)
2425-
if sym.node.tuple_type:
2426-
all_types.append(sym.node.tuple_type)
2427-
self._patch_indirect_dependencies(self.type_checker().module_refs, all_types)
2410+
self._patch_indirect_dependencies(
2411+
# Two possible sources of indirect dependencies:
2412+
# * Symbols not directly imported in this module but accessed via an attribute
2413+
# or via a re-export (vast majority of these recorded in semantic analysis).
2414+
# * For each expression type we need to record definitions of type components
2415+
# since "meaning" of the type may be updated when definitions are updated.
2416+
self.tree.module_refs | self.type_checker().module_refs,
2417+
set(self.type_map().values()),
2418+
)
24282419

24292420
if self.options.dump_inference_stats:
24302421
dump_type_stats(
@@ -2453,7 +2444,7 @@ def free_state(self) -> None:
24532444
self._type_checker.reset()
24542445
self._type_checker = None
24552446

2456-
def _patch_indirect_dependencies(self, module_refs: set[str], types: list[Type]) -> None:
2447+
def _patch_indirect_dependencies(self, module_refs: set[str], types: set[Type]) -> None:
24572448
assert None not in types
24582449
valid = self.valid_references()
24592450

@@ -2507,7 +2498,7 @@ def valid_references(self) -> set[str]:
25072498

25082499
return valid_refs
25092500

2510-
def write_cache(self) -> None:
2501+
def write_cache(self) -> tuple[dict[str, Any], str, str] | None:
25112502
assert self.tree is not None, "Internal error: method must be called on parsed file only"
25122503
# We don't support writing cache files in fine-grained incremental mode.
25132504
if (
@@ -2525,20 +2516,19 @@ def write_cache(self) -> None:
25252516
except Exception:
25262517
print(f"Error serializing {self.id}", file=self.manager.stdout)
25272518
raise # Propagate to display traceback
2528-
return
2519+
return None
25292520
is_errors = self.transitive_error
25302521
if is_errors:
25312522
delete_cache(self.id, self.path, self.manager)
25322523
self.meta = None
2533-
self.mark_interface_stale(on_errors=True)
2534-
return
2524+
return None
25352525
dep_prios = self.dependency_priorities()
25362526
dep_lines = self.dependency_lines()
25372527
assert self.source_hash is not None
25382528
assert len(set(self.dependencies)) == len(
25392529
self.dependencies
25402530
), f"Duplicates in dependencies list for {self.id} ({self.dependencies})"
2541-
new_interface_hash, self.meta = write_cache(
2531+
new_interface_hash, meta_tuple = write_cache(
25422532
self.id,
25432533
self.path,
25442534
self.tree,
@@ -2557,6 +2547,7 @@ def write_cache(self) -> None:
25572547
self.manager.log(f"Cached module {self.id} has changed interface")
25582548
self.mark_interface_stale()
25592549
self.interface_hash = new_interface_hash
2550+
return meta_tuple
25602551

25612552
def verify_dependencies(self, suppressed_only: bool = False) -> None:
25622553
"""Report errors for import targets in modules that don't exist.
@@ -3287,7 +3278,19 @@ def process_graph(graph: Graph, manager: BuildManager) -> None:
32873278
for id in scc:
32883279
deps.update(graph[id].dependencies)
32893280
deps -= ascc
3290-
stale_deps = {id for id in deps if id in graph and not graph[id].is_interface_fresh()}
3281+
3282+
# Verify that interfaces of dependencies still present in graph are up-to-date (fresh).
3283+
# Note: if a dependency is not in graph anymore, it should be considered interface-stale.
3284+
# This is important to trigger any relevant updates from indirect dependencies that were
3285+
# removed in load_graph().
3286+
stale_deps = set()
3287+
for id in ascc:
3288+
for dep in graph[id].dep_hashes:
3289+
if dep not in graph:
3290+
stale_deps.add(dep)
3291+
continue
3292+
if graph[dep].interface_hash != graph[id].dep_hashes[dep]:
3293+
stale_deps.add(dep)
32913294
fresh = fresh and not stale_deps
32923295
undeps = set()
32933296
if fresh:
@@ -3518,14 +3521,25 @@ def process_stale_scc(graph: Graph, scc: list[str], manager: BuildManager) -> No
35183521
if any(manager.errors.is_errors_for_file(graph[id].xpath) for id in stale):
35193522
for id in stale:
35203523
graph[id].transitive_error = True
3524+
meta_tuples = {}
35213525
for id in stale:
35223526
if graph[id].xpath not in manager.errors.ignored_files:
35233527
errors = manager.errors.file_messages(
35243528
graph[id].xpath, formatter=manager.error_formatter
35253529
)
35263530
manager.flush_errors(manager.errors.simplify_path(graph[id].xpath), errors, False)
3527-
graph[id].write_cache()
3531+
meta_tuples[id] = graph[id].write_cache()
35283532
graph[id].mark_as_rechecked()
3533+
for id in stale:
3534+
meta_tuple = meta_tuples[id]
3535+
if meta_tuple is None:
3536+
graph[id].meta = None
3537+
continue
3538+
meta, meta_json, data_json = meta_tuple
3539+
meta["dep_hashes"] = {
3540+
dep: graph[dep].interface_hash for dep in graph[id].dependencies if dep in graph
3541+
}
3542+
graph[id].meta = write_cache_meta(meta, manager, meta_json, data_json)
35293543

35303544

35313545
def sorted_components(

mypy/checker.py

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@
8080
AssertStmt,
8181
AssignmentExpr,
8282
AssignmentStmt,
83+
AwaitExpr,
8384
Block,
8485
BreakStmt,
8586
BytesExpr,
@@ -377,11 +378,9 @@ class TypeChecker(NodeVisitor[None], TypeCheckerSharedApi):
377378
inferred_attribute_types: dict[Var, Type] | None = None
378379
# Don't infer partial None types if we are processing assignment from Union
379380
no_partial_types: bool = False
380-
381-
# The set of all dependencies (suppressed or not) that this module accesses, either
382-
# directly or indirectly.
381+
# Extra module references not detected during semantic analysis (these are rare cases
382+
# e.g. access to class-level import via instance).
383383
module_refs: set[str]
384-
385384
# A map from variable nodes to a snapshot of the frame ids of the
386385
# frames that were active when the variable was declared. This can
387386
# be used to determine nearest common ancestor frame of a variable's
@@ -4924,7 +4923,11 @@ def check_return_stmt(self, s: ReturnStmt) -> None:
49244923
allow_none_func_call = is_lambda or declared_none_return or declared_any_return
49254924

49264925
# Return with a value.
4927-
if isinstance(s.expr, (CallExpr, ListExpr, TupleExpr, DictExpr, SetExpr, OpExpr)):
4926+
if (
4927+
isinstance(s.expr, (CallExpr, ListExpr, TupleExpr, DictExpr, SetExpr, OpExpr))
4928+
or isinstance(s.expr, AwaitExpr)
4929+
and isinstance(s.expr.expr, CallExpr)
4930+
):
49284931
# For expressions that (strongly) depend on type context (i.e. those that
49294932
# are handled like a function call), we allow fallback to empty type context
49304933
# in case of errors, this improves user experience in some cases,
@@ -5677,8 +5680,8 @@ def visit_match_stmt(self, s: MatchStmt) -> None:
56775680
self.push_type_map(else_map, from_assignment=False)
56785681
unmatched_types = else_map
56795682

5680-
if unmatched_types is not None:
5681-
for typ in list(unmatched_types.values()):
5683+
if unmatched_types is not None and not self.current_node_deferred:
5684+
for typ in unmatched_types.values():
56825685
self.msg.match_statement_inexhaustive_match(typ, s)
56835686

56845687
# This is needed due to a quirk in frame_context. Without it types will stay narrowed

0 commit comments

Comments
 (0)