Skip to content

Commit fa6ef09

Browse files
committed
Only propagate walrus narrowing where the binder cannot carry it
1 parent 7c72636 commit fa6ef09

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
@@ -6592,27 +6592,27 @@ def find_isinstance_check(
65926592
if_map, else_map = self.find_isinstance_check_helper(
65936593
node, in_boolean_context=in_boolean_context
65946594
)
6595-
self.propagate_walrus_assignments(node, if_map, else_map)
65966595
new_if_map = self.propagate_up_typemap_info(if_map)
65976596
new_else_map = self.propagate_up_typemap_info(else_map)
65986597
return new_if_map, new_else_map
65996598

66006599
def propagate_walrus_assignments(
66016600
self, node: Expression, if_map: TypeMap, else_map: TypeMap
66026601
) -> None:
6603-
"""Narrow the targets of walrus assignments nested within a condition.
6604-
6605-
Such an assignment has already happened by the time the condition has
6606-
been evaluated, so the assigned type applies to both branches, and both
6607-
maps are updated in place. Which branches are actually reached is decided
6608-
by the callers combining these maps: `and` carries the right operand's if
6609-
map into the if branch, and `or` carries its else map into the else
6610-
branch.
6602+
"""Narrow the targets of walrus assignments nested within `node`.
6603+
6604+
Only used for the right operand of `and` and `or`. Elsewhere the operand
6605+
is always evaluated, so the binder already carries the assignment and
6606+
adding it here would only widen the result: an entry makes the branches
6607+
join through the target's declaration, which may be wider than the type
6608+
assigned.
6609+
6610+
The assignment has happened once `node` has been evaluated, whatever
6611+
value it produced, so both maps are updated in place. Which branch that
6612+
reaches is decided by the caller combining them: `and` carries the right
6613+
operand's if map into the if branch, and `or` carries its else map into
6614+
the else branch.
66116615
"""
6612-
if isinstance(node, NameExpr):
6613-
# The most common condition, and it has no subexpressions.
6614-
return
6615-
66166616
collector = WalrusAssignmentCollector()
66176617
node.accept(collector)
66186618
if not collector.assignments:
@@ -6744,6 +6744,7 @@ def find_isinstance_check_helper(
67446744
elif isinstance(node, OpExpr) and node.op == "and":
67456745
left_if_vars, left_else_vars = self.find_isinstance_check(node.left)
67466746
right_if_vars, right_else_vars = self.find_isinstance_check(node.right)
6747+
self.propagate_walrus_assignments(node.right, right_if_vars, right_else_vars)
67476748

67486749
# (e1 and e2) is true if both e1 and e2 are true,
67496750
# and false if at least one of e1 and e2 is false.
@@ -6757,6 +6758,7 @@ def find_isinstance_check_helper(
67576758
elif isinstance(node, OpExpr) and node.op == "or":
67586759
left_if_vars, left_else_vars = self.find_isinstance_check(node.left)
67596760
right_if_vars, right_else_vars = self.find_isinstance_check(node.right)
6761+
self.propagate_walrus_assignments(node.right, right_if_vars, right_else_vars)
67606762

67616763
# (e1 or e2) is true if at least one of e1 or e2 is true,
67626764
# 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)