Skip to content

Commit 0c12b21

Browse files
authored
Fine-grained: Don't infer partial types from multiple targets (#4553)
Previously if partial types were inferred from assignments in multiple scopes, like in the example below, fine-grained incremental mode could display bogus messages, since only one of the scopes could be reprocessed in one incremental update: ``` x = None def f() -> None: global x x = '' ``` This PR fixes the issue by always inferring partial types within a single fine-grained incremental target. This is always enabled in fine-grained incremental mode and can also be turned on in other modes through a new (but hidden) `--local-partial-types` option. This was complicated by various edge cases that are covered by the new test cases and/or documented in comments. If the new option is not enabled, the old behavior should (mostly) be preserved, module some fixes that may affect also the default mode. Work towards #4492.
1 parent c294efa commit 0c12b21

12 files changed

+471
-20
lines changed

mypy/checker.py

Lines changed: 86 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,12 @@
107107
('is_upper_bound', bool), # False => precise type
108108
])
109109

110+
# Keeps track of partial types in a single scope. In fine-grained incremental
111+
# mode partial types initially defined at the top level cannot be completed in
112+
# a function, and we use the 'is_function' attribute to enforce this.
113+
PartialTypeScope = NamedTuple('PartialTypeScope', [('map', Dict[Var, Context]),
114+
('is_function', bool)])
115+
110116

111117
class TypeChecker(NodeVisitor[None], CheckerPluginInterface):
112118
"""Mypy type checker.
@@ -136,7 +142,7 @@ class TypeChecker(NodeVisitor[None], CheckerPluginInterface):
136142
# Flags; true for dynamically typed functions
137143
dynamic_funcs = None # type: List[bool]
138144
# Stack of collections of variables with partial types
139-
partial_types = None # type: List[Dict[Var, Context]]
145+
partial_types = None # type: List[PartialTypeScope]
140146
# Vars for which partial type errors are already reported
141147
# (to avoid logically duplicate errors with different error context).
142148
partial_reported = None # type: Set[Var]
@@ -632,7 +638,7 @@ def check_func_item(self, defn: FuncItem,
632638
self.dynamic_funcs.append(defn.is_dynamic() and not type_override)
633639

634640
with self.errors.enter_function(fdef.name()) if fdef else nothing():
635-
with self.enter_partial_types():
641+
with self.enter_partial_types(is_function=True):
636642
typ = self.function_type(defn)
637643
if type_override:
638644
typ = type_override.copy_modified(line=typ.line, column=typ.column)
@@ -1244,7 +1250,7 @@ def visit_class_def(self, defn: ClassDef) -> None:
12441250
typ = defn.info
12451251
if typ.is_protocol and typ.defn.type_vars:
12461252
self.check_protocol_variance(defn)
1247-
with self.errors.enter_type(defn.name), self.enter_partial_types():
1253+
with self.errors.enter_type(defn.name), self.enter_partial_types(is_class=True):
12481254
old_binder = self.binder
12491255
self.binder = ConditionalTypeBinder()
12501256
with self.binder.top_frame_context():
@@ -1487,7 +1493,7 @@ def check_assignment(self, lvalue: Lvalue, rvalue: Expression, infer_lvalue_type
14871493
lvalue_type.item.type.is_protocol)):
14881494
self.msg.concrete_only_assign(lvalue_type, rvalue)
14891495
return
1490-
if rvalue_type and infer_lvalue_type:
1496+
if rvalue_type and infer_lvalue_type and not isinstance(lvalue_type, PartialType):
14911497
self.binder.assign_type(lvalue, rvalue_type, lvalue_type, False)
14921498
elif index_lvalue:
14931499
self.check_indexed_assignment(index_lvalue, rvalue, lvalue)
@@ -1996,7 +2002,7 @@ def infer_partial_type(self, name: Var, lvalue: Lvalue, init_type: Type) -> bool
19962002
else:
19972003
return False
19982004
self.set_inferred_type(name, lvalue, partial_type)
1999-
self.partial_types[-1][name] = lvalue
2005+
self.partial_types[-1].map[name] = lvalue
20002006
return True
20012007

20022008
def set_inferred_type(self, var: Var, lvalue: Lvalue, type: Type) -> None:
@@ -3111,33 +3117,99 @@ def lookup_qualified(self, name: str) -> SymbolTableNode:
31113117
raise KeyError(msg.format(last, name))
31123118

31133119
@contextmanager
3114-
def enter_partial_types(self) -> Iterator[None]:
3120+
def enter_partial_types(self, *, is_function: bool = False,
3121+
is_class: bool = False) -> Iterator[None]:
31153122
"""Enter a new scope for collecting partial types.
31163123
3117-
Also report errors for variables which still have partial
3124+
Also report errors for (some) variables which still have partial
31183125
types, i.e. we couldn't infer a complete type.
31193126
"""
3120-
self.partial_types.append({})
3127+
self.partial_types.append(PartialTypeScope({}, is_function))
31213128
yield
31223129

3123-
partial_types = self.partial_types.pop()
3130+
partial_types, _ = self.partial_types.pop()
31243131
if not self.current_node_deferred:
31253132
for var, context in partial_types.items():
3126-
if isinstance(var.type, PartialType) and var.type.type is None:
3127-
# None partial type: assume variable is intended to have type None
3133+
# If we require local partial types, there are a few exceptions where
3134+
# we fall back to inferring just "None" as the type from a None initaliazer:
3135+
#
3136+
# 1. If all happens within a single function this is acceptable, since only
3137+
# the topmost function is a separate target in fine-grained incremental mode.
3138+
# We primarily want to avoid "splitting" partial types across targets.
3139+
#
3140+
# 2. A None initializer in the class body if the attribute is defined in a base
3141+
# class is fine, since the attribute is already defined and it's currently okay
3142+
# to vary the type of an attribute covariantly. The None type will still be
3143+
# checked for compatibility with base classes elsewhere. Without this exception
3144+
# mypy could require an annotation for an attribute that already has been
3145+
# declared in a base class, which would be bad.
3146+
allow_none = (not self.options.local_partial_types
3147+
or is_function
3148+
or (is_class and self.is_defined_in_base_class(var)))
3149+
if (allow_none
3150+
and isinstance(var.type, PartialType)
3151+
and var.type.type is None):
31283152
var.type = NoneTyp()
31293153
else:
31303154
if var not in self.partial_reported:
31313155
self.msg.need_annotation_for_var(var, context)
31323156
self.partial_reported.add(var)
3157+
# Give the variable an 'Any' type to avoid generating multiple errors
3158+
# from a single missing annotation.
31333159
var.type = AnyType(TypeOfAny.from_error)
31343160

3161+
def is_defined_in_base_class(self, var: Var) -> bool:
3162+
if var.info is not None:
3163+
for base in var.info.mro[1:]:
3164+
if base.get(var.name()) is not None:
3165+
return True
3166+
if var.info.fallback_to_any:
3167+
return True
3168+
return False
3169+
31353170
def find_partial_types(self, var: Var) -> Optional[Dict[Var, Context]]:
3136-
for partial_types in reversed(self.partial_types):
3137-
if var in partial_types:
3138-
return partial_types
3171+
"""Look for an active partial type scope containing variable.
3172+
3173+
A scope is active if assignments in the current context can refine a partial
3174+
type originally defined in the scope. This is affected by the local_partial_types
3175+
configuration option.
3176+
"""
3177+
in_scope, partial_types = self.find_partial_types_in_all_scopes(var)
3178+
if in_scope:
3179+
return partial_types
31393180
return None
31403181

3182+
def find_partial_types_in_all_scopes(self, var: Var) -> Tuple[bool,
3183+
Optional[Dict[Var, Context]]]:
3184+
"""Look for partial type scope containing variable.
3185+
3186+
Return tuple (is the scope active, scope).
3187+
"""
3188+
active = self.partial_types
3189+
inactive = [] # type: List[PartialTypeScope]
3190+
if self.options.local_partial_types:
3191+
# All scopes within the outermost function are active. Scopes out of
3192+
# the outermost function are inactive to allow local reasoning (important
3193+
# for fine-grained incremental mode).
3194+
for i, t in enumerate(self.partial_types):
3195+
if t.is_function:
3196+
active = self.partial_types[i:]
3197+
inactive = self.partial_types[:i]
3198+
break
3199+
else:
3200+
# Not within a function -- only the innermost scope is in scope.
3201+
active = self.partial_types[-1:]
3202+
inactive = self.partial_types[:-1]
3203+
# First look within in-scope partial types.
3204+
for scope in reversed(active):
3205+
if var in scope.map:
3206+
return True, scope.map
3207+
# Then for out-of-scope partial types.
3208+
for scope in reversed(inactive):
3209+
if var in scope.map:
3210+
return False, scope.map
3211+
return False, None
3212+
31413213
def temp_node(self, t: Type, context: Optional[Context] = None) -> TempNode:
31423214
"""Create a temporary node with the given, fixed type."""
31433215
temp = TempNode(t)

mypy/checkexpr.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -136,16 +136,20 @@ def analyze_ref_expr(self, e: RefExpr, lvalue: bool = False) -> Type:
136136
# Variable reference.
137137
result = self.analyze_var_ref(node, e)
138138
if isinstance(result, PartialType):
139-
if result.type is None:
139+
in_scope, partial_types = self.chk.find_partial_types_in_all_scopes(node)
140+
if result.type is None and in_scope:
140141
# 'None' partial type. It has a well-defined type. In an lvalue context
141142
# we want to preserve the knowledge of it being a partial type.
142143
if not lvalue:
143144
result = NoneTyp()
144145
else:
145-
partial_types = self.chk.find_partial_types(node)
146146
if partial_types is not None and not self.chk.current_node_deferred:
147-
context = partial_types[node]
148-
self.msg.need_annotation_for_var(node, context)
147+
if in_scope:
148+
context = partial_types[node]
149+
self.msg.need_annotation_for_var(node, context)
150+
else:
151+
# Defer the node -- we might get a better type in the outer scope
152+
self.chk.handle_cannot_determine_type(node.name(), e)
149153
result = AnyType(TypeOfAny.special_form)
150154
elif isinstance(node, FuncDef):
151155
# Reference to a global function.

mypy/dmypy_server.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,9 @@ def __init__(self, flags: List[str]) -> None:
111111
options.cache_fine_grained = True # set this so that cache options match
112112
else:
113113
options.cache_dir = os.devnull
114+
# Fine-grained incremental doesn't support general partial types
115+
# (details in https://github.com/python/mypy/issues/4492)
116+
options.local_partial_types = True
114117

115118
def serve(self) -> None:
116119
"""Serve requests, synchronously (no thread or fork)."""
@@ -180,7 +183,7 @@ def cmd_stop(self) -> Dict[str, object]:
180183
"""Stop daemon."""
181184
return {}
182185

183-
last_sources = None
186+
last_sources = None # type: List[mypy.build.BuildSource]
184187

185188
def cmd_check(self, files: Sequence[str]) -> Dict[str, object]:
186189
"""Check a list of files."""

mypy/main.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -370,6 +370,9 @@ def add_invertible_flag(flag: str,
370370
parser.add_argument('--dump-graph', action='store_true', help=argparse.SUPPRESS)
371371
# --semantic-analysis-only does exactly that.
372372
parser.add_argument('--semantic-analysis-only', action='store_true', help=argparse.SUPPRESS)
373+
# --local-partial-types disallows partial types spanning module top level and a function
374+
# (implicitly defined in fine-grained incremental mode)
375+
parser.add_argument('--local-partial-types', action='store_true', help=argparse.SUPPRESS)
373376
# deprecated options
374377
parser.add_argument('--disallow-any', dest='special-opts:disallow_any',
375378
help=argparse.SUPPRESS)

mypy/options.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,8 @@ def __init__(self) -> None:
174174
self.show_column_numbers = False # type: bool
175175
self.dump_graph = False
176176
self.dump_deps = False
177+
# If True, partial types can't span a module top level and a function
178+
self.local_partial_types = False
177179

178180
def __eq__(self, other: object) -> bool:
179181
return self.__class__ == other.__class__ and self.__dict__ == other.__dict__

mypy/test/testdmypy.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ def run_case_once(self, testcase: DataDrivenTestCase, incremental_step: int) ->
121121
if 'fine-grained' in testcase.file:
122122
server_options.append('--experimental')
123123
options.fine_grained_incremental = True
124+
options.local_partial_types = True
124125
self.server = dmypy_server.Server(server_options) # TODO: Fix ugly API
125126
self.server.options = options
126127

mypy/test/testfinegrained.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@ def build(self,
142142
options.fine_grained_incremental = not build_cache
143143
options.use_fine_grained_cache = enable_cache and not build_cache
144144
options.cache_fine_grained = enable_cache
145+
options.local_partial_types = True
145146

146147
main_path = os.path.join(test_temp_dir, 'main')
147148
with open(main_path, 'w') as f:

test-data/unit/check-dmypy-fine-grained.test

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,3 +181,21 @@ tmp/a.py:1: error: "int" not callable
181181
[delete nonexistent_stub.pyi.2]
182182
[out1]
183183
[out2]
184+
185+
[case testPartialNoneTypeFineGrainedIncremental]
186+
# cmd: mypy -m a b
187+
[file a.py]
188+
import b
189+
b.y
190+
x = None
191+
def f() -> None:
192+
global x
193+
x = ''
194+
[file b.py]
195+
y = 0
196+
[file b.py.2]
197+
y = ''
198+
[out1]
199+
tmp/a.py:3: error: Need type annotation for 'x'
200+
[out2]
201+
tmp/a.py:3: error: Need type annotation for 'x'

0 commit comments

Comments
 (0)