diff --git a/mypy/checker.py b/mypy/checker.py index 96e41a5e1786..855855ed0e67 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: @@ -6547,7 +6545,14 @@ 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) + 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: @@ -6586,7 +6591,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 @@ -6651,20 +6656,18 @@ 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 # explicit type(x) == some_type check if_map, else_map = self.narrow_type_by_equality( - operator, - operands, - operand_types, - expr_indices, - narrowable_operand_index_to_hash.keys(), + operator, operands, operand_types, expr_indices, narrowable_indices=narrowable_indices ) - 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( @@ -6689,35 +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 - 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 + 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 = all( - map(has_no_custom_eq_checks, expr_types) + 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_map: TypeMap = {} - else_map: TypeMap = {} - if should_narrow_by_identity: - if_map, else_map = self.refine_identity_comparison_expression( + if should_narrow_by_identity_equality: + return self.narrow_identity_equality_comparison( operands, operand_types, expr_indices, @@ -6726,11 +6720,11 @@ 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 + # 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 + ) def propagate_up_typemap_info(self, new_types: TypeMap) -> TypeMap: """Attempts refining parent expressions of any MemberExpr or IndexExprs in new_types. @@ -6913,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], @@ -6948,19 +6942,19 @@ 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 = [] + value_targets = [] + type_targets = [] for i in chain_indices: expr_type = operand_types[i] if should_coerce: @@ -6970,91 +6964,45 @@ def should_coerce_inner(typ: Type) -> bool: # `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 - - target_type = [TypeRange(target, is_upper_bound=False)] + if is_valid_target(get_proper_type(expr_type)): + 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 = [] - 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) + if value_targets: + for i in chain_indices: + if i not in narrowable_operand_indices: + continue + 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)) - # 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 type_targets: + for i in chain_indices: + if i not in narrowable_operand_indices: + continue + 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)) - return reduce_conditional_maps(partial_type_maps) + # 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( self, @@ -8579,7 +8527,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]) + meet_result = meet_types(m1[n1], m2[n2]) + if isinstance(meet_result, UninhabitedType): + return None + result[n1] = meet_result return result @@ -8648,6 +8599,41 @@ def reduce_conditional_maps( return final_if_map, final_else_map +def is_singleton_value(t: Type) -> bool: + t = get_proper_type(t) + return isinstance(t, LiteralType) or t.is_singleton_type() + + +BUILTINS_CUSTOM_EQ_CHECKS: Final = { + "builtins.bytes", + "builtins.bytearray", + "builtins.memoryview", + "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) + # 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 + ) + ) + + +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/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) 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)]) diff --git a/mypyc/test-data/run-classes.test b/mypyc/test-data/run-classes.test index cb4d21904678..5535034faa50 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,6 +4998,7 @@ def test_setattr_overridden() -> None: assert i.one == 1 assert i.two == None assert i.const == 42 + i = i i.__setattr__("two", "2") assert i.two == "2" @@ -5064,6 +5067,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 +5138,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 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..89d07a6b03c6 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] @@ -2760,6 +2760,7 @@ 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 diff --git a/test-data/unit/check-narrowing.test b/test-data/unit/check-narrowing.test index 03586e4109f6..237271558ac6 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,13 +2290,13 @@ 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"]: # 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] @@ -2699,3 +2712,97 @@ 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] + + +[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 [_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] diff --git a/test-data/unit/check-optional.test b/test-data/unit/check-optional.test index 393de00c41d5..d38c2c01a64e 100644 --- a/test-data/unit/check-optional.test +++ b/test-data/unit/check-optional.test @@ -445,54 +445,61 @@ 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] from typing import Union -x = '' # type: Union[str, int, None] -if x == object(): - 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 object(): - 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 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] +# 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) # 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" [builtins fixtures/ops.pyi] [case testInferEqualsStillOptionalWithBothOptional] @@ -500,35 +507,38 @@ 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] [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" + 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" 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" -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] diff --git a/test-data/unit/check-python310.test b/test-data/unit/check-python310.test index 98d625958368..78af1dc0a709 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 @@ -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]" 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 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]):