Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Optimize is_unsafe_overlapping_overload_signatures for overlapping typevars case #18159

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
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
128 changes: 76 additions & 52 deletions mypy/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@
from typing import (
AbstractSet,
Callable,
Collection,
Dict,
Final,
Generator,
Generic,
Iterable,
Iterator,
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -8112,46 +8110,72 @@ 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,
check_args_covariantly=True,
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


Expand Down
43 changes: 41 additions & 2 deletions test-data/unit/check-overloading.test
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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): ...