From 28fefe61bd6596314d6a5df96a00484257370e22 Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Tue, 30 Dec 2025 01:11:16 -0800 Subject: [PATCH 01/14] correct tests --- test-data/unit/check-enum.test | 9 ++-- test-data/unit/check-flags.test | 2 +- test-data/unit/check-isinstance.test | 3 +- test-data/unit/check-narrowing.test | 19 ++++++-- test-data/unit/check-optional.test | 61 ++++++++++++++------------ test-data/unit/check-python310.test | 2 +- test-data/unit/fixtures/primitives.pyi | 3 ++ 7 files changed, 59 insertions(+), 40 deletions(-) diff --git a/test-data/unit/check-enum.test b/test-data/unit/check-enum.test index b33f583ad9bc..7589dcc9d300 100644 --- a/test-data/unit/check-enum.test +++ b/test-data/unit/check-enum.test @@ -1032,7 +1032,7 @@ else: reveal_type(z) # No output: this branch is unreachable [builtins fixtures/bool.pyi] -[case testEnumReachabilityNoNarrowingForUnionMessiness] +[case testEnumReachabilityNarrowingForUnionMessiness] from enum import Enum from typing import Literal @@ -1045,17 +1045,16 @@ x: Foo y: Literal[Foo.A, Foo.B] z: Literal[Foo.B, Foo.C] -# For the sake of simplicity, no narrowing is done when the narrower type is a Union. if x is y: - reveal_type(x) # N: Revealed type is "__main__.Foo" + reveal_type(x) # N: Revealed type is "Literal[__main__.Foo.A] | Literal[__main__.Foo.B]" reveal_type(y) # N: Revealed type is "Literal[__main__.Foo.A] | Literal[__main__.Foo.B]" else: reveal_type(x) # N: Revealed type is "__main__.Foo" reveal_type(y) # N: Revealed type is "Literal[__main__.Foo.A] | Literal[__main__.Foo.B]" if y is z: - reveal_type(y) # N: Revealed type is "Literal[__main__.Foo.A] | Literal[__main__.Foo.B]" - reveal_type(z) # N: Revealed type is "Literal[__main__.Foo.B] | Literal[__main__.Foo.C]" + reveal_type(y) # N: Revealed type is "Literal[__main__.Foo.B]" + reveal_type(z) # N: Revealed type is "Literal[__main__.Foo.B]" else: reveal_type(y) # N: Revealed type is "Literal[__main__.Foo.A] | Literal[__main__.Foo.B]" reveal_type(z) # N: Revealed type is "Literal[__main__.Foo.B] | Literal[__main__.Foo.C]" diff --git a/test-data/unit/check-flags.test b/test-data/unit/check-flags.test index a892aeba5a2c..a56e00f6b54c 100644 --- a/test-data/unit/check-flags.test +++ b/test-data/unit/check-flags.test @@ -2377,7 +2377,7 @@ x: int = "" # E: Incompatible types in assignment (expression has type "str", v x: int = "" # E: Incompatible types in assignment (expression has type "str", variable has type "int") [case testDisableBytearrayPromotion] -# flags: --disable-bytearray-promotion --strict-equality +# flags: --disable-bytearray-promotion --strict-equality --warn-unreachable def f(x: bytes) -> None: ... f(bytearray(b"asdf")) # E: Argument 1 to "f" has incompatible type "bytearray"; expected "bytes" f(memoryview(b"asdf")) diff --git a/test-data/unit/check-isinstance.test b/test-data/unit/check-isinstance.test index ff403a9f5d8c..fbe405b519e5 100644 --- a/test-data/unit/check-isinstance.test +++ b/test-data/unit/check-isinstance.test @@ -2760,13 +2760,14 @@ else: reveal_type(x) # N: Revealed type is "builtins.int | builtins.str" [case testTypeEqualsMultipleTypesShouldntNarrow] +# flags: --warn-unreachable # make sure we don't do any narrowing if there are multiple types being compared from typing import Union x: Union[int, str] if type(x) == int == str: - reveal_type(x) # N: Revealed type is "builtins.int | builtins.str" + reveal_type(x) # E: Statement is unreachable else: reveal_type(x) # N: Revealed type is "builtins.int | builtins.str" diff --git a/test-data/unit/check-narrowing.test b/test-data/unit/check-narrowing.test index 03586e4109f6..ed652f11aea0 100644 --- a/test-data/unit/check-narrowing.test +++ b/test-data/unit/check-narrowing.test @@ -1364,13 +1364,13 @@ class A: ... val: Optional[A] if val == None: - reveal_type(val) # N: Revealed type is "__main__.A | None" + reveal_type(val) # N: Revealed type is "None" else: reveal_type(val) # N: Revealed type is "__main__.A" if val != None: reveal_type(val) # N: Revealed type is "__main__.A" else: - reveal_type(val) # N: Revealed type is "__main__.A | None" + reveal_type(val) # N: Revealed type is "None" if val in (None,): reveal_type(val) # N: Revealed type is "__main__.A | None" @@ -1380,6 +1380,19 @@ if val not in (None,): reveal_type(val) # N: Revealed type is "__main__.A | None" else: reveal_type(val) # N: Revealed type is "__main__.A | None" + +class Hmm: + def __eq__(self, other) -> bool: ... + +hmm: Optional[Hmm] +if hmm == None: + reveal_type(hmm) # N: Revealed type is "__main__.Hmm | None" +else: + reveal_type(hmm) # N: Revealed type is "__main__.Hmm" +if hmm != None: + reveal_type(hmm) # N: Revealed type is "__main__.Hmm" +else: + reveal_type(hmm) # N: Revealed type is "__main__.Hmm | None" [builtins fixtures/primitives.pyi] [case testNarrowingWithTupleOfTypes] @@ -2277,7 +2290,7 @@ def f4(x: SE) -> None: # https://github.com/python/mypy/issues/17864 def f(x: str | int) -> None: if x == "x": - reveal_type(x) # N: Revealed type is "builtins.str | builtins.int" + reveal_type(x) # N: Revealed type is "builtins.str" y = x if x in ["x"]: diff --git a/test-data/unit/check-optional.test b/test-data/unit/check-optional.test index 393de00c41d5..022371e749fe 100644 --- a/test-data/unit/check-optional.test +++ b/test-data/unit/check-optional.test @@ -445,28 +445,29 @@ foo([f]) # E: List item 0 has incompatible type "Callable[[], int]"; expected " [case testInferEqualsNotOptional] from typing import Optional -x = '' # type: Optional[str] -if x == '': - reveal_type(x) # N: Revealed type is "builtins.str" -else: - reveal_type(x) # N: Revealed type is "builtins.str | None" -if x is '': - reveal_type(x) # N: Revealed type is "builtins.str" -else: - reveal_type(x) # N: Revealed type is "builtins.str | None" + +def main(x: Optional[str]): + if x == '': + reveal_type(x) # N: Revealed type is "builtins.str" + else: + reveal_type(x) # N: Revealed type is "builtins.str | None" + if x is '': + reveal_type(x) # N: Revealed type is "Literal['']" + else: + reveal_type(x) # N: Revealed type is "builtins.str | None" [builtins fixtures/ops.pyi] [case testInferEqualsNotOptionalWithUnion] from typing import Union -x = '' # type: Union[str, int, None] -if x == '': - reveal_type(x) # N: Revealed type is "builtins.str | builtins.int" -else: - reveal_type(x) # N: Revealed type is "builtins.str | builtins.int | None" -if x is '': - reveal_type(x) # N: Revealed type is "builtins.str | builtins.int" -else: - reveal_type(x) # N: Revealed type is "builtins.str | builtins.int | None" +def main(x: Union[str, int, None]): + if x == '': + reveal_type(x) # N: Revealed type is "builtins.str" + else: + reveal_type(x) # N: Revealed type is "builtins.str | builtins.int | None" + if x is '': + reveal_type(x) # N: Revealed type is "Literal['']" + else: + reveal_type(x) # N: Revealed type is "builtins.str | builtins.int | None" [builtins fixtures/ops.pyi] [case testInferEqualsNotOptionalWithOverlap] @@ -483,16 +484,18 @@ else: [builtins fixtures/ops.pyi] [case testInferEqualsStillOptionalWithNoOverlap] +# flags: --warn-unreachable from typing import Optional -x = '' # type: Optional[str] -if x == 0: - reveal_type(x) # N: Revealed type is "builtins.str | None" -else: - reveal_type(x) # N: Revealed type is "builtins.str | None" -if x is 0: - reveal_type(x) # N: Revealed type is "builtins.str | None" -else: - reveal_type(x) # N: Revealed type is "builtins.str | None" + +def main(x: Optional[str]): + if x == 0: + reveal_type(x) # E: Statement is unreachable + else: + reveal_type(x) # N: Revealed type is "builtins.str | None" + if x is 0: + reveal_type(x) # N: Revealed type is "builtins.str | None" + else: + reveal_type(x) # N: Revealed type is "builtins.str | None" [builtins fixtures/ops.pyi] [case testInferEqualsStillOptionalWithBothOptional] @@ -500,11 +503,11 @@ from typing import Union x = '' # type: Union[str, int, None] y = '' # type: Union[str, None] if x == y: - reveal_type(x) # N: Revealed type is "builtins.str | builtins.int | None" + reveal_type(x) # N: Revealed type is "builtins.str | None" else: reveal_type(x) # N: Revealed type is "builtins.str | builtins.int | None" if x is y: - reveal_type(x) # N: Revealed type is "builtins.str | builtins.int | None" + reveal_type(x) # N: Revealed type is "builtins.str | None" else: reveal_type(x) # N: Revealed type is "builtins.str | builtins.int | None" [builtins fixtures/ops.pyi] diff --git a/test-data/unit/check-python310.test b/test-data/unit/check-python310.test index 98d625958368..f03f85ea0c22 100644 --- a/test-data/unit/check-python310.test +++ b/test-data/unit/check-python310.test @@ -61,7 +61,7 @@ m: object match m: case b.b: - reveal_type(m) # N: Revealed type is "builtins.object" + reveal_type(m) # N: Revealed type is "builtins.int" [file b.py] b: int diff --git a/test-data/unit/fixtures/primitives.pyi b/test-data/unit/fixtures/primitives.pyi index 98e604e9e81e..b94574793fb9 100644 --- a/test-data/unit/fixtures/primitives.pyi +++ b/test-data/unit/fixtures/primitives.pyi @@ -39,16 +39,19 @@ class bytes(Sequence[int]): def __iter__(self) -> Iterator[int]: pass def __contains__(self, other: object) -> bool: pass def __getitem__(self, item: int) -> int: pass + def __eq__(self, other: object) -> bool: pass class bytearray(Sequence[int]): def __init__(self, x: bytes) -> None: pass def __iter__(self) -> Iterator[int]: pass def __contains__(self, other: object) -> bool: pass def __getitem__(self, item: int) -> int: pass + def __eq__(self, other: object) -> bool: pass class memoryview(Sequence[int]): def __init__(self, x: bytes) -> None: pass def __iter__(self) -> Iterator[int]: pass def __contains__(self, other: object) -> bool: pass def __getitem__(self, item: int) -> int: pass + def __eq__(self, other: object) -> bool: pass class tuple(Generic[T]): def __contains__(self, other: object) -> bool: pass class list(Sequence[T]): From cce6a713cc3daf9b5965e9ab973cffb1301cf089 Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Tue, 30 Dec 2025 01:10:27 -0800 Subject: [PATCH 02/14] wip --- mypy/checker.py | 273 +++++++++++++++++++++++------------------------- mypy/typeops.py | 6 +- 2 files changed, 132 insertions(+), 147 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 96e41a5e1786..3355330b5dc6 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -6247,43 +6247,41 @@ def find_type_equals_check( expr_indices: The list of indices of expressions in ``node`` that are being compared """ - - def is_type_call(expr: CallExpr) -> bool: - """Is expr a call to type with one argument?""" - return refers_to_fullname(expr.callee, "builtins.type") and len(expr.args) == 1 - # exprs that are being passed into type exprs_in_type_calls: list[Expression] = [] - # type that is being compared to type(expr) - type_being_compared: list[TypeRange] | None = None - # whether the type being compared to is final - is_final = False for index in expr_indices: expr = node.operands[index] - if isinstance(expr, CallExpr) and is_type_call(expr): exprs_in_type_calls.append(expr.args[0]) - else: - current_type = self.get_isinstance_type(expr) - if current_type is None: - continue - if type_being_compared is not None: - # It doesn't really make sense to have several types being - # compared to the output of type (like type(x) == int == str) - # because whether that's true is solely dependent on what the - # types being compared are, so we don't try to narrow types any - # further because we can't really get any information about the - # type of x from that check - return {}, {} - else: - if isinstance(expr, RefExpr) and isinstance(expr.node, TypeInfo): - is_final = expr.node.is_final - type_being_compared = current_type if not exprs_in_type_calls: return {}, {} + # type that is being compared to type(expr) + type_being_compared: list[TypeRange] | None = None + # whether the type being compared to is final + is_final = False + + for index in expr_indices: + expr = node.operands[index] + if isinstance(expr, CallExpr) and is_type_call(expr): + continue + current_type = self.get_isinstance_type(expr) + if current_type is None: + continue + if type_being_compared is not None: + # It doesn't really make sense to have several types being + # compared to the output of type (like type(x) == int == str) + # because whether that's true is solely dependent on what the + # types being compared are, so we don't try to narrow types any + # further because we can't really get any information about the + # type of x from that check + return {}, {} + if isinstance(expr, RefExpr) and isinstance(expr.node, TypeInfo): + is_final = expr.node.is_final + type_being_compared = current_type + if_maps: list[TypeMap] = [] else_maps: list[TypeMap] = [] for expr in exprs_in_type_calls: @@ -6663,8 +6661,10 @@ def equality_type_narrowing_helper( expr_indices, narrowable_operand_index_to_hash.keys(), ) - if if_map == {} and else_map == {} and node is not None: - if_map, else_map = self.find_type_equals_check(node, expr_indices) + if node is not None: + type_if_map, type_else_map = self.find_type_equals_check(node, expr_indices) + if_map = and_conditional_maps(if_map, type_if_map) + else_map = and_conditional_maps(else_map, type_else_map) return if_map, else_map def narrow_type_by_equality( @@ -6696,28 +6696,19 @@ def narrow_type_by_equality( is_valid_target: Callable[[Type], bool] = is_singleton_type coerce_only_in_literal_context = False should_narrow_by_identity = True - else: - - def is_exactly_literal_type(t: Type) -> bool: - return isinstance(get_proper_type(t), LiteralType) - - def has_no_custom_eq_checks(t: Type) -> bool: - return not custom_special_method( - t, "__eq__", check_all=False - ) and not custom_special_method(t, "__ne__", check_all=False) - - is_valid_target = is_exactly_literal_type + elif operator in {"==", "!="}: + is_valid_target = is_singleton_value coerce_only_in_literal_context = True expr_types = [operand_types[i] for i in expr_indices] - should_narrow_by_identity = all( - map(has_no_custom_eq_checks, expr_types) + should_narrow_by_identity = not any( + map(has_custom_eq_checks, expr_types) ) and not is_ambiguous_mix_of_enums(expr_types) + else: + raise AssertionError - if_map: TypeMap = {} - else_map: TypeMap = {} if should_narrow_by_identity: - if_map, else_map = self.refine_identity_comparison_expression( + return self.refine_identity_comparison_expression( operands, operand_types, expr_indices, @@ -6726,11 +6717,9 @@ def has_no_custom_eq_checks(t: Type) -> bool: coerce_only_in_literal_context, ) - if if_map == {} and else_map == {}: - if_map, else_map = self.refine_away_none_in_comparison( - operands, operand_types, expr_indices, narrowable_indices - ) - return if_map, else_map + return self.refine_away_none_in_comparison( + operands, operand_types, expr_indices, narrowable_indices + ) def propagate_up_typemap_info(self, new_types: TypeMap) -> TypeMap: """Attempts refining parent expressions of any MemberExpr or IndexExprs in new_types. @@ -6948,113 +6937,73 @@ def refine_identity_comparison_expression( expressions in the chain to a Literal type. Performing this coercion is sometimes too aggressive of a narrowing, depending on context. """ - should_coerce = True - if coerce_only_in_literal_context: - def should_coerce_inner(typ: Type) -> bool: - typ = get_proper_type(typ) - return is_literal_type_like(typ) or ( - isinstance(typ, Instance) and typ.type.is_enum - ) - - should_coerce = any(should_coerce_inner(operand_types[i]) for i in chain_indices) + if coerce_only_in_literal_context: + should_coerce = False + for i in chain_indices: + typ = get_proper_type(operand_types[i]) + if is_literal_type_like(typ) or (isinstance(typ, Instance) and typ.type.is_enum): + should_coerce = True + break + else: + should_coerce = True - target: Type | None = None - possible_target_indices = [] + operator_specific_targets = [] + type_targets = [] for i in chain_indices: expr_type = operand_types[i] if should_coerce: - # TODO: doing this prevents narrowing a single-member Enum to literal - # of its member, because we expand it here and then refuse to add equal - # types to typemaps. As a result, `x: Foo; x == Foo.A` does not narrow - # `x` to `Literal[Foo.A]` iff `Foo` has exactly one member. - # See testMatchEnumSingleChoice expr_type = coerce_to_literal(expr_type) - if not is_valid_target(get_proper_type(expr_type)): - continue - if target and not is_same_type(target, expr_type): - # We have multiple disjoint target types. So the 'if' branch - # must be unreachable. - return None, {} - target = expr_type - possible_target_indices.append(i) - - # There's nothing we can currently infer if none of the operands are valid targets, - # so we end early and infer nothing. - if target is None: - return {}, {} - - # If possible, use an unassignable expression as the target. - # We skip refining the type of the target below, so ideally we'd - # want to pick an expression we were going to skip anyways. - singleton_index = -1 - for i in possible_target_indices: - if i not in narrowable_operand_indices: - singleton_index = i - - # But if none of the possible singletons are unassignable ones, we give up - # and arbitrarily pick the last item, mostly because other parts of the - # type narrowing logic bias towards picking the rightmost item and it'd be - # nice to stay consistent. - # - # That said, it shouldn't matter which index we pick. For example, suppose we - # have this if statement, where 'x' and 'y' both have singleton types: - # - # if x is y: - # reveal_type(x) - # reveal_type(y) - # else: - # reveal_type(x) - # reveal_type(y) - # - # At this point, 'x' and 'y' *must* have the same singleton type: we would have - # ended early in the first for-loop in this function if they weren't. - # - # So, we should always get the same result in the 'if' case no matter which - # index we pick. And while we do end up getting different results in the 'else' - # case depending on the index (e.g. if we pick 'y', then its type stays the same - # while 'x' is narrowed to ''), this distinction is also moot: mypy - # currently will just mark the whole branch as unreachable if either operand is - # narrowed to . - if singleton_index == -1: - singleton_index = possible_target_indices[-1] - - sum_type_name = None - target = get_proper_type(target) - if isinstance(target, LiteralType) and ( - target.is_enum_literal() or isinstance(target.value, bool) - ): - sum_type_name = target.fallback.type.fullname + if is_valid_target(get_proper_type(expr_type)): + operator_specific_targets.append((i, TypeRange(expr_type, is_upper_bound=False))) + else: + type_targets.append((i, TypeRange(expr_type, is_upper_bound=False))) - target_type = [TypeRange(target, is_upper_bound=False)] + # print = lambda *a: None + print() + print("operands", operands) + print("operand_types", operand_types) + print("operator_specific_targets", operator_specific_targets) + print("type_targets", type_targets) partial_type_maps = [] - for i in chain_indices: - # If we try refining a type against itself, conditional_type_map - # will end up assuming that the 'else' branch is unreachable. This is - # typically not what we want: generally the user will intend for the - # target type to be some fixed 'sentinel' value and will want to refine - # the other exprs against this one instead. - if i == singleton_index: - continue - - # Naturally, we can't refine operands which are not permitted to be refined. - if i not in narrowable_operand_indices: - continue - - expr = operands[i] - expr_type = coerce_to_literal(operand_types[i]) - - if sum_type_name is not None: - expr_type = try_expanding_sum_type_to_union(expr_type, sum_type_name) - # We intentionally use 'conditional_types' directly here instead of - # 'self.conditional_types_with_intersection': we only compute ad-hoc - # intersections when working with pure instances. - types = conditional_types(expr_type, target_type) - partial_type_maps.append(conditional_types_to_typemaps(expr, *types)) + if operator_specific_targets: + for i in chain_indices: + if i not in narrowable_operand_indices: + continue + targets = [t for j, t in operator_specific_targets if j != i] + if targets: + expr_type = coerce_to_literal(operand_types[i]) + expr_type = try_expanding_sum_type_to_union(expr_type, None) + if_map, else_map = conditional_types_to_typemaps( + operands[i], *conditional_types(expr_type, targets) + ) + print("ooo if_map", if_map) + print("ooo else_map", else_map) + partial_type_maps.append((if_map, else_map)) - return reduce_conditional_maps(partial_type_maps) + if type_targets: + for i in chain_indices: + if i not in narrowable_operand_indices: + continue + targets = [t for j, t in type_targets if j != i] + if targets: + expr_type = operand_types[i] + if_map, else_map = conditional_types_to_typemaps( + operands[i], *conditional_types(expr_type, targets) + ) + if if_map: + else_map = {} + print("ttt targets", targets) + print("ttt if_map", if_map) + print("ttt else_map", else_map) + partial_type_maps.append((if_map, else_map)) + + final_if_map, final_else_map = reduce_conditional_maps(partial_type_maps) + print("final_if_map", final_if_map) + print("final_else_map", final_else_map) + return final_if_map, final_else_map def refine_away_none_in_comparison( self, @@ -8648,6 +8597,40 @@ def reduce_conditional_maps( return final_if_map, final_else_map +def is_singleton_value(t: Type) -> bool: + t = get_proper_type(t) + # TODO: check the type object thing + ret = isinstance(t, LiteralType) or t.is_singleton_type() or (isinstance(t, CallableType) and t.is_type_obj()) + print("!!!", t, type(t), ret) + return ret + + +BUILTINS_CUSTOM_EQ_CHECKS: Final = { + "builtins.bytes", + "builtins.bytearray", + "builtins.memoryview", + "builtins.tuple", + "builtins.list", + "builtins.dict", + "builtins.set", +} + + +def has_custom_eq_checks(t: Type) -> bool: + return ( + custom_special_method(t, "__eq__", check_all=False) + or custom_special_method(t, "__ne__", check_all=False) + # TODO: make the hack more principled. explain what and why we're doing this though + # custom_special_method has special casing for builtins + or (isinstance(t, Instance) and t.type.fullname in BUILTINS_CUSTOM_EQ_CHECKS) + ) + + +def is_type_call(expr: CallExpr) -> bool: + """Is expr a call to type with one argument?""" + return refers_to_fullname(expr.callee, "builtins.type") and len(expr.args) == 1 + + def convert_to_typetype(type_map: TypeMap) -> TypeMap: converted_type_map: dict[Expression, Type] = {} if type_map is None: diff --git a/mypy/typeops.py b/mypy/typeops.py index 02e1dbda514a..d2a6881206e9 100644 --- a/mypy/typeops.py +++ b/mypy/typeops.py @@ -1005,7 +1005,7 @@ def is_singleton_type(typ: Type) -> bool: return typ.is_singleton_type() -def try_expanding_sum_type_to_union(typ: Type, target_fullname: str) -> Type: +def try_expanding_sum_type_to_union(typ: Type, target_fullname: str | None) -> Type: """Attempts to recursively expand any enum Instances with the given target_fullname into a Union of all of its component LiteralTypes. @@ -1034,7 +1034,9 @@ class Status(Enum): ] return UnionType.make_union(items) - if isinstance(typ, Instance) and typ.type.fullname == target_fullname: + if isinstance(typ, Instance) and ( + target_fullname is None or typ.type.fullname == target_fullname + ): if typ.type.fullname == "builtins.bool": return UnionType([LiteralType(True, typ), LiteralType(False, typ)]) From cdbd7376612f68e715c5643674c5d6b972e57690 Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Tue, 30 Dec 2025 01:44:32 -0800 Subject: [PATCH 03/14] incorrect tests --- test-data/unit/check-enum.test | 6 +++--- test-data/unit/check-isinstance.test | 2 +- test-data/unit/check-narrowing.test | 2 +- test-data/unit/check-optional.test | 26 +++++++++++++------------- test-data/unit/check-python310.test | 4 ++-- 5 files changed, 20 insertions(+), 20 deletions(-) diff --git a/test-data/unit/check-enum.test b/test-data/unit/check-enum.test index 7589dcc9d300..0753239be0db 100644 --- a/test-data/unit/check-enum.test +++ b/test-data/unit/check-enum.test @@ -1315,7 +1315,7 @@ reveal_type(x) # N: Revealed type is "__main__.Foo" # ..and we get the same result if we have two disjoint groups within the same comp expr if x is Foo.A < x is Foo.B: - reveal_type(x) # E: Statement is unreachable + reveal_type(x) # N: Revealed type is "__main__.Foo" else: reveal_type(x) # N: Revealed type is "__main__.Foo" reveal_type(x) # N: Revealed type is "__main__.Foo" @@ -1333,9 +1333,9 @@ class Foo(Enum): x: Foo if x is Foo.A is Foo.B: - reveal_type(x) # E: Statement is unreachable + reveal_type(x) # N: Revealed type is "Literal[__main__.Foo.A] | Literal[__main__.Foo.B]" else: - reveal_type(x) # N: Revealed type is "__main__.Foo" + reveal_type(x) # N: Revealed type is "Literal[__main__.Foo.C]" reveal_type(x) # N: Revealed type is "__main__.Foo" literal_a: Literal[Foo.A] diff --git a/test-data/unit/check-isinstance.test b/test-data/unit/check-isinstance.test index fbe405b519e5..1f3425bd23da 100644 --- a/test-data/unit/check-isinstance.test +++ b/test-data/unit/check-isinstance.test @@ -2729,7 +2729,7 @@ from typing import Union y: str if type(y) is int: # E: Subclass of "str" and "int" cannot exist: would have incompatible method signatures - y # E: Statement is unreachable + y # E: Statement is unreachable else: reveal_type(y) # N: Revealed type is "builtins.str" [builtins fixtures/isinstance.pyi] diff --git a/test-data/unit/check-narrowing.test b/test-data/unit/check-narrowing.test index ed652f11aea0..2147dee212aa 100644 --- a/test-data/unit/check-narrowing.test +++ b/test-data/unit/check-narrowing.test @@ -2296,7 +2296,7 @@ def f(x: str | int) -> None: if x in ["x"]: # TODO: we should fix this reveal https://github.com/python/mypy/issues/3229 reveal_type(x) # N: Revealed type is "builtins.str | builtins.int" - y = x + y = x # E: Incompatible types in assignment (expression has type "str | int", variable has type "str") z = x z = y [builtins fixtures/primitives.pyi] diff --git a/test-data/unit/check-optional.test b/test-data/unit/check-optional.test index 022371e749fe..726b1b856689 100644 --- a/test-data/unit/check-optional.test +++ b/test-data/unit/check-optional.test @@ -474,11 +474,11 @@ def main(x: Union[str, int, None]): from typing import Union x = '' # type: Union[str, int, None] if x == object(): - reveal_type(x) # N: Revealed type is "builtins.str | builtins.int" + reveal_type(x) # N: Revealed type is "builtins.str | builtins.int | None" else: reveal_type(x) # N: Revealed type is "builtins.str | builtins.int | None" if x is object(): - reveal_type(x) # N: Revealed type is "builtins.str | builtins.int" + reveal_type(x) # N: Revealed type is "builtins.str | builtins.int | None" else: reveal_type(x) # N: Revealed type is "builtins.str | builtins.int | None" [builtins fixtures/ops.pyi] @@ -489,7 +489,7 @@ from typing import Optional def main(x: Optional[str]): if x == 0: - reveal_type(x) # E: Statement is unreachable + reveal_type(x) # N: Revealed type is "builtins.str | None" else: reveal_type(x) # N: Revealed type is "builtins.str | None" if x is 0: @@ -514,21 +514,21 @@ else: [case testInferEqualsNotOptionalWithMultipleArgs] from typing import Optional -x: Optional[int] -y: Optional[int] -if x == y == 1: - reveal_type(x) # N: Revealed type is "builtins.int" - reveal_type(y) # N: Revealed type is "builtins.int" -else: - reveal_type(x) # N: Revealed type is "builtins.int | None" - reveal_type(y) # N: Revealed type is "builtins.int | None" + +def main(x: Optional[int], y: Optional[int]): + if x == y == 1: + reveal_type(x) # N: Revealed type is "builtins.int | None" + reveal_type(y) # N: Revealed type is "builtins.int | None" + else: + reveal_type(x) # N: Revealed type is "builtins.int | None" + reveal_type(y) # N: Revealed type is "builtins.int | None" class A: pass a: Optional[A] b: Optional[A] if a == b == object(): - reveal_type(a) # N: Revealed type is "__main__.A" - reveal_type(b) # N: Revealed type is "__main__.A" + reveal_type(a) # N: Revealed type is "__main__.A | None" + reveal_type(b) # N: Revealed type is "__main__.A | None" else: reveal_type(a) # N: Revealed type is "__main__.A | None" reveal_type(b) # N: Revealed type is "__main__.A | None" diff --git a/test-data/unit/check-python310.test b/test-data/unit/check-python310.test index f03f85ea0c22..78af1dc0a709 100644 --- a/test-data/unit/check-python310.test +++ b/test-data/unit/check-python310.test @@ -2938,9 +2938,9 @@ T_Choice = TypeVar("T_Choice", bound=b.One | b.Two) def switch(choice: type[T_Choice]) -> None: match choice: case b.One: - reveal_type(choice) # N: Revealed type is "type[T_Choice`-1]" + reveal_type(choice) # N: Revealed type is "def () -> b.One" case b.Two: - reveal_type(choice) # N: Revealed type is "type[T_Choice`-1]" + reveal_type(choice) # N: Revealed type is "def () -> b.Two" case _: reveal_type(choice) # N: Revealed type is "type[T_Choice`-1]" From 4dbd1070851daf4a4c8265a5b50cf2e4936f819d Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Tue, 30 Dec 2025 03:20:03 -0800 Subject: [PATCH 04/14] comment --- mypy/checker.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 3355330b5dc6..b8a8f85f818a 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -6960,11 +6960,11 @@ def refine_identity_comparison_expression( type_targets.append((i, TypeRange(expr_type, is_upper_bound=False))) # print = lambda *a: None - print() - print("operands", operands) - print("operand_types", operand_types) - print("operator_specific_targets", operator_specific_targets) - print("type_targets", type_targets) + # print() + # print("operands", operands) + # print("operand_types", operand_types) + # print("operator_specific_targets", operator_specific_targets) + # print("type_targets", type_targets) partial_type_maps = [] @@ -6979,8 +6979,8 @@ def refine_identity_comparison_expression( if_map, else_map = conditional_types_to_typemaps( operands[i], *conditional_types(expr_type, targets) ) - print("ooo if_map", if_map) - print("ooo else_map", else_map) + # print("ooo if_map", if_map) + # print("ooo else_map", else_map) partial_type_maps.append((if_map, else_map)) if type_targets: @@ -6995,14 +6995,14 @@ def refine_identity_comparison_expression( ) if if_map: else_map = {} - print("ttt targets", targets) - print("ttt if_map", if_map) - print("ttt else_map", else_map) + # print("ttt targets", targets) + # print("ttt if_map", if_map) + # print("ttt else_map", else_map) partial_type_maps.append((if_map, else_map)) final_if_map, final_else_map = reduce_conditional_maps(partial_type_maps) - print("final_if_map", final_if_map) - print("final_else_map", final_else_map) + # print("final_if_map", final_if_map) + # print("final_else_map", final_else_map) return final_if_map, final_else_map def refine_away_none_in_comparison( @@ -8601,7 +8601,7 @@ def is_singleton_value(t: Type) -> bool: t = get_proper_type(t) # TODO: check the type object thing ret = isinstance(t, LiteralType) or t.is_singleton_type() or (isinstance(t, CallableType) and t.is_type_obj()) - print("!!!", t, type(t), ret) + # print("!!!", t, type(t), ret) return ret From 4a552d58db0a99345bf3790e06093b3838f4c77a Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Tue, 30 Dec 2025 13:20:13 -0800 Subject: [PATCH 05/14] avoid narrowing builtins --- mypy/checker.py | 9 +++++--- test-data/unit/check-narrowing.test | 27 ++++++++++++++++++++++ test-data/unit/fixtures/notimplemented.pyi | 1 + 3 files changed, 34 insertions(+), 3 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index b8a8f85f818a..9a2aa011943c 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -6545,6 +6545,9 @@ def comparison_type_narrowing_helper(self, node: ComparisonExpr) -> tuple[TypeMa if ( literal(expr) == LITERAL_TYPE and not is_literal_none(expr) + and not is_literal_not_implemented(expr) + and not is_false_literal(expr) + and not is_true_literal(expr) and not self.is_literal_enum(expr) ): h = literal_hash(expr) @@ -6584,7 +6587,7 @@ def comparison_type_narrowing_helper(self, node: ComparisonExpr) -> tuple[TypeMa operands, operand_types, expr_indices, - narrowable_operand_index_to_hash, + narrowable_indices=narrowable_operand_index_to_hash.keys(), ) elif operator in {"in", "not in"}: assert len(expr_indices) == 2 @@ -6649,7 +6652,7 @@ def equality_type_narrowing_helper( operands: list[Expression], operand_types: list[Type], expr_indices: list[int], - narrowable_operand_index_to_hash: dict[int, tuple[Key, ...]], + narrowable_indices: AbstractSet[int], ) -> tuple[TypeMap, TypeMap]: """Calculate type maps for '==', '!=', 'is' or 'is not' expression.""" # If we haven't been able to narrow types yet, we might be dealing with a @@ -6659,7 +6662,7 @@ def equality_type_narrowing_helper( operands, operand_types, expr_indices, - narrowable_operand_index_to_hash.keys(), + narrowable_indices=narrowable_indices, ) if node is not None: type_if_map, type_else_map = self.find_type_equals_check(node, expr_indices) diff --git a/test-data/unit/check-narrowing.test b/test-data/unit/check-narrowing.test index 2147dee212aa..b4960ee8ff6d 100644 --- a/test-data/unit/check-narrowing.test +++ b/test-data/unit/check-narrowing.test @@ -2712,3 +2712,30 @@ reveal_type(t.foo) # N: Revealed type is "__main__.D" t.foo = C1() reveal_type(t.foo) # N: Revealed type is "__main__.C" [builtins fixtures/property.pyi] + +[case testNarrowingNotImplemented] +from __future__ import annotations +from typing_extensions import Self + +class X: + def __divmod__(self, other: Self | int) -> tuple[Self, Self]: ... + + def __floordiv__(self, other: Self | int) -> Self: + qr = self.__divmod__(other) + if qr is NotImplemented: + return NotImplemented + return qr[0] +[builtins fixtures/notimplemented.pyi] + + +[case testNarrowingBooleans] +# flags: --warn-return-any +from typing import Any + +def foo(x: dict[str, Any]) -> bool: + if x.get("event") is False: + return False + if x.get("event") is True: + return True + raise +[builtins fixtures/dict.pyi] diff --git a/test-data/unit/fixtures/notimplemented.pyi b/test-data/unit/fixtures/notimplemented.pyi index c9e58f099477..9f3f276a7d48 100644 --- a/test-data/unit/fixtures/notimplemented.pyi +++ b/test-data/unit/fixtures/notimplemented.pyi @@ -12,6 +12,7 @@ class str: pass class dict: pass class tuple: pass class ellipsis: pass +class list: pass import sys From ca5120dac41891349b34a4e47e85ab0468518884 Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Tue, 30 Dec 2025 13:58:11 -0800 Subject: [PATCH 06/14] more changes --- mypy/checker.py | 46 +++++++++++++--------------- test-data/unit/check-enum.test | 6 ++-- test-data/unit/check-isinstance.test | 2 +- test-data/unit/check-optional.test | 4 +-- 4 files changed, 27 insertions(+), 31 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 9a2aa011943c..2bbdd9a6d8f3 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -6977,14 +6977,13 @@ def refine_identity_comparison_expression( continue targets = [t for j, t in operator_specific_targets if j != i] if targets: - expr_type = coerce_to_literal(operand_types[i]) - expr_type = try_expanding_sum_type_to_union(expr_type, None) - if_map, else_map = conditional_types_to_typemaps( - operands[i], *conditional_types(expr_type, targets) - ) - # print("ooo if_map", if_map) - # print("ooo else_map", else_map) - partial_type_maps.append((if_map, else_map)) + for target in targets: + expr_type = coerce_to_literal(operand_types[i]) + expr_type = try_expanding_sum_type_to_union(expr_type, None) + if_map, else_map = conditional_types_to_typemaps( + operands[i], *conditional_types(expr_type, [target]) + ) + partial_type_maps.append((if_map, else_map)) if type_targets: for i in chain_indices: @@ -6992,20 +6991,18 @@ def refine_identity_comparison_expression( continue targets = [t for j, t in type_targets if j != i] if targets: - expr_type = operand_types[i] - if_map, else_map = conditional_types_to_typemaps( - operands[i], *conditional_types(expr_type, targets) - ) - if if_map: - else_map = {} - # print("ttt targets", targets) - # print("ttt if_map", if_map) - # print("ttt else_map", else_map) - partial_type_maps.append((if_map, else_map)) + for target in targets: + expr_type = operand_types[i] + if_map, else_map = conditional_types_to_typemaps( + operands[i], *conditional_types(expr_type, [target]) + ) + if if_map: + else_map = {} + partial_type_maps.append((if_map, else_map)) - final_if_map, final_else_map = reduce_conditional_maps(partial_type_maps) - # print("final_if_map", final_if_map) - # print("final_else_map", final_else_map) + final_if_map, final_else_map = reduce_conditional_maps(partial_type_maps, use_meet=len(operands) > 2) + # print("final_if_map", {str(k): str(v) for k, v in final_if_map.items()}) + # print("final_else_map", {str(k): str(v) for k, v in final_else_map.items()}) return final_if_map, final_else_map def refine_away_none_in_comparison( @@ -8532,6 +8529,8 @@ def and_conditional_maps(m1: TypeMap, m2: TypeMap, use_meet: bool = False) -> Ty for n2 in m2: if literal_hash(n1) == literal_hash(n2): result[n1] = meet_types(m1[n1], m2[n2]) + if isinstance(result[n1], UninhabitedType): + return None return result @@ -8602,10 +8601,7 @@ def reduce_conditional_maps( def is_singleton_value(t: Type) -> bool: t = get_proper_type(t) - # TODO: check the type object thing - ret = isinstance(t, LiteralType) or t.is_singleton_type() or (isinstance(t, CallableType) and t.is_type_obj()) - # print("!!!", t, type(t), ret) - return ret + return isinstance(t, LiteralType) or t.is_singleton_type() BUILTINS_CUSTOM_EQ_CHECKS: Final = { diff --git a/test-data/unit/check-enum.test b/test-data/unit/check-enum.test index 0753239be0db..7589dcc9d300 100644 --- a/test-data/unit/check-enum.test +++ b/test-data/unit/check-enum.test @@ -1315,7 +1315,7 @@ reveal_type(x) # N: Revealed type is "__main__.Foo" # ..and we get the same result if we have two disjoint groups within the same comp expr if x is Foo.A < x is Foo.B: - reveal_type(x) # N: Revealed type is "__main__.Foo" + reveal_type(x) # E: Statement is unreachable else: reveal_type(x) # N: Revealed type is "__main__.Foo" reveal_type(x) # N: Revealed type is "__main__.Foo" @@ -1333,9 +1333,9 @@ class Foo(Enum): x: Foo if x is Foo.A is Foo.B: - reveal_type(x) # N: Revealed type is "Literal[__main__.Foo.A] | Literal[__main__.Foo.B]" + reveal_type(x) # E: Statement is unreachable else: - reveal_type(x) # N: Revealed type is "Literal[__main__.Foo.C]" + reveal_type(x) # N: Revealed type is "__main__.Foo" reveal_type(x) # N: Revealed type is "__main__.Foo" literal_a: Literal[Foo.A] diff --git a/test-data/unit/check-isinstance.test b/test-data/unit/check-isinstance.test index 1f3425bd23da..89d07a6b03c6 100644 --- a/test-data/unit/check-isinstance.test +++ b/test-data/unit/check-isinstance.test @@ -2767,7 +2767,7 @@ from typing import Union x: Union[int, str] if type(x) == int == str: - reveal_type(x) # E: Statement is unreachable + reveal_type(x) # N: Revealed type is "builtins.int | builtins.str" else: reveal_type(x) # N: Revealed type is "builtins.int | builtins.str" diff --git a/test-data/unit/check-optional.test b/test-data/unit/check-optional.test index 726b1b856689..96ba8dfc5eec 100644 --- a/test-data/unit/check-optional.test +++ b/test-data/unit/check-optional.test @@ -517,8 +517,8 @@ from typing import Optional def main(x: Optional[int], y: Optional[int]): if x == y == 1: - reveal_type(x) # N: Revealed type is "builtins.int | None" - reveal_type(y) # N: Revealed type is "builtins.int | None" + reveal_type(x) # N: Revealed type is "builtins.int" + reveal_type(y) # N: Revealed type is "builtins.int" else: reveal_type(x) # N: Revealed type is "builtins.int | None" reveal_type(y) # N: Revealed type is "builtins.int | None" From 94c5be7cfc6b62932d4c19f586fd665696101808 Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Tue, 30 Dec 2025 14:06:03 -0800 Subject: [PATCH 07/14] improve test --- test-data/unit/check-optional.test | 41 +++++++++++++++++------------- 1 file changed, 24 insertions(+), 17 deletions(-) diff --git a/test-data/unit/check-optional.test b/test-data/unit/check-optional.test index 96ba8dfc5eec..d38c2c01a64e 100644 --- a/test-data/unit/check-optional.test +++ b/test-data/unit/check-optional.test @@ -472,15 +472,19 @@ def main(x: Union[str, int, None]): [case testInferEqualsNotOptionalWithOverlap] from typing import Union -x = '' # type: Union[str, int, None] -if x == object(): - reveal_type(x) # N: Revealed type is "builtins.str | builtins.int | None" -else: - reveal_type(x) # N: Revealed type is "builtins.str | builtins.int | None" -if x is object(): - reveal_type(x) # N: Revealed type is "builtins.str | builtins.int | None" -else: - reveal_type(x) # N: Revealed type is "builtins.str | builtins.int | None" + +def make_random_object() -> object: + return None + +def main(x: Union[str, int, None]): + if x == make_random_object(): + reveal_type(x) # N: Revealed type is "builtins.str | builtins.int | None" + else: + reveal_type(x) # N: Revealed type is "builtins.str | builtins.int | None" + if x is make_random_object(): + reveal_type(x) # N: Revealed type is "builtins.str | builtins.int | None" + else: + reveal_type(x) # N: Revealed type is "builtins.str | builtins.int | None" [builtins fixtures/ops.pyi] [case testInferEqualsStillOptionalWithNoOverlap] @@ -524,14 +528,17 @@ def main(x: Optional[int], y: Optional[int]): reveal_type(y) # N: Revealed type is "builtins.int | None" class A: pass -a: Optional[A] -b: Optional[A] -if a == b == object(): - reveal_type(a) # N: Revealed type is "__main__.A | None" - reveal_type(b) # N: Revealed type is "__main__.A | None" -else: - reveal_type(a) # N: Revealed type is "__main__.A | None" - reveal_type(b) # N: Revealed type is "__main__.A | None" + +def returns_random_object() -> object: + return None + +def main2(a: Optional[A], b: Optional[A]): + if a == b == returns_random_object(): + reveal_type(a) # N: Revealed type is "__main__.A | None" + reveal_type(b) # N: Revealed type is "__main__.A | None" + else: + reveal_type(a) # N: Revealed type is "__main__.A | None" + reveal_type(b) # N: Revealed type is "__main__.A | None" [builtins fixtures/ops.pyi] [case testInferInWithErasedTypes] From 92051d994189a8a4f3eae6ad256610b6ab90857f Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Tue, 30 Dec 2025 14:41:59 -0800 Subject: [PATCH 08/14] new test --- test-data/unit/check-narrowing.test | 31 +++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/test-data/unit/check-narrowing.test b/test-data/unit/check-narrowing.test index b4960ee8ff6d..6a321972bee4 100644 --- a/test-data/unit/check-narrowing.test +++ b/test-data/unit/check-narrowing.test @@ -2739,3 +2739,34 @@ def foo(x: dict[str, Any]) -> bool: return True raise [builtins fixtures/dict.pyi] + + +[case testNarrowingTypeObjects] +from __future__ import annotations +from typing import Callable, Any, TypeVar, Generic, Protocol +_T_co = TypeVar('_T_co', covariant=True) + +class Boxxy(Protocol[_T_co]): + def get_box(self) -> _T_co: ... + +class TupleLike(Generic[_T_co]): + def __init__(self, iterable: Boxxy[_T_co], /) -> None: + raise + +class Box1(Generic[_T_co]): + def __init__(self, content: _T_co, /) -> None: ... + def get_box(self) -> _T_co: raise + +class Box2(Generic[_T_co]): + def __init__(self, content: _T_co, /) -> None: ... + def get_box(self) -> _T_co: raise + +def get_type(setting_name: str) -> Callable[[Box1], Any] | type[Any]: + raise + +def main(key: str): + existing_value_type = get_type(key) + if existing_value_type is TupleLike: + reveal_type(TupleLike) # N: Revealed type is "(def (__main__.Box1[Any]) -> Any) | type[Any]" + TupleLike(Box2("str")) # E: Argument 1 has incompatible type "Box2[str]"; expected "Box1[Any]" +[builtins fixtures/tuple.pyi] From 586d2a1b01051bafbccfd6ef5b00a99191806ac5 Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Tue, 30 Dec 2025 14:42:18 -0800 Subject: [PATCH 09/14] fix --- mypy/checker.py | 34 +++++++++++++++++------------ test-data/unit/check-narrowing.test | 4 ++-- 2 files changed, 22 insertions(+), 16 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 2bbdd9a6d8f3..9246283832a0 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -6549,6 +6549,7 @@ def comparison_type_narrowing_helper(self, node: ComparisonExpr) -> tuple[TypeMa and not is_false_literal(expr) and not is_true_literal(expr) and not self.is_literal_enum(expr) + and not (isinstance(expr_type, CallableType) and expr_type.is_type_obj()) ): h = literal_hash(expr) if h is not None: @@ -6658,11 +6659,7 @@ def equality_type_narrowing_helper( # If we haven't been able to narrow types yet, we might be dealing with a # explicit type(x) == some_type check if_map, else_map = self.narrow_type_by_equality( - operator, - operands, - operand_types, - expr_indices, - narrowable_indices=narrowable_indices, + operator, operands, operand_types, expr_indices, narrowable_indices=narrowable_indices ) if node is not None: type_if_map, type_else_map = self.find_type_equals_check(node, expr_indices) @@ -6962,12 +6959,12 @@ def refine_identity_comparison_expression( else: type_targets.append((i, TypeRange(expr_type, is_upper_bound=False))) - # print = lambda *a: None - # print() - # print("operands", operands) - # print("operand_types", operand_types) - # print("operator_specific_targets", operator_specific_targets) - # print("type_targets", type_targets) + if False: + print() + print("operands", operands) + print("operand_types", operand_types) + print("operator_specific_targets", operator_specific_targets) + print("type_targets", type_targets) partial_type_maps = [] @@ -7000,9 +6997,18 @@ def refine_identity_comparison_expression( else_map = {} partial_type_maps.append((if_map, else_map)) - final_if_map, final_else_map = reduce_conditional_maps(partial_type_maps, use_meet=len(operands) > 2) - # print("final_if_map", {str(k): str(v) for k, v in final_if_map.items()}) - # print("final_else_map", {str(k): str(v) for k, v in final_else_map.items()}) + final_if_map, final_else_map = reduce_conditional_maps( + partial_type_maps, use_meet=len(operands) > 2 + ) + if False: + print( + "final_if_map", + {str(k): str(v) for k, v in final_if_map.items()} if final_if_map else None, + ) + print( + "final_else_map", + {str(k): str(v) for k, v in final_else_map.items()} if final_else_map else None, + ) return final_if_map, final_else_map def refine_away_none_in_comparison( diff --git a/test-data/unit/check-narrowing.test b/test-data/unit/check-narrowing.test index 6a321972bee4..b18bb257a128 100644 --- a/test-data/unit/check-narrowing.test +++ b/test-data/unit/check-narrowing.test @@ -2767,6 +2767,6 @@ def get_type(setting_name: str) -> Callable[[Box1], Any] | type[Any]: def main(key: str): existing_value_type = get_type(key) if existing_value_type is TupleLike: - reveal_type(TupleLike) # N: Revealed type is "(def (__main__.Box1[Any]) -> Any) | type[Any]" - TupleLike(Box2("str")) # E: Argument 1 has incompatible type "Box2[str]"; expected "Box1[Any]" + reveal_type(TupleLike) # N: Revealed type is "def [_T_co] (__main__.Boxxy[_T_co`1]) -> __main__.TupleLike[_T_co`1]" + TupleLike(Box2("str")) [builtins fixtures/tuple.pyi] From 7bc5dacc75ccba24f038cd0031dd2abd28b86bff Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Tue, 30 Dec 2025 15:03:49 -0800 Subject: [PATCH 10/14] clean up --- mypy/checker.py | 38 +++++++++++++------------------------- mypy/test/testargs.py | 3 ++- mypy/test/testtypes.py | 4 ++-- 3 files changed, 17 insertions(+), 28 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 9246283832a0..df39357b8c90 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -6549,7 +6549,10 @@ def comparison_type_narrowing_helper(self, node: ComparisonExpr) -> tuple[TypeMa and not is_false_literal(expr) and not is_true_literal(expr) and not self.is_literal_enum(expr) - and not (isinstance(expr_type, CallableType) and expr_type.is_type_obj()) + and not ( + isinstance(p_expr := get_proper_type(expr_type), CallableType) + and p_expr.is_type_obj() + ) ): h = literal_hash(expr) if h is not None: @@ -6959,13 +6962,6 @@ def refine_identity_comparison_expression( else: type_targets.append((i, TypeRange(expr_type, is_upper_bound=False))) - if False: - print() - print("operands", operands) - print("operand_types", operand_types) - print("operator_specific_targets", operator_specific_targets) - print("type_targets", type_targets) - partial_type_maps = [] if operator_specific_targets: @@ -6997,19 +6993,7 @@ def refine_identity_comparison_expression( else_map = {} partial_type_maps.append((if_map, else_map)) - final_if_map, final_else_map = reduce_conditional_maps( - partial_type_maps, use_meet=len(operands) > 2 - ) - if False: - print( - "final_if_map", - {str(k): str(v) for k, v in final_if_map.items()} if final_if_map else None, - ) - print( - "final_else_map", - {str(k): str(v) for k, v in final_else_map.items()} if final_else_map else None, - ) - return final_if_map, final_else_map + return reduce_conditional_maps(partial_type_maps, use_meet=len(operands) > 2) def refine_away_none_in_comparison( self, @@ -8534,9 +8518,10 @@ def and_conditional_maps(m1: TypeMap, m2: TypeMap, use_meet: bool = False) -> Ty for n1 in m1: for n2 in m2: if literal_hash(n1) == literal_hash(n2): - result[n1] = meet_types(m1[n1], m2[n2]) - if isinstance(result[n1], UninhabitedType): + meet_result = meet_types(m1[n1], m2[n2]) + if isinstance(meet_result, UninhabitedType): return None + result[n1] = meet_result return result @@ -8614,7 +8599,7 @@ def is_singleton_value(t: Type) -> bool: "builtins.bytes", "builtins.bytearray", "builtins.memoryview", - "builtins.tuple", + # for Collection[Never] "builtins.list", "builtins.dict", "builtins.set", @@ -8627,7 +8612,10 @@ def has_custom_eq_checks(t: Type) -> bool: or custom_special_method(t, "__ne__", check_all=False) # TODO: make the hack more principled. explain what and why we're doing this though # custom_special_method has special casing for builtins - or (isinstance(t, Instance) and t.type.fullname in BUILTINS_CUSTOM_EQ_CHECKS) + or ( + isinstance(pt := get_proper_type(t), Instance) + and pt.type.fullname in BUILTINS_CUSTOM_EQ_CHECKS + ) ) diff --git a/mypy/test/testargs.py b/mypy/test/testargs.py index 7c139902fe90..7af9981e6d34 100644 --- a/mypy/test/testargs.py +++ b/mypy/test/testargs.py @@ -9,6 +9,7 @@ import argparse import sys +from typing import Any, cast from mypy.main import infer_python_executable, process_options from mypy.options import Options @@ -63,7 +64,7 @@ def test_executable_inference(self) -> None: # first test inferring executable from version options = Options() - options.python_executable = None + options.python_executable = cast(Any, None) options.python_version = sys.version_info[:2] infer_python_executable(options, special_opts) assert options.python_version == sys.version_info[:2] diff --git a/mypy/test/testtypes.py b/mypy/test/testtypes.py index f5f4c6797db2..090796ec9f44 100644 --- a/mypy/test/testtypes.py +++ b/mypy/test/testtypes.py @@ -194,12 +194,12 @@ def test_generic_function_type(self) -> None: def test_type_alias_expand_once(self) -> None: A, target = self.fx.def_alias_1(self.fx.a) - assert get_proper_type(A) == target assert get_proper_type(target) == target + assert get_proper_type(A) == target A, target = self.fx.def_alias_2(self.fx.a) - assert get_proper_type(A) == target assert get_proper_type(target) == target + assert get_proper_type(A) == target def test_recursive_nested_in_non_recursive(self) -> None: A, _ = self.fx.def_alias_1(self.fx.a) From 1ef7ab710deafd51edfdf1f2d76fb881cb9e498b Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Tue, 30 Dec 2025 18:28:33 -0800 Subject: [PATCH 11/14] setattr --- mypyc/test-data/run-classes.test | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/mypyc/test-data/run-classes.test b/mypyc/test-data/run-classes.test index cb4d21904678..e178535f48fb 100644 --- a/mypyc/test-data/run-classes.test +++ b/mypyc/test-data/run-classes.test @@ -4920,6 +4920,7 @@ def test_setattr() -> None: assert i.one == 1 assert i.two == None assert i.const == 42 + i = i i.__setattr__("two", "2") assert i.two == "2" @@ -4957,6 +4958,7 @@ def test_setattr_inherited() -> None: assert i.one == 1 assert i.two == None assert i.const == 42 + i = i i.__setattr__("two", "2") assert i.two == "2" @@ -4996,7 +4998,9 @@ def test_setattr_overridden() -> None: assert i.one == 1 assert i.two == None assert i.const == 42 + i = i + i = SetAttrOverridden(99, 1, {"one": 1}) i.__setattr__("two", "2") assert i.two == "2" i.__setattr__("regular_attr", 101) @@ -5064,6 +5068,7 @@ def test_setattr_nonnative() -> None: assert i.one == 1 assert i.two == None assert i.const == 42 + i = i i.__setattr__("two", "2") assert i.two == "2" @@ -5134,6 +5139,8 @@ def test_no_setattr_nonnative() -> None: object.__setattr__(i, "three", 102) assert i.three == 102 + i = i + del i.three assert i.three == None From 5cc76be1cc64480d74033b9357e92559c212a907 Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Tue, 30 Dec 2025 18:59:46 -0800 Subject: [PATCH 12/14] improve --- mypy/checker.py | 46 ++++++++++++++++++++++++---------------------- 1 file changed, 24 insertions(+), 22 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index df39357b8c90..42cfdf8cbf72 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -6951,48 +6951,50 @@ def refine_identity_comparison_expression( else: should_coerce = True - operator_specific_targets = [] + value_targets = [] type_targets = [] for i in chain_indices: expr_type = operand_types[i] if should_coerce: expr_type = coerce_to_literal(expr_type) if is_valid_target(get_proper_type(expr_type)): - operator_specific_targets.append((i, TypeRange(expr_type, is_upper_bound=False))) + value_targets.append((i, TypeRange(expr_type, is_upper_bound=False))) else: type_targets.append((i, TypeRange(expr_type, is_upper_bound=False))) partial_type_maps = [] - if operator_specific_targets: + if value_targets: for i in chain_indices: if i not in narrowable_operand_indices: continue - targets = [t for j, t in operator_specific_targets if j != i] - if targets: - for target in targets: - expr_type = coerce_to_literal(operand_types[i]) - expr_type = try_expanding_sum_type_to_union(expr_type, None) - if_map, else_map = conditional_types_to_typemaps( - operands[i], *conditional_types(expr_type, [target]) - ) - partial_type_maps.append((if_map, else_map)) + for j, target in value_targets: + if i == j: + continue + expr_type = coerce_to_literal(operand_types[i]) + expr_type = try_expanding_sum_type_to_union(expr_type, None) + if_map, else_map = conditional_types_to_typemaps( + operands[i], *conditional_types(expr_type, [target]) + ) + partial_type_maps.append((if_map, else_map)) if type_targets: for i in chain_indices: if i not in narrowable_operand_indices: continue - targets = [t for j, t in type_targets if j != i] - if targets: - for target in targets: - expr_type = operand_types[i] - if_map, else_map = conditional_types_to_typemaps( - operands[i], *conditional_types(expr_type, [target]) - ) - if if_map: - else_map = {} - partial_type_maps.append((if_map, else_map)) + for j, target in type_targets: + if i == j: + continue + expr_type = operand_types[i] + if_map, else_map = conditional_types_to_typemaps( + operands[i], *conditional_types(expr_type, [target]) + ) + if if_map: + else_map = {} + partial_type_maps.append((if_map, else_map)) + # We will not have duplicate entries in our type maps if we only have two operands, + # so we can skip running meets on the intersections return reduce_conditional_maps(partial_type_maps, use_meet=len(operands) > 2) def refine_away_none_in_comparison( From 54b4c8ed8f53e66679c18bfb311cf65372cc6ab4 Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Tue, 30 Dec 2025 19:08:36 -0800 Subject: [PATCH 13/14] . --- mypy/checker.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 42cfdf8cbf72..160d33e23b71 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -6692,26 +6692,26 @@ def narrow_type_by_equality( # with narrowing when using 'is' and conservative when using '==' seems # to break the least amount of real-world code. # - # should_narrow_by_identity: + # should_narrow_by_identity_equality: # Set to 'false' only if the user defines custom __eq__ or __ne__ methods # that could cause identity-based narrowing to produce invalid results. if operator in {"is", "is not"}: is_valid_target: Callable[[Type], bool] = is_singleton_type coerce_only_in_literal_context = False - should_narrow_by_identity = True + should_narrow_by_identity_equality = True elif operator in {"==", "!="}: is_valid_target = is_singleton_value coerce_only_in_literal_context = True expr_types = [operand_types[i] for i in expr_indices] - should_narrow_by_identity = not any( + should_narrow_by_identity_equality = not any( map(has_custom_eq_checks, expr_types) ) and not is_ambiguous_mix_of_enums(expr_types) else: raise AssertionError - if should_narrow_by_identity: - return self.refine_identity_comparison_expression( + if should_narrow_by_identity_equality: + return self.narrow_identity_equality_comparison( operands, operand_types, expr_indices, @@ -6720,6 +6720,8 @@ def narrow_type_by_equality( coerce_only_in_literal_context, ) + # This is a bit of a legacy code path that might be a little unsound since it ignores + # custom __eq__. We should see if we can get rid of it. return self.refine_away_none_in_comparison( operands, operand_types, expr_indices, narrowable_indices ) @@ -6905,7 +6907,7 @@ def _propagate_walrus_assignments( return parent_expr return expr - def refine_identity_comparison_expression( + def narrow_identity_equality_comparison( self, operands: list[Expression], operand_types: list[Type], From ce2728eb7acfbe6c3a4bb7f74eac12cb7d6f64f2 Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Tue, 30 Dec 2025 19:20:59 -0800 Subject: [PATCH 14/14] . --- mypy/checker.py | 12 +++++++--- mypyc/test-data/run-classes.test | 1 - test-data/unit/check-narrowing.test | 36 +++++++++++++++++++++++++++++ 3 files changed, 45 insertions(+), 4 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 160d33e23b71..855855ed0e67 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -6958,6 +6958,11 @@ def narrow_identity_equality_comparison( for i in chain_indices: expr_type = operand_types[i] if should_coerce: + # TODO: doing this prevents narrowing a single-member Enum to literal + # of its member, because we expand it here and then refuse to add equal + # types to typemaps. As a result, `x: Foo; x == Foo.A` does not narrow + # `x` to `Literal[Foo.A]` iff `Foo` has exactly one member. + # See testMatchEnumSingleChoice expr_type = coerce_to_literal(expr_type) if is_valid_target(get_proper_type(expr_type)): value_targets.append((i, TypeRange(expr_type, is_upper_bound=False))) @@ -8603,7 +8608,6 @@ def is_singleton_value(t: Type) -> bool: "builtins.bytes", "builtins.bytearray", "builtins.memoryview", - # for Collection[Never] "builtins.list", "builtins.dict", "builtins.set", @@ -8614,8 +8618,10 @@ def has_custom_eq_checks(t: Type) -> bool: return ( custom_special_method(t, "__eq__", check_all=False) or custom_special_method(t, "__ne__", check_all=False) - # TODO: make the hack more principled. explain what and why we're doing this though - # custom_special_method has special casing for builtins + # custom_special_method has special casing for builtins.* and typing.* that make the + # above always return False. So here we return True if the a value of a builtin type + # will ever compare equal to value of another type, e.g. a bytes value can compare equal + # to a bytearray value. We also include builtins collections, see testNarrowingCollections or ( isinstance(pt := get_proper_type(t), Instance) and pt.type.fullname in BUILTINS_CUSTOM_EQ_CHECKS diff --git a/mypyc/test-data/run-classes.test b/mypyc/test-data/run-classes.test index e178535f48fb..5535034faa50 100644 --- a/mypyc/test-data/run-classes.test +++ b/mypyc/test-data/run-classes.test @@ -5000,7 +5000,6 @@ def test_setattr_overridden() -> None: assert i.const == 42 i = i - i = SetAttrOverridden(99, 1, {"one": 1}) i.__setattr__("two", "2") assert i.two == "2" i.__setattr__("regular_attr", 101) diff --git a/test-data/unit/check-narrowing.test b/test-data/unit/check-narrowing.test index b18bb257a128..237271558ac6 100644 --- a/test-data/unit/check-narrowing.test +++ b/test-data/unit/check-narrowing.test @@ -2770,3 +2770,39 @@ def main(key: str): reveal_type(TupleLike) # N: Revealed type is "def [_T_co] (__main__.Boxxy[_T_co`1]) -> __main__.TupleLike[_T_co`1]" TupleLike(Box2("str")) [builtins fixtures/tuple.pyi] + +[case testNarrowingCollections] +# flags: --warn-unreachable +from typing import cast + +class X: + def __init__(self) -> None: + self.x: dict[str, str] = {} + self.y: list[str] = [] + + def xxx(self) -> None: + if self.x == {}: + reveal_type(self.x) # N: Revealed type is "builtins.dict[builtins.str, builtins.str]" + self.x["asdf"] + + if self.x == dict(): + reveal_type(self.x) # N: Revealed type is "builtins.dict[builtins.str, builtins.str]" + self.x["asdf"] + + if self.x == cast(dict[int, int], {}): + reveal_type(self.x) # N: Revealed type is "builtins.dict[builtins.str, builtins.str]" + self.x["asdf"] + + def yyy(self) -> None: + if self.y == []: + reveal_type(self.y) # N: Revealed type is "builtins.list[builtins.str]" + self.y[0].does_not_exist # E: "str" has no attribute "does_not_exist" + + if self.y == list(): + reveal_type(self.y) # N: Revealed type is "builtins.list[builtins.str]" + self.y[0].does_not_exist # E: "str" has no attribute "does_not_exist" + + if self.y == cast(list[int], []): + reveal_type(self.y) # N: Revealed type is "builtins.list[builtins.str]" + self.y[0].does_not_exist # E: "str" has no attribute "does_not_exist" +[builtins fixtures/dict.pyi]