From 9a3c07d3c160c04cec92abae93dd5a3b80650e44 Mon Sep 17 00:00:00 2001 From: Randolf Scholz Date: Fri, 1 Aug 2025 16:33:05 +0200 Subject: [PATCH 1/4] refactor visit_conditional_expr --- mypy/checkexpr.py | 66 +++----------- mypyc/irbuild/statement.py | 4 +- test-data/unit/check-literal.test | 134 +++++++++++++++++++++++++++++ test-data/unit/check-optional.test | 4 +- 4 files changed, 154 insertions(+), 54 deletions(-) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index ecae451299d7..f594b26b342c 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -141,7 +141,6 @@ get_type_vars, is_literal_type_like, make_simplified_union, - simple_literal_type, true_only, try_expanding_sum_type_to_union, try_getting_str_literals, @@ -5903,63 +5902,26 @@ def visit_conditional_expr(self, e: ConditionalExpr, allow_none_return: bool = F elif else_map is None: self.msg.redundant_condition_in_if(True, e.cond) + if ctx is None: + # When no context is provided, compute each branch individually, and + # use the union of the results as artificial context. Important for: + # - testUnificationDict + # - testConditionalExpressionWithEmpty + ctx_if_type = self.analyze_cond_branch( + if_map, e.if_expr, context=ctx, allow_none_return=allow_none_return + ) + ctx_else_type = self.analyze_cond_branch( + else_map, e.else_expr, context=ctx, allow_none_return=allow_none_return + ) + ctx = make_simplified_union([ctx_if_type, ctx_else_type]) + if_type = self.analyze_cond_branch( if_map, e.if_expr, context=ctx, allow_none_return=allow_none_return ) - - # we want to keep the narrowest value of if_type for union'ing the branches - # however, it would be silly to pass a literal as a type context. Pass the - # underlying fallback type instead. - if_type_fallback = simple_literal_type(get_proper_type(if_type)) or if_type - - # Analyze the right branch using full type context and store the type - full_context_else_type = self.analyze_cond_branch( + else_type = self.analyze_cond_branch( else_map, e.else_expr, context=ctx, allow_none_return=allow_none_return ) - if not mypy.checker.is_valid_inferred_type(if_type, self.chk.options): - # Analyze the right branch disregarding the left branch. - else_type = full_context_else_type - # we want to keep the narrowest value of else_type for union'ing the branches - # however, it would be silly to pass a literal as a type context. Pass the - # underlying fallback type instead. - else_type_fallback = simple_literal_type(get_proper_type(else_type)) or else_type - - # If it would make a difference, re-analyze the left - # branch using the right branch's type as context. - if ctx is None or not is_equivalent(else_type_fallback, ctx): - # TODO: If it's possible that the previous analysis of - # the left branch produced errors that are avoided - # using this context, suppress those errors. - if_type = self.analyze_cond_branch( - if_map, - e.if_expr, - context=else_type_fallback, - allow_none_return=allow_none_return, - ) - - elif if_type_fallback == ctx: - # There is no point re-running the analysis if if_type is equal to ctx. - # That would be an exact duplicate of the work we just did. - # This optimization is particularly important to avoid exponential blowup with nested - # if/else expressions: https://github.com/python/mypy/issues/9591 - # TODO: would checking for is_proper_subtype also work and cover more cases? - else_type = full_context_else_type - else: - # Analyze the right branch in the context of the left - # branch's type. - else_type = self.analyze_cond_branch( - else_map, - e.else_expr, - context=if_type_fallback, - allow_none_return=allow_none_return, - ) - - # In most cases using if_type as a context for right branch gives better inferred types. - # This is however not the case for literal types, so use the full context instead. - if is_literal_type_like(full_context_else_type) and not is_literal_type_like(else_type): - else_type = full_context_else_type - res: Type = make_simplified_union([if_type, else_type]) if has_uninhabited_component(res) and not isinstance( get_proper_type(self.type_context[-1]), UnionType diff --git a/mypyc/irbuild/statement.py b/mypyc/irbuild/statement.py index eeeb40ac672f..80e2264a4765 100644 --- a/mypyc/irbuild/statement.py +++ b/mypyc/irbuild/statement.py @@ -565,7 +565,9 @@ def make_entry(type: Expression) -> tuple[ValueGenFunc, int]: (make_entry(type) if type else None, var, make_handler(body)) for type, var, body in zip(t.types, t.vars, t.handlers) ] - else_body = (lambda: builder.accept(t.else_body)) if t.else_body else None + + _else_body = t.else_body + else_body = (lambda: builder.accept(_else_body)) if _else_body else None transform_try_except(builder, body, handlers, else_body, t.line) diff --git a/test-data/unit/check-literal.test b/test-data/unit/check-literal.test index 3c9290b8dbbb..7174d0fd831a 100644 --- a/test-data/unit/check-literal.test +++ b/test-data/unit/check-literal.test @@ -2946,6 +2946,140 @@ reveal_type(C().collection) # N: Revealed type is "builtins.list[Literal['word' reveal_type(C().word) # N: Revealed type is "Literal['word']" [builtins fixtures/tuple.pyi] +[case testStringLiteralTernary] +def test(b: bool) -> None: + l = "foo" if b else "bar" + reveal_type(l) # N: Revealed type is "builtins.str" +[builtins fixtures/tuple.pyi] + +[case testintLiteralTernary] +def test(b: bool) -> None: + l = 0 if b else 1 + reveal_type(l) # N: Revealed type is "builtins.int" +[builtins fixtures/tuple.pyi] + +[case testStringIntUnionTernary] +def test(b: bool) -> None: + l = 1 if b else "a" + reveal_type(l) # N: Revealed type is "Union[builtins.int, builtins.str]" +[builtins fixtures/tuple.pyi] + +[case testListComprehensionTernary] +# gh-19534 +def test(b: bool) -> None: + l = [1] if b else ["a"] + reveal_type(l) # N: Revealed type is "Union[builtins.list[builtins.int], builtins.list[builtins.str]]" +[builtins fixtures/list.pyi] + +[case testSetComprehensionTernary] +# gh-19534 +def test(b: bool) -> None: + s = {1} if b else {"a"} + reveal_type(s) # N: Revealed type is "Union[builtins.set[builtins.int], builtins.set[builtins.str]]" +[builtins fixtures/set.pyi] + +[case testDictComprehensionTernary] +# gh-19534 +def test(b: bool) -> None: + d = {1:1} if "" else {"a": "a"} + reveal_type(d) # N: Revealed type is "Union[builtins.dict[builtins.int, builtins.int], builtins.dict[builtins.str, builtins.str]]" +[builtins fixtures/dict.pyi] + +[case testLambdaTernary] +from typing import TypeVar, Union, Callable, reveal_type + +NOOP = lambda: None +class A: pass +class B: + attr: Union[A, None] + +def test_static(x: Union[A, None]) -> None: + def foo(t: A) -> None: ... + + l1: Callable[[], object] = (lambda: foo(x)) if x is not None else NOOP + r1: Callable[[], object] = NOOP if x is None else (lambda: foo(x)) + l2 = (lambda: foo(x)) if x is not None else NOOP + r2 = NOOP if x is None else (lambda: foo(x)) + reveal_type(l2) # N: Revealed type is "def ()" + reveal_type(r2) # N: Revealed type is "def ()" + +def test_generic(x: Union[A, None]) -> None: + T = TypeVar("T") + def bar(t: T) -> T: return t + + l1: Callable[[], None] = (lambda: bar(x)) if x is None else NOOP + r1: Callable[[], None] = NOOP if x is not None else (lambda: bar(x)) + l2 = (lambda: bar(x)) if x is None else NOOP + r2 = NOOP if x is not None else (lambda: bar(x)) + reveal_type(l2) # N: Revealed type is "def ()" + reveal_type(r2) # N: Revealed type is "def ()" + + +[case testLambdaTernaryIndirectAttribute] +# fails due to binder issue inside `check_func_def` +# gh-19561 +from typing import TypeVar, Union, Callable, reveal_type + +NOOP = lambda: None +class A: pass +class B: + attr: Union[A, None] + +def test_static_with_attr(x: B) -> None: + def foo(t: A) -> None: ... + + l1: Callable[[], None] = (lambda: foo(x.attr)) if x.attr is not None else NOOP # E: Argument 1 to "foo" has incompatible type "Optional[A]"; expected "A" + r1: Callable[[], None] = NOOP if x.attr is None else (lambda: foo(x.attr)) # E: Argument 1 to "foo" has incompatible type "Optional[A]"; expected "A" + l2 = (lambda: foo(x.attr)) if x.attr is not None else NOOP # E: Argument 1 to "foo" has incompatible type "Optional[A]"; expected "A" + r2 = NOOP if x.attr is None else (lambda: foo(x.attr)) # E: Argument 1 to "foo" has incompatible type "Optional[A]"; expected "A" + reveal_type(l2) # N: Revealed type is "def ()" + reveal_type(r2) # N: Revealed type is "def ()" + +def test_generic_with_attr(x: B) -> None: + T = TypeVar("T") + def bar(t: T) -> T: return t + + l1: Callable[[], None] = (lambda: bar(x.attr)) if x.attr is None else NOOP # E: Incompatible types in assignment (expression has type "Callable[[], Optional[A]]", variable has type "Callable[[], None]") + r1: Callable[[], None] = NOOP if x.attr is not None else (lambda: bar(x.attr)) # E: Incompatible types in assignment (expression has type "Callable[[], Optional[A]]", variable has type "Callable[[], None]") + l2 = (lambda: bar(x.attr)) if x.attr is None else NOOP + r2 = NOOP if x.attr is not None else (lambda: bar(x.attr)) + reveal_type(l2) # N: Revealed type is "def () -> Union[__main__.A, None]" + reveal_type(r2) # N: Revealed type is "def () -> Union[__main__.A, None]" + +[case testLambdaTernaryDoubleIndirectAttribute] +# fails due to binder issue inside `check_func_def` +# gh-19561 +from typing import TypeVar, Union, Callable, reveal_type + +NOOP = lambda: None +class A: pass +class B: + attr: Union[A, None] +class C: + attr: B + +def test_static_with_attr(x: C) -> None: + def foo(t: A) -> None: ... + + l1: Callable[[], None] = (lambda: foo(x.attr.attr)) if x.attr.attr is not None else NOOP # E: Argument 1 to "foo" has incompatible type "Optional[A]"; expected "A" + r1: Callable[[], None] = NOOP if x.attr.attr is None else (lambda: foo(x.attr.attr)) # E: Argument 1 to "foo" has incompatible type "Optional[A]"; expected "A" + l2 = (lambda: foo(x.attr.attr)) if x.attr.attr is not None else NOOP # E: Argument 1 to "foo" has incompatible type "Optional[A]"; expected "A" + r2 = NOOP if x.attr.attr is None else (lambda: foo(x.attr.attr)) # E: Argument 1 to "foo" has incompatible type "Optional[A]"; expected "A" + reveal_type(l2) # N: Revealed type is "def ()" + reveal_type(r2) # N: Revealed type is "def ()" + +def test_generic_with_attr(x: C) -> None: + T = TypeVar("T") + def bar(t: T) -> T: return t + + l1: Callable[[], None] = (lambda: bar(x.attr.attr)) if x.attr.attr is None else NOOP # E: Incompatible types in assignment (expression has type "Callable[[], Optional[A]]", variable has type "Callable[[], None]") + r1: Callable[[], None] = NOOP if x.attr.attr is not None else (lambda: bar(x.attr.attr)) # E: Incompatible types in assignment (expression has type "Callable[[], Optional[A]]", variable has type "Callable[[], None]") + l2 = (lambda: bar(x.attr.attr)) if x.attr.attr is None else NOOP + r2 = NOOP if x.attr.attr is not None else (lambda: bar(x.attr.attr)) + reveal_type(l2) # N: Revealed type is "def () -> Union[__main__.A, None]" + reveal_type(r2) # N: Revealed type is "def () -> Union[__main__.A, None]" + + [case testLiteralTernaryUnionNarrowing] from typing import Literal, Optional diff --git a/test-data/unit/check-optional.test b/test-data/unit/check-optional.test index 679906b0e00e..7c8d64714324 100644 --- a/test-data/unit/check-optional.test +++ b/test-data/unit/check-optional.test @@ -427,7 +427,9 @@ reveal_type(l) # N: Revealed type is "builtins.list[typing.Generator[builtins.s [builtins fixtures/list.pyi] [case testNoneListTernary] -x = [None] if "" else [1] # E: List item 0 has incompatible type "int"; expected "None" +# gh-19534 +x = [None] if "" else [1] +reveal_type(x) # N: Revealed type is "Union[builtins.list[None], builtins.list[builtins.int]]" [builtins fixtures/list.pyi] [case testListIncompatibleErrorMessage] From 381362bb2c1c1086f4c0c63304154bacaacfae43 Mon Sep 17 00:00:00 2001 From: Randolf Scholz Date: Fri, 1 Aug 2025 16:51:01 +0200 Subject: [PATCH 2/4] added testTernaryOperatorWithDefault --- test-data/unit/check-expressions.test | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/test-data/unit/check-expressions.test b/test-data/unit/check-expressions.test index 33271a3cc04c..e2668f8f7856 100644 --- a/test-data/unit/check-expressions.test +++ b/test-data/unit/check-expressions.test @@ -2463,3 +2463,21 @@ x + T # E: Unsupported left operand type for + ("int") T() # E: "TypeVar" not callable [builtins fixtures/tuple.pyi] [typing fixtures/typing-full.pyi] + +[case testTernaryOperatorWithDefault] +# flags: --python-version 3.13 +# gh-18817 + +class Ok[T, E = None]: + def __init__(self, value: T) -> None: + self._value = value + +class Err[E, T = None]: + def __init__(self, value: E) -> None: + self._value = value + +type Result[T, E] = Ok[T, E] | Err[E, T] + +class Bar[U]: + def foo(data: U, cond: bool) -> Result[U, str]: + return Ok(data) if cond else Err("Error") From 79b145f771a4643cd83e47d8f96d7fcaf3cfff29 Mon Sep 17 00:00:00 2001 From: Randolf Scholz Date: Fri, 1 Aug 2025 17:15:40 +0200 Subject: [PATCH 3/4] added type hint for mypyc --- mypy/checkexpr.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index f594b26b342c..2c16c757a8a5 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -5891,7 +5891,7 @@ def check_for_comp(self, e: GeneratorExpr | DictionaryComprehension) -> None: def visit_conditional_expr(self, e: ConditionalExpr, allow_none_return: bool = False) -> Type: self.accept(e.cond) - ctx = self.type_context[-1] + ctx: Type | None = self.type_context[-1] # Gain type information from isinstance if it is there # but only for the current expression From 681ec16e093cf0683b5c4e82f7f3001e512cb3ce Mon Sep 17 00:00:00 2001 From: Randolf Scholz Date: Fri, 1 Aug 2025 18:34:13 +0200 Subject: [PATCH 4/4] move testTernaryOperatorWithTypeVarDefault to python313.test --- test-data/unit/check-expressions.test | 18 ------------------ test-data/unit/check-python313.test | 18 ++++++++++++++++++ 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/test-data/unit/check-expressions.test b/test-data/unit/check-expressions.test index e2668f8f7856..33271a3cc04c 100644 --- a/test-data/unit/check-expressions.test +++ b/test-data/unit/check-expressions.test @@ -2463,21 +2463,3 @@ x + T # E: Unsupported left operand type for + ("int") T() # E: "TypeVar" not callable [builtins fixtures/tuple.pyi] [typing fixtures/typing-full.pyi] - -[case testTernaryOperatorWithDefault] -# flags: --python-version 3.13 -# gh-18817 - -class Ok[T, E = None]: - def __init__(self, value: T) -> None: - self._value = value - -class Err[E, T = None]: - def __init__(self, value: E) -> None: - self._value = value - -type Result[T, E] = Ok[T, E] | Err[E, T] - -class Bar[U]: - def foo(data: U, cond: bool) -> Result[U, str]: - return Ok(data) if cond else Err("Error") diff --git a/test-data/unit/check-python313.test b/test-data/unit/check-python313.test index b46ae0fecfc4..75ba5102da72 100644 --- a/test-data/unit/check-python313.test +++ b/test-data/unit/check-python313.test @@ -290,3 +290,21 @@ reveal_type(A1().x) # N: Revealed type is "builtins.int" reveal_type(A2().x) # N: Revealed type is "builtins.int" reveal_type(A3().x) # N: Revealed type is "builtins.int" [builtins fixtures/tuple.pyi] + + +[case testTernaryOperatorWithTypeVarDefault] +# gh-18817 + +class Ok[T, E = None]: + def __init__(self, value: T) -> None: + self._value = value + +class Err[E, T = None]: + def __init__(self, value: E) -> None: + self._value = value + +type Result[T, E] = Ok[T, E] | Err[E, T] + +class Bar[U]: + def foo(data: U, cond: bool) -> Result[U, str]: + return Ok(data) if cond else Err("Error")