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
118 changes: 15 additions & 103 deletions mypy/checkexpr.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,9 @@
find_member,
is_same_type,
is_subtype,
merge_typevars_in_callables_by_name,
non_method_protocol_members,
union_function_signatures,
)
from mypy.traverser import (
all_name_and_member_expressions,
Expand Down Expand Up @@ -3298,75 +3300,30 @@ def type_overrides_set(

def combine_function_signatures(self, types: list[ProperType]) -> AnyType | CallableType:
"""Accepts a list of function signatures and attempts to combine them together into a
new CallableType consisting of the union of all of the given arguments and return types.
new CallableType consisting of the union of all the given arguments and return types.

If there is at least one non-callable type, return Any (this can happen if there is
an ambiguity because of Any in arguments).
"""
assert types, "Trying to merge no callables"
if not all(isinstance(c, CallableType) for c in types):
return AnyType(TypeOfAny.special_form)

callables = cast("list[CallableType]", types)
if len(callables) == 1:
return callables[0]

# Note: we are assuming here that if a user uses some TypeVar 'T' in
# two different functions, they meant for that TypeVar to mean the
# same thing.
#
# This function will make sure that all instances of that TypeVar 'T'
# refer to the same underlying TypeVarType objects to simplify the union-ing
# logic below.
#
# (If the user did *not* mean for 'T' to be consistently bound to the
# same type in their overloads, well, their code is probably too
# confusing and ought to be re-written anyways.)
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_returns: list[Type] = []
too_complex = False

for target in callables:
# We fall back to Callable[..., Union[<returns>]] if the functions do not have
# the exact same signature. The only exception is if one arg is optional and
# the other is positional: in that case, we continue unioning (and expect a
# positional arg).
# TODO: Enhance the merging logic to handle a wider variety of signatures.
if len(new_kinds) != len(target.arg_kinds):
too_complex = True
break
for i, (new_kind, target_kind) in enumerate(zip(new_kinds, target.arg_kinds)):
if new_kind == target_kind:
continue
if new_kind.is_positional() and target_kind.is_positional():
new_kinds[i] = ARG_POS
else:
too_complex = True
break
if too_complex:
break
for i, arg in enumerate(target.arg_types):
new_args[i].append(arg)
new_returns.append(target.ret_type)

union_return = make_simplified_union(new_returns)
if too_complex:
any = AnyType(TypeOfAny.special_form)
return callables[0].copy_modified(
arg_types=[any, any],
arg_kinds=[ARG_STAR, ARG_STAR2],
arg_names=[None, None],
ret_type=union_return,
variables=variables,
implicit=True,
)
combined = union_function_signatures(callables)
if combined is not None:
return combined

# We fall back to Callable[..., Union[<returns>]] if the functions do not have
# the exact same signature. The only exception is if one arg is optional and
# the other is positional (and expect a positional arg).
callables, variables = merge_typevars_in_callables_by_name(callables)
union_return = make_simplified_union([c.ret_type for c in callables])
any = AnyType(TypeOfAny.special_form)
return callables[0].copy_modified(
arg_types=[make_simplified_union(args) for args in new_args],
arg_kinds=new_kinds,
arg_types=[any, any],
arg_kinds=[ARG_STAR, ARG_STAR2],
arg_names=[None, None],
ret_type=union_return,
variables=variables,
implicit=True,
Expand Down Expand Up @@ -6965,51 +6922,6 @@ def all_same_types(types: list[Type]) -> bool:
return all(is_same_type(t, types[0]) for t in types[1:])


def merge_typevars_in_callables_by_name(
callables: Sequence[CallableType],
) -> tuple[list[CallableType], list[TypeVarType]]:
"""Takes all the typevars present in the callables and 'combines' the ones with the same name.

For example, suppose we have two callables with signatures "f(x: T, y: S) -> T" and
"f(x: List[Tuple[T, S]]) -> Tuple[T, S]". Both callables use typevars named "T" and
"S", but we treat them as distinct, unrelated typevars. (E.g. they could both have
distinct ids.)

If we pass in both callables into this function, it returns a list containing two
new callables that are identical in signature, but use the same underlying TypeVarType
for T and S.

This is useful if we want to take the output lists and "merge" them into one callable
in some way -- for example, when unioning together overloads.

Returns both the new list of callables and a list of all distinct TypeVarType objects used.
"""
output: list[CallableType] = []
unique_typevars: dict[str, TypeVarType] = {}
variables: list[TypeVarType] = []

for target in callables:
if target.is_generic():
target = freshen_function_type_vars(target)

rename = {} # Dict[TypeVarId, TypeVar]
for tv in target.variables:
name = tv.fullname
if name not in unique_typevars:
# TODO: support ParamSpecType and TypeVarTuple.
if isinstance(tv, (ParamSpecType, TypeVarTupleType)):
continue
assert isinstance(tv, TypeVarType)
unique_typevars[name] = tv
variables.append(tv)
rename[tv.id] = unique_typevars[name]

target = expand_type(target, rename)
output.append(target)

return output, variables


def try_getting_literal(typ: Type) -> ProperType:
"""If possible, get a more precise literal type for a given type."""
typ = get_proper_type(typ)
Expand Down
96 changes: 95 additions & 1 deletion mypy/subtypes.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from __future__ import annotations

from collections.abc import Callable, Iterable, Iterator
from collections.abc import Callable, Iterable, Iterator, Sequence
from contextlib import contextmanager
from typing import Any, Final, TypeAlias as _TypeAlias, TypeVar, cast

Expand All @@ -20,6 +20,7 @@
# Circular import; done in the function instead.
# import mypy.solve
from mypy.nodes import (
ARG_POS,
ARG_STAR,
ARG_STAR2,
CONTRAVARIANT,
Expand Down Expand Up @@ -60,6 +61,7 @@
TypedDictType,
TypeOfAny,
TypeType,
TypeVarLikeType,
TypeVarTupleType,
TypeVarType,
TypeVisitor,
Expand Down Expand Up @@ -1013,6 +1015,12 @@ def visit_overloaded(self, left: Overloaded) -> bool:
for item in left.items:
if self._is_subtype(item, right):
return True
# If simple logic failed, check a (somewhat ad hoc but important)
# edge case: Overloaded(def (int) -> int, def (str) -> str) is
# a subtype of def (int | str) -> int | str.
combined = union_function_signatures(left.items)
if combined is not None and self._is_subtype(combined, right):
return True
return False
elif isinstance(right, Overloaded):
if left == self.right:
Expand Down Expand Up @@ -2147,6 +2155,92 @@ def report(*args: Any) -> None:
return cast(NormalizedCallableType, applied)


def union_function_signatures(callables: list[CallableType]) -> 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]

# Note: we are assuming here that if a user uses some TypeVar 'T' in
# two different functions, they meant for that TypeVar to mean the
# same thing.
#
# This function will make sure that all instances of that TypeVar 'T'
# refer to the same underlying TypeVarType objects to simplify the union-ing
# logic below.
#
# (If the user did *not* mean for 'T' to be consistently bound to the
# same type in their overloads, well, their code is probably too
# 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_returns: list[Type] = []

for target in callables:
# TODO: Enhance the merging logic to handle a wider variety of signatures.
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 new_kind == target_kind:
continue
if new_kind.is_positional() and target_kind.is_positional():
new_kinds[i] = ARG_POS
else:
return None

for i, arg in enumerate(target.arg_types):
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],
arg_kinds=new_kinds,
ret_type=mypy.typeops.make_simplified_union(new_returns),
variables=variables,
implicit=True,
)


def merge_typevars_in_callables_by_name(
callables: Sequence[CallableType],
) -> tuple[list[CallableType], list[TypeVarLikeType]]:
"""Takes all the typevars present in the callables and 'combines' the ones with the same name.

For example, suppose we have two callables with signatures "f(x: T, y: S) -> T" and
"f(x: List[Tuple[T, S]]) -> Tuple[T, S]". Both callables use typevars named "T" and
"S", but we treat them as distinct, unrelated typevars. (E.g. they could both have
distinct ids.)

If we pass in both callables into this function, it returns a list containing two
new callables that are identical in signature, but use the same underlying TypeVarType
for T and S.

This is useful if we want to take the output lists and "merge" them into one callable
in some way -- for example, when unioning together overloads.

Returns both the new list of callables and a list of all distinct TypeVarType objects used.
"""
output: list[CallableType] = []
unique_typevars: dict[str, TypeVarLikeType] = {}
variables: list[TypeVarLikeType] = []

for target in callables:
if target.is_generic():
rename = {} # Mapping TypeVarId -> TypeVar
for tv in target.variables:
name = tv.fullname
if name not in unique_typevars:
unique_typevars[name] = tv
variables.append(tv)
rename[tv.id] = unique_typevars[name]

target = expand_type(target, rename)
output.append(target)

return output, variables


def try_restrict_literal_union(t: UnionType, s: Type) -> list[Type] | None:
"""Return the items of t, excluding any occurrence of s, if and only if
- t only contains simple literals
Expand Down
45 changes: 45 additions & 0 deletions test-data/unit/check-overloading.test
Original file line number Diff line number Diff line change
Expand Up @@ -6856,3 +6856,48 @@ if isinstance(headers, dict):

reveal_type(headers) # N: Revealed type is "__main__.Headers | typing.Iterable[tuple[builtins.bytes, builtins.bytes]]"
[builtins fixtures/isinstancelist.pyi]

[case testOverloadUnionCallableSubtype]
from typing import Callable, Union, overload

def f(fn: Callable[[Union[int, str]], Union[int, str]]) -> None: pass
def f1(fn: Callable[[Union[int, str, bytes]], Union[int, str]]) -> None: pass
def f2(fn: Callable[[Union[int, str]], int]) -> None: pass

@overload
def g(x: int) -> int: ...
@overload
def g(x: str) -> str: ...
def g(x): ...

f(g)
f1(g) # E: Argument 1 to "f1" has incompatible type overloaded function; expected "Callable[[int | str | bytes], int | str]"
f2(g) # E: Argument 1 to "f2" has incompatible type overloaded function; expected "Callable[[int | str], int]"

[case testOverloadOverridesUnionCallable]
from typing import overload, Union

class A:
def f(self, x: Union[int, str]) -> None: ...

class B(A):
@overload
def f(self, x: int) -> None: ...
@overload
def f(self, x: str) -> None: ...
def f(self, x: Union[int, str]) -> None: ...

[case testOverloadOverridesUnionCallableGeneric]
from typing import overload, Optional, TypeVar

T = TypeVar("T")

class A:
def f(self, x: Optional[T]) -> Optional[T]: ...

class B(A):
@overload
def f(self, x: None) -> None: ...
@overload
def f(self, x: T) -> T: ...
def f(self, x: Optional[T]) -> Optional[T]: ...
Loading