Skip to content

Commit ef28d9c

Browse files
authored
Narrow Any in and_conditional_map (#21167)
Prior to this, mypy's behaviour is inconsistent depending on the order of the condition or the presence of terms in the condition other than `isinstance` Fixes #17100 , fixes #20363 , fixes #16462
1 parent 507cfb0 commit ef28d9c

File tree

3 files changed

+33
-9
lines changed

3 files changed

+33
-9
lines changed

mypy/checker.py

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8633,15 +8633,19 @@ def and_conditional_maps(m1: TypeMap, m2: TypeMap, *, use_meet: bool = False) ->
86338633
the truth of e2.
86348634
"""
86358635
# Both conditions can be true; combine the information. Anything
8636-
# we learn from either conditions' truth is valid. If the same
8637-
# expression's type is refined by both conditions, we somewhat
8638-
# arbitrarily give precedence to m2 unless m1 value is Any.
8639-
# In the future, we could use an intersection type or meet_types().
8636+
# we learn from either conditions' truth is valid.
8637+
# If the same expression's type is refined by both conditions and use_meet=False, we somewhat
8638+
# arbitrarily give precedence to m2 unless m2's value is Any or m1's value is Never.
86408639
result = m2.copy()
8641-
m2_keys = {literal_hash(n2) for n2 in m2}
8640+
m2_exprs = {literal_hash(n2): n2 for n2 in m2}
86428641
for n1 in m1:
8643-
if literal_hash(n1) not in m2_keys or isinstance(
8644-
get_proper_type(m1[n1]), (AnyType, UninhabitedType)
8642+
n1_hash = literal_hash(n1)
8643+
if n1_hash not in m2_exprs or (
8644+
not use_meet
8645+
and (
8646+
isinstance(get_proper_type(m1[n1]), UninhabitedType)
8647+
or isinstance(get_proper_type(m2[m2_exprs[n1_hash]]), AnyType)
8648+
)
86458649
):
86468650
result[n1] = m1[n1]
86478651
if use_meet:

test-data/unit/check-isinstance.test

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1478,6 +1478,26 @@ def f(x: Any):
14781478
x + "foo" # E: Unsupported operand types for + ("int" and "str")
14791479
[builtins fixtures/isinstance.pyi]
14801480

1481+
[case testIsinstanceNarrowingAny]
1482+
# flags: --warn-unreachable
1483+
from typing import Any
1484+
1485+
class A: pass
1486+
class B(A): pass
1487+
1488+
def f1(x: Any) -> None:
1489+
if x and isinstance(x, A):
1490+
reveal_type(x) # N: Revealed type is "__main__.A"
1491+
if isinstance(x, A) and x:
1492+
reveal_type(x) # N: Revealed type is "__main__.A"
1493+
1494+
def f2(x: Any) -> None:
1495+
if isinstance(x, A) and not isinstance(x, B):
1496+
reveal_type(x) # N: Revealed type is "__main__.A"
1497+
if not isinstance(x, B) and isinstance(x, A):
1498+
reveal_type(x) # N: Revealed type is "__main__.A"
1499+
[builtins fixtures/isinstance.pyi]
1500+
14811501
[case testIsinstanceOfGenericClassRetainsParameters]
14821502
# flags: --warn-unreachable
14831503
from typing import List, Union

test-data/unit/check-narrowing.test

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1727,7 +1727,7 @@ reveal_type(c) # N: Revealed type is "__main__.C"
17271727

17281728
c1: C
17291729
if isinstance(c1, tp) and isinstance(c1, D):
1730-
reveal_type(c1) # N: Revealed type is "Any"
1730+
reveal_type(c1) # N: Revealed type is "__main__.D"
17311731
else:
17321732
reveal_type(c1) # N: Revealed type is "__main__.C"
17331733
reveal_type(c1) # N: Revealed type is "__main__.C"
@@ -1741,7 +1741,7 @@ reveal_type(c2) # N: Revealed type is "__main__.C"
17411741

17421742
c3: C
17431743
if isinstance(c3, D) and isinstance(c3, tp):
1744-
reveal_type(c3) # N: Revealed type is "Any"
1744+
reveal_type(c3) # N: Revealed type is "__main__.D"
17451745
else:
17461746
reveal_type(c3) # N: Revealed type is "__main__.C"
17471747
reveal_type(c3) # N: Revealed type is "__main__.C"

0 commit comments

Comments
 (0)