Skip to content

Commit 0cf9c02

Browse files
authored
Fix binder special-casing to apply to nested unions (#20912)
The change itself is minimal: I add a call to `flatten_nested_unions()`. But I also do some reshuffling to only make this call _once_. Note this change affects both `--allow-redefiniton-new` and regular mode. The logic is a bit complicated, I double-checked it is still consistent (note that function arguments are considered inferred for the purpose of redefinition).
1 parent 34abecb commit 0cf9c02

2 files changed

Lines changed: 72 additions & 17 deletions

File tree

mypy/binder.py

Lines changed: 18 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
UnionType,
3535
UnpackType,
3636
find_unpack_in_list,
37+
flatten_nested_unions,
3738
get_proper_type,
3839
)
3940
from mypy.typevars import fill_typevars_with_any
@@ -498,24 +499,24 @@ def assign_type(self, expr: Expression, type: Type, declared_type: Type | None)
498499
# First case: a local/global variable without explicit annotation,
499500
# in this case we just assign Any (essentially following the SSA logic).
500501
self.put(expr, type)
501-
elif isinstance(p_declared, UnionType) and any(
502-
isinstance(get_proper_type(item), NoneType) for item in p_declared.items
503-
):
504-
# Second case: explicit optional type, in this case we optimize for a common
505-
# pattern when an untyped value used as a fallback replacing None.
506-
new_items = [
507-
type if isinstance(get_proper_type(item), NoneType) else item
508-
for item in p_declared.items
509-
]
510-
self.put(expr, UnionType(new_items))
511-
elif isinstance(p_declared, UnionType) and any(
512-
isinstance(get_proper_type(item), AnyType) for item in p_declared.items
513-
):
514-
# Third case: a union already containing Any (most likely from an un-imported
515-
# name), in this case we allow assigning Any as well.
516-
self.put(expr, type)
502+
elif isinstance(p_declared, UnionType):
503+
all_items = flatten_nested_unions(p_declared.items)
504+
if any(isinstance(get_proper_type(item), NoneType) for item in all_items):
505+
# Second case: explicit optional type, in this case we optimize for
506+
# a common pattern when an untyped value used as a fallback replacing None.
507+
new_items = [
508+
type if isinstance(get_proper_type(item), NoneType) else item
509+
for item in all_items
510+
]
511+
self.put(expr, UnionType(new_items))
512+
elif any(isinstance(get_proper_type(item), AnyType) for item in all_items):
513+
# Third case: a union already containing Any (most likely from
514+
# an un-imported name), in this case we allow assigning Any as well.
515+
self.put(expr, type)
516+
else:
517+
# In all other cases we don't narrow to Any to minimize false negatives.
518+
self.put(expr, declared_type)
517519
else:
518-
# In all other cases we don't narrow to Any to minimize false negatives.
519520
self.put(expr, declared_type)
520521
elif isinstance(p_declared, AnyType):
521522
# Mirroring the first case above, we don't narrow to a precise type if the variable

test-data/unit/check-redefine2.test

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1398,6 +1398,60 @@ def process3(items: list[str]) -> None:
13981398
reveal_type(items) # N: Revealed type is "builtins.list[builtins.str]"
13991399
[builtins fixtures/primitives.pyi]
14001400

1401+
[case testNewRedefineNarrowingForNestedUnionWithAny]
1402+
# flags: --allow-redefinition-new --local-partial-types
1403+
from typing import Any, Union
1404+
1405+
a: Any
1406+
Int = Union[int, Any]
1407+
1408+
def test(x: Union[str, Int]) -> None:
1409+
x = a
1410+
reveal_type(x) # N: Revealed type is "Any"
1411+
y: Union[str, Int]
1412+
y = a
1413+
reveal_type(y) # N: Revealed type is "Any"
1414+
1415+
[case testNewRedefineNarrowingForNestedUnionWithNone]
1416+
# flags: --allow-redefinition-new --local-partial-types
1417+
from typing import Any, Union
1418+
1419+
a: Any
1420+
Int = Union[int, None]
1421+
1422+
def test(x: Union[str, Int]) -> None:
1423+
x = a
1424+
reveal_type(x) # N: Revealed type is "Any"
1425+
y: Union[str, Int]
1426+
y = a
1427+
reveal_type(y) # N: Revealed type is "builtins.str | Any | builtins.int"
1428+
1429+
[case testRegularNarrowingForNestedUnionWithAny]
1430+
from typing import Any, Union
1431+
1432+
a: Any
1433+
Int = Union[int, Any]
1434+
1435+
def test(x: Union[str, Int]) -> None:
1436+
x = a
1437+
reveal_type(x) # N: Revealed type is "Any"
1438+
y: Union[str, Int]
1439+
y = a
1440+
reveal_type(y) # N: Revealed type is "Any"
1441+
1442+
[case testRegularNarrowingForNestedUnionWithNone]
1443+
from typing import Any, Union
1444+
1445+
a: Any
1446+
Int = Union[int, None]
1447+
1448+
def test(x: Union[str, Int]) -> None:
1449+
x = a
1450+
reveal_type(x) # N: Revealed type is "builtins.str | Any | builtins.int"
1451+
y: Union[str, Int]
1452+
y = a
1453+
reveal_type(y) # N: Revealed type is "builtins.str | Any | builtins.int"
1454+
14011455
[case testNewRedefineWidenedArgumentDeferral]
14021456
# flags: --allow-redefinition-new --local-partial-types
14031457
def foo(x: int) -> None:

0 commit comments

Comments
 (0)