Skip to content

Commit f012a53

Browse files
authored
Narrow unions containing Any in and_conditional_map (#21231)
Fixes #21225 I expanded it out from comprehensions to try to make it a little clearer. The heuristics do make sense, but would be good to explore biting the bullet and just doing the principled thing of use_meet=True
1 parent 0eb7292 commit f012a53

File tree

2 files changed

+39
-17
lines changed

2 files changed

+39
-17
lines changed

mypy/checker.py

Lines changed: 28 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -8635,28 +8635,39 @@ def and_conditional_maps(m1: TypeMap, m2: TypeMap, *, use_meet: bool = False) ->
86358635
"""
86368636
# Both conditions can be true; combine the information. Anything
86378637
# we learn from either conditions' truth is valid.
8638-
# If the same expression's type is refined by both conditions and use_meet=False, we somewhat
8639-
# arbitrarily give precedence to m2 unless m2's value is Any or m1's value is Never.
86408638
result = m2.copy()
8641-
m2_exprs = {literal_hash(n2): n2 for n2 in m2}
8642-
for n1 in m1:
8643-
n1_hash = literal_hash(n1)
8644-
if n1_hash not in m2_exprs or (
8645-
not use_meet
8646-
and (
8647-
isinstance(get_proper_type(m1[n1]), UninhabitedType)
8648-
or isinstance(get_proper_type(m2[m2_exprs[n1_hash]]), AnyType)
8649-
)
8650-
):
8651-
result[n1] = m1[n1]
8639+
m2_exprs = {literal_hash(e2): e2 for e2 in m2}
8640+
for e1 in m1:
8641+
e1_hash = literal_hash(e1)
8642+
if e1_hash not in m2_exprs:
8643+
result[e1] = m1[e1]
8644+
continue
8645+
8646+
if not use_meet:
8647+
# If use_meet=False and the same expression's type is refined by both conditions,
8648+
# we somewhat arbitrarily give precedence to m2, unless a) m1's value is Never, or
8649+
# b) m2's value is Any and m1 isn't a union that could have been narrowed to Any.
8650+
t1 = m1[e1]
8651+
pt1 = get_proper_type(t1)
8652+
if isinstance(pt1, UninhabitedType):
8653+
result[e1] = t1
8654+
else:
8655+
e2 = m2_exprs[e1_hash]
8656+
pt2 = get_proper_type(m2[e2])
8657+
if isinstance(pt2, AnyType) and not (
8658+
isinstance(pt1, UnionType)
8659+
and any(isinstance(get_proper_type(item), AnyType) for item in pt1.items)
8660+
):
8661+
result[e1] = t1
8662+
86528663
if use_meet:
86538664
# For now, meet common keys only if specifically requested.
86548665
# This is currently used for tuple types narrowing, where having
86558666
# a precise result is important.
8656-
for n1 in m1:
8657-
for n2 in m2:
8658-
if literal_hash(n1) == literal_hash(n2):
8659-
result[n1] = meet_types(m1[n1], m2[n2])
8667+
for e1 in m1:
8668+
for e2 in m2:
8669+
if literal_hash(e1) == literal_hash(e2):
8670+
result[e1] = meet_types(m1[e1], m2[e2])
86608671
return result
86618672

86628673

test-data/unit/check-isinstance.test

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2319,6 +2319,17 @@ assert isinstance(x, B)
23192319
reveal_type(x) # N: Revealed type is "Any"
23202320
[builtins fixtures/isinstance.pyi]
23212321

2322+
[case testIsinstanceNarrowsAfterNoneCheckWithAnyUnion]
2323+
# flags: --warn-unreachable
2324+
from typing import Any, Union
2325+
2326+
def f(val: Union[Any, str]) -> None:
2327+
if val is None or isinstance(val, str):
2328+
reveal_type(val) # N: Revealed type is "None | builtins.str"
2329+
return
2330+
reveal_type(val) # N: Revealed type is "Any"
2331+
[builtins fixtures/isinstance.pyi]
2332+
23222333
[case testIsinstanceIgnoredImport]
23232334
# flags: --warn-unreachable
23242335
from typing import Union

0 commit comments

Comments
 (0)