Skip to content

Commit

Permalink
Fix instance vs tuple subtyping edge case (#18664)
Browse files Browse the repository at this point in the history
Previously a code path was introduced that made fallback a subtype of
its tuple type for non-generic tuples, while the intention was to cover
`tuple[Any, ...]` and similar. I add a unit test + some refactoring to
make this mistake much harder in future.

This may need to wait for #18663 to
avoid "regressions" (the other fix needed to avoid "regressions" is
already merged).
  • Loading branch information
ilevkivskyi authored Feb 13, 2025
1 parent 0d01f18 commit 1ec3f44
Show file tree
Hide file tree
Showing 2 changed files with 33 additions and 28 deletions.
56 changes: 29 additions & 27 deletions mypy/subtypes.py
Original file line number Diff line number Diff line change
Expand Up @@ -477,21 +477,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 @@ -559,19 +555,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 @@ -2176,3 +2161,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

0 comments on commit 1ec3f44

Please sign in to comment.