Skip to content

Commit b41bb66

Browse files
authored
Defer subclass methods if superclass has not been analyzed (#5637)
Fixes #5560 Fixes #5548 Half of the PR is updating various function signatures to also accept `Decorator`. I still prohibit `Decorator` as a target in fine-grained mode, but I think there should be no harm to defer it in normal mode.
1 parent 28b1bfe commit b41bb66

File tree

6 files changed

+384
-45
lines changed

6 files changed

+384
-45
lines changed

mypy/checker.py

+78-29
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
from contextlib import contextmanager
66

77
from typing import (
8-
Dict, Set, List, cast, Tuple, TypeVar, Union, Optional, NamedTuple, Iterator, Iterable, Any
8+
Dict, Set, List, cast, Tuple, TypeVar, Union, Optional, NamedTuple, Iterator, Iterable,
9+
Sequence
910
)
1011

1112
from mypy.errors import Errors, report_internal_error
@@ -72,19 +73,31 @@
7273

7374
DEFAULT_LAST_PASS = 1 # type: Final # Pass numbers start at 0
7475

76+
DeferredNodeType = Union[FuncDef, LambdaExpr, OverloadedFuncDef, Decorator]
77+
FineGrainedDeferredNodeType = Union[FuncDef, MypyFile, OverloadedFuncDef]
7578

7679
# A node which is postponed to be processed during the next pass.
77-
# This is used for both batch mode and fine-grained incremental mode.
80+
# In normal mode one can defer functions and methods (also decorated and/or overloaded)
81+
# and lambda expressions. Nested functions can't be deferred -- only top-level functions
82+
# and methods of classes not defined within a function can be deferred.
7883
DeferredNode = NamedTuple(
7984
'DeferredNode',
8085
[
81-
# In batch mode only FuncDef and LambdaExpr are supported
82-
('node', Union[FuncDef, LambdaExpr, MypyFile, OverloadedFuncDef]),
86+
('node', DeferredNodeType),
8387
('context_type_name', Optional[str]), # Name of the surrounding class (for error messages)
8488
('active_typeinfo', Optional[TypeInfo]), # And its TypeInfo (for semantic analysis
8589
# self type handling)
8690
])
8791

92+
# Same as above, but for fine-grained mode targets. Only top-level functions/methods
93+
# and module top levels are allowed as such.
94+
FineGrainedDeferredNode = NamedTuple(
95+
'FineDeferredNode',
96+
[
97+
('node', FineGrainedDeferredNodeType),
98+
('context_type_name', Optional[str]),
99+
('active_typeinfo', Optional[TypeInfo]),
100+
])
88101

89102
# Data structure returned by find_isinstance_check representing
90103
# information learned from the truth or falsehood of a condition. The
@@ -283,7 +296,10 @@ def check_first_pass(self) -> None:
283296

284297
self.tscope.leave()
285298

286-
def check_second_pass(self, todo: Optional[List[DeferredNode]] = None) -> bool:
299+
def check_second_pass(self,
300+
todo: Optional[Sequence[Union[DeferredNode,
301+
FineGrainedDeferredNode]]] = None
302+
) -> bool:
287303
"""Run second or following pass of type checking.
288304
289305
This goes through deferred nodes, returning True if there were any.
@@ -300,7 +316,7 @@ def check_second_pass(self, todo: Optional[List[DeferredNode]] = None) -> bool:
300316
else:
301317
assert not self.deferred_nodes
302318
self.deferred_nodes = []
303-
done = set() # type: Set[Union[FuncDef, LambdaExpr, MypyFile, OverloadedFuncDef]]
319+
done = set() # type: Set[Union[DeferredNodeType, FineGrainedDeferredNodeType]]
304320
for node, type_name, active_typeinfo in todo:
305321
if node in done:
306322
continue
@@ -314,10 +330,7 @@ def check_second_pass(self, todo: Optional[List[DeferredNode]] = None) -> bool:
314330
self.tscope.leave()
315331
return True
316332

317-
def check_partial(self, node: Union[FuncDef,
318-
LambdaExpr,
319-
MypyFile,
320-
OverloadedFuncDef]) -> None:
333+
def check_partial(self, node: Union[DeferredNodeType, FineGrainedDeferredNodeType]) -> None:
321334
if isinstance(node, MypyFile):
322335
self.check_top_level(node)
323336
else:
@@ -338,20 +351,32 @@ def check_top_level(self, node: MypyFile) -> None:
338351
assert not self.current_node_deferred
339352
# TODO: Handle __all__
340353

354+
def defer_node(self, node: DeferredNodeType, enclosing_class: Optional[TypeInfo]) -> None:
355+
"""Defer a node for processing during next type-checking pass.
356+
357+
Args:
358+
node: function/method being deferred
359+
enclosing_class: for methods, the class where the method is defined
360+
NOTE: this can't handle nested functions/methods.
361+
"""
362+
if self.errors.type_name:
363+
type_name = self.errors.type_name[-1]
364+
else:
365+
type_name = None
366+
# We don't freeze the entire scope since only top-level functions and methods
367+
# can be deferred. Only module/class level scope information is needed.
368+
# Module-level scope information is preserved in the TypeChecker instance.
369+
self.deferred_nodes.append(DeferredNode(node, type_name, enclosing_class))
370+
341371
def handle_cannot_determine_type(self, name: str, context: Context) -> None:
342372
node = self.scope.top_non_lambda_function()
343373
if self.pass_num < self.last_pass and isinstance(node, FuncDef):
344374
# Don't report an error yet. Just defer. Note that we don't defer
345375
# lambdas because they are coupled to the surrounding function
346376
# through the binder and the inferred type of the lambda, so it
347377
# would get messy.
348-
if self.errors.type_name:
349-
type_name = self.errors.type_name[-1]
350-
else:
351-
type_name = None
352-
# Shouldn't we freeze the entire scope?
353378
enclosing_class = self.scope.enclosing_class()
354-
self.deferred_nodes.append(DeferredNode(node, type_name, enclosing_class))
379+
self.defer_node(node, enclosing_class)
355380
# Set a marker so that we won't infer additional types in this
356381
# function. Any inferred types could be bogus, because there's at
357382
# least one type that we don't know.
@@ -1256,15 +1281,26 @@ def expand_typevars(self, defn: FuncItem,
12561281
else:
12571282
return [(defn, typ)]
12581283

1259-
def check_method_override(self, defn: Union[FuncBase, Decorator]) -> None:
1260-
"""Check if function definition is compatible with base classes."""
1284+
def check_method_override(self, defn: Union[FuncDef, OverloadedFuncDef, Decorator]) -> None:
1285+
"""Check if function definition is compatible with base classes.
1286+
1287+
This may defer the method if a signature is not available in at least one base class.
1288+
"""
12611289
# Check against definitions in base classes.
12621290
for base in defn.info.mro[1:]:
1263-
self.check_method_or_accessor_override_for_base(defn, base)
1291+
if self.check_method_or_accessor_override_for_base(defn, base):
1292+
# Node was deferred, we will have another attempt later.
1293+
return
1294+
1295+
def check_method_or_accessor_override_for_base(self, defn: Union[FuncDef,
1296+
OverloadedFuncDef,
1297+
Decorator],
1298+
base: TypeInfo) -> bool:
1299+
"""Check if method definition is compatible with a base class.
12641300
1265-
def check_method_or_accessor_override_for_base(self, defn: Union[FuncBase, Decorator],
1266-
base: TypeInfo) -> None:
1267-
"""Check if method definition is compatible with a base class."""
1301+
Return True if the node was deferred because one of the corresponding
1302+
superclass nodes is not ready.
1303+
"""
12681304
if base:
12691305
name = defn.name()
12701306
base_attr = base.names.get(name)
@@ -1280,19 +1316,26 @@ def check_method_or_accessor_override_for_base(self, defn: Union[FuncBase, Decor
12801316
if name not in ('__init__', '__new__', '__init_subclass__'):
12811317
# Check method override
12821318
# (__init__, __new__, __init_subclass__ are special).
1283-
self.check_method_override_for_base_with_name(defn, name, base)
1319+
if self.check_method_override_for_base_with_name(defn, name, base):
1320+
return True
12841321
if name in nodes.inplace_operator_methods:
12851322
# Figure out the name of the corresponding operator method.
12861323
method = '__' + name[3:]
12871324
# An inplace operator method such as __iadd__ might not be
12881325
# always introduced safely if a base class defined __add__.
12891326
# TODO can't come up with an example where this is
12901327
# necessary; now it's "just in case"
1291-
self.check_method_override_for_base_with_name(defn, method,
1292-
base)
1328+
return self.check_method_override_for_base_with_name(defn, method,
1329+
base)
1330+
return False
12931331

12941332
def check_method_override_for_base_with_name(
1295-
self, defn: Union[FuncBase, Decorator], name: str, base: TypeInfo) -> None:
1333+
self, defn: Union[FuncDef, OverloadedFuncDef, Decorator],
1334+
name: str, base: TypeInfo) -> bool:
1335+
"""Check if overriding an attribute `name` of `base` with `defn` is valid.
1336+
1337+
Return True if the supertype node was not analysed yet, and `defn` was deferred.
1338+
"""
12961339
base_attr = base.names.get(name)
12971340
if base_attr:
12981341
# The name of the method is defined in the base class.
@@ -1305,7 +1348,7 @@ def check_method_override_for_base_with_name(
13051348
context = defn.func
13061349

13071350
# Construct the type of the overriding method.
1308-
if isinstance(defn, FuncBase):
1351+
if isinstance(defn, (FuncDef, OverloadedFuncDef)):
13091352
typ = self.function_type(defn) # type: Type
13101353
override_class_or_static = defn.is_class or defn.is_static
13111354
else:
@@ -1320,13 +1363,18 @@ def check_method_override_for_base_with_name(
13201363
original_type = base_attr.type
13211364
original_node = base_attr.node
13221365
if original_type is None:
1323-
if isinstance(original_node, FuncBase):
1366+
if self.pass_num < self.last_pass:
1367+
# If there are passes left, defer this node until next pass,
1368+
# otherwise try reconstructing the method type from available information.
1369+
self.defer_node(defn, defn.info)
1370+
return True
1371+
elif isinstance(original_node, (FuncDef, OverloadedFuncDef)):
13241372
original_type = self.function_type(original_node)
13251373
elif isinstance(original_node, Decorator):
13261374
original_type = self.function_type(original_node.func)
13271375
else:
13281376
assert False, str(base_attr.node)
1329-
if isinstance(original_node, FuncBase):
1377+
if isinstance(original_node, (FuncDef, OverloadedFuncDef)):
13301378
original_class_or_static = original_node.is_class or original_node.is_static
13311379
elif isinstance(original_node, Decorator):
13321380
fdef = original_node.func
@@ -1362,6 +1410,7 @@ def check_method_override_for_base_with_name(
13621410
else:
13631411
self.msg.signature_incompatible_with_supertype(
13641412
defn.name(), name, base.name(), context)
1413+
return False
13651414

13661415
def get_op_other_domain(self, tp: FunctionLike) -> Optional[Type]:
13671416
if isinstance(tp, CallableType):

mypy/semanal.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -317,7 +317,7 @@ def visit_file(self, file_node: MypyFile, fnam: str, options: Options,
317317
del self.cur_mod_node
318318
del self.globals
319319

320-
def refresh_partial(self, node: Union[MypyFile, FuncItem, OverloadedFuncDef],
320+
def refresh_partial(self, node: Union[MypyFile, FuncDef, OverloadedFuncDef],
321321
patches: List[Tuple[int, Callable[[], None]]]) -> None:
322322
"""Refresh a stale target in fine-grained incremental mode."""
323323
self.patches = patches

mypy/semanal_pass3.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ def visit_file(self, file_node: MypyFile, fnam: str, options: Options,
7373
del self.cur_mod_node
7474
self.patches = []
7575

76-
def refresh_partial(self, node: Union[MypyFile, FuncItem, OverloadedFuncDef],
76+
def refresh_partial(self, node: Union[MypyFile, FuncDef, OverloadedFuncDef],
7777
patches: List[Tuple[int, Callable[[], None]]]) -> None:
7878
"""Refresh a stale target in fine-grained incremental mode."""
7979
self.options = self.sem.options

mypy/server/aststrip.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@
5252
from mypy.typestate import TypeState
5353

5454

55-
def strip_target(node: Union[MypyFile, FuncItem, OverloadedFuncDef]) -> None:
55+
def strip_target(node: Union[MypyFile, FuncDef, OverloadedFuncDef]) -> None:
5656
"""Reset a fine-grained incremental target to state after semantic analysis pass 1.
5757
5858
NOTE: Currently we opportunistically only reset changes that are known to otherwise

mypy/server/update.py

+11-13
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@
122122
BuildManager, State, BuildSource, BuildResult, Graph, load_graph,
123123
process_fresh_modules, DEBUG_FINE_GRAINED,
124124
)
125-
from mypy.checker import DeferredNode
125+
from mypy.checker import FineGrainedDeferredNode
126126
from mypy.errors import CompileError
127127
from mypy.nodes import (
128128
MypyFile, FuncDef, TypeInfo, SymbolNode, Decorator,
@@ -780,15 +780,15 @@ def find_targets_recursive(
780780
graph: Graph,
781781
triggers: Set[str],
782782
deps: Dict[str, Set[str]],
783-
up_to_date_modules: Set[str]) -> Tuple[Dict[str, Set[DeferredNode]],
783+
up_to_date_modules: Set[str]) -> Tuple[Dict[str, Set[FineGrainedDeferredNode]],
784784
Set[str], Set[TypeInfo]]:
785785
"""Find names of all targets that need to reprocessed, given some triggers.
786786
787787
Returns: A tuple containing a:
788788
* Dictionary from module id to a set of stale targets.
789789
* A set of module ids for unparsed modules with stale targets.
790790
"""
791-
result = {} # type: Dict[str, Set[DeferredNode]]
791+
result = {} # type: Dict[str, Set[FineGrainedDeferredNode]]
792792
worklist = triggers
793793
processed = set() # type: Set[str]
794794
stale_protos = set() # type: Set[TypeInfo]
@@ -834,7 +834,7 @@ def find_targets_recursive(
834834
def reprocess_nodes(manager: BuildManager,
835835
graph: Dict[str, State],
836836
module_id: str,
837-
nodeset: Set[DeferredNode],
837+
nodeset: Set[FineGrainedDeferredNode],
838838
deps: Dict[str, Set[str]]) -> Set[str]:
839839
"""Reprocess a set of nodes within a single module.
840840
@@ -850,7 +850,7 @@ def reprocess_nodes(manager: BuildManager,
850850
old_symbols = {name: names.copy() for name, names in old_symbols.items()}
851851
old_symbols_snapshot = snapshot_symbol_table(file_node.fullname(), file_node.names)
852852

853-
def key(node: DeferredNode) -> int:
853+
def key(node: FineGrainedDeferredNode) -> int:
854854
# Unlike modules which are sorted by name within SCC,
855855
# nodes within the same module are sorted by line number, because
856856
# this is how they are processed in normal mode.
@@ -959,7 +959,7 @@ def find_symbol_tables_recursive(prefix: str, symbols: SymbolTable) -> Dict[str,
959959

960960

961961
def update_deps(module_id: str,
962-
nodes: List[DeferredNode],
962+
nodes: List[FineGrainedDeferredNode],
963963
graph: Dict[str, State],
964964
deps: Dict[str, Set[str]],
965965
options: Options) -> None:
@@ -977,7 +977,7 @@ def update_deps(module_id: str,
977977

978978

979979
def lookup_target(manager: BuildManager,
980-
target: str) -> Tuple[List[DeferredNode], Optional[TypeInfo]]:
980+
target: str) -> Tuple[List[FineGrainedDeferredNode], Optional[TypeInfo]]:
981981
"""Look up a target by fully-qualified name.
982982
983983
The first item in the return tuple is a list of deferred nodes that
@@ -1025,7 +1025,7 @@ def not_found() -> None:
10251025
# a deserialized TypeInfo with missing attributes.
10261026
not_found()
10271027
return [], None
1028-
result = [DeferredNode(file, None, None)]
1028+
result = [FineGrainedDeferredNode(file, None, None)]
10291029
stale_info = None # type: Optional[TypeInfo]
10301030
if node.is_protocol:
10311031
stale_info = node
@@ -1050,15 +1050,15 @@ def not_found() -> None:
10501050
# context will be wrong and it could be a partially initialized deserialized node.
10511051
not_found()
10521052
return [], None
1053-
return [DeferredNode(node, active_class_name, active_class)], None
1053+
return [FineGrainedDeferredNode(node, active_class_name, active_class)], None
10541054

10551055

10561056
def is_verbose(manager: BuildManager) -> bool:
10571057
return manager.options.verbosity >= 1 or DEBUG_FINE_GRAINED
10581058

10591059

10601060
def target_from_node(module: str,
1061-
node: Union[FuncDef, MypyFile, OverloadedFuncDef, LambdaExpr]
1061+
node: Union[FuncDef, MypyFile, OverloadedFuncDef]
10621062
) -> Optional[str]:
10631063
"""Return the target name corresponding to a deferred node.
10641064
@@ -1073,10 +1073,8 @@ def target_from_node(module: str,
10731073
# Actually a reference to another module -- likely a stale dependency.
10741074
return None
10751075
return module
1076-
elif isinstance(node, (OverloadedFuncDef, FuncDef)):
1076+
else: # OverloadedFuncDef or FuncDef
10771077
if node.info:
10781078
return '%s.%s' % (node.info.fullname(), node.name())
10791079
else:
10801080
return '%s.%s' % (module, node.name())
1081-
else:
1082-
assert False, "Lambda expressions can't be deferred in fine-grained incremental mode"

0 commit comments

Comments
 (0)