diff --git a/mypy/checker.py b/mypy/checker.py index 754a407331ed..20f4e0fb141b 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -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. @@ -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): diff --git a/test-data/unit/check-inference.test b/test-data/unit/check-inference.test index 3926627df5ad..c0ab968f3cbb 100644 --- a/test-data/unit/check-inference.test +++ b/test-data/unit/check-inference.test @@ -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