From 1ec3f447272b8719ed417277bb7ac771a5dae063 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Thu, 13 Feb 2025 18:22:00 +0000 Subject: [PATCH] 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 https://github.com/python/mypy/pull/18663 to avoid "regressions" (the other fix needed to avoid "regressions" is already merged). --- mypy/subtypes.py | 56 ++++++++++++++++++++------------------- mypy/test/testsubtypes.py | 5 +++- 2 files changed, 33 insertions(+), 28 deletions(-) diff --git a/mypy/subtypes.py b/mypy/subtypes.py index 938be21201e98..41bb4601e23fd 100644 --- a/mypy/subtypes.py +++ b/mypy/subtypes.py @@ -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[], 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). @@ -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) @@ -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 diff --git a/mypy/test/testsubtypes.py b/mypy/test/testsubtypes.py index 175074a2b140a..b75c22bca7f79 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