diff --git a/mypy/subtypes.py b/mypy/subtypes.py index 75cc7e25fde3..8a4ddb87e5d0 100644 --- a/mypy/subtypes.py +++ b/mypy/subtypes.py @@ -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[], 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). @@ -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) @@ -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 diff --git a/mypy/test/testsubtypes.py b/mypy/test/testsubtypes.py index 175074a2b140..b75c22bca7f7 100644 --- a/mypy/test/testsubtypes.py +++ b/mypy/test/testsubtypes.py @@ -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): @@ -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