Skip to content

Commit 53303c0

Browse files
committed
Only propagate walrus narrowing where the binder cannot carry it
1 parent 0a103f0 commit 53303c0

2 files changed

Lines changed: 47 additions & 31 deletions

File tree

mypy/checker.py

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6628,27 +6628,27 @@ def find_isinstance_check(
66286628
if_map, else_map = self.find_isinstance_check_helper(
66296629
node, in_boolean_context=in_boolean_context
66306630
)
6631-
self.propagate_walrus_assignments(node, if_map, else_map)
66326631
new_if_map = self.propagate_up_typemap_info(if_map)
66336632
new_else_map = self.propagate_up_typemap_info(else_map)
66346633
return new_if_map, new_else_map
66356634

66366635
def propagate_walrus_assignments(
66376636
self, node: Expression, if_map: TypeMap, else_map: TypeMap
66386637
) -> None:
6639-
"""Narrow the targets of walrus assignments nested within a condition.
6640-
6641-
Such an assignment has already happened by the time the condition has
6642-
been evaluated, so the assigned type applies to both branches, and both
6643-
maps are updated in place. Which branches are actually reached is decided
6644-
by the callers combining these maps: `and` carries the right operand's if
6645-
map into the if branch, and `or` carries its else map into the else
6646-
branch.
6638+
"""Narrow the targets of walrus assignments nested within `node`.
6639+
6640+
Only used for the right operand of `and` and `or`. Elsewhere the operand
6641+
is always evaluated, so the binder already carries the assignment and
6642+
adding it here would only widen the result: an entry makes the branches
6643+
join through the target's declaration, which may be wider than the type
6644+
assigned.
6645+
6646+
The assignment has happened once `node` has been evaluated, whatever
6647+
value it produced, so both maps are updated in place. Which branch that
6648+
reaches is decided by the caller combining them: `and` carries the right
6649+
operand's if map into the if branch, and `or` carries its else map into
6650+
the else branch.
66476651
"""
6648-
if isinstance(node, NameExpr):
6649-
# The most common condition, and it has no subexpressions.
6650-
return
6651-
66526652
collector = WalrusAssignmentCollector()
66536653
node.accept(collector)
66546654
if not collector.assignments:
@@ -6780,6 +6780,7 @@ def find_isinstance_check_helper(
67806780
elif isinstance(node, OpExpr) and node.op == "and":
67816781
left_if_vars, left_else_vars = self.find_isinstance_check(node.left)
67826782
right_if_vars, right_else_vars = self.find_isinstance_check(node.right)
6783+
self.propagate_walrus_assignments(node.right, right_if_vars, right_else_vars)
67836784

67846785
# (e1 and e2) is true if both e1 and e2 are true,
67856786
# and false if at least one of e1 and e2 is false.
@@ -6793,6 +6794,7 @@ def find_isinstance_check_helper(
67936794
elif isinstance(node, OpExpr) and node.op == "or":
67946795
left_if_vars, left_else_vars = self.find_isinstance_check(node.left)
67956796
right_if_vars, right_else_vars = self.find_isinstance_check(node.right)
6797+
self.propagate_walrus_assignments(node.right, right_if_vars, right_else_vars)
67966798

67976799
# (e1 or e2) is true if at least one of e1 or e2 is true,
67986800
# and false if both e1 and e2 are false.

test-data/unit/check-inference.test

Lines changed: 32 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4358,48 +4358,62 @@ def check_nested_and(maybe: bool) -> None:
43584358
[case testInferWalrusAssignmentNestedInConditionNotAlwaysEvaluated]
43594359
from typing import List
43604360

4361+
# Each condition puts the walrus on the right of an `and`, which is where the
4362+
# assignment is not carried by the binder and this narrowing applies.
4363+
43614364
def check_ternary_branch(maybe: bool) -> None:
43624365
woo = None
4363-
if 1 if maybe else (woo := 5):
4366+
if maybe and (1 if maybe else (woo := 5)):
43644367
reveal_type(woo) # N: Revealed type is "builtins.int | None"
43654368
else:
43664369
reveal_type(woo) # N: Revealed type is "builtins.int | None"
43674370

43684371
def check_ternary_condition(maybe: bool) -> None:
43694372
woo = None
4370-
if 1 if (woo := 5) else 0:
4373+
if maybe and (1 if (woo := 5) else 0):
43714374
reveal_type(woo) # N: Revealed type is "builtins.int"
43724375

4373-
def check_comprehension(xs: List[int]) -> None:
4376+
def check_comprehension(maybe: bool, xs: List[int]) -> None:
43744377
woo = None
4375-
if [y for y in xs if (woo := y)]:
4378+
if maybe and [y for y in xs if (woo := y)]:
43764379
reveal_type(woo) # N: Revealed type is "builtins.int | None"
43774380

4378-
def check_chained_comparison(a: int, b: int) -> None:
4379-
# The else branch should stay optional, and does not. Pre-existing: chain
4380-
# operands are checked in one binder frame, so the assignment is recorded
4381-
# even when short-circuiting means it never ran.
4381+
def check_chained_comparison(maybe: bool, a: int, b: int) -> None:
4382+
# Conservative: entering the branch does imply a < b was true and so the
4383+
# walrus ran, but operands after the second are not walked into.
43824384
woo = None
4383-
if a < b < (woo := 5):
4384-
reveal_type(woo) # N: Revealed type is "builtins.int"
4385-
else:
4386-
reveal_type(woo) # N: Revealed type is "builtins.int"
4385+
if maybe and a < b < (woo := 5):
4386+
reveal_type(woo) # N: Revealed type is "builtins.int | None"
43874387
[builtins fixtures/len.pyi]
43884388

43894389
[case testInferWalrusAssignmentDoesNotWeakenNarrowing]
43904390
from typing import Optional, Union
43914391

4392-
def check_truthiness(val: Optional[int]) -> None:
4393-
if x := val:
4392+
# The walrus goes on the right of an `and` so that the narrowing added for the
4393+
# assignment has to give way to the more precise narrowing from the condition.
4394+
4395+
def check_truthiness(maybe: bool, val: Optional[int]) -> None:
4396+
if maybe and (x := val):
43944397
reveal_type(x) # N: Revealed type is "builtins.int"
43954398

4396-
def check_isinstance(val: Union[int, str]) -> None:
4397-
if isinstance(x := val, int):
4399+
def check_isinstance(maybe: bool, val: Union[int, str]) -> None:
4400+
if maybe and isinstance(x := val, int):
43984401
reveal_type(x) # N: Revealed type is "builtins.int"
43994402

4400-
def check_is_not_none(val: Optional[int]) -> None:
4401-
if (x := val) is not None:
4403+
def check_is_not_none(maybe: bool, val: Optional[int]) -> None:
4404+
if maybe and (x := val) is not None:
44024405
reveal_type(x) # N: Revealed type is "builtins.int"
4406+
4407+
def truthy(x: object) -> bool: ...
4408+
4409+
def check_declaration_wider_than_assignment(val: Optional[int], n: int) -> None:
4410+
# An operand that is always evaluated must not be given a map entry: the
4411+
# branches would then join through the declaration of x, which is wider than
4412+
# what the walrus assigned. Reported by mypy_primer against rotki.
4413+
x = val
4414+
if truthy(x := n):
4415+
pass
4416+
reveal_type(x) # N: Revealed type is "builtins.int"
44034417
[builtins fixtures/isinstancelist.pyi]
44044418

44054419
[case testInferOptionalAgainstAny]

0 commit comments

Comments
 (0)