diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 67c525bec613..6cdd64e84302 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -3311,7 +3311,7 @@ def combine_function_signatures(self, types: list[ProperType]) -> AnyType | Call return AnyType(TypeOfAny.special_form) callables = cast("list[CallableType]", types) - combined = union_function_signatures(callables) + combined = union_function_signatures(callables, simplify_unions=True) if combined is not None: return combined diff --git a/mypy/subtypes.py b/mypy/subtypes.py index a2f2929cbecd..35a6cb5c4aab 100644 --- a/mypy/subtypes.py +++ b/mypy/subtypes.py @@ -740,17 +740,16 @@ def visit_callable_type(self, left: CallableType) -> bool: # Similarly, if one function has `TypeIs` and the other does not, # they are not compatible. return False + strict_concatenate = False + if options := self.options: + strict_concatenate = options.extra_checks or options.strict_concatenate return is_callable_compatible( left, right, is_compat=self._is_subtype, is_proper_subtype=self.proper_subtype, ignore_pos_arg_names=self.subtype_context.ignore_pos_arg_names, - strict_concatenate=( - (self.options.extra_checks or self.options.strict_concatenate) - if self.options - else False - ), + strict_concatenate=strict_concatenate, ) elif isinstance(right, Overloaded): return all(self._is_subtype(left, item) for item in right.items) @@ -2201,7 +2200,9 @@ def report(*args: Any) -> None: return cast(NormalizedCallableType, applied) -def union_function_signatures(callables: list[CallableType]) -> CallableType | None: +def union_function_signatures( + callables: list[CallableType], *, simplify_unions: bool = False +) -> CallableType | None: """Combine a list of functions by taking the union of all the arguments and return types.""" if len(callables) == 1: return callables[0] @@ -2219,15 +2220,27 @@ def union_function_signatures(callables: list[CallableType]) -> CallableType | N # confusing and ought to be re-written anyway.) callables, variables = merge_typevars_in_callables_by_name(callables) - new_args: list[list[Type]] = [[] for _ in callables[0].arg_types] - new_kinds = list(callables[0].arg_kinds) + new_callable = callables[0].with_unpacked_kwargs().with_normalized_var_args() + new_args: list[list[Type]] = [[] for _ in new_callable.arg_types] + new_kinds = list(new_callable.arg_kinds) + new_names = list(new_callable.arg_names) new_returns: list[Type] = [] for target in callables: + target = target.with_unpacked_kwargs().with_normalized_var_args() # TODO: Enhance the merging logic to handle a wider variety of signatures. + # In particular, allow name-only arguments that appear in different order. if len(new_kinds) != len(target.arg_kinds): return None for i, (new_kind, target_kind) in enumerate(zip(new_kinds, target.arg_kinds)): + if target.arg_names[i] != new_callable.arg_names[i]: + if target_kind.is_named(): + return None + if target_kind.is_positional(): + new_names[i] = None + if isinstance(target.arg_types[i], (ParamSpecType, UnpackType)): + # It is risky to put these inside a union. + return None if new_kind == target_kind: continue if new_kind.is_positional() and target_kind.is_positional(): @@ -2239,12 +2252,19 @@ def union_function_signatures(callables: list[CallableType]) -> CallableType | N new_args[i].append(arg) new_returns.append(target.ret_type) - return callables[0].copy_modified( - arg_types=[mypy.typeops.make_simplified_union(args) for args in new_args], + if simplify_unions: + arg_types = [mypy.typeops.make_simplified_union(args) for args in new_args] + ret_type = mypy.typeops.make_simplified_union(new_returns) + else: + arg_types = [UnionType.make_union(args) for args in new_args] + ret_type = UnionType.make_union(new_returns) + + return new_callable.copy_modified( + arg_types=arg_types, arg_kinds=new_kinds, - ret_type=mypy.typeops.make_simplified_union(new_returns), + arg_names=new_names, + ret_type=ret_type, variables=variables, - implicit=True, ) diff --git a/test-data/unit/check-overloading.test b/test-data/unit/check-overloading.test index 54d0ea264e70..5c1d6e61aa34 100644 --- a/test-data/unit/check-overloading.test +++ b/test-data/unit/check-overloading.test @@ -6901,3 +6901,34 @@ class B(A): @overload def f(self, x: T) -> T: ... def f(self, x: Optional[T]) -> Optional[T]: ... + +[case testOverloadSubtypingRespectPosArgNames] +from typing import Protocol, overload, Union + +class P(Protocol): + def __call__(self, __x: Union[int, str]) -> Union[int, str]: ... + +class PBad(Protocol): + def __call__(self, x: Union[int, str]) -> Union[int, str]: ... + +@overload +def f(x: int) -> int: ... +@overload +def f(y: str) -> str: ... +def f(*args, **kwargs): + pass + +p: P = f +pb: PBad = f # E: Incompatible types in assignment (expression has type overloaded function, variable has type "PBad") \ + # N: "PBad.__call__" has type "def __call__(self, x: int | str) -> int | str" + +class A: + def f(self, x: Union[int, str]) -> None: ... + +class B(A): + @overload + def f(self, x: int) -> None: ... + @overload + def f(self, y: str) -> None: ... # This is currently allowed (note different name) + def f(self, *args, **kwargs) -> None: ... +[builtins fixtures/tuple.pyi] diff --git a/test-data/unit/check-typeddict.test b/test-data/unit/check-typeddict.test index 7013bc5859f9..978f0d849c23 100644 --- a/test-data/unit/check-typeddict.test +++ b/test-data/unit/check-typeddict.test @@ -6126,3 +6126,25 @@ class Callback(Model): def __init__(self, **kwargs: Unpack[_CallbackInit]) -> None: ... [builtins fixtures/dict.pyi] [typing fixtures/typing-typeddict.pyi] + +[case testOverloadTypedDictUnpackNoCrash] +from typing import Any, overload, Callable +from typing_extensions import Unpack, TypedDict + +class A(TypedDict): + bar: int + +class B(TypedDict): + baz: int + +@overload +def foo(x: int, **kwargs: Unpack[A]) -> int: ... +@overload +def foo(x: str, **kwargs: Unpack[B]) -> str: ... +def foo(x, **kwargs): + pass + +def test(f: Callable[[object], object]) -> None: ... +test(foo) # E: Argument 1 to "test" has incompatible type overloaded function; expected "Callable[[object], object]" +[builtins fixtures/dict.pyi] +[typing fixtures/typing-typeddict.pyi] diff --git a/test-data/unit/check-typevar-tuple.test b/test-data/unit/check-typevar-tuple.test index f514af2f63d4..232d516da85b 100644 --- a/test-data/unit/check-typevar-tuple.test +++ b/test-data/unit/check-typevar-tuple.test @@ -3215,3 +3215,20 @@ reveal_type(mix2(b1)) # N: Revealed type is "tuple[builtins.str, tuple[builtins reveal_type(mix2(b2)) # N: Revealed type is "tuple[Any, tuple[builtins.int, builtins.str]]" reveal_type(mix2(b3)) # N: Revealed type is "tuple[builtins.int, tuple[Any, builtins.str]]" [builtins fixtures/tuple.pyi] + +[case testTypeVarTupleOverloadNoCrash] +from typing import Any, overload, Callable +from typing_extensions import TypeVarTuple, Unpack + +Ts = TypeVarTuple("Ts") + +@overload +def foo(x: int, *args: Unpack[Ts]) -> tuple[int, Unpack[Ts]]: ... +@overload +def foo(x: str, *args: Unpack[Ts]) -> tuple[str, Unpack[Ts]]: ... +def foo(x, *args): + pass + +def test(f: Callable[[object], object]) -> None: ... +test(foo) # E: Argument 1 to "test" has incompatible type overloaded function; expected "Callable[[object], object]" +[builtins fixtures/tuple.pyi]