From 4d0bd6b134e4e76a8b364ee8747170fb376be1dd Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Wed, 12 Feb 2025 11:01:38 +0000 Subject: [PATCH 1/3] Fix instance vs tuple subtyping edge case --- mypy/subtypes.py | 57 ++++++++++++++++++++------------------- mypy/test/testsubtypes.py | 5 +++- 2 files changed, 34 insertions(+), 28 deletions(-) diff --git a/mypy/subtypes.py b/mypy/subtypes.py index 75cc7e25fde3..9e746b3e4c7c 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,21 @@ 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 map(get_proper_type, 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(arg, AnyType): + return False + else: + 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 From be5e608418290b299fbc1c97acd700cf41248daa Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Wed, 12 Feb 2025 19:31:34 +0000 Subject: [PATCH 2/3] Perf improvement --- mypy/subtypes.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mypy/subtypes.py b/mypy/subtypes.py index 9e746b3e4c7c..e118e7f837f5 100644 --- a/mypy/subtypes.py +++ b/mypy/subtypes.py @@ -2142,7 +2142,7 @@ 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 map(get_proper_type, t.args): + for arg in t.args: if isinstance(arg, UnpackType): unpacked = get_proper_type(arg.type) if not isinstance(unpacked, Instance): @@ -2150,7 +2150,7 @@ def is_erased_instance(t: Instance) -> bool: assert unpacked.type.fullname == "builtins.tuple" if not isinstance(get_proper_type(unpacked.args[0]), AnyType): return False - elif not isinstance(arg, AnyType): + elif not isinstance(get_proper_type(arg), AnyType): return False else: return True From 330f7afe4ef83a34004339c2b8eaf82444bd8e74 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Thu, 13 Feb 2025 17:18:50 +0000 Subject: [PATCH 3/3] Minor code simplification --- mypy/subtypes.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/mypy/subtypes.py b/mypy/subtypes.py index e118e7f837f5..8a4ddb87e5d0 100644 --- a/mypy/subtypes.py +++ b/mypy/subtypes.py @@ -2152,5 +2152,4 @@ def is_erased_instance(t: Instance) -> bool: return False elif not isinstance(get_proper_type(arg), AnyType): return False - else: - return True + return True