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

Fix overload overlap check for UninhabitedType #13461

Merged
merged 3 commits into from Aug 21, 2022
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
18 changes: 11 additions & 7 deletions mypy/checker.py
Expand Up @@ -747,14 +747,14 @@ def check_overlapping_overloads(self, defn: OverloadedFuncDef) -> None:

# Is the overload alternative's arguments subtypes of the implementation's?
if not is_callable_compatible(
impl, sig1, is_compat=is_subtype_no_promote, ignore_return=True
impl, sig1, is_compat=is_subtype, ignore_return=True
):
self.msg.overloaded_signatures_arg_specific(i + 1, defn.impl)

# Is the overload alternative's return type a subtype of the implementation's?
if not (
is_subtype_no_promote(sig1.ret_type, impl.ret_type)
or is_subtype_no_promote(impl.ret_type, sig1.ret_type)
is_subtype(sig1.ret_type, impl.ret_type)
or is_subtype(impl.ret_type, sig1.ret_type)
):
self.msg.overloaded_signatures_ret_specific(i + 1, defn.impl)

Expand Down Expand Up @@ -6484,15 +6484,15 @@ def is_unsafe_overlapping_overload_signatures(
return is_callable_compatible(
signature,
other,
is_compat=is_overlapping_types_no_promote,
is_compat=is_overlapping_types_no_promote_no_uninhabited,
is_compat_return=lambda l, r: not is_subtype_no_promote(l, r),
ignore_return=False,
check_args_covariantly=True,
allow_partial_overlap=True,
) or is_callable_compatible(
other,
signature,
is_compat=is_overlapping_types_no_promote,
is_compat=is_overlapping_types_no_promote_no_uninhabited,
is_compat_return=lambda l, r: not is_subtype_no_promote(r, l),
ignore_return=False,
check_args_covariantly=False,
Expand Down Expand Up @@ -6976,8 +6976,12 @@ def is_subtype_no_promote(left: Type, right: Type) -> bool:
return is_subtype(left, right, ignore_promotions=True)


def is_overlapping_types_no_promote(left: Type, right: Type) -> bool:
return is_overlapping_types(left, right, ignore_promotions=True)
def is_overlapping_types_no_promote_no_uninhabited(left: Type, right: Type) -> bool:
# For the purpose of unsafe overload checks we consider list[<nothing>] and list[int]
# non-overlapping. This is consistent with how we treat list[int] and list[str] as
# non-overlapping, despite [] belongs to both. Also this will prevent false positives
# for failed type inference during unification.
return is_overlapping_types(left, right, ignore_promotions=True, ignore_uninhabited=True)


def is_private(node_name: str) -> bool:
Expand Down
16 changes: 11 additions & 5 deletions mypy/meet.py
Expand Up @@ -211,6 +211,7 @@ def is_overlapping_types(
right: Type,
ignore_promotions: bool = False,
prohibit_none_typevar_overlap: bool = False,
ignore_uninhabited: bool = False,
) -> bool:
"""Can a value of type 'left' also be of type 'right' or vice-versa?

Expand All @@ -235,6 +236,7 @@ def _is_overlapping_types(left: Type, right: Type) -> bool:
right,
ignore_promotions=ignore_promotions,
prohibit_none_typevar_overlap=prohibit_none_typevar_overlap,
ignore_uninhabited=ignore_uninhabited,
)

# We should never encounter this type.
Expand Down Expand Up @@ -282,8 +284,10 @@ def _is_overlapping_types(left: Type, right: Type) -> bool:
):
return True

if is_proper_subtype(left, right, ignore_promotions=ignore_promotions) or is_proper_subtype(
right, left, ignore_promotions=ignore_promotions
if is_proper_subtype(
left, right, ignore_promotions=ignore_promotions, ignore_uninhabited=ignore_uninhabited
) or is_proper_subtype(
right, left, ignore_promotions=ignore_promotions, ignore_uninhabited=ignore_uninhabited
):
return True

Expand Down Expand Up @@ -425,8 +429,10 @@ def _callable_overlap(left: CallableType, right: CallableType) -> bool:
if isinstance(left, Instance) and isinstance(right, Instance):
# First we need to handle promotions and structural compatibility for instances
# that came as fallbacks, so simply call is_subtype() to avoid code duplication.
if is_subtype(left, right, ignore_promotions=ignore_promotions) or is_subtype(
right, left, ignore_promotions=ignore_promotions
if is_subtype(
left, right, ignore_promotions=ignore_promotions, ignore_uninhabited=ignore_uninhabited
) or is_subtype(
right, left, ignore_promotions=ignore_promotions, ignore_uninhabited=ignore_uninhabited
):
return True

Expand Down Expand Up @@ -467,7 +473,7 @@ def _callable_overlap(left: CallableType, right: CallableType) -> bool:
# Note: it's unclear however, whether returning False is the right thing
# to do when inferring reachability -- see https://github.com/python/mypy/issues/5529

assert type(left) != type(right)
assert type(left) != type(right), f"{type(left)} vs {type(right)}"
return False


Expand Down
66 changes: 51 additions & 15 deletions mypy/subtypes.py
Expand Up @@ -67,7 +67,7 @@
IS_CLASSVAR: Final = 2
IS_CLASS_OR_STATIC: Final = 3

TypeParameterChecker: _TypeAlias = Callable[[Type, Type, int, bool], bool]
TypeParameterChecker: _TypeAlias = Callable[[Type, Type, int, bool, "SubtypeContext"], bool]


class SubtypeContext:
Expand All @@ -80,6 +80,7 @@ def __init__(
ignore_declared_variance: bool = False,
# Supported for both proper and non-proper
ignore_promotions: bool = False,
ignore_uninhabited: bool = False,
# Proper subtype flags
erase_instances: bool = False,
keep_erased_types: bool = False,
Expand All @@ -89,6 +90,7 @@ def __init__(
self.ignore_pos_arg_names = ignore_pos_arg_names
self.ignore_declared_variance = ignore_declared_variance
self.ignore_promotions = ignore_promotions
self.ignore_uninhabited = ignore_uninhabited
self.erase_instances = erase_instances
self.keep_erased_types = keep_erased_types
self.options = options
Expand All @@ -115,6 +117,7 @@ def is_subtype(
ignore_pos_arg_names: bool = False,
ignore_declared_variance: bool = False,
ignore_promotions: bool = False,
ignore_uninhabited: bool = False,
options: Options | None = None,
) -> bool:
"""Is 'left' subtype of 'right'?
Expand All @@ -134,6 +137,7 @@ def is_subtype(
ignore_pos_arg_names=ignore_pos_arg_names,
ignore_declared_variance=ignore_declared_variance,
ignore_promotions=ignore_promotions,
ignore_uninhabited=ignore_uninhabited,
options=options,
)
else:
Expand All @@ -143,6 +147,7 @@ def is_subtype(
ignore_pos_arg_names,
ignore_declared_variance,
ignore_promotions,
ignore_uninhabited,
options,
}
), "Don't pass both context and individual flags"
Expand Down Expand Up @@ -177,6 +182,7 @@ def is_proper_subtype(
*,
subtype_context: SubtypeContext | None = None,
ignore_promotions: bool = False,
ignore_uninhabited: bool = False,
erase_instances: bool = False,
keep_erased_types: bool = False,
) -> bool:
Expand All @@ -192,12 +198,19 @@ def is_proper_subtype(
if subtype_context is None:
subtype_context = SubtypeContext(
ignore_promotions=ignore_promotions,
ignore_uninhabited=ignore_uninhabited,
erase_instances=erase_instances,
keep_erased_types=keep_erased_types,
)
else:
assert not any(
{ignore_promotions, erase_instances, keep_erased_types}
{
ignore_promotions,
ignore_uninhabited,
erase_instances,
keep_erased_types,
ignore_uninhabited,
}
), "Don't pass both context and individual flags"
if TypeState.is_assumed_proper_subtype(left, right):
return True
Expand All @@ -215,23 +228,28 @@ def is_equivalent(
ignore_type_params: bool = False,
ignore_pos_arg_names: bool = False,
options: Options | None = None,
subtype_context: SubtypeContext | None = None,
) -> bool:
return is_subtype(
a,
b,
ignore_type_params=ignore_type_params,
ignore_pos_arg_names=ignore_pos_arg_names,
options=options,
subtype_context=subtype_context,
) and is_subtype(
b,
a,
ignore_type_params=ignore_type_params,
ignore_pos_arg_names=ignore_pos_arg_names,
options=options,
subtype_context=subtype_context,
)


def is_same_type(a: Type, b: Type, ignore_promotions: bool = True) -> bool:
def is_same_type(
a: Type, b: Type, ignore_promotions: bool = True, subtype_context: SubtypeContext | None = None
) -> bool:
"""Are these types proper subtypes of each other?

This means types may have different representation (e.g. an alias, or
Expand All @@ -241,8 +259,10 @@ def is_same_type(a: Type, b: Type, ignore_promotions: bool = True) -> bool:
# considered not the same type (which is the case at runtime).
# Also Union[bool, int] (if it wasn't simplified before) will be different
# from plain int, etc.
return is_proper_subtype(a, b, ignore_promotions=ignore_promotions) and is_proper_subtype(
b, a, ignore_promotions=ignore_promotions
return is_proper_subtype(
a, b, ignore_promotions=ignore_promotions, subtype_context=subtype_context
) and is_proper_subtype(
b, a, ignore_promotions=ignore_promotions, subtype_context=subtype_context
)


Expand Down Expand Up @@ -306,23 +326,34 @@ def check_item(left: Type, right: Type, subtype_context: SubtypeContext) -> bool
return left.accept(SubtypeVisitor(orig_right, subtype_context, proper_subtype))


# TODO: should we pass on the original flags here and in couple other places?
# This seems logical but was never done in the past for some reasons.
def check_type_parameter(lefta: Type, righta: Type, variance: int, proper_subtype: bool) -> bool:
def check_type_parameter(
lefta: Type, righta: Type, variance: int, proper_subtype: bool, subtype_context: SubtypeContext
) -> bool:
def check(left: Type, right: Type) -> bool:
return is_proper_subtype(left, right) if proper_subtype else is_subtype(left, right)
return (
is_proper_subtype(left, right, subtype_context=subtype_context)
if proper_subtype
else is_subtype(left, right, subtype_context=subtype_context)
)

if variance == COVARIANT:
return check(lefta, righta)
elif variance == CONTRAVARIANT:
return check(righta, lefta)
else:
if proper_subtype:
return is_same_type(lefta, righta)
return is_equivalent(lefta, righta)
# We pass ignore_promotions=False because it is a default for subtype checks.
# The actual value will be taken from the subtype_context, and it is whatever
# the original caller passed.
return is_same_type(
lefta, righta, ignore_promotions=False, subtype_context=subtype_context
)
return is_equivalent(lefta, righta, subtype_context=subtype_context)


def ignore_type_parameter(lefta: Type, righta: Type, variance: int, proper_subtype: bool) -> bool:
def ignore_type_parameter(
lefta: Type, righta: Type, variance: int, proper_subtype: bool, subtype_context: SubtypeContext
) -> bool:
return True


Expand Down Expand Up @@ -385,7 +416,11 @@ def visit_none_type(self, left: NoneType) -> bool:
return True

def visit_uninhabited_type(self, left: UninhabitedType) -> bool:
return True
# We ignore this for unsafe overload checks, so that and empty list and
# a list of int will be considered non-overlapping.
if isinstance(self.right, UninhabitedType):
return True
return not self.subtype_context.ignore_uninhabited

def visit_erased_type(self, left: ErasedType) -> bool:
# This may be encountered during type inference. The result probably doesn't
Expand Down Expand Up @@ -521,12 +556,12 @@ def check_mixed(
for lefta, righta, tvar in type_params:
if isinstance(tvar, TypeVarType):
if not self.check_type_parameter(
lefta, righta, tvar.variance, self.proper_subtype
lefta, righta, tvar.variance, self.proper_subtype, self.subtype_context
):
nominal = False
else:
if not self.check_type_parameter(
lefta, righta, COVARIANT, self.proper_subtype
lefta, righta, COVARIANT, self.proper_subtype, self.subtype_context
):
nominal = False
if nominal:
Expand Down Expand Up @@ -694,6 +729,7 @@ def visit_typeddict_type(self, left: TypedDictType) -> bool:
if not left.names_are_wider_than(right):
return False
for name, l, r in left.zip(right):
# TODO: should we pass on the full subtype_context here and below?
if self.proper_subtype:
check = is_same_type(l, r)
else:
Expand Down
26 changes: 26 additions & 0 deletions test-data/unit/check-overloading.test
Expand Up @@ -6467,3 +6467,29 @@ spam: Callable[..., str] = lambda x, y: 'baz'
reveal_type(func(spam)) # N: Revealed type is "def (*Any, **Any) -> builtins.str"

[builtins fixtures/paramspec.pyi]

[case testGenericOverloadOverlapWithType]
import m

[file m.pyi]
from typing import TypeVar, Type, overload, Callable

T = TypeVar("T", bound=str)
@overload
def foo(x: Type[T] | int) -> int: ...
@overload
def foo(x: Callable[[int], bool]) -> str: ...

[case testGenericOverloadOverlapWithCollection]
import m

[file m.pyi]
from typing import TypeVar, Sequence, overload, List

T = TypeVar("T", bound=str)

@overload
def foo(x: List[T]) -> str: ...
@overload
def foo(x: Sequence[int]) -> int: ...
[builtins fixtures/list.pyi]