Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion mypy/checkexpr.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
44 changes: 32 additions & 12 deletions mypy/subtypes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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]
Expand All @@ -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():
Expand All @@ -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,
)


Expand Down
31 changes: 31 additions & 0 deletions test-data/unit/check-overloading.test
Original file line number Diff line number Diff line change
Expand Up @@ -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]
22 changes: 22 additions & 0 deletions test-data/unit/check-typeddict.test
Original file line number Diff line number Diff line change
Expand Up @@ -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]
17 changes: 17 additions & 0 deletions test-data/unit/check-typevar-tuple.test
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Loading