diff --git a/mypy/checker.py b/mypy/checker.py index c55305530ccc..096ec7abade1 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -5,7 +5,7 @@ from contextlib import contextmanager from typing import ( - Dict, Set, List, cast, Tuple, TypeVar, Union, Optional, NamedTuple, Iterator, Any + Dict, Set, List, cast, Tuple, TypeVar, Union, Optional, NamedTuple, Iterator, Iterable, Any ) from mypy.errors import Errors, report_internal_error @@ -30,7 +30,7 @@ Type, AnyType, CallableType, FunctionLike, Overloaded, TupleType, TypedDictType, Instance, NoneTyp, strip_type, TypeType, TypeOfAny, UnionType, TypeVarId, TypeVarType, PartialType, DeletedType, UninhabitedType, TypeVarDef, - true_only, false_only, function_type, is_named_instance, union_items, + true_only, false_only, function_type, is_named_instance, union_items, TypeQuery ) from mypy.sametypes import is_same_type, is_same_types from mypy.messages import MessageBuilder, make_inferred_type_note @@ -55,7 +55,7 @@ from mypy.join import join_types from mypy.treetransform import TransformVisitor from mypy.binder import ConditionalTypeBinder, get_declaration -from mypy.meet import is_overlapping_types, is_partially_overlapping_types +from mypy.meet import is_overlapping_erased_types, is_overlapping_types from mypy.options import Options from mypy.plugin import Plugin, CheckerPluginInterface from mypy.sharedparse import BINARY_MAGIC_METHODS @@ -495,12 +495,12 @@ def check_overlapping_overloads(self, defn: OverloadedFuncDef) -> None: # Is the overload alternative's arguments subtypes of the implementation's? if not is_callable_compatible(impl, sig1, - is_compat=is_subtype, + is_compat=is_subtype_no_promote, ignore_return=True): self.msg.overloaded_signatures_arg_specific(i + 1, defn.impl) # Is the overload alternative's return type a subtype of the implementation's? - if not is_subtype(sig1.ret_type, impl.ret_type): + if not is_subtype_no_promote(sig1.ret_type, impl.ret_type): self.msg.overloaded_signatures_ret_specific(i + 1, defn.impl) # Here's the scoop about generators and coroutines. @@ -3156,7 +3156,7 @@ def find_isinstance_check(self, node: Expression else: optional_type, comp_type = second_type, first_type optional_expr = node.operands[1] - if is_overlapping_types(optional_type, comp_type): + if is_overlapping_erased_types(optional_type, comp_type): return {optional_expr: remove_optional(optional_type)}, {} elif node.operators in [['in'], ['not in']]: expr = node.operands[0] @@ -3167,7 +3167,7 @@ def find_isinstance_check(self, node: Expression right_type.type.fullname() != 'builtins.object')) if (right_type and right_ok and is_optional(left_type) and literal(expr) == LITERAL_TYPE and not is_literal_none(expr) and - is_overlapping_types(left_type, right_type)): + is_overlapping_erased_types(left_type, right_type)): if node.operators == ['in']: return {expr: remove_optional(left_type)}, {} if node.operators == ['not in']: @@ -3515,7 +3515,8 @@ def conditional_type_map(expr: Expression, and is_proper_subtype(current_type, proposed_type)): # Expression is always of one of the types in proposed_type_ranges return {}, None - elif not is_overlapping_types(current_type, proposed_type): + elif not is_overlapping_types(current_type, proposed_type, + prohibit_none_typevar_overlap=True): # Expression is never of any type in proposed_type_ranges return None, {} else: @@ -3731,9 +3732,9 @@ def are_argument_counts_overlapping(t: CallableType, s: CallableType) -> bool: def is_unsafe_overlapping_overload_signatures(signature: CallableType, other: CallableType) -> bool: - """Check if two overloaded function signatures may be unsafely overlapping. + """Check if two overloaded signatures are unsafely overlapping or partially overlapping. - We consider two functions 's' and 't' to be unsafely overlapping both if + We consider two functions 's' and 't' to be unsafely overlapping if both of the following are true: 1. s's parameters are all more precise or partially overlapping with t's @@ -3742,26 +3743,98 @@ def is_unsafe_overlapping_overload_signatures(signature: CallableType, Assumes that 'signature' appears earlier in the list of overload alternatives then 'other' and that their argument counts are overlapping. """ - # TODO: Handle partially overlapping parameter types + # Try detaching callables from the containing class so that all TypeVars + # are treated as being free. # - # For example, the signatures "f(x: Union[A, B]) -> int" and "f(x: Union[B, C]) -> str" - # is unsafe: the parameter types are partially overlapping. + # This lets us identify cases where the two signatures use completely + # incompatible types -- e.g. see the testOverloadingInferUnionReturnWithMixedTypevars + # test case. + signature = detach_callable(signature) + other = detach_callable(other) + + # Note: We repeat this check twice in both directions due to a slight + # asymmetry in 'is_callable_compatible'. When checking for partial overlaps, + # we attempt to unify 'signature' and 'other' both against each other. # - # To fix this, we need to either modify meet.is_overlapping_types or add a new - # function and use "is_more_precise(...) or is_partially_overlapping(...)" for the is_compat - # checks. + # If 'signature' cannot be unified with 'other', we end early. However, + # if 'other' cannot be modified with 'signature', the function continues + # using the older version of 'other'. # - # (We already have a rudimentary implementation of 'is_partially_overlapping', but it only - # attempts to handle the obvious cases -- see its docstring for more info.) - - def is_more_precise_or_partially_overlapping(t: Type, s: Type) -> bool: - return is_more_precise(t, s) or is_partially_overlapping_types(t, s) - - return is_callable_compatible(signature, other, - is_compat=is_more_precise_or_partially_overlapping, - is_compat_return=lambda l, r: not is_subtype(l, r), + # This discrepancy is unfortunately difficult to get rid of, so we repeat the + # checks twice in both directions for now. + return (is_callable_compatible(signature, other, + is_compat=is_overlapping_types_no_promote, + is_compat_return=lambda l, r: not is_subtype_no_promote(l, r), + ignore_return=False, check_args_covariantly=True, - allow_partial_overlap=True) + allow_partial_overlap=True) or + is_callable_compatible(other, signature, + is_compat=is_overlapping_types_no_promote, + is_compat_return=lambda l, r: not is_subtype_no_promote(r, l), + ignore_return=False, + check_args_covariantly=False, + allow_partial_overlap=True)) + + +def detach_callable(typ: CallableType) -> CallableType: + """Ensures that the callable's type variables are 'detached' and independent of the context. + + A callable normally keeps track of the type variables it uses within its 'variables' field. + However, if the callable is from a method and that method is using a class type variable, + the callable will not keep track of that type variable since it belongs to the class. + + This function will traverse the callable and find all used type vars and add them to the + variables field if it isn't already present. + + The caller can then unify on all type variables whether or not the callable is originally + from a class or not.""" + type_list = typ.arg_types + [typ.ret_type] + + appear_map = {} # type: Dict[str, List[int]] + for i, inner_type in enumerate(type_list): + typevars_available = inner_type.accept(TypeVarExtractor()) + for var in typevars_available: + if var.fullname not in appear_map: + appear_map[var.fullname] = [] + appear_map[var.fullname].append(i) + + used_type_var_names = set() + for var_name, appearances in appear_map.items(): + used_type_var_names.add(var_name) + + all_type_vars = typ.accept(TypeVarExtractor()) + new_variables = [] + for var in set(all_type_vars): + if var.fullname not in used_type_var_names: + continue + new_variables.append(TypeVarDef( + name=var.name, + fullname=var.fullname, + id=var.id, + values=var.values, + upper_bound=var.upper_bound, + variance=var.variance, + )) + out = typ.copy_modified( + variables=new_variables, + arg_types=type_list[:-1], + ret_type=type_list[-1], + ) + return out + + +class TypeVarExtractor(TypeQuery[List[TypeVarType]]): + def __init__(self) -> None: + super().__init__(self._merge) + + def _merge(self, iter: Iterable[List[TypeVarType]]) -> List[TypeVarType]: + out = [] + for item in iter: + out.extend(item) + return out + + def visit_type_var(self, t: TypeVarType) -> List[TypeVarType]: + return [t] def overload_can_never_match(signature: CallableType, other: CallableType) -> bool: @@ -3787,69 +3860,6 @@ def overload_can_never_match(signature: CallableType, other: CallableType) -> bo ignore_return=True) -def is_unsafe_overlapping_operator_signatures(signature: Type, other: Type) -> bool: - """Check if two operator method signatures may be unsafely overlapping. - - Two signatures s and t are overlapping if both can be valid for the same - statically typed values and the return types are incompatible. - - Assume calls are first checked against 'signature', then against 'other'. - Thus if 'signature' is more general than 'other', there is no unsafe - overlapping. - - TODO: Clean up this function and make it not perform type erasure. - - Context: This function was previously used to make sure both overloaded - functions and operator methods were not unsafely overlapping. - - We changed the semantics for we should handle overloaded definitions, - but not operator functions. (We can't reuse the same semantics for both: - the overload semantics are too restrictive here). - - We should rewrite this method so that: - - 1. It uses many of the improvements made to overloads: in particular, - eliminating type erasure. - - 2. It contains just the logic necessary for operator methods. - """ - if isinstance(signature, CallableType): - if isinstance(other, CallableType): - # TODO varargs - # TODO keyword args - # TODO erasure - # TODO allow to vary covariantly - # Check if the argument counts are overlapping. - min_args = max(signature.min_args, other.min_args) - max_args = min(len(signature.arg_types), len(other.arg_types)) - if min_args > max_args: - # Argument counts are not overlapping. - return False - # Signatures are overlapping iff if they are overlapping for the - # smallest common argument count. - for i in range(min_args): - t1 = signature.arg_types[i] - t2 = other.arg_types[i] - if not is_overlapping_types(t1, t2): - return False - # All arguments types for the smallest common argument count are - # overlapping => the signature is overlapping. The overlapping is - # safe if the return types are identical. - if is_same_type(signature.ret_type, other.ret_type): - return False - # If the first signature has more general argument types, the - # latter will never be called - if is_more_general_arg_prefix(signature, other): - return False - # Special case: all args are subtypes, and returns are subtypes - if (all(is_proper_subtype(s, o) - for (s, o) in zip(signature.arg_types, other.arg_types)) and - is_subtype(signature.ret_type, other.ret_type)): - return False - return not is_more_precise_signature(signature, other) - return True - - def is_more_general_arg_prefix(t: FunctionLike, s: FunctionLike) -> bool: """Does t have wider arguments than s?""" # TODO should an overload with additional items be allowed to be more @@ -3867,20 +3877,6 @@ def is_more_general_arg_prefix(t: FunctionLike, s: FunctionLike) -> bool: return False -def is_equivalent_type_var_def(tv1: TypeVarDef, tv2: TypeVarDef) -> bool: - """Are type variable definitions equivalent? - - Ignore ids, locations in source file and names. - """ - return ( - tv1.variance == tv2.variance - and is_same_types(tv1.values, tv2.values) - and ((tv1.upper_bound is None and tv2.upper_bound is None) - or (tv1.upper_bound is not None - and tv2.upper_bound is not None - and is_same_type(tv1.upper_bound, tv2.upper_bound)))) - - def is_same_arg_prefix(t: CallableType, s: CallableType) -> bool: return is_callable_compatible(t, s, is_compat=is_same_type, @@ -3889,21 +3885,6 @@ def is_same_arg_prefix(t: CallableType, s: CallableType) -> bool: ignore_pos_arg_names=True) -def is_more_precise_signature(t: CallableType, s: CallableType) -> bool: - """Is t more precise than s? - A signature t is more precise than s if all argument types and the return - type of t are more precise than the corresponding types in s. - Assume that the argument kinds and names are compatible, and that the - argument counts are overlapping. - """ - # TODO generic function types - # Only consider the common prefix of argument types. - for argt, args in zip(t.arg_types, s.arg_types): - if not is_more_precise(argt, args): - return False - return is_more_precise(t.ret_type, s.ret_type) - - def infer_operator_assignment_method(typ: Type, operator: str) -> Tuple[bool, str]: """Determine if operator assignment on given value type is in-place, and the method name. @@ -4045,3 +4026,11 @@ def is_static(func: Union[FuncBase, Decorator]) -> bool: elif isinstance(func, FuncBase): return func.is_static assert False, "Unexpected func type: {}".format(type(func)) + + +def is_subtype_no_promote(left: Type, right: Type) -> bool: + return is_subtype(left, right, ignore_promotions=True) + + +def is_overlapping_types_no_promote(left: Type, right: Type) -> bool: + return is_overlapping_types(left, right, ignore_promotions=True) diff --git a/mypy/meet.py b/mypy/meet.py index ccc70d60bddd..4386c543e43e 100644 --- a/mypy/meet.py +++ b/mypy/meet.py @@ -7,9 +7,14 @@ from mypy.types import ( Type, AnyType, TypeVisitor, UnboundType, NoneTyp, TypeVarType, Instance, CallableType, TupleType, TypedDictType, ErasedType, UnionType, PartialType, DeletedType, - UninhabitedType, TypeType, TypeOfAny, Overloaded, FunctionLike + UninhabitedType, TypeType, TypeOfAny, Overloaded, FunctionLike, ) -from mypy.subtypes import is_equivalent, is_subtype, is_protocol_implementation, is_proper_subtype +from mypy.subtypes import ( + is_equivalent, is_subtype, is_protocol_implementation, is_callable_compatible, + is_proper_subtype, +) +from mypy.erasetype import erase_type +from mypy.maptype import map_instance_to_supertype from mypy import experiments @@ -34,7 +39,8 @@ def narrow_declared_type(declared: Type, narrowed: Type) -> Type: if isinstance(declared, UnionType): return UnionType.make_simplified_union([narrow_declared_type(x, narrowed) for x in declared.relevant_items()]) - elif not is_overlapping_types(declared, narrowed, use_promotions=True): + elif not is_overlapping_types(declared, narrowed, + prohibit_none_typevar_overlap=True): if experiments.STRICT_OPTIONAL: return UninhabitedType() else: @@ -51,155 +57,283 @@ def narrow_declared_type(declared: Type, narrowed: Type) -> Type: return narrowed -def is_partially_overlapping_types(t: Type, s: Type) -> bool: - """Returns 'true' if the two types are partially, but not completely, overlapping. +def get_possible_variants(typ: Type) -> List[Type]: + """This function takes any "Union-like" type and returns a list of the available "options". + + Specifically, there are currently exactly three different types that can have + "variants" or are "union-like": + + - Unions + - TypeVars with value restrictions + - Overloads + + This function will return a list of each "option" present in those types. + + If this function receives any other type, we return a list containing just that + original type. (E.g. pretend the type was contained within a singleton union). + + The only exception is regular TypeVars: we return a list containing that TypeVar's + upper bound. + + This function is useful primarily when checking to see if two types are overlapping: + the algorithm to check if two unions are overlapping is fundamentally the same as + the algorithm for checking if two overloads are overlapping. + + Normalizing both kinds of types in the same way lets us reuse the same algorithm + for both. + """ + if isinstance(typ, TypeVarType): + if len(typ.values) > 0: + return typ.values + else: + return [typ.upper_bound] + elif isinstance(typ, UnionType): + return typ.items + elif isinstance(typ, Overloaded): + # Note: doing 'return typ.items()' makes mypy + # infer a too-specific return type of List[CallableType] + return list(typ.items()) + else: + return [typ] + - NOTE: This function is only a partial implementation. +def is_overlapping_types(left: Type, + right: Type, + ignore_promotions: bool = False, + prohibit_none_typevar_overlap: bool = False) -> bool: + """Can a value of type 'left' also be of type 'right' or vice-versa? - It exists mostly so that overloads correctly handle partial - overlaps for the more obvious cases. + If 'ignore_promotions' is True, we ignore promotions while checking for overlaps. + If 'prohibit_none_typevar_overlap' is True, we disallow None from overlapping with + TypeVars (in both strict-optional and non-strict-optional mode). """ - # Are unions partially overlapping? - if isinstance(t, UnionType) and isinstance(s, UnionType): - t_set = set(t.items) - s_set = set(s.items) - num_same = len(t_set.intersection(s_set)) - num_diff = len(t_set.symmetric_difference(s_set)) - return num_same > 0 and num_diff > 0 - - # Are tuples partially overlapping? - tup_overlap = is_overlapping_tuples(t, s, use_promotions=True) - if tup_overlap is not None and tup_overlap: - return tup_overlap - - def is_object(t: Type) -> bool: - return isinstance(t, Instance) and t.type.fullname() == 'builtins.object' - - # Is either 't' or 's' an unrestricted TypeVar? - if isinstance(t, TypeVarType) and is_object(t.upper_bound) and len(t.values) == 0: + + def _is_overlapping_types(left: Type, right: Type) -> bool: + '''Encode the kind of overlapping check to perform. + + This function mostly exists so we don't have to repeat keyword arguments everywhere.''' + return is_overlapping_types( + left, right, + ignore_promotions=ignore_promotions, + prohibit_none_typevar_overlap=prohibit_none_typevar_overlap) + + # We should never encounter this type. + if isinstance(left, PartialType) or isinstance(right, PartialType): + assert False, "Unexpectedly encountered partial type" + + # We should also never encounter these types, but it's possible a few + # have snuck through due to unrelated bugs. For now, we handle these + # in the same way we handle 'Any'. + # + # TODO: Replace these with an 'assert False' once we are more confident. + illegal_types = (UnboundType, ErasedType, DeletedType) + if isinstance(left, illegal_types) or isinstance(right, illegal_types): return True - if isinstance(s, TypeVarType) and is_object(s.upper_bound) and len(s.values) == 0: + # 'Any' may or may not be overlapping with the other type + if isinstance(left, AnyType) or isinstance(right, AnyType): return True - return False + # When running under non-strict optional mode, simplify away types of + # the form 'Union[A, B, C, None]' into just 'Union[A, B, C]'. + if not experiments.STRICT_OPTIONAL: + if isinstance(left, UnionType): + left = UnionType.make_union(left.relevant_items()) + if isinstance(right, UnionType): + right = UnionType.make_union(right.relevant_items()) -def is_overlapping_types(t: Type, s: Type, use_promotions: bool = False) -> bool: - """Can a value of type t be a value of type s, or vice versa? + # We check for complete overlaps next as a general-purpose failsafe. + # If this check fails, we start checking to see if there exists a + # *partial* overlap between types. + # + # These checks will also handle the NoneTyp and UninhabitedType cases for us. - Note that this effectively checks against erased types, since type - variables are erased at runtime and the overlapping check is based - on runtime behavior. The exception is protocol types, it is not safe, - but convenient and is an opt-in behavior. + if (is_proper_subtype(left, right, ignore_promotions=ignore_promotions) + or is_proper_subtype(right, left, ignore_promotions=ignore_promotions)): + return True - If use_promotions is True, also consider type promotions (int and - float would only be overlapping if it's True). + # See the docstring for 'get_possible_variants' for more info on what the + # following lines are doing. - This does not consider multiple inheritance. For example, A and B in - the following example are not considered overlapping, even though - via C they can be overlapping: + left_possible = get_possible_variants(left) + right_possible = get_possible_variants(right) - class A: ... - class B: ... - class C(A, B): ... + # We start by checking multi-variant types like Unions first. We also perform + # the same logic if either type happens to be a TypeVar. + # + # Handling the TypeVars now lets us simulate having them bind to the corresponding + # type -- if we deferred these checks, the "return-early" logic of the other + # checks will prevent us from detecting certain overlaps. + # + # If both types are singleton variants (and are not TypeVars), we've hit the base case: + # we skip these checks to avoid infinitely recursing. - The rationale is that this case is usually very unlikely as multiple - inheritance is rare. Also, we can't reliably determine whether - multiple inheritance actually occurs somewhere in a program, due to - stub files hiding implementation details, dynamic loading etc. + def is_none_typevar_overlap(t1: Type, t2: Type) -> bool: + return isinstance(t1, NoneTyp) and isinstance(t2, TypeVarType) - TODO: Don't consider callables always overlapping. - TODO: Don't consider type variables with values always overlapping. - """ - # Any overlaps with everything - if isinstance(t, AnyType) or isinstance(s, AnyType): - return True - # object overlaps with everything - if (isinstance(t, Instance) and t.type.fullname() == 'builtins.object' or - isinstance(s, Instance) and s.type.fullname() == 'builtins.object'): - return True + if prohibit_none_typevar_overlap: + if is_none_typevar_overlap(left, right) or is_none_typevar_overlap(right, left): + return False - if is_proper_subtype(t, s) or is_proper_subtype(s, t): - return True - # Since we are effectively working with the erased types, we only - # need to handle occurrences of TypeVarType at the top level. - if isinstance(t, TypeVarType): - t = t.erase_to_union_or_bound() - if isinstance(s, TypeVarType): - s = s.erase_to_union_or_bound() - if isinstance(t, TypedDictType): - t = t.as_anonymous().fallback - if isinstance(s, TypedDictType): - s = s.as_anonymous().fallback - - if isinstance(t, UnionType): - return any(is_overlapping_types(item, s) - for item in t.relevant_items()) - if isinstance(s, UnionType): - return any(is_overlapping_types(t, item) - for item in s.relevant_items()) - - # We must check for TupleTypes before Instances, since Tuple[A, ...] - # is an Instance - tup_overlap = is_overlapping_tuples(t, s, use_promotions) - if tup_overlap is not None: - return tup_overlap - - if isinstance(t, Instance): - if isinstance(s, Instance): - # Consider two classes non-disjoint if one is included in the mro - # of another. - if use_promotions: - # Consider cases like int vs float to be overlapping where - # there is only a type promotion relationship but not proper - # subclassing. - if t.type._promote and is_overlapping_types(t.type._promote, s): + if (len(left_possible) > 1 or len(right_possible) > 1 + or isinstance(left, TypeVarType) or isinstance(right, TypeVarType)): + for l in left_possible: + for r in right_possible: + if _is_overlapping_types(l, r): return True - if s.type._promote and is_overlapping_types(s.type._promote, t): + return False + + # Now that we've finished handling TypeVars, we're free to end early + # if one one of the types is None and we're running in strict-optional mode. + # (None only overlaps with None in strict-optional mode). + # + # We must perform this check after the TypeVar checks because + # a TypeVar could be bound to None, for example. + + if experiments.STRICT_OPTIONAL and isinstance(left, NoneTyp) != isinstance(right, NoneTyp): + return False + + # Next, we handle single-variant types that may be inherently partially overlapping: + # + # - TypedDicts + # - Tuples + # + # If we cannot identify a partial overlap and end early, we degrade these two types + # into their 'Instance' fallbacks. + + if isinstance(left, TypedDictType) and isinstance(right, TypedDictType): + return are_typed_dicts_overlapping(left, right, ignore_promotions=ignore_promotions) + elif isinstance(left, TypedDictType): + left = left.fallback + elif isinstance(right, TypedDictType): + right = right.fallback + + if is_tuple(left) and is_tuple(right): + return are_tuples_overlapping(left, right, ignore_promotions=ignore_promotions) + elif isinstance(left, TupleType): + left = left.fallback + elif isinstance(right, TupleType): + right = right.fallback + + # Next, we handle single-variant types that cannot be inherently partially overlapping, + # but do require custom logic to inspect. + # + # As before, we degrade into 'Instance' whenever possible. + + if isinstance(left, TypeType) and isinstance(right, TypeType): + # TODO: Can Callable[[...], T] and Type[T] be partially overlapping? + return _is_overlapping_types(left.item, right.item) + + if isinstance(left, CallableType) and isinstance(right, CallableType): + return is_callable_compatible(left, right, + is_compat=_is_overlapping_types, + ignore_pos_arg_names=True, + allow_partial_overlap=True) + elif isinstance(left, CallableType): + left = left.fallback + elif isinstance(right, CallableType): + right = right.fallback + + # Finally, we handle the case where left and right are instances. + + if isinstance(left, Instance) and isinstance(right, Instance): + if left.type.is_protocol and is_protocol_implementation(right, left): + return True + if right.type.is_protocol and is_protocol_implementation(left, right): + return True + + # Two unrelated types cannot be partially overlapping: they're disjoint. + # We don't need to handle promotions because they've already been handled + # by the calls to `is_subtype(...)` up above (and promotable types never + # have any generic arguments we need to recurse on). + if left.type.has_base(right.type.fullname()): + left = map_instance_to_supertype(left, right.type) + elif right.type.has_base(left.type.fullname()): + right = map_instance_to_supertype(right, left.type) + else: + return False + + if len(left.args) == len(right.args): + # Note: we don't really care about variance here, since the overlapping check + # is symmetric and since we want to return 'True' even for partial overlaps. + # + # For example, suppose we have two types Wrapper[Parent] and Wrapper[Child]. + # It doesn't matter whether Wrapper is covariant or contravariant since + # either way, one of the two types will overlap with the other. + # + # Similarly, if Wrapper was invariant, the two types could still be partially + # overlapping -- what if Wrapper[Parent] happened to contain only instances of + # specifically Child? + # + # Or, to use a more concrete example, List[Union[A, B]] and List[Union[B, C]] + # would be considered partially overlapping since it's possible for both lists + # to contain only instances of B at runtime. + for left_arg, right_arg in zip(left.args, right.args): + if _is_overlapping_types(left_arg, right_arg): return True - if t.type in s.type.mro or s.type in t.type.mro: - return True - if t.type.is_protocol and is_protocol_implementation(s, t): - return True - if s.type.is_protocol and is_protocol_implementation(t, s): - return True + + return False + + # We ought to have handled every case by now: we conclude the + # two types are not overlapping, either completely or partially. + # + # Note: it's unclear however, whether returning False is the right thing + # to do when inferring reachability -- see https://github.com/python/mypy/issues/5529 + + assert type(left) != type(right) + return False + + +def is_overlapping_erased_types(left: Type, right: Type, *, + ignore_promotions: bool = False) -> bool: + """The same as 'is_overlapping_erased_types', except the types are erased first.""" + return is_overlapping_types(erase_type(left), erase_type(right), + ignore_promotions=ignore_promotions, + prohibit_none_typevar_overlap=True) + + +def are_typed_dicts_overlapping(left: TypedDictType, right: TypedDictType, *, + ignore_promotions: bool = False, + prohibit_none_typevar_overlap: bool = False) -> bool: + """Returns 'true' if left and right are overlapping TypeDictTypes.""" + # All required keys in left are present and overlapping with something in right + for key in left.required_keys: + if key not in right.items: return False - if isinstance(t, TypeType) and isinstance(s, TypeType): - # If both types are TypeType, compare their inner types. - return is_overlapping_types(t.item, s.item, use_promotions) - elif isinstance(t, TypeType) or isinstance(s, TypeType): - # If exactly only one of t or s is a TypeType, check if one of them - # is an `object` or a `type` and otherwise assume no overlap. - one = t if isinstance(t, TypeType) else s - other = s if isinstance(t, TypeType) else t - if isinstance(other, Instance): - return other.type.fullname() in {'builtins.object', 'builtins.type'} - else: - return isinstance(other, CallableType) and is_subtype(other, one) - if experiments.STRICT_OPTIONAL: - if isinstance(t, NoneTyp) != isinstance(s, NoneTyp): - # NoneTyp does not overlap with other non-Union types under strict Optional checking + if not is_overlapping_types(left.items[key], right.items[key], + ignore_promotions=ignore_promotions, + prohibit_none_typevar_overlap=prohibit_none_typevar_overlap): + return False + + # Repeat check in the other direction + for key in right.required_keys: + if key not in left.items: + return False + if not is_overlapping_types(left.items[key], right.items[key], + ignore_promotions=ignore_promotions): return False - # We conservatively assume that non-instance, non-union, non-TupleType and non-TypeType types - # can overlap any other types. + + # The presence of any additional optional keys does not affect whether the two + # TypedDicts are partially overlapping: the dicts would be overlapping if the + # keys happened to be missing. return True -def is_overlapping_tuples(t: Type, s: Type, use_promotions: bool) -> Optional[bool]: - """Part of is_overlapping_types(), for tuples only""" - t = adjust_tuple(t, s) or t - s = adjust_tuple(s, t) or s - if isinstance(t, TupleType) or isinstance(s, TupleType): - if isinstance(t, TupleType) and isinstance(s, TupleType): - if t.length() == s.length(): - if all(is_overlapping_types(ti, si, use_promotions) - for ti, si in zip(t.items, s.items)): - return True - # TupleType and non-tuples do not overlap +def are_tuples_overlapping(left: Type, right: Type, *, + ignore_promotions: bool = False, + prohibit_none_typevar_overlap: bool = False) -> bool: + """Returns true if left and right are overlapping tuples.""" + left = adjust_tuple(left, right) or left + right = adjust_tuple(right, left) or right + assert isinstance(left, TupleType), 'Type {} is not a tuple'.format(left) + assert isinstance(right, TupleType), 'Type {} is not a tuple'.format(right) + if len(left.items) != len(right.items): return False - # No tuples are involved here - return None + return all(is_overlapping_types(l, r, + ignore_promotions=ignore_promotions, + prohibit_none_typevar_overlap=prohibit_none_typevar_overlap) + for l, r in zip(left.items, right.items)) def adjust_tuple(left: Type, r: Type) -> Optional[TupleType]: @@ -210,6 +344,11 @@ def adjust_tuple(left: Type, r: Type) -> Optional[TupleType]: return None +def is_tuple(typ: Type) -> bool: + return (isinstance(typ, TupleType) + or (isinstance(typ, Instance) and typ.type.fullname() == 'builtins.tuple')) + + class TypeMeetVisitor(TypeVisitor[Type]): def __init__(self, s: Type) -> None: self.s = s diff --git a/mypy/messages.py b/mypy/messages.py index 0edb95c6694b..1b277c68f25a 100644 --- a/mypy/messages.py +++ b/mypy/messages.py @@ -976,6 +976,12 @@ def overloaded_signatures_overlap(self, index1: int, index2: int, context: Conte self.fail('Overloaded function signatures {} and {} overlap with ' 'incompatible return types'.format(index1, index2), context) + def overloaded_signatures_partial_overlap(self, index1: int, index2: int, + context: Context) -> None: + self.fail('Overloaded function signatures {} and {} '.format(index1, index2) + + 'are partially overlapping: the two signatures may return ' + + 'incompatible types given certain calls', context) + def overloaded_signature_will_never_match(self, index1: int, index2: int, context: Context) -> None: self.fail( diff --git a/test-data/unit/check-classes.test b/test-data/unit/check-classes.test index 9a0ec6feb196..475984824607 100644 --- a/test-data/unit/check-classes.test +++ b/test-data/unit/check-classes.test @@ -1953,13 +1953,61 @@ class B: def __radd__(*self) -> int: pass def __rsub__(*self: 'B') -> int: pass -[case testReverseOperatorTypeVar] +[case testReverseOperatorTypeVar1] +from typing import TypeVar, Any +T = TypeVar("T", bound='Real') +class Real: + def __add__(self, other: Any) -> str: ... +class Fraction(Real): + def __radd__(self, other: T) -> T: ... # E: Signatures of "__radd__" of "Fraction" and "__add__" of "T" are unsafely overlapping + +# Note: When doing A + B and if B is a subtype of A, we will always call B.__radd__(A) first +# and only try A.__add__(B) second if necessary. +reveal_type(Real() + Fraction()) # E: Revealed type is '__main__.Real*' + +# Note: When doing A + A, we only ever call A.__add__(A), never A.__radd__(A). +reveal_type(Fraction() + Fraction()) # E: Revealed type is 'builtins.str' + +[case testReverseOperatorTypeVar2a] from typing import TypeVar T = TypeVar("T", bound='Real') class Real: def __add__(self, other: Fraction) -> str: ... class Fraction(Real): - def __radd__(self, other: T) -> T: ... # TODO: This should be unsafely overlapping + def __radd__(self, other: T) -> T: ... # E: Signatures of "__radd__" of "Fraction" and "__add__" of "T" are unsafely overlapping + +reveal_type(Real() + Fraction()) # E: Revealed type is '__main__.Real*' +reveal_type(Fraction() + Fraction()) # E: Revealed type is 'builtins.str' + + +[case testReverseOperatorTypeVar2b] +from typing import TypeVar +T = TypeVar("T", Real, Fraction) +class Real: + def __add__(self, other: Fraction) -> str: ... +class Fraction(Real): + def __radd__(self, other: T) -> T: ... # E: Signatures of "__radd__" of "Fraction" and "__add__" of "Real" are unsafely overlapping + +reveal_type(Real() + Fraction()) # E: Revealed type is '__main__.Real*' +reveal_type(Fraction() + Fraction()) # E: Revealed type is 'builtins.str' + +[case testReverseOperatorTypeVar3] +from typing import TypeVar, Any +T = TypeVar("T", bound='Real') +class Real: + def __add__(self, other: FractionChild) -> str: ... +class Fraction(Real): + def __radd__(self, other: T) -> T: ... # E: Signatures of "__radd__" of "Fraction" and "__add__" of "T" are unsafely overlapping +class FractionChild(Fraction): pass + +reveal_type(Real() + Fraction()) # E: Revealed type is '__main__.Real*' +reveal_type(FractionChild() + Fraction()) # E: Revealed type is '__main__.FractionChild*' +reveal_type(FractionChild() + FractionChild()) # E: Revealed type is 'builtins.str' + +# Runtime error: we try calling __add__, it doesn't match, and we don't try __radd__ since +# the LHS and the RHS are not the same. +Fraction() + Fraction() # E: Unsupported operand types for + ("Fraction" and "Fraction") \ + # N: __radd__ will not be called when evaluating 'Fraction + Fraction': must define __add__ [case testReverseOperatorTypeType] from typing import TypeVar, Type diff --git a/test-data/unit/check-isinstance.test b/test-data/unit/check-isinstance.test index db242652bdc5..a7e08ba9a22b 100644 --- a/test-data/unit/check-isinstance.test +++ b/test-data/unit/check-isinstance.test @@ -2005,3 +2005,125 @@ def f(x: Union[A, str]) -> None: if isinstance(x, A): x.method_only_in_a() [builtins fixtures/isinstance.pyi] + +[case testIsInstanceInitialNoneCheckSkipsImpossibleCasesNoStrictOptional] +# flags: --strict-optional +from typing import Optional, Union + +class A: pass + +def foo1(x: Union[A, str, None]) -> None: + if x is None: + reveal_type(x) # E: Revealed type is 'None' + elif isinstance(x, A): + reveal_type(x) # E: Revealed type is '__main__.A' + else: + reveal_type(x) # E: Revealed type is 'builtins.str' + +def foo2(x: Optional[str]) -> None: + if x is None: + reveal_type(x) # E: Revealed type is 'None' + elif isinstance(x, A): + reveal_type(x) + else: + reveal_type(x) # E: Revealed type is 'builtins.str' +[builtins fixtures/isinstance.pyi] + +[case testIsInstanceInitialNoneCheckSkipsImpossibleCasesInNoStrictOptional] +# flags: --no-strict-optional +from typing import Optional, Union + +class A: pass + +def foo1(x: Union[A, str, None]) -> None: + if x is None: + # Since None is a subtype of all types in no-strict-optional, + # we can't really narrow the type here + reveal_type(x) # E: Revealed type is 'Union[__main__.A, builtins.str, None]' + elif isinstance(x, A): + # Note that Union[None, A] == A in no-strict-optional + reveal_type(x) # E: Revealed type is '__main__.A' + else: + reveal_type(x) # E: Revealed type is 'builtins.str' + +def foo2(x: Optional[str]) -> None: + if x is None: + reveal_type(x) # E: Revealed type is 'Union[builtins.str, None]' + elif isinstance(x, A): + # Mypy should, however, be able to skip impossible cases + reveal_type(x) + else: + reveal_type(x) # E: Revealed type is 'Union[builtins.str, None]' +[builtins fixtures/isinstance.pyi] + +[case testNoneCheckDoesNotNarrowWhenUsingTypeVars] +# flags: --strict-optional + +# Note: this test (and the following one) are testing checker.conditional_type_map: +# if you set the 'prohibit_none_typevar_overlap' keyword argument to False when calling +# 'is_overlapping_types', the binder will incorrectly infer that 'out' has a type of +# Union[T, None] after the if statement. + +from typing import TypeVar + +T = TypeVar('T') + +def foo(x: T) -> T: + out = None + out = x + if out is None: + pass + return out +[builtins fixtures/isinstance.pyi] + +[case testNoneCheckDoesNotNarrowWhenUsingTypeVarsNoStrictOptional] +# flags: --no-strict-optional +from typing import TypeVar + +T = TypeVar('T') + +def foo(x: T) -> T: + out = None + out = x + if out is None: + pass + return out +[builtins fixtures/isinstance.pyi] + +[case testNoneAndGenericTypesOverlapNoStrictOptional] +# flags: --no-strict-optional +from typing import Union, Optional, List + +# Note: this test is indirectly making sure meet.is_overlapping_types +# correctly ignores 'None' in unions. + +def foo(x: Optional[List[str]]) -> None: + reveal_type(x) # E: Revealed type is 'Union[builtins.list[builtins.str], None]' + assert isinstance(x, list) + reveal_type(x) # E: Revealed type is 'builtins.list[builtins.str]' + +def bar(x: Union[List[str], List[int], None]) -> None: + reveal_type(x) # E: Revealed type is 'Union[builtins.list[builtins.str], builtins.list[builtins.int], None]' + assert isinstance(x, list) + reveal_type(x) # E: Revealed type is 'Union[builtins.list[builtins.str], builtins.list[builtins.int]]' +[builtins fixtures/isinstancelist.pyi] + +[case testNoneAndGenericTypesOverlapStrictOptional] +# flags: --strict-optional +from typing import Union, Optional, List + +# This test is the same as the one above, except for strict-optional. +# It isn't testing anything explicitly and mostly exists for the sake +# of completeness. + +def foo(x: Optional[List[str]]) -> None: + reveal_type(x) # E: Revealed type is 'Union[builtins.list[builtins.str], None]' + assert isinstance(x, list) + reveal_type(x) # E: Revealed type is 'builtins.list[builtins.str]' + +def bar(x: Union[List[str], List[int], None]) -> None: + reveal_type(x) # E: Revealed type is 'Union[builtins.list[builtins.str], builtins.list[builtins.int], None]' + assert isinstance(x, list) + reveal_type(x) # E: Revealed type is 'Union[builtins.list[builtins.str], builtins.list[builtins.int]]' +[builtins fixtures/isinstancelist.pyi] + diff --git a/test-data/unit/check-overloading.test b/test-data/unit/check-overloading.test index 73c04616a535..ac56d09329ae 100644 --- a/test-data/unit/check-overloading.test +++ b/test-data/unit/check-overloading.test @@ -334,8 +334,8 @@ def bar(x: Union[T, C]) -> Union[T, int]: [builtins fixtures/isinstancelist.pyi] -[case testTypeCheckOverloadImplementationTypeVarDifferingUsage] -from typing import overload, Union, List, TypeVar +[case testTypeCheckOverloadImplementationTypeVarDifferingUsage1] +from typing import overload, Union, List, TypeVar, Generic T = TypeVar('T') @@ -348,6 +348,48 @@ def foo(t: Union[List[T], T]) -> T: return t[0] else: return t + +class Wrapper(Generic[T]): + @overload + def foo(self, t: List[T]) -> T: ... + @overload + def foo(self, t: T) -> T: ... + def foo(self, t: Union[List[T], T]) -> T: + if isinstance(t, list): + return t[0] + else: + return t +[builtins fixtures/isinstancelist.pyi] + +[case testTypeCheckOverloadImplementationTypeVarDifferingUsage2] +from typing import overload, Union, List, TypeVar, Generic + +T = TypeVar('T') + +# Note: this is unsafe when T = object +@overload +def foo(t: List[T], s: T) -> int: ... # E: Overloaded function signatures 1 and 2 overlap with incompatible return types +@overload +def foo(t: T, s: T) -> str: ... +def foo(t, s): pass + +class Wrapper(Generic[T]): + @overload + def foo(self, t: List[T], s: T) -> int: ... # E: Overloaded function signatures 1 and 2 overlap with incompatible return types + @overload + def foo(self, t: T, s: T) -> str: ... + def foo(self, t, s): pass + +class Dummy(Generic[T]): pass + +# Same root issue: why does the additional constraint bound T <: T +# cause the constraint solver to not infer T = object like it did in the +# first example? +@overload +def bar(d: Dummy[T], t: List[T], s: T) -> int: ... # E: Overloaded function signatures 1 and 2 overlap with incompatible return types +@overload +def bar(d: Dummy[T], t: T, s: T) -> str: ... +def bar(d: Dummy[T], t, s): pass [builtins fixtures/isinstancelist.pyi] [case testTypeCheckOverloadedFunctionBody] @@ -1524,14 +1566,25 @@ reveal_type(f(z='', x=a, y=1)) # E: Revealed type is 'Any' [case testOverloadWithOverlappingItemsAndAnyArgument5] from typing import overload, Any, Union +class A: pass +class B(A): pass + @overload -def f(x: int) -> int: ... +def f(x: B) -> B: ... @overload -def f(x: Union[int, float]) -> float: ... +def f(x: Union[A, B]) -> A: ... def f(x): pass +# Note: overloads ignore promotions so we treat 'int' and 'float' as distinct types +@overload +def g(x: int) -> int: ... # E: Overloaded function signatures 1 and 2 overlap with incompatible return types +@overload +def g(x: Union[int, float]) -> float: ... +def g(x): pass + a: Any reveal_type(f(a)) # E: Revealed type is 'Any' +reveal_type(g(a)) # E: Revealed type is 'Any' [case testOverloadWithOverlappingItemsAndAnyArgument6] from typing import overload, Any @@ -2004,7 +2057,7 @@ def foo(x: None, y: None) -> str: ... # E: Overloaded function signatures 1 and def foo(x: T, y: T) -> int: ... def foo(x): ... -# TODO: We should allow this; T can't be bound to two distinct types +# What if 'T' is 'object'? @overload def bar(x: None, y: int) -> str: ... # E: Overloaded function signatures 1 and 2 overlap with incompatible return types @overload @@ -2012,17 +2065,16 @@ def bar(x: T, y: T) -> int: ... def bar(x, y): ... class Wrapper(Generic[T]): - # TODO: This should be an error @overload - def foo(self, x: None, y: None) -> str: ... + def foo(self, x: None, y: None) -> str: ... # E: Overloaded function signatures 1 and 2 overlap with incompatible return types @overload - def foo(self, x: T, y: None) -> str: ... + def foo(self, x: T, y: None) -> int: ... def foo(self, x): ... @overload - def bar(self, x: None, y: int) -> str: ... + def bar(self, x: None, y: int) -> str: ... # E: Overloaded function signatures 1 and 2 overlap with incompatible return types @overload - def bar(self, x: T, y: T) -> str: ... + def bar(self, x: T, y: T) -> int: ... def bar(self, x, y): ... [case testOverloadFlagsPossibleMatches] @@ -2534,15 +2586,55 @@ def f(x): ... @overload def g(x: Union[A, B]) -> int: ... @overload -def g(x: Union[C, D]) -> str: ... +def g(x: Union[B, C]) -> int: ... def g(x): ... @overload -def h(x: Union[A, B]) -> int: ... # E: Overloaded function signatures 1 and 2 overlap with incompatible return types +def h(x: Union[A, B]) -> int: ... @overload -def h(x: Union[A, B, C]) -> str: ... +def h(x: Union[C, D]) -> str: ... def h(x): ... +@overload +def i(x: Union[A, B]) -> int: ... # E: Overloaded function signatures 1 and 2 overlap with incompatible return types +@overload +def i(x: Union[A, B, C]) -> str: ... +def i(x): ... + +[case testOverloadWithPartiallyOverlappingUnionsNested] +from typing import overload, Union, List + +class A: ... +class B: ... +class C: ... +class D: ... + +@overload +def f(x: List[Union[A, B]]) -> int: ... # E: Overloaded function signatures 1 and 2 overlap with incompatible return types +@overload +def f(x: List[Union[B, C]]) -> str: ... +def f(x): ... + +@overload +def g(x: List[Union[A, B]]) -> int: ... +@overload +def g(x: List[Union[B, C]]) -> int: ... +def g(x): ... + +@overload +def h(x: List[Union[A, B]]) -> int: ... +@overload +def h(x: List[Union[C, D]]) -> str: ... +def h(x): ... + +@overload +def i(x: List[Union[A, B]]) -> int: ... # E: Overloaded function signatures 1 and 2 overlap with incompatible return types +@overload +def i(x: List[Union[A, B, C]]) -> str: ... +def i(x): ... + +[builtins fixtures/list.pyi] + [case testOverloadPartialOverlapWithUnrestrictedTypeVar] from typing import TypeVar, overload @@ -2554,6 +2646,307 @@ def f(x: int) -> str: ... # E: Overloaded function signatures 1 and 2 overlap w def f(x: T) -> T: ... def f(x): ... +@overload +def g(x: int) -> int: ... +@overload +def g(x: T) -> T: ... +def g(x): ... + +[case testOverloadPartialOverlapWithUnrestrictedTypeVarNested] +from typing import TypeVar, overload, List + +T = TypeVar('T') + +@overload +def f1(x: List[int]) -> str: ... # E: Overloaded function signatures 1 and 2 overlap with incompatible return types +@overload +def f1(x: List[T]) -> T: ... +def f1(x): ... + +@overload +def f2(x: List[int]) -> List[str]: ... # E: Overloaded function signatures 1 and 2 overlap with incompatible return types +@overload +def f2(x: List[T]) -> List[T]: ... +def f2(x): ... + +@overload +def g1(x: List[int]) -> int: ... +@overload +def g1(x: List[T]) -> T: ... +def g1(x): ... + +@overload +def g2(x: List[int]) -> List[int]: ... +@overload +def g2(x: List[T]) -> List[T]: ... +def g2(x): ... + +[builtins fixtures/list.pyi] + +[case testOverloadPartialOverlapWithUnrestrictedTypeVarInClass] +from typing import TypeVar, overload, Generic + +T = TypeVar('T') + +class Wrapper(Generic[T]): + @overload + def f(self, x: int) -> str: ... # E: Overloaded function signatures 1 and 2 overlap with incompatible return types + @overload + def f(self, x: T) -> T: ... + def f(self, x): ... + + # TODO: This shouldn't trigger an error message? + # Related to testTypeCheckOverloadImplementationTypeVarDifferingUsage2? + # See https://github.com/python/mypy/issues/5510 + @overload + def g(self, x: int) -> int: ... # E: Overloaded function signatures 1 and 2 overlap with incompatible return types + @overload + def g(self, x: T) -> T: ... + def g(self, x): ... + +[case testOverloadPartialOverlapWithUnrestrictedTypeVarInClassNested] +from typing import TypeVar, overload, Generic, List + +T = TypeVar('T') + +class Wrapper(Generic[T]): + @overload + def f1(self, x: List[int]) -> str: ... # E: Overloaded function signatures 1 and 2 overlap with incompatible return types + @overload + def f1(self, x: List[T]) -> T: ... + def f1(self, x): ... + + @overload + def f2(self, x: List[int]) -> List[str]: ... # E: Overloaded function signatures 1 and 2 overlap with incompatible return types + @overload + def f2(self, x: List[T]) -> List[T]: ... + def f2(self, x): ... + + # TODO: This shouldn't trigger an error message? + # See https://github.com/python/mypy/issues/5510 + @overload + def g1(self, x: List[int]) -> int: ... # E: Overloaded function signatures 1 and 2 overlap with incompatible return types + @overload + def g1(self, x: List[T]) -> T: ... + def g1(self, x): ... + + @overload + def g2(self, x: List[int]) -> List[int]: ... # E: Overloaded function signatures 1 and 2 overlap with incompatible return types + @overload + def g2(self, x: List[T]) -> List[T]: ... + def g2(self, x): ... + +[builtins fixtures/list.pyi] + +[case testOverloadTypedDictDifferentRequiredKeysMeansDictsAreDisjoint] +from typing import overload +from mypy_extensions import TypedDict + +A = TypedDict('A', {'x': int, 'y': int}) +B = TypedDict('B', {'x': int, 'y': str}) + +@overload +def f(x: A) -> int: ... +@overload +def f(x: B) -> str: ... +def f(x): pass +[builtins fixtures/dict.pyi] + +[case testOverloadedTypedDictPartiallyOverlappingRequiredKeys] +from typing import overload, Union +from mypy_extensions import TypedDict + +A = TypedDict('A', {'x': int, 'y': Union[int, str]}) +B = TypedDict('B', {'x': int, 'y': Union[str, float]}) + +@overload +def f(x: A) -> int: ... # E: Overloaded function signatures 1 and 2 overlap with incompatible return types +@overload +def f(x: B) -> str: ... +def f(x): pass + +@overload +def g(x: A) -> int: ... +@overload +def g(x: B) -> object: ... +def g(x): pass +[builtins fixtures/dict.pyi] + +[case testOverloadedTypedDictFullyNonTotalDictsAreAlwaysPartiallyOverlapping] +from typing import overload +from mypy_extensions import TypedDict + +A = TypedDict('A', {'x': int, 'y': str}, total=False) +B = TypedDict('B', {'a': bool}, total=False) +C = TypedDict('C', {'x': str, 'y': int}, total=False) + +@overload +def f(x: A) -> int: ... # E: Overloaded function signatures 1 and 2 overlap with incompatible return types +@overload +def f(x: B) -> str: ... +def f(x): pass + +@overload +def g(x: A) -> int: ... # E: Overloaded function signatures 1 and 2 overlap with incompatible return types +@overload +def g(x: C) -> str: ... +def g(x): pass +[builtins fixtures/dict.pyi] + +[case testOverloadedTotalAndNonTotalTypedDictsCanPartiallyOverlap] +from typing import overload, Union +from mypy_extensions import TypedDict + +A = TypedDict('A', {'x': int, 'y': str}) +B = TypedDict('B', {'x': Union[int, str], 'y': str, 'z': int}, total=False) + +@overload +def f1(x: A) -> int: ... # E: Overloaded function signatures 1 and 2 overlap with incompatible return types +@overload +def f1(x: B) -> str: ... +def f1(x): pass + +@overload +def f2(x: B) -> int: ... # E: Overloaded function signatures 1 and 2 overlap with incompatible return types +@overload +def f2(x: A) -> str: ... +def f2(x): pass + +[builtins fixtures/dict.pyi] + +[case testOverloadedTypedDictsWithSomeOptionalKeysArePartiallyOverlapping] +from typing import overload, Union +from mypy_extensions import TypedDict + +class A(TypedDict): + x: int + y: int + +class B(TypedDict, total=False): + z: str + +class C(TypedDict, total=False): + z: int + +@overload +def f(x: B) -> int: ... # E: Overloaded function signatures 1 and 2 overlap with incompatible return types +@overload +def f(x: C) -> str: ... +def f(x): pass + +[builtins fixtures/dict.pyi] + +[case testOverloadedPartiallyOverlappingInheritedTypes1] +from typing import overload, List, Union, TypeVar, Generic + +class A: pass +class B: pass +class C: pass + +T = TypeVar('T') + +class ListSubclass(List[T]): pass +class Unrelated(Generic[T]): pass + +@overload +def f(x: List[Union[A, B]]) -> int: ... # E: Overloaded function signatures 1 and 2 overlap with incompatible return types +@overload +def f(x: ListSubclass[Union[B, C]]) -> str: ... +def f(x): pass + +@overload +def g(x: List[Union[A, B]]) -> int: ... +@overload +def g(x: Unrelated[Union[B, C]]) -> str: ... +def g(x): pass + +[builtins fixtures/list.pyi] + +[case testOverloadedPartiallyOverlappingInheritedTypes2] +from typing import overload, List, Union + +class A: pass +class B: pass +class C: pass + +class ListSubclass(List[Union[B, C]]): pass + +@overload +def f(x: List[Union[A, B]]) -> int: ... # E: Overloaded function signatures 1 and 2 overlap with incompatible return types +@overload +def f(x: ListSubclass) -> str: ... +def f(x): pass + +[builtins fixtures/list.pyi] + +[case testOverloadedPartiallyOverlappingInheritedTypes3] +from typing import overload, Union, Dict, TypeVar + +class A: pass +class B: pass +class C: pass + +S = TypeVar('S') + +class DictSubclass(Dict[str, S]): pass + +@overload +def f(x: Dict[str, Union[A, B]]) -> int: ... # E: Overloaded function signatures 1 and 2 overlap with incompatible return types +@overload +def f(x: DictSubclass[Union[B, C]]) -> str: ... +def f(x): pass + +[builtins fixtures/dict.pyi] + +[case testOverloadedPartiallyOverlappingTypeVarsAndUnion] +from typing import overload, TypeVar, Union + +class A: pass +class B: pass +class C: pass + +S = TypeVar('S', A, B) + +@overload +def f(x: S) -> int: ... # E: Overloaded function signatures 1 and 2 overlap with incompatible return types +@overload +def f(x: Union[B, C]) -> str: ... +def f(x): pass + +@overload +def g(x: Union[B, C]) -> int: ... # E: Overloaded function signatures 1 and 2 overlap with incompatible return types +@overload +def g(x: S) -> str: ... +def g(x): pass + +[case testOverloadPartiallyOverlappingTypeVarsIdentical] +from typing import overload, TypeVar, Union + +T = TypeVar('T') + +class A: pass +class B: pass +class C: pass + +@overload +def f(x: T, y: T, z: Union[A, B]) -> int: ... # E: Overloaded function signatures 1 and 2 overlap with incompatible return types +@overload +def f(x: T, y: T, z: Union[B, C]) -> str: ... +def f(x, y, z): pass + +[case testOverloadedPartiallyOverlappingCallables] +from typing import overload, Union, Callable + +class A: pass +class B: pass +class C: pass + +@overload +def f(x: Callable[[Union[A, B]], int]) -> int: ... # E: Overloaded function signatures 1 and 2 overlap with incompatible return types +@overload +def f(x: Callable[[Union[B, C]], int]) -> str: ... +def f(x): pass + [case testOverloadNotConfusedForProperty] from typing import overload @@ -4375,6 +4768,57 @@ def bar(x: T) -> T: ... def bar(x: Any) -> int: ... [out] +[case testOverloadsIgnorePromotions] +from typing import overload, List, Union, _promote + +class Parent: pass +class Child(Parent): pass + +children: List[Child] +parents: List[Parent] + +@overload +def f(x: Child) -> List[Child]: pass # E: Overloaded function signatures 1 and 2 overlap with incompatible return types +@overload +def f(x: Parent) -> List[Parent]: pass +def f(x: Union[Child, Parent]) -> Union[List[Child], List[Parent]]: + if isinstance(x, Child): + reveal_type(x) # E: Revealed type is '__main__.Child' + return children + else: + reveal_type(x) # E: Revealed type is '__main__.Parent' + return parents + +ints: List[int] +floats: List[float] + +@overload +def g(x: int) -> List[int]: pass +@overload +def g(x: float) -> List[float]: pass +def g(x: Union[int, float]) -> Union[List[int], List[float]]: + if isinstance(x, int): + reveal_type(x) # E: Revealed type is 'builtins.int' + return ints + else: + reveal_type(x) # E: Revealed type is 'builtins.float' + return floats + +[builtins fixtures/isinstancelist.pyi] + +[case testOverloadsTypesAndUnions] +from typing import overload, Type, Union + +class A: pass +class B: pass + +@overload +def f(x: Type[A]) -> int: ... # E: Overloaded function signatures 1 and 2 overlap with incompatible return types +@overload +def f(x: Union[Type[A], Type[B]]) -> str: ... +def f(x: Union[Type[A], Type[B]]) -> Union[int, str]: + return 1 + [case testBadOverloadProbableMatch] from typing import overload, List, Type diff --git a/test-data/unit/check-unreachable-code.test b/test-data/unit/check-unreachable-code.test index b86154302f8c..988038264d54 100644 --- a/test-data/unit/check-unreachable-code.test +++ b/test-data/unit/check-unreachable-code.test @@ -564,3 +564,43 @@ if typing.TYPE_CHECKING: reveal_type(x) # E: Revealed type is '__main__.B' [builtins fixtures/isinstancelist.pyi] + +[case testUnreachableWhenSuperclassIsAny] +# flags: --strict-optional +from typing import Any + +# This can happen if we're importing a class from a missing module +Parent: Any +class Child(Parent): + def foo(self) -> int: + reveal_type(self) # E: Revealed type is '__main__.Child' + if self is None: + reveal_type(self) + return None + reveal_type(self) # E: Revealed type is '__main__.Child' + return 3 + + def bar(self) -> int: + self = super(Child, self).something() + reveal_type(self) # E: Revealed type is '__main__.Child' + if self is None: + reveal_type(self) + return None + reveal_type(self) # E: Revealed type is '__main__.Child' + return 3 +[builtins fixtures/isinstance.pyi] + +[case testUnreachableWhenSuperclassIsAnyNoStrictOptional] +# flags: --no-strict-optional +from typing import Any + +Parent: Any +class Child(Parent): + def foo(self) -> int: + reveal_type(self) # E: Revealed type is '__main__.Child' + if self is None: + reveal_type(self) # E: Revealed type is '__main__.Child' + return None + reveal_type(self) # E: Revealed type is '__main__.Child' + return 3 +[builtins fixtures/isinstance.pyi] diff --git a/test-data/unit/pythoneval.test b/test-data/unit/pythoneval.test index 961cfc0768fa..446cb0f697fd 100644 --- a/test-data/unit/pythoneval.test +++ b/test-data/unit/pythoneval.test @@ -396,9 +396,7 @@ print('y' in x) True False -[case testOverlappingOperatorMethods-skip] -# TODO: This test will be repaired by my follow-up PR improving support for -# detecting partially-overlapping types in general +[case testOverlappingOperatorMethods] class X: pass class A: