diff --git a/mypy/build.py b/mypy/build.py index 5226021494bf1..48c444c2f55bb 100644 --- a/mypy/build.py +++ b/mypy/build.py @@ -35,7 +35,7 @@ from mypy.nodes import (MODULE_REF, MypyFile, Node, ImportBase, Import, ImportFrom, ImportAll) from mypy.semanal_pass1 import SemanticAnalyzerPass1 -from mypy.semanal import SemanticAnalyzerPass2 +from mypy.semanal import SemanticAnalyzerPass2, apply_semantic_analyzer_patches from mypy.semanal_pass3 import SemanticAnalyzerPass3 from mypy.checker import TypeChecker from mypy.indirection import TypeIndirectionVisitor @@ -1958,9 +1958,7 @@ def semantic_analysis_pass_three(self) -> None: self.patches = patches + self.patches def semantic_analysis_apply_patches(self) -> None: - patches_by_priority = sorted(self.patches, key=lambda x: x[0]) - for priority, patch_func in patches_by_priority: - patch_func() + apply_semantic_analyzer_patches(self.patches) def type_check_first_pass(self) -> None: if self.options.semantic_analysis_only: diff --git a/mypy/semanal.py b/mypy/semanal.py index 5bdefe2e6817b..e53f60e153836 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -309,8 +309,10 @@ def visit_file(self, file_node: MypyFile, fnam: str, options: Options, del self.cur_mod_node del self.globals - def refresh_partial(self, node: Union[MypyFile, FuncItem, OverloadedFuncDef]) -> None: + def refresh_partial(self, node: Union[MypyFile, FuncItem, OverloadedFuncDef], + patches: List[Tuple[int, Callable[[], None]]]) -> None: """Refresh a stale target in fine-grained incremental mode.""" + self.patches = patches self.scope.enter_file(self.cur_mod_id) if isinstance(node, MypyFile): self.refresh_top_level(node) @@ -318,15 +320,13 @@ def refresh_partial(self, node: Union[MypyFile, FuncItem, OverloadedFuncDef]) -> self.recurse_into_functions = True self.accept(node) self.scope.leave() + del self.patches def refresh_top_level(self, file_node: MypyFile) -> None: """Reanalyze a stale module top-level in fine-grained incremental mode.""" - # TODO: Invoke patches in fine-grained incremental mode. - self.patches = [] self.recurse_into_functions = False for d in file_node.defs: self.accept(d) - del self.patches @contextmanager def file_context(self, file_node: MypyFile, fnam: str, options: Options, @@ -4307,3 +4307,13 @@ def visit_any(self, t: AnyType) -> Type: if t.type_of_any == TypeOfAny.explicit: return t.copy_modified(TypeOfAny.special_form) return t + + +def apply_semantic_analyzer_patches(patches: List[Tuple[int, Callable[[], None]]]) -> None: + """Call patch callbacks in the right order. + + This should happen after semantic analyzer pass 3. + """ + patches_by_priority = sorted(patches, key=lambda x: x[0]) + for priority, patch_func in patches_by_priority: + patch_func() diff --git a/mypy/semanal_pass3.py b/mypy/semanal_pass3.py index 5a8b67adfc4b8..1b815dd450e41 100644 --- a/mypy/semanal_pass3.py +++ b/mypy/semanal_pass3.py @@ -71,8 +71,10 @@ def visit_file(self, file_node: MypyFile, fnam: str, options: Options, del self.cur_mod_node self.patches = [] - def refresh_partial(self, node: Union[MypyFile, FuncItem, OverloadedFuncDef]) -> None: + def refresh_partial(self, node: Union[MypyFile, FuncItem, OverloadedFuncDef], + patches: List[Tuple[int, Callable[[], None]]]) -> None: """Refresh a stale target in fine-grained incremental mode.""" + self.patches = patches self.scope.enter_file(self.sem.cur_mod_id) if isinstance(node, MypyFile): self.recurse_into_functions = False @@ -81,6 +83,7 @@ def refresh_partial(self, node: Union[MypyFile, FuncItem, OverloadedFuncDef]) -> self.recurse_into_functions = True self.accept(node) self.scope.leave() + self.patches = [] def refresh_top_level(self, file_node: MypyFile) -> None: """Reanalyze a stale module top-level in fine-grained incremental mode.""" diff --git a/mypy/server/aststrip.py b/mypy/server/aststrip.py index d9fdf7b2da8d5..c949d72cfd2bf 100644 --- a/mypy/server/aststrip.py +++ b/mypy/server/aststrip.py @@ -78,20 +78,24 @@ def strip_file_top_level(self, file_node: MypyFile) -> None: def visit_class_def(self, node: ClassDef) -> None: """Strip class body and type info, but don't strip methods.""" - node.info.type_vars = [] - node.info.bases = [] - node.info.abstract_attributes = [] - node.info.mro = [] - node.info.add_type_vars() - node.info.tuple_type = None - node.info.typeddict_type = None - node.info._cache = set() - node.info._cache_proper = set() + self.strip_type_info(node.info) node.base_type_exprs.extend(node.removed_base_type_exprs) node.removed_base_type_exprs = [] with self.enter_class(node.info): super().visit_class_def(node) + def strip_type_info(self, info: TypeInfo) -> None: + info.type_vars = [] + info.bases = [] + info.abstract_attributes = [] + info.mro = [] + info.add_type_vars() + info.tuple_type = None + info.typeddict_type = None + info.tuple_type = None + info._cache = set() + info._cache_proper = set() + def visit_func_def(self, node: FuncDef) -> None: if not self.recurse_into_functions: return diff --git a/mypy/server/update.py b/mypy/server/update.py index efc1224ae833f..d4acbbf1f8d33 100644 --- a/mypy/server/update.py +++ b/mypy/server/update.py @@ -119,7 +119,9 @@ """ import os.path -from typing import Dict, List, Set, Tuple, Iterable, Union, Optional, Mapping, NamedTuple +from typing import ( + Dict, List, Set, Tuple, Iterable, Union, Optional, Mapping, NamedTuple, Callable +) from mypy.build import ( BuildManager, State, BuildSource, Graph, load_graph, SavedCache, CacheMeta, @@ -133,6 +135,7 @@ ) from mypy.options import Options from mypy.types import Type +from mypy.semanal import apply_semantic_analyzer_patches from mypy.server.astdiff import ( snapshot_symbol_table, compare_symbol_table_snapshots, is_identical_type, SnapshotItem ) @@ -837,6 +840,8 @@ def key(node: DeferredNode) -> int: strip_target(deferred.node) semantic_analyzer = manager.semantic_analyzer + patches = [] # type: List[Tuple[int, Callable[[], None]]] + # Second pass of semantic analysis. We don't redo the first pass, because it only # does local things that won't go stale. for deferred in nodes: @@ -845,7 +850,7 @@ def key(node: DeferredNode) -> int: fnam=file_node.path, options=manager.options, active_type=deferred.active_typeinfo): - manager.semantic_analyzer.refresh_partial(deferred.node) + manager.semantic_analyzer.refresh_partial(deferred.node, patches) # Third pass of semantic analysis. for deferred in nodes: @@ -854,7 +859,9 @@ def key(node: DeferredNode) -> int: fnam=file_node.path, options=manager.options, active_type=deferred.active_typeinfo): - manager.semantic_analyzer_pass3.refresh_partial(deferred.node) + manager.semantic_analyzer_pass3.refresh_partial(deferred.node, patches) + + apply_semantic_analyzer_patches(patches) # Merge symbol tables to preserve identities of AST nodes. The file node will remain # the same, but other nodes may have been recreated with different identities, such as diff --git a/test-data/unit/fine-grained.test b/test-data/unit/fine-grained.test index 17b022744829d..265142118d9cc 100644 --- a/test-data/unit/fine-grained.test +++ b/test-data/unit/fine-grained.test @@ -2475,3 +2475,37 @@ else: [builtins fixtures/ops.pyi] [out] == + +[case testNamedTupleWithinFunction] +from typing import NamedTuple +import b +def f() -> None: + b.x + n = NamedTuple('n', []) +[file b.py] +x = 0 +[file b.py.2] +x = '' +[out] +== + +[case testNamedTupleFallback] +# This test will fail without semantic analyzer pass 2 patches +import a +[file a.py] +import b +[file b.py] +from typing import NamedTuple +import c +c.x +class N(NamedTuple): + count: int +[file c.py] +x = 0 +[file c.py.2] +x = '' +[builtins fixtures/tuple.pyi] +[out] +b.py:5: error: Incompatible types in assignment (expression has type "int", base class "tuple" defined the type as "Callable[[Tuple[int, ...], Any], int]") +== +b.py:5: error: Incompatible types in assignment (expression has type "int", base class "tuple" defined the type as "Callable[[Tuple[int, ...], Any], int]")