Skip to content

Commit 1a3ae3c

Browse files
committed
Only propagate walrus narrowing where the binder cannot carry it
1 parent e66afaa commit 1a3ae3c

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
@@ -6711,27 +6711,27 @@ def find_isinstance_check(
67116711
if_map, else_map = self.find_isinstance_check_helper(
67126712
node, in_boolean_context=in_boolean_context
67136713
)
6714-
self.propagate_walrus_assignments(node, if_map, else_map)
67156714
new_if_map = self.propagate_up_typemap_info(if_map)
67166715
new_else_map = self.propagate_up_typemap_info(else_map)
67176716
return new_if_map, new_else_map
67186717

67196718
def propagate_walrus_assignments(
67206719
self, node: Expression, if_map: TypeMap, else_map: TypeMap
67216720
) -> None:
6722-
"""Narrow the targets of walrus assignments nested within a condition.
6723-
6724-
Such an assignment has already happened by the time the condition has
6725-
been evaluated, so the assigned type applies to both branches, and both
6726-
maps are updated in place. Which branches are actually reached is decided
6727-
by the callers combining these maps: `and` carries the right operand's if
6728-
map into the if branch, and `or` carries its else map into the else
6729-
branch.
6721+
"""Narrow the targets of walrus assignments nested within `node`.
6722+
6723+
Only used for the right operand of `and` and `or`. Elsewhere the operand
6724+
is always evaluated, so the binder already carries the assignment and
6725+
adding it here would only widen the result: an entry makes the branches
6726+
join through the target's declaration, which may be wider than the type
6727+
assigned.
6728+
6729+
The assignment has happened once `node` has been evaluated, whatever
6730+
value it produced, so both maps are updated in place. Which branch that
6731+
reaches is decided by the caller combining them: `and` carries the right
6732+
operand's if map into the if branch, and `or` carries its else map into
6733+
the else branch.
67306734
"""
6731-
if isinstance(node, NameExpr):
6732-
# The most common condition, and it has no subexpressions.
6733-
return
6734-
67356735
collector = WalrusAssignmentCollector()
67366736
node.accept(collector)
67376737
if not collector.assignments:
@@ -6863,6 +6863,7 @@ def find_isinstance_check_helper(
68636863
elif isinstance(node, OpExpr) and node.op == "and":
68646864
left_if_vars, left_else_vars = self.find_isinstance_check(node.left)
68656865
right_if_vars, right_else_vars = self.find_isinstance_check(node.right)
6866+
self.propagate_walrus_assignments(node.right, right_if_vars, right_else_vars)
68666867

68676868
# (e1 and e2) is true if both e1 and e2 are true,
68686869
# and false if at least one of e1 and e2 is false.
@@ -6876,6 +6877,7 @@ def find_isinstance_check_helper(
68766877
elif isinstance(node, OpExpr) and node.op == "or":
68776878
left_if_vars, left_else_vars = self.find_isinstance_check(node.left)
68786879
right_if_vars, right_else_vars = self.find_isinstance_check(node.right)
6880+
self.propagate_walrus_assignments(node.right, right_if_vars, right_else_vars)
68796881

68806882
# (e1 or e2) is true if at least one of e1 or e2 is true,
68816883
# 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
@@ -4294,48 +4294,62 @@ def check_nested_and(maybe: bool) -> None:
42944294
[case testInferWalrusAssignmentNestedInConditionNotAlwaysEvaluated]
42954295
from typing import List
42964296

4297+
# Each condition puts the walrus on the right of an `and`, which is where the
4298+
# assignment is not carried by the binder and this narrowing applies.
4299+
42974300
def check_ternary_branch(maybe: bool) -> None:
42984301
woo = None
4299-
if 1 if maybe else (woo := 5):
4302+
if maybe and (1 if maybe else (woo := 5)):
43004303
reveal_type(woo) # N: Revealed type is "builtins.int | None"
43014304
else:
43024305
reveal_type(woo) # N: Revealed type is "builtins.int | None"
43034306

43044307
def check_ternary_condition(maybe: bool) -> None:
43054308
woo = None
4306-
if 1 if (woo := 5) else 0:
4309+
if maybe and (1 if (woo := 5) else 0):
43074310
reveal_type(woo) # N: Revealed type is "builtins.int"
43084311

4309-
def check_comprehension(xs: List[int]) -> None:
4312+
def check_comprehension(maybe: bool, xs: List[int]) -> None:
43104313
woo = None
4311-
if [y for y in xs if (woo := y)]:
4314+
if maybe and [y for y in xs if (woo := y)]:
43124315
reveal_type(woo) # N: Revealed type is "builtins.int | None"
43134316

4314-
def check_chained_comparison(a: int, b: int) -> None:
4315-
# The else branch should stay optional, and does not. Pre-existing: chain
4316-
# operands are checked in one binder frame, so the assignment is recorded
4317-
# even when short-circuiting means it never ran.
4317+
def check_chained_comparison(maybe: bool, a: int, b: int) -> None:
4318+
# Conservative: entering the branch does imply a < b was true and so the
4319+
# walrus ran, but operands after the second are not walked into.
43184320
woo = None
4319-
if a < b < (woo := 5):
4320-
reveal_type(woo) # N: Revealed type is "builtins.int"
4321-
else:
4322-
reveal_type(woo) # N: Revealed type is "builtins.int"
4321+
if maybe and a < b < (woo := 5):
4322+
reveal_type(woo) # N: Revealed type is "builtins.int | None"
43234323
[builtins fixtures/len.pyi]
43244324

43254325
[case testInferWalrusAssignmentDoesNotWeakenNarrowing]
43264326
from typing import Optional, Union
43274327

4328-
def check_truthiness(val: Optional[int]) -> None:
4329-
if x := val:
4328+
# The walrus goes on the right of an `and` so that the narrowing added for the
4329+
# assignment has to give way to the more precise narrowing from the condition.
4330+
4331+
def check_truthiness(maybe: bool, val: Optional[int]) -> None:
4332+
if maybe and (x := val):
43304333
reveal_type(x) # N: Revealed type is "builtins.int"
43314334

4332-
def check_isinstance(val: Union[int, str]) -> None:
4333-
if isinstance(x := val, int):
4335+
def check_isinstance(maybe: bool, val: Union[int, str]) -> None:
4336+
if maybe and isinstance(x := val, int):
43344337
reveal_type(x) # N: Revealed type is "builtins.int"
43354338

4336-
def check_is_not_none(val: Optional[int]) -> None:
4337-
if (x := val) is not None:
4339+
def check_is_not_none(maybe: bool, val: Optional[int]) -> None:
4340+
if maybe and (x := val) is not None:
43384341
reveal_type(x) # N: Revealed type is "builtins.int"
4342+
4343+
def truthy(x: object) -> bool: ...
4344+
4345+
def check_declaration_wider_than_assignment(val: Optional[int], n: int) -> None:
4346+
# An operand that is always evaluated must not be given a map entry: the
4347+
# branches would then join through the declaration of x, which is wider than
4348+
# what the walrus assigned. Reported by mypy_primer against rotki.
4349+
x = val
4350+
if truthy(x := n):
4351+
pass
4352+
reveal_type(x) # N: Revealed type is "builtins.int"
43394353
[builtins fixtures/isinstancelist.pyi]
43404354

43414355
[case testInferOptionalAgainstAny]

0 commit comments

Comments
 (0)