diff --git a/mypy/checker.py b/mypy/checker.py index 1b11caf01f18..58ed6da80bd5 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -8,8 +8,10 @@ from typing import ( AbstractSet, Callable, + Collection, Dict, Final, + Generator, Generic, Iterable, Iterator, @@ -8047,33 +8049,29 @@ def are_argument_counts_overlapping(t: CallableType, s: CallableType) -> bool: return min_args <= max_args -def expand_callable_variants(c: CallableType) -> list[CallableType]: - """Expand a generic callable using all combinations of type variables' values/bounds.""" - for tv in c.variables: - # We need to expand self-type before other variables, because this is the only - # type variable that can have other type variables in the upper bound. - if tv.id.is_self(): - c = expand_type(c, {tv.id: tv.upper_bound}).copy_modified( - variables=[v for v in c.variables if not v.id.is_self()] - ) - break - - if not c.is_generic(): - # Fast path. - return [c] +def get_type_var_group_variants( + variables: Collection[TypeVarLikeType], +) -> Generator[dict[TypeVarId, Type], None, None]: + """Expand a group of type variables into all possible combinations of their values.""" tvar_values = [] - for tvar in c.variables: + for tvar in variables: if isinstance(tvar, TypeVarType) and tvar.values: tvar_values.append(tvar.values) else: tvar_values.append([tvar.upper_bound]) - variants = [] for combination in itertools.product(*tvar_values): - tvar_map = {tv.id: subst for (tv, subst) in zip(c.variables, combination)} - variants.append(expand_type(c, tvar_map).copy_modified(variables=[])) - return variants + yield {tv.id: subst for (tv, subst) in zip(variables, combination)} + + +def expand_callable_self(c: CallableType) -> CallableType: + for tv in c.variables: + if tv.id.is_self(): + return expand_type(c, {tv.id: tv.upper_bound}).copy_modified( + variables=[v for v in c.variables if not v.id.is_self()] + ) + return c def is_unsafe_overlapping_overload_signatures( @@ -8112,37 +8110,53 @@ def is_unsafe_overlapping_overload_signatures( # Note: We repeat this check twice in both directions compensate for slight # asymmetries in 'is_callable_compatible'. - for sig_variant in expand_callable_variants(signature): - for other_variant in expand_callable_variants(other): - # Using only expanded callables may cause false negatives, we can add - # more variants (e.g. using inference between callables) in the future. - if is_subset_no_promote(sig_variant.ret_type, other_variant.ret_type): - continue - if not ( - is_callable_compatible( - sig_variant, - other_variant, - is_compat=is_overlapping_types_for_overload, - check_args_covariantly=False, - is_proper_subtype=False, - is_compat_return=lambda l, r: not is_subset_no_promote(l, r), - allow_partial_overlap=True, - ) - or is_callable_compatible( - other_variant, - sig_variant, - is_compat=is_overlapping_types_for_overload, - check_args_covariantly=True, - is_proper_subtype=False, - is_compat_return=lambda l, r: not is_subset_no_promote(r, l), - allow_partial_overlap=True, - ) - ): - continue - # Using the same `allow_partial_overlap` flag as before, can cause false - # negatives in case where star argument is used in a catch-all fallback overload. - # But again, practicality beats purity here. - if not partial_only or not is_callable_compatible( + # We need to expand self-type before other variables, because this is the only + # type variable that can have other type variables in the upper bound. + signature = expand_callable_self(signature) + other = expand_callable_self(other) + + all_variables = {v.id: v for v in signature.variables} + all_variables.update({v.id: v for v in other.variables}) + + for tvar_map in get_type_var_group_variants(all_variables.values()): + sig_variant = expand_type(signature, tvar_map).copy_modified(variables=[]) + other_variant = expand_type(other, tvar_map).copy_modified(variables=[]) + + if is_subset_no_promote(sig_variant.ret_type, other_variant.ret_type): + continue + if not ( + is_callable_compatible( + sig_variant, + other_variant, + is_compat=is_overlapping_types_for_overload, + check_args_covariantly=False, + is_proper_subtype=False, + is_compat_return=lambda l, r: not is_subset_no_promote(l, r), + allow_partial_overlap=True, + ) + or is_callable_compatible( + other_variant, + sig_variant, + is_compat=is_overlapping_types_for_overload, + check_args_covariantly=True, + is_proper_subtype=False, + is_compat_return=lambda l, r: not is_subset_no_promote(r, l), + allow_partial_overlap=True, + ) + ): + continue + # Using the same `allow_partial_overlap` flag as before, can cause false + # negatives in case where star argument is used in a catch-all fallback overload. + # But again, practicality beats purity here. + + # Also earlier overload may not be more general overall but is more general when + # narrowed to common calls is handled here, so there is no unsafe overlap because + # it will still be caught by the earlier overload. + # Exeption here is when signature variants are exactly the same, in which case we + # should still consider them overlapping. + if ( + not partial_only + or not is_callable_compatible( other_variant, sig_variant, is_compat=is_subset_no_promote, @@ -8150,8 +8164,18 @@ def is_unsafe_overlapping_overload_signatures( is_proper_subtype=False, ignore_return=True, allow_partial_overlap=True, - ): - return True + ) + or is_callable_compatible( + sig_variant, + other_variant, + is_compat=lambda l, r: l == r, + check_args_covariantly=False, + is_proper_subtype=False, + ignore_return=True, + allow_partial_overlap=False, + ) + ): + return True return False diff --git a/test-data/unit/check-overloading.test b/test-data/unit/check-overloading.test index 9d01ce6bd480..d15e1e06266d 100644 --- a/test-data/unit/check-overloading.test +++ b/test-data/unit/check-overloading.test @@ -1327,9 +1327,8 @@ def h(x: Sequence[str]) -> int: pass @overload def h(x: Sequence[T]) -> None: pass # E: Overloaded function signature 2 will never be matched: signature 1's parameter type(s) are the same or broader -# Safety of this highly depends on the implementation, so we lean towards being silent. @overload -def i(x: List[str]) -> int: pass +def i(x: List[str]) -> int: pass # E: Overloaded function signatures 1 and 2 overlap with incompatible return types @overload def i(x: List[T]) -> None: pass [builtins fixtures/list.pyi] @@ -6768,3 +6767,43 @@ class D(Generic[T]): a: D[str] # E: Type argument "str" of "D" must be a subtype of "C" reveal_type(a.f(1)) # N: Revealed type is "builtins.int" reveal_type(a.f("x")) # N: Revealed type is "builtins.str" + + +[case testOverloadOnExactSameTypeVariantWithIncompetibleReturnTypes] +from typing import TypeVar, overload + +class A: pass +class B: pass + +T = TypeVar('T', A, B) + +@overload +def f(x: B) -> int: ... # E: Overloaded function signatures 1 and 2 overlap with incompatible return types +@overload +def f(x: T) -> str: ... +def f(x): ... + + + +[case testOverloadOnManyTypeVariablesGenericClassDoesNotSignificantlyDegradePerformance] +from typing import Generic, TypeVar, overload + +class A: pass + +class B: pass + +class C: pass + +T1 = TypeVar("T1", A, B, C) +T2 = TypeVar("T2", A, B, C) +T3 = TypeVar("T3", A, B, C) +T4 = TypeVar("T4", A, B, C) +T5 = TypeVar("T5", A, B, C) +T6 = TypeVar("T6", A, B, C) + +class Container(Generic[T1, T2, T3, T4, T5, T6]): + @overload + def f(self, t1: int, t2: T2, t3: T3, t4: T4, t5: T5, t6: T6) -> int: ... + @overload + def f(self, t1: str, t2: T2, t3: T3, t4: T4, t5: T5, t6: T6) -> str: ... + def f(self, t1, t2, t3, t4, t5, t6): ...