Skip to content

Commit 1ec3f44

Browse files
authored
Fix instance vs tuple subtyping edge case (#18664)
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).
1 parent 0d01f18 commit 1ec3f44

File tree

2 files changed

+33
-28
lines changed

2 files changed

+33
-28
lines changed

mypy/subtypes.py

Lines changed: 29 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -477,21 +477,17 @@ def visit_instance(self, left: Instance) -> bool:
477477
return self._is_subtype(left, unpacked)
478478
if left.type.has_base(right.partial_fallback.type.fullname):
479479
if not self.proper_subtype:
480-
# Special case to consider Foo[*tuple[Any, ...]] (i.e. bare Foo) a
481-
# subtype of Foo[<whatever>], when Foo is user defined variadic tuple type.
480+
# Special cases to consider:
481+
# * Plain tuple[Any, ...] instance is a subtype of all tuple types.
482+
# * Foo[*tuple[Any, ...]] (normalized) instance is a subtype of all
483+
# tuples with fallback to Foo (e.g. for variadic NamedTuples).
482484
mapped = map_instance_to_supertype(left, right.partial_fallback.type)
483-
for arg in map(get_proper_type, mapped.args):
484-
if isinstance(arg, UnpackType):
485-
unpacked = get_proper_type(arg.type)
486-
if not isinstance(unpacked, Instance):
487-
break
488-
assert unpacked.type.fullname == "builtins.tuple"
489-
if not isinstance(get_proper_type(unpacked.args[0]), AnyType):
490-
break
491-
elif not isinstance(arg, AnyType):
492-
break
493-
else:
494-
return True
485+
if is_erased_instance(mapped):
486+
if (
487+
mapped.type.fullname == "builtins.tuple"
488+
or mapped.type.has_type_var_tuple_type
489+
):
490+
return True
495491
return False
496492
if isinstance(right, TypeVarTupleType):
497493
# tuple[Any, ...] is like Any in the world of tuples (see special case above).
@@ -559,19 +555,8 @@ def visit_instance(self, left: Instance) -> bool:
559555
right_args = (
560556
right_prefix + (TupleType(list(right_middle), fallback),) + right_suffix
561557
)
562-
if not self.proper_subtype and t.args:
563-
for arg in map(get_proper_type, t.args):
564-
if isinstance(arg, UnpackType):
565-
unpacked = get_proper_type(arg.type)
566-
if not isinstance(unpacked, Instance):
567-
break
568-
assert unpacked.type.fullname == "builtins.tuple"
569-
if not isinstance(get_proper_type(unpacked.args[0]), AnyType):
570-
break
571-
elif not isinstance(arg, AnyType):
572-
break
573-
else:
574-
return True
558+
if not self.proper_subtype and is_erased_instance(t):
559+
return True
575560
if len(left_args) != len(right_args):
576561
return False
577562
type_params = zip(left_args, right_args, right.type.defn.type_vars)
@@ -2176,3 +2161,20 @@ def erase_return_self_types(typ: Type, self_type: Instance) -> Type:
21762161
]
21772162
)
21782163
return typ
2164+
2165+
2166+
def is_erased_instance(t: Instance) -> bool:
2167+
"""Is this an instance where all args are Any types?"""
2168+
if not t.args:
2169+
return False
2170+
for arg in t.args:
2171+
if isinstance(arg, UnpackType):
2172+
unpacked = get_proper_type(arg.type)
2173+
if not isinstance(unpacked, Instance):
2174+
return False
2175+
assert unpacked.type.fullname == "builtins.tuple"
2176+
if not isinstance(get_proper_type(unpacked.args[0]), AnyType):
2177+
return False
2178+
elif not isinstance(get_proper_type(arg), AnyType):
2179+
return False
2180+
return True

mypy/test/testsubtypes.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from mypy.subtypes import is_subtype
55
from mypy.test.helpers import Suite
66
from mypy.test.typefixture import InterfaceTypeFixture, TypeFixture
7-
from mypy.types import Instance, Type, UninhabitedType, UnpackType
7+
from mypy.types import Instance, TupleType, Type, UninhabitedType, UnpackType
88

99

1010
class SubtypingSuite(Suite):
@@ -274,6 +274,9 @@ def test_type_var_tuple_unpacked_variable_length_tuple(self) -> None:
274274
Instance(self.fx.gvi, [UnpackType(Instance(self.fx.std_tuplei, [self.fx.a]))]),
275275
)
276276

277+
def test_fallback_not_subtype_of_tuple(self) -> None:
278+
self.assert_not_subtype(self.fx.a, TupleType([self.fx.b], fallback=self.fx.a))
279+
277280
# IDEA: Maybe add these test cases (they are tested pretty well in type
278281
# checker tests already):
279282
# * more interface subtyping test cases

0 commit comments

Comments
 (0)