From be66d9d7ce2b3954914e267fac3963e5799d35d8 Mon Sep 17 00:00:00 2001 From: STerliakov Date: Sat, 7 Jun 2025 20:41:44 +0200 Subject: [PATCH 1/3] Use union of current context and left side for right side narrowing --- mypy/checkexpr.py | 8 ++++++-- test-data/unit/check-inference-context.test | 21 +++++++++++++++++++++ 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index e0c7e829309c..27fbfa88ea26 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -5879,16 +5879,20 @@ def analyze_cond_branch( allow_none_return: bool = False, suppress_unreachable_errors: bool = True, ) -> Type: + if self.type_context and self.type_context[-1] is not None: + ctx = make_simplified_union([context, self.type_context[-1]]) + else: + ctx = context with self.chk.binder.frame_context(can_skip=True, fall_through=0): if map is None: # We still need to type check node, in case we want to # process it for isinstance checks later. Since the branch was # determined to be unreachable, any errors should be suppressed. with self.msg.filter_errors(filter_errors=suppress_unreachable_errors): - self.accept(node, type_context=context, allow_none_return=allow_none_return) + self.accept(node, type_context=ctx, allow_none_return=allow_none_return) return UninhabitedType() self.chk.push_type_map(map) - return self.accept(node, type_context=context, allow_none_return=allow_none_return) + return self.accept(node, type_context=ctx, allow_none_return=allow_none_return) # # Helpers diff --git a/test-data/unit/check-inference-context.test b/test-data/unit/check-inference-context.test index 0aa67b2bf7f3..67ae22a369b1 100644 --- a/test-data/unit/check-inference-context.test +++ b/test-data/unit/check-inference-context.test @@ -1510,3 +1510,24 @@ def mymin( def check(paths: Iterable[str], key: Callable[[str], int]) -> Union[str, None]: return mymin(paths, key=key, default=None) [builtins fixtures/tuple.pyi] + +[case testBinaryOpInferenceContext] +from typing import Literal, TypeVar + +T = TypeVar("T") + +def identity(x: T) -> T: + return x + +def check1(use: bool, val: str) -> "str | Literal[True]": + return use or identity(val) + +def check2(use: bool, val: str) -> "str | bool": + return use or identity(val) + +def check3(use: bool, val: str) -> "str | Literal[False]": + return use and identity(val) + +def check4(use: bool, val: str) -> "str | bool": + return use and identity(val) +[builtins fixtures/tuple.pyi] From 6ef38dcc86e56e3ff572de0668d83fe811d6e5d6 Mon Sep 17 00:00:00 2001 From: STerliakov Date: Sat, 7 Jun 2025 20:46:48 +0200 Subject: [PATCH 2/3] Fix selfcheck --- mypy/checkexpr.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 27fbfa88ea26..6e5b2dbaf726 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -5879,10 +5879,16 @@ def analyze_cond_branch( allow_none_return: bool = False, suppress_unreachable_errors: bool = True, ) -> Type: + ctx_items = [] + if context is not None: + ctx_items.append(context) if self.type_context and self.type_context[-1] is not None: - ctx = make_simplified_union([context, self.type_context[-1]]) + ctx_items.append(self.type_context[-1]) + ctx: Type | None + if ctx_items: + ctx = make_simplified_union(ctx_items) else: - ctx = context + ctx = None with self.chk.binder.frame_context(can_skip=True, fall_through=0): if map is None: # We still need to type check node, in case we want to From 862a8d09177e7bdc17ae7986f8163e3080741b30 Mon Sep 17 00:00:00 2001 From: STerliakov Date: Sat, 7 Jun 2025 23:16:31 +0200 Subject: [PATCH 3/3] Only do that for boolean ops, not ternaries --- mypy/checkexpr.py | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 6e5b2dbaf726..f0435e728e1f 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -4271,7 +4271,9 @@ def check_boolean_op(self, e: OpExpr, context: Context) -> Type: ): self.msg.unreachable_right_operand(e.op, e.right) - right_type = self.analyze_cond_branch(right_map, e.right, expanded_left_type) + right_type = self.analyze_cond_branch( + right_map, e.right, self._combined_context(expanded_left_type) + ) if left_map is None and right_map is None: return UninhabitedType() @@ -5879,26 +5881,26 @@ def analyze_cond_branch( allow_none_return: bool = False, suppress_unreachable_errors: bool = True, ) -> Type: - ctx_items = [] - if context is not None: - ctx_items.append(context) - if self.type_context and self.type_context[-1] is not None: - ctx_items.append(self.type_context[-1]) - ctx: Type | None - if ctx_items: - ctx = make_simplified_union(ctx_items) - else: - ctx = None with self.chk.binder.frame_context(can_skip=True, fall_through=0): if map is None: # We still need to type check node, in case we want to # process it for isinstance checks later. Since the branch was # determined to be unreachable, any errors should be suppressed. with self.msg.filter_errors(filter_errors=suppress_unreachable_errors): - self.accept(node, type_context=ctx, allow_none_return=allow_none_return) + self.accept(node, type_context=context, allow_none_return=allow_none_return) return UninhabitedType() self.chk.push_type_map(map) - return self.accept(node, type_context=ctx, allow_none_return=allow_none_return) + return self.accept(node, type_context=context, allow_none_return=allow_none_return) + + def _combined_context(self, ty: Type | None) -> Type | None: + ctx_items = [] + if ty is not None: + ctx_items.append(ty) + if self.type_context and self.type_context[-1] is not None: + ctx_items.append(self.type_context[-1]) + if ctx_items: + return make_simplified_union(ctx_items) + return None # # Helpers