Skip to content

Commit fae0084

Browse files
authored
Unify empty context fallback in assignments (#20851)
Fixes #11455 (kind of accidentally) As we discussed with @JukkaL before, using full type context as a fallback will help with #19918 for `--allow-redefinition-new`. This allowed to fix most errors on self-check, so that we can now switch at any moment (when performance is adequate). Implementation is mostly straightforward, it is even smaller than it looks. It is mostly moving blocks of code around and adding/updating comments. Now we have just two `accept()` calls. I unify the fallback logic for the two cases: * Redefinition + invalid inferred type in first accept * Union type context (this was added a while ago to help with various common patterns) I didn't to detailed performance measurements, but i don't see visible difference on average of 5 runs. (Also in default mode there are should be only very minor semantic changes in edge cases.)
1 parent b972d9a commit fae0084

File tree

9 files changed

+210
-100
lines changed

9 files changed

+210
-100
lines changed

mypy/binder.py

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -312,46 +312,51 @@ def update_from_options(self, frames: list[Frame]) -> bool:
312312
keys = list(set(keys))
313313
for key in keys:
314314
current_value = self._get(key)
315-
resulting_values = [f.types.get(key, current_value) for f in frames]
315+
all_resulting_values = [f.types.get(key, current_value) for f in frames]
316316
# Keys can be narrowed using two different semantics. The new semantics
317317
# is enabled for plain variables when bind_all is true, and it allows
318318
# variable types to be widened using subsequent assignments. This is
319319
# tricky to support for instance attributes (primarily due to deferrals),
320320
# so we don't use it for them.
321321
old_semantics = not self.bind_all or extract_var_from_literal_hash(key) is None
322-
if old_semantics and any(x is None for x in resulting_values):
322+
if old_semantics and any(x is None for x in all_resulting_values):
323323
# We didn't know anything about key before
324324
# (current_value must be None), and we still don't
325325
# know anything about key in at least one possible frame.
326326
continue
327327

328-
resulting_values = [x for x in resulting_values if x is not None]
328+
resulting_values = [x for x in all_resulting_values if x is not None]
329329

330-
if all_reachable and all(
331-
x is not None and not x.from_assignment for x in resulting_values
332-
):
330+
if all_reachable and all(not x.from_assignment for x in resulting_values):
333331
# Do not synthesize a new type if we encountered a conditional block
334332
# (if, while or match-case) without assignments.
335333
# See check-isinstance.test::testNoneCheckDoesNotMakeTypeVarOptional
336334
# This is a safe assumption: the fact that we checked something with `is`
337335
# or `isinstance` does not change the type of the value.
338336
continue
339337

340-
current_type = resulting_values[0]
341-
assert current_type is not None
342-
type = current_type.type
338+
# Remove exact duplicates to save pointless work later, this is
339+
# a micro-optimization for --allow-redefinition-new.
340+
seen_types = set()
341+
resulting_types = []
342+
for rv in resulting_values:
343+
assert rv is not None
344+
if rv.type in seen_types:
345+
continue
346+
resulting_types.append(rv.type)
347+
seen_types.add(rv.type)
348+
349+
type = resulting_types[0]
343350
declaration_type = get_proper_type(self.declarations.get(key))
344351
if isinstance(declaration_type, AnyType):
345352
# At this point resulting values can't contain None, see continue above
346-
if not all(
347-
t is not None and is_same_type(type, t.type) for t in resulting_values[1:]
348-
):
353+
if not all(is_same_type(type, t) for t in resulting_types[1:]):
349354
type = AnyType(TypeOfAny.from_another_any, source_any=declaration_type)
350355
else:
351356
possible_types = []
352-
for t in resulting_values:
357+
for t in resulting_types:
353358
assert t is not None
354-
possible_types.append(t.type)
359+
possible_types.append(t)
355360
if len(possible_types) == 1:
356361
# This is to avoid calling get_proper_type() unless needed, as this may
357362
# interfere with our (hacky) TypeGuard support.

mypy/checker.py

Lines changed: 138 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -4619,7 +4619,7 @@ def set_inference_error_fallback_type(self, var: Var, lvalue: Lvalue, type: Type
46194619
"""Store best known type for variable if type inference failed.
46204620
46214621
If a program ignores error on type inference error, the variable should get some
4622-
inferred type so that it can used later on in the program. Example:
4622+
inferred type so that it can be used later on in the program. Example:
46234623
46244624
x = [] # type: ignore
46254625
x.append(1) # Should be ok!
@@ -4654,6 +4654,82 @@ def simple_rvalue(self, rvalue: Expression) -> bool:
46544654
return not any(item.variables for item in typ.items)
46554655
return False
46564656

4657+
def infer_rvalue_with_fallback_context(
4658+
self,
4659+
lvalue_type: Type | None,
4660+
rvalue: Expression,
4661+
preferred_context: Type | None,
4662+
fallback_context: Type | None,
4663+
inferred: Var | None,
4664+
always_allow_any: bool,
4665+
) -> Type:
4666+
"""Infer rvalue type in two type context and select the best one.
4667+
4668+
See comments below for supported fallback scenarios.
4669+
"""
4670+
assert fallback_context is not preferred_context
4671+
# TODO: make assignment checking correct in presence of walrus in r.h.s.
4672+
# We may accept r.h.s. twice. In presence of walrus this can lead to weird
4673+
# false negatives and "back action". A proper solution would be to use
4674+
# binder.accumulate_type_assignments() and assign the types inferred for type
4675+
# context that is ultimately used. This is however tricky with redefinitions.
4676+
# For now we simply disable second accept in cases known to cause problems,
4677+
# see e.g. testAssignToOptionalTupleWalrus.
4678+
binder_version = self.binder.version
4679+
4680+
fallback_context_used = False
4681+
with (
4682+
self.msg.filter_errors(save_filtered_errors=True) as local_errors,
4683+
self.local_type_map as type_map,
4684+
):
4685+
rvalue_type = self.expr_checker.accept(
4686+
rvalue, type_context=preferred_context, always_allow_any=always_allow_any
4687+
)
4688+
4689+
# There are two cases where we want to try re-inferring r.h.s. in a fallback
4690+
# type context. First case is when redefinitions are allowed, and we got
4691+
# invalid type when using the preferred (empty) type context.
4692+
redefinition_fallback = inferred is not None and not is_valid_inferred_type(
4693+
rvalue_type, self.options
4694+
)
4695+
# Try re-inferring r.h.s. in empty context for union with explicit annotation,
4696+
# and use it results in a narrower type. This helps with various practical
4697+
# examples, see e.g. testOptionalTypeNarrowedByGenericCall.
4698+
union_fallback = (
4699+
inferred is None
4700+
and isinstance(get_proper_type(lvalue_type), UnionType)
4701+
and binder_version == self.binder.version
4702+
)
4703+
4704+
# Skip literal types, as they have special logic (for better errors).
4705+
if (redefinition_fallback or union_fallback) and not is_literal_type_like(rvalue_type):
4706+
with (
4707+
self.msg.filter_errors(save_filtered_errors=True) as alt_local_errors,
4708+
self.local_type_map as alt_type_map,
4709+
):
4710+
alt_rvalue_type = self.expr_checker.accept(
4711+
rvalue, fallback_context, always_allow_any=always_allow_any
4712+
)
4713+
if (
4714+
not alt_local_errors.has_new_errors()
4715+
and is_valid_inferred_type(alt_rvalue_type, self.options)
4716+
and (
4717+
# For redefinition fallback we are fine getting not a subtype.
4718+
redefinition_fallback
4719+
# Skip Any type, since it is special cased in binder.
4720+
or not isinstance(get_proper_type(alt_rvalue_type), AnyType)
4721+
and is_proper_subtype(alt_rvalue_type, rvalue_type)
4722+
)
4723+
):
4724+
fallback_context_used = True
4725+
rvalue_type = alt_rvalue_type
4726+
self.store_types(alt_type_map)
4727+
4728+
if not fallback_context_used:
4729+
self.msg.add_errors(local_errors.filtered_errors())
4730+
self.store_types(type_map)
4731+
return rvalue_type
4732+
46574733
def check_simple_assignment(
46584734
self,
46594735
lvalue_type: Type | None,
@@ -4674,39 +4750,45 @@ def check_simple_assignment(
46744750
always_allow_any = lvalue_type is not None and not isinstance(
46754751
get_proper_type(lvalue_type), AnyType
46764752
)
4677-
if inferred is None or is_typeddict_type_context(lvalue_type):
4678-
type_context = lvalue_type
4753+
4754+
# If redefinitions are allowed (i.e. we have --allow-redefinition-new
4755+
# and a variable without annotation) then we start with an empty context,
4756+
# since this gives somewhat more intuitive behavior. The only exception
4757+
# is TypedDicts, they are often useless without context.
4758+
try_fallback = (
4759+
inferred is not None or isinstance(get_proper_type(lvalue_type), UnionType)
4760+
) and not self.simple_rvalue(rvalue)
4761+
4762+
if not try_fallback or lvalue_type is None or is_typeddict_type_context(lvalue_type):
4763+
rvalue_type = self.expr_checker.accept(
4764+
rvalue, type_context=lvalue_type, always_allow_any=always_allow_any
4765+
)
46794766
else:
4680-
type_context = None
4681-
4682-
# TODO: make assignment checking correct in presence of walrus in r.h.s.
4683-
# Right now we can accept the r.h.s. up to four(!) times. In presence of
4684-
# walrus this can result in weird false negatives and "back action". A proper
4685-
# solution would be to:
4686-
# * Refactor the code to reduce number of times we accept the r.h.s.
4687-
# (two should be enough: empty context + l.h.s. context).
4688-
# * For each accept use binder.accumulate_type_assignments() and assign
4689-
# the types inferred for context that is ultimately used.
4690-
# For now we simply disable some logic that is known to cause problems in
4691-
# presence of walrus, see e.g. testAssignToOptionalTupleWalrus.
4692-
binder_version = self.binder.version
4767+
if inferred is not None:
4768+
preferred = None
4769+
fallback = lvalue_type
4770+
else:
4771+
preferred = lvalue_type
4772+
fallback = None
4773+
4774+
rvalue_type = self.infer_rvalue_with_fallback_context(
4775+
lvalue_type, rvalue, preferred, fallback, inferred, always_allow_any
4776+
)
46934777

4694-
rvalue_type = self.expr_checker.accept(
4695-
rvalue, type_context=type_context, always_allow_any=always_allow_any
4696-
)
46974778
if (
4698-
lvalue_type is not None
4699-
and type_context is None
4779+
inferred is not None
47004780
and not is_valid_inferred_type(rvalue_type, self.options)
4701-
):
4702-
# Inference in an empty type context didn't produce a valid type, so
4703-
# try using lvalue type as context instead.
4704-
rvalue_type = self.expr_checker.accept(
4705-
rvalue, type_context=lvalue_type, always_allow_any=always_allow_any
4781+
and (
4782+
not inferred.type
4783+
or isinstance(inferred.type, PartialType)
4784+
# This additional check is to give an error instead of inferring
4785+
# a useless type like None | list[Never] in case of "double-partial"
4786+
# types that are not supported yet, see issue #20257.
4787+
or not is_proper_subtype(rvalue_type, inferred.type)
47064788
)
4707-
if not is_valid_inferred_type(rvalue_type, self.options) and inferred is not None:
4708-
self.msg.need_annotation_for_var(inferred, context, self.options)
4709-
rvalue_type = rvalue_type.accept(SetNothingToAny())
4789+
):
4790+
self.msg.need_annotation_for_var(inferred, inferred, self.options)
4791+
rvalue_type = rvalue_type.accept(SetNothingToAny())
47104792

47114793
if (
47124794
isinstance(lvalue, NameExpr)
@@ -4715,49 +4797,25 @@ def check_simple_assignment(
47154797
and not inferred.is_final
47164798
):
47174799
new_inferred = remove_instance_last_known_values(rvalue_type)
4718-
if not is_same_type(inferred.type, new_inferred):
4719-
# Should we widen the inferred type or the lvalue? Variables defined
4720-
# at module level or class bodies can't be widened in functions, or
4721-
# in another module.
4722-
if not self.refers_to_different_scope(lvalue):
4723-
lvalue_type = make_simplified_union([inferred.type, new_inferred])
4724-
if not is_same_type(lvalue_type, inferred.type) and not isinstance(
4725-
inferred.type, PartialType
4726-
):
4727-
# Widen the type to the union of original and new type.
4728-
self.widened_vars.append(inferred.name)
4729-
self.set_inferred_type(inferred, lvalue, lvalue_type)
4730-
self.binder.put(lvalue, rvalue_type)
4731-
# TODO: A bit hacky, maybe add a binder method that does put and
4732-
# updates declaration?
4733-
lit = literal_hash(lvalue)
4734-
if lit is not None:
4735-
self.binder.declarations[lit] = lvalue_type
4736-
if (
4737-
isinstance(get_proper_type(lvalue_type), UnionType)
4738-
# Skip literal types, as they have special logic (for better errors).
4739-
and not is_literal_type_like(rvalue_type)
4740-
and not self.simple_rvalue(rvalue)
4741-
and binder_version == self.binder.version
4742-
):
4743-
# Try re-inferring r.h.s. in empty context, and use that if it
4744-
# results in a narrower type. We don't do this always because this
4745-
# may cause some perf impact, plus we want to partially preserve
4746-
# the old behavior. This helps with various practical examples, see
4747-
# e.g. testOptionalTypeNarrowedByGenericCall.
4748-
with self.msg.filter_errors() as local_errors, self.local_type_map as type_map:
4749-
alt_rvalue_type = self.expr_checker.accept(
4750-
rvalue, None, always_allow_any=always_allow_any
4751-
)
4800+
# Should we widen the inferred type or the lvalue? Variables defined
4801+
# at module level or class bodies can't be widened in functions, or
4802+
# in another module.
47524803
if (
4753-
not local_errors.has_new_errors()
4754-
# Skip Any type, since it is special cased in binder.
4755-
and not isinstance(get_proper_type(alt_rvalue_type), AnyType)
4756-
and is_valid_inferred_type(alt_rvalue_type, self.options)
4757-
and is_proper_subtype(alt_rvalue_type, rvalue_type)
4804+
not self.refers_to_different_scope(lvalue)
4805+
and not isinstance(inferred.type, PartialType)
4806+
and not is_proper_subtype(new_inferred, inferred.type)
47584807
):
4759-
rvalue_type = alt_rvalue_type
4760-
self.store_types(type_map)
4808+
lvalue_type = make_simplified_union([inferred.type, new_inferred])
4809+
# Widen the type to the union of original and new type.
4810+
self.widened_vars.append(inferred.name)
4811+
self.set_inferred_type(inferred, lvalue, lvalue_type)
4812+
self.binder.put(lvalue, rvalue_type)
4813+
# TODO: A bit hacky, maybe add a binder method that does put and
4814+
# updates declaration?
4815+
lit = literal_hash(lvalue)
4816+
if lit is not None:
4817+
self.binder.declarations[lit] = lvalue_type
4818+
47614819
if isinstance(rvalue_type, DeletedType):
47624820
self.msg.deleted_as_rvalue(rvalue_type, context)
47634821
if isinstance(lvalue_type, DeletedType):
@@ -6065,7 +6123,7 @@ def partition_by_callable(
60656123

60666124
if isinstance(typ, UnionType):
60676125
callables = []
6068-
uncallables = []
6126+
uncallables: list[Type] = []
60696127
for subtype in typ.items:
60706128
# Use unsound_partition when handling unions in order to
60716129
# allow the expected type discrimination.
@@ -9479,11 +9537,15 @@ def ambiguous_enum_equality_keys(t: Type) -> set[str]:
94799537
return result
94809538

94819539

9482-
def is_typeddict_type_context(lvalue_type: Type | None) -> bool:
9483-
if lvalue_type is None:
9484-
return False
9485-
lvalue_proper = get_proper_type(lvalue_type)
9486-
return isinstance(lvalue_proper, TypedDictType)
9540+
def is_typeddict_type_context(lvalue_type: Type) -> bool:
9541+
lvalue_type = get_proper_type(lvalue_type)
9542+
if isinstance(lvalue_type, TypedDictType):
9543+
return True
9544+
if isinstance(lvalue_type, UnionType):
9545+
for item in lvalue_type.items:
9546+
if is_typeddict_type_context(item):
9547+
return True
9548+
return False
94879549

94889550

94899551
def is_method(node: SymbolNode | None) -> bool:

mypy/checkpattern.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -293,7 +293,7 @@ def visit_sequence_pattern(self, o: SequencePattern) -> PatternType:
293293
#
294294
unpack_index = None
295295
if isinstance(current_type, TupleType):
296-
inner_types = current_type.items
296+
inner_types: list[Type] = current_type.items
297297
unpack_index = find_unpack_in_list(inner_types)
298298
if unpack_index is None:
299299
size_diff = len(inner_types) - required_patterns

mypy/config_parser.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -551,7 +551,7 @@ def parse_section(
551551
dv = getattr(template, options_key, None)
552552
else:
553553
continue
554-
ct = type(dv)
554+
ct = type(dv) if dv is not None else None
555555
v: Any = None
556556
try:
557557
if ct is bool:

mypy/messages.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -796,10 +796,10 @@ def incompatible_argument(
796796
)
797797
expected_type = get_proper_type(expected_type)
798798
if isinstance(expected_type, UnionType):
799-
expected_types = list(expected_type.items)
799+
expected_types = get_proper_types(expected_type.items)
800800
else:
801801
expected_types = [expected_type]
802-
for type in get_proper_types(expected_types):
802+
for type in expected_types:
803803
if isinstance(arg_type, Instance) and isinstance(type, Instance):
804804
notes = append_invariance_notes(notes, arg_type, type)
805805
notes = append_numbers_notes(notes, arg_type, type)

mypy/types.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -377,7 +377,9 @@ def _expand_once(self) -> Type:
377377

378378
# TODO: this logic duplicates the one in expand_type_by_instance().
379379
if self.alias.tvar_tuple_index is None:
380-
mapping = {v.id: s for (v, s) in zip(self.alias.alias_tvars, self.args)}
380+
mapping: dict[TypeVarId, Type] = {
381+
v.id: s for (v, s) in zip(self.alias.alias_tvars, self.args)
382+
}
381383
else:
382384
prefix = self.alias.tvar_tuple_index
383385
suffix = len(self.alias.alias_tvars) - self.alias.tvar_tuple_index - 1

test-data/unit/check-python38.test

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -200,11 +200,11 @@ def h(arg=0, /): ...
200200
i = lambda arg=0, /: arg
201201

202202
f(1)
203-
g(1)
203+
reveal_type(g(1)) # N: Revealed type is "Any"
204204
h()
205205
h(1)
206-
i()
207-
i(1)
206+
reveal_type(i()) # N: Revealed type is "Any"
207+
reveal_type(i(1)) # N: Revealed type is "Any"
208208
f(arg=0) # E: Unexpected keyword argument "arg" for "f"
209209
g(arg=0) # E: Unexpected keyword argument "arg"
210210
h(arg=0) # E: Unexpected keyword argument "arg" for "h"

0 commit comments

Comments
 (0)