Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions mypy/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -6497,6 +6497,10 @@ def find_isinstance_check_helper(
elif isinstance(node, OpExpr) and node.op == "and":
left_if_vars, left_else_vars = self.find_isinstance_check(node.left)
right_if_vars, right_else_vars = self.find_isinstance_check(node.right)
# Collect type narrowings from any walrus assignments nested in the
# right operand. In the true branch of (A and B), all walrus assignments
# in B are guaranteed to have executed, so we can narrow their targets.
self._collect_walrus_type_map(node.right, right_if_vars)

# (e1 and e2) is true if both e1 and e2 are true,
# and false if at least one of e1 and e2 is false.
Expand Down Expand Up @@ -7123,6 +7127,38 @@ def _propagate_walrus_assignments(
return parent_expr
return expr

def _collect_walrus_type_map(self, expr: Expression, type_map: dict[Expression, Type]) -> None:
"""Collect type narrowings from walrus assignments nested anywhere in expr.

Unlike _propagate_walrus_assignments, this recurses into arbitrary
expression types (OpExpr, CallExpr, UnaryExpr, etc.) to find any
AssignmentExpr nodes and register the assigned type for narrowing.
This is used when processing the true-branch of an `and` expression,
where any walrus in the right operand is guaranteed to have executed.
"""
if isinstance(expr, AssignmentExpr):
assigned_type = self.lookup_type_or_none(expr.value)
target = collapse_walrus(expr)
if assigned_type is not None:
type_map[target] = assigned_type
self._collect_walrus_type_map(expr.value, type_map)
elif isinstance(expr, OpExpr):
self._collect_walrus_type_map(expr.left, type_map)
self._collect_walrus_type_map(expr.right, type_map)
elif isinstance(expr, UnaryExpr):
self._collect_walrus_type_map(expr.expr, type_map)
elif isinstance(expr, CallExpr):
for arg in expr.args:
self._collect_walrus_type_map(arg, type_map)
elif isinstance(expr, MemberExpr):
self._collect_walrus_type_map(expr.expr, type_map)
elif isinstance(expr, IndexExpr):
self._collect_walrus_type_map(expr.base, type_map)
self._collect_walrus_type_map(expr.index, type_map)
elif isinstance(expr, (TupleExpr, ListExpr)):
for item in expr.items:
self._collect_walrus_type_map(item, type_map)

def is_len_of_tuple(self, expr: Expression) -> bool:
"""Is this expression a `len(x)` call where x is a tuple or union of tuples?"""
if not isinstance(expr, CallExpr):
Expand Down
22 changes: 22 additions & 0 deletions test-data/unit/check-inference.test
Original file line number Diff line number Diff line change
Expand Up @@ -4264,6 +4264,28 @@ def check_or_nested(maybe: bool) -> None:
reveal_type(bar) # N: Revealed type is "builtins.list[builtins.int]"
reveal_type(baz) # N: Revealed type is "builtins.list[builtins.int]"

[case testInferWalrusAssignmentNestedInAndExpr]
# Walrus narrowing should propagate when the assignment is nested inside
# an arbitrary expression on the right side of an "and" condition.
# Regression test for https://github.com/python/mypy/issues/19430
class Node:
def __init__(self, value: bool) -> None:
self.value = value

def check_walrus_in_unary(cond: bool) -> None:
woo = None
if cond and not (woo := Node(True)).value:
reveal_type(woo) # N: Revealed type is "__main__.Node"
else:
reveal_type(woo) # N: Revealed type is "__main__.Node | None"

def check_walrus_in_nested_call(cond: bool) -> None:
woo = None
if cond and (woo := Node(True)).value:
reveal_type(woo) # N: Revealed type is "__main__.Node"
else:
reveal_type(woo) # N: Revealed type is "__main__.Node | None"

[case testInferOptionalAgainstAny]
from typing import Any, Optional, TypeVar

Expand Down
Loading