Skip to content

Commit ff31bb2

Browse files
authored
Correctly aggregate narrowing information on parent expressions (#21206)
Fixes #21204 , fixes #20596 The extra narrowing we perform in mypy 1.20 exposes this much longer standing issue
1 parent 20acf99 commit ff31bb2

File tree

4 files changed

+31
-22
lines changed

4 files changed

+31
-22
lines changed

mypy/checker.py

Lines changed: 6 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -6957,10 +6957,6 @@ def narrow_type_by_identity_equality(
69576957
def propagate_up_typemap_info(self, new_types: TypeMap) -> TypeMap:
69586958
"""Attempts refining parent expressions of any MemberExpr or IndexExprs in new_types.
69596959
6960-
Specifically, this function accepts two mappings of expression to original types:
6961-
the original mapping (existing_types), and a new mapping (new_types) intended to
6962-
update the original.
6963-
69646960
This function iterates through new_types and attempts to use the information to try
69656961
refining any parent types that happen to be unions.
69666962
@@ -6979,23 +6975,12 @@ def propagate_up_typemap_info(self, new_types: TypeMap) -> TypeMap:
69796975
69806976
We return the newly refined map. This map is guaranteed to be a superset of 'new_types'.
69816977
"""
6982-
output_map = {}
6978+
all_mappings = [new_types]
69836979
for expr, expr_type in new_types.items():
6984-
# The original inferred type should always be present in the output map, of course
6985-
output_map[expr] = expr_type
6986-
6987-
# Next, try using this information to refine the parent types, if applicable.
6988-
new_mapping = self.refine_parent_types(expr, expr_type)
6989-
for parent_expr, proposed_parent_type in new_mapping.items():
6990-
# We don't try inferring anything if we've already inferred something for
6991-
# the parent expression.
6992-
# TODO: Consider picking the narrower type instead of always discarding this?
6993-
if parent_expr in new_types:
6994-
continue
6995-
output_map[parent_expr] = proposed_parent_type
6996-
return output_map
6980+
all_mappings.append(self.refine_parent_types(expr, expr_type))
6981+
return reduce_and_conditional_type_maps(all_mappings, use_meet=True)
69976982

6998-
def refine_parent_types(self, expr: Expression, expr_type: Type) -> Mapping[Expression, Type]:
6983+
def refine_parent_types(self, expr: Expression, expr_type: Type) -> TypeMap:
69996984
"""Checks if the given expr is a 'lookup operation' into a union and iteratively refines
70006985
the parent types based on the 'expr_type'.
70016986
@@ -8744,6 +8729,8 @@ def reduce_and_conditional_type_maps(ms: list[TypeMap], *, use_meet: bool) -> Ty
87448729
return ms[0]
87458730
result = ms[0]
87468731
for m in ms[1:]:
8732+
if not m:
8733+
continue # this is a micro-optimisation
87478734
result = and_conditional_maps(result, m, use_meet=use_meet)
87488735
return result
87498736

test-data/unit/check-isinstance.test

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3070,7 +3070,7 @@ if hasattr(mod, "y"):
30703070
def __getattr__(attr: str) -> str: ...
30713071
[builtins fixtures/module.pyi]
30723072

3073-
[case testMultipleHasAttr-xfail]
3073+
[case testMultipleHasAttr]
30743074
# flags: --warn-unreachable
30753075
# https://github.com/python/mypy/issues/20596
30763076
from __future__ import annotations

test-data/unit/check-narrowing.test

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3993,3 +3993,25 @@ def f2(func: Callable[..., T], arg: str) -> T:
39933993
return func(arg)
39943994
return func(arg)
39953995
[builtins fixtures/primitives.pyi]
3996+
3997+
3998+
[case testPropagatedParentNarrowingMeet]
3999+
# flags: --strict-equality --warn-unreachable
4000+
from __future__ import annotations
4001+
4002+
class A:
4003+
tag: int
4004+
4005+
class B:
4006+
tag: int
4007+
name = "b"
4008+
4009+
class C:
4010+
tag: str
4011+
4012+
def stringify(value: A | B | C) -> str:
4013+
if isinstance(value.tag, int) and isinstance(value, B):
4014+
reveal_type(value) # N: Revealed type is "__main__.B"
4015+
return value.name
4016+
return ""
4017+
[builtins fixtures/tuple.pyi]

test-data/unit/check-python310.test

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3699,9 +3699,9 @@ class B(TypedDict):
36993699
num: int
37003700

37013701
d: A | B
3702-
match d["tag"]: # E: Match statement has unhandled case for values of type "Literal['b']" \
3702+
match d["tag"]: # E: Match statement has unhandled case for values of type "B" \
37033703
# N: If match statement is intended to be non-exhaustive, add `case _: pass` \
3704-
# E: Match statement has unhandled case for values of type "B"
3704+
# E: Match statement has unhandled case for values of type "Literal['b']"
37053705
case "a":
37063706
reveal_type(d) # N: Revealed type is "TypedDict('__main__.A', {'tag': Literal['a'], 'name': builtins.str})"
37073707
reveal_type(d["name"]) # N: Revealed type is "builtins.str"

0 commit comments

Comments
 (0)