Skip to content

Fix instance vs tuple subtyping edge case #18664

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

Merged
merged 3 commits into from
Feb 13, 2025
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
56 changes: 29 additions & 27 deletions mypy/subtypes.py
Original file line number Diff line number Diff line change
Expand Up @@ -474,21 +474,17 @@ def visit_instance(self, left: Instance) -> bool:
return self._is_subtype(left, unpacked)
if left.type.has_base(right.partial_fallback.type.fullname):
if not self.proper_subtype:
# Special case to consider Foo[*tuple[Any, ...]] (i.e. bare Foo) a
# subtype of Foo[<whatever>], when Foo is user defined variadic tuple type.
# Special cases to consider:
# * Plain tuple[Any, ...] instance is a subtype of all tuple types.
# * Foo[*tuple[Any, ...]] (normalized) instance is a subtype of all
# tuples with fallback to Foo (e.g. for variadic NamedTuples).
mapped = map_instance_to_supertype(left, right.partial_fallback.type)
for arg in map(get_proper_type, mapped.args):
if isinstance(arg, UnpackType):
unpacked = get_proper_type(arg.type)
if not isinstance(unpacked, Instance):
break
assert unpacked.type.fullname == "builtins.tuple"
if not isinstance(get_proper_type(unpacked.args[0]), AnyType):
break
elif not isinstance(arg, AnyType):
break
else:
return True
if is_erased_instance(mapped):
if (
mapped.type.fullname == "builtins.tuple"
or mapped.type.has_type_var_tuple_type
):
return True
return False
if isinstance(right, TypeVarTupleType):
# tuple[Any, ...] is like Any in the world of tuples (see special case above).
Expand Down Expand Up @@ -556,19 +552,8 @@ def visit_instance(self, left: Instance) -> bool:
right_args = (
right_prefix + (TupleType(list(right_middle), fallback),) + right_suffix
)
if not self.proper_subtype and t.args:
for arg in map(get_proper_type, t.args):
if isinstance(arg, UnpackType):
unpacked = get_proper_type(arg.type)
if not isinstance(unpacked, Instance):
break
assert unpacked.type.fullname == "builtins.tuple"
if not isinstance(get_proper_type(unpacked.args[0]), AnyType):
break
elif not isinstance(arg, AnyType):
break
else:
return True
if not self.proper_subtype and is_erased_instance(t):
return True
if len(left_args) != len(right_args):
return False
type_params = zip(left_args, right_args, right.type.defn.type_vars)
Expand Down Expand Up @@ -2151,3 +2136,20 @@ def erase_return_self_types(typ: Type, self_type: Instance) -> Type:
]
)
return typ


def is_erased_instance(t: Instance) -> bool:
"""Is this an instance where all args are Any types?"""
if not t.args:
return False
for arg in t.args:
if isinstance(arg, UnpackType):
unpacked = get_proper_type(arg.type)
if not isinstance(unpacked, Instance):
return False
assert unpacked.type.fullname == "builtins.tuple"
if not isinstance(get_proper_type(unpacked.args[0]), AnyType):
return False
elif not isinstance(get_proper_type(arg), AnyType):
return False
return True
5 changes: 4 additions & 1 deletion mypy/test/testsubtypes.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from mypy.subtypes import is_subtype
from mypy.test.helpers import Suite
from mypy.test.typefixture import InterfaceTypeFixture, TypeFixture
from mypy.types import Instance, Type, UninhabitedType, UnpackType
from mypy.types import Instance, TupleType, Type, UninhabitedType, UnpackType


class SubtypingSuite(Suite):
Expand Down Expand Up @@ -274,6 +274,9 @@ def test_type_var_tuple_unpacked_variable_length_tuple(self) -> None:
Instance(self.fx.gvi, [UnpackType(Instance(self.fx.std_tuplei, [self.fx.a]))]),
)

def test_fallback_not_subtype_of_tuple(self) -> None:
self.assert_not_subtype(self.fx.a, TupleType([self.fx.b], fallback=self.fx.a))

# IDEA: Maybe add these test cases (they are tested pretty well in type
# checker tests already):
# * more interface subtyping test cases
Expand Down