From 8bebb702e1144f91430f6873a04c0b0666d0c9dd Mon Sep 17 00:00:00 2001 From: Jannic Warken Date: Fri, 10 May 2024 11:44:38 +0200 Subject: [PATCH 1/9] Fix TypeIs for types with type params in Unions --- mypy/checker.py | 31 +++++++++++++++++++++++++------ mypy/subtypes.py | 26 +++++++++++++++++--------- test-data/unit/check-typeis.test | 11 +++++++++++ 3 files changed, 53 insertions(+), 15 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 9c10cd2fc30d..0a77386fed8a 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -5787,6 +5787,7 @@ def find_isinstance_check_helper(self, node: Expression) -> tuple[TypeMap, TypeM self.lookup_type(expr), [TypeRange(node.callee.type_is, is_upper_bound=False)], expr, + ignore_type_params=False, ), ) elif isinstance(node, ComparisonExpr): @@ -7160,11 +7161,17 @@ def conditional_types_with_intersection( type_ranges: list[TypeRange] | None, ctx: Context, default: None = None, + ignore_type_params: bool = True, ) -> tuple[Type | None, Type | None]: ... @overload def conditional_types_with_intersection( - self, expr_type: Type, type_ranges: list[TypeRange] | None, ctx: Context, default: Type + self, + expr_type: Type, + type_ranges: list[TypeRange] | None, + ctx: Context, + default: Type, + ignore_type_params: bool = True, ) -> tuple[Type, Type]: ... def conditional_types_with_intersection( @@ -7173,8 +7180,9 @@ def conditional_types_with_intersection( type_ranges: list[TypeRange] | None, ctx: Context, default: Type | None = None, + ignore_type_params: bool = True, ) -> tuple[Type | None, Type | None]: - initial_types = conditional_types(expr_type, type_ranges, default) + initial_types = conditional_types(expr_type, type_ranges, default, ignore_type_params) # For some reason, doing "yes_map, no_map = conditional_types_to_typemaps(...)" # doesn't work: mypyc will decide that 'yes_map' is of type None if we try. yes_type: Type | None = initial_types[0] @@ -7422,18 +7430,27 @@ def visit_type_var(self, t: TypeVarType) -> None: @overload def conditional_types( - current_type: Type, proposed_type_ranges: list[TypeRange] | None, default: None = None + current_type: Type, + proposed_type_ranges: list[TypeRange] | None, + default: None = None, + ignore_type_params: bool = True, ) -> tuple[Type | None, Type | None]: ... @overload def conditional_types( - current_type: Type, proposed_type_ranges: list[TypeRange] | None, default: Type + current_type: Type, + proposed_type_ranges: list[TypeRange] | None, + default: Type, + ignore_type_params: bool = True, ) -> tuple[Type, Type]: ... def conditional_types( - current_type: Type, proposed_type_ranges: list[TypeRange] | None, default: Type | None = None + current_type: Type, + proposed_type_ranges: list[TypeRange] | None, + default: Type | None = None, + ignore_type_params: bool = True, ) -> tuple[Type | None, Type | None]: """Takes in the current type and a proposed type of an expression. @@ -7477,7 +7494,9 @@ def conditional_types( if not type_range.is_upper_bound ] ) - remaining_type = restrict_subtype_away(current_type, proposed_precise_type) + remaining_type = restrict_subtype_away( + current_type, proposed_precise_type, ignore_type_params=ignore_type_params + ) return proposed_type, remaining_type else: # An isinstance check, but we don't understand the type diff --git a/mypy/subtypes.py b/mypy/subtypes.py index 4d5e7335b14f..7130a9187a60 100644 --- a/mypy/subtypes.py +++ b/mypy/subtypes.py @@ -1910,37 +1910,45 @@ def try_restrict_literal_union(t: UnionType, s: Type) -> list[Type] | None: return new_items -def restrict_subtype_away(t: Type, s: Type) -> Type: - """Return t minus s for runtime type assertions. +def restrict_subtype_away(t: Type, s: Type, ignore_type_params: bool = True) -> Type: + """Return t minus s for runtime type assertions and TypeIs[]. If we can't determine a precise result, return a supertype of the ideal result (just t is a valid result). This is used for type inference of runtime type checks such as - isinstance(). Currently, this just removes elements of a union type. + isinstance() or TypeIs[]. Currently, this just removes elements + of a union type. """ p_t = get_proper_type(t) if isinstance(p_t, UnionType): new_items = try_restrict_literal_union(p_t, s) if new_items is None: new_items = [ - restrict_subtype_away(item, s) + restrict_subtype_away(item, s, ignore_type_params=ignore_type_params) for item in p_t.relevant_items() - if (isinstance(get_proper_type(item), AnyType) or not covers_at_runtime(item, s)) + if ( + isinstance(get_proper_type(item), AnyType) + or not covers_type(item, s, ignore_type_params) + ) ] return UnionType.make_union(new_items) - elif covers_at_runtime(t, s): + elif covers_type(t, s, ignore_type_params): return UninhabitedType() else: return t -def covers_at_runtime(item: Type, supertype: Type) -> bool: - """Will isinstance(item, supertype) always return True at runtime?""" +def covers_type(item: Type, supertype: Type, ignore_type_params: bool = True) -> bool: + """Checks if item is covered by supertype.""" item = get_proper_type(item) supertype = get_proper_type(supertype) - # Since runtime type checks will ignore type arguments, erase the types. + if not ignore_type_params: + return is_proper_subtype(item, supertype, ignore_promotions=True) + + # The following code is used for isinstance(), where ignore the type + # params is important (since this happens at runtime) supertype = erase_type(supertype) if is_proper_subtype( erase_type(item), supertype, ignore_promotions=True, erase_instances=True diff --git a/test-data/unit/check-typeis.test b/test-data/unit/check-typeis.test index 83467d5e3683..71656e54496b 100644 --- a/test-data/unit/check-typeis.test +++ b/test-data/unit/check-typeis.test @@ -136,6 +136,17 @@ def main(a: object) -> None: reveal_type(a) # N: Revealed type is "Union[builtins.int, builtins.str]" [builtins fixtures/tuple.pyi] +[case testTypeIsUnionWithTypeParams] +from typing_extensions import TypeIs +from typing import Iterable, List, Union +def is_iterable_int(val: object) -> TypeIs[Iterable[int]]: pass +def main(a: Union[List[int], List[str]]) -> None: + if is_iterable_int(a): + reveal_type(a) # N: Revealed type is "builtins.list[builtins.int]" + else: + reveal_type(a) # N: Revealed type is "builtins.list[builtins.str]" +[builtins fixtures/tuple.pyi] + [case testTypeIsNonzeroFloat] from typing_extensions import TypeIs def is_nonzero(a: object) -> TypeIs[float]: pass From 2aef4f3954a4319d92fa52f66aa1a5563be372da Mon Sep 17 00:00:00 2001 From: Jannic Warken Date: Sat, 11 May 2024 19:35:35 +0200 Subject: [PATCH 2/9] Fix issues related to primer output --- mypy/checker.py | 31 ++++++------------------------- mypy/subtypes.py | 31 +++++++++++-------------------- test-data/unit/check-typeis.test | 12 ++++++++++++ 3 files changed, 29 insertions(+), 45 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 0a77386fed8a..9c10cd2fc30d 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -5787,7 +5787,6 @@ def find_isinstance_check_helper(self, node: Expression) -> tuple[TypeMap, TypeM self.lookup_type(expr), [TypeRange(node.callee.type_is, is_upper_bound=False)], expr, - ignore_type_params=False, ), ) elif isinstance(node, ComparisonExpr): @@ -7161,17 +7160,11 @@ def conditional_types_with_intersection( type_ranges: list[TypeRange] | None, ctx: Context, default: None = None, - ignore_type_params: bool = True, ) -> tuple[Type | None, Type | None]: ... @overload def conditional_types_with_intersection( - self, - expr_type: Type, - type_ranges: list[TypeRange] | None, - ctx: Context, - default: Type, - ignore_type_params: bool = True, + self, expr_type: Type, type_ranges: list[TypeRange] | None, ctx: Context, default: Type ) -> tuple[Type, Type]: ... def conditional_types_with_intersection( @@ -7180,9 +7173,8 @@ def conditional_types_with_intersection( type_ranges: list[TypeRange] | None, ctx: Context, default: Type | None = None, - ignore_type_params: bool = True, ) -> tuple[Type | None, Type | None]: - initial_types = conditional_types(expr_type, type_ranges, default, ignore_type_params) + initial_types = conditional_types(expr_type, type_ranges, default) # For some reason, doing "yes_map, no_map = conditional_types_to_typemaps(...)" # doesn't work: mypyc will decide that 'yes_map' is of type None if we try. yes_type: Type | None = initial_types[0] @@ -7430,27 +7422,18 @@ def visit_type_var(self, t: TypeVarType) -> None: @overload def conditional_types( - current_type: Type, - proposed_type_ranges: list[TypeRange] | None, - default: None = None, - ignore_type_params: bool = True, + current_type: Type, proposed_type_ranges: list[TypeRange] | None, default: None = None ) -> tuple[Type | None, Type | None]: ... @overload def conditional_types( - current_type: Type, - proposed_type_ranges: list[TypeRange] | None, - default: Type, - ignore_type_params: bool = True, + current_type: Type, proposed_type_ranges: list[TypeRange] | None, default: Type ) -> tuple[Type, Type]: ... def conditional_types( - current_type: Type, - proposed_type_ranges: list[TypeRange] | None, - default: Type | None = None, - ignore_type_params: bool = True, + current_type: Type, proposed_type_ranges: list[TypeRange] | None, default: Type | None = None ) -> tuple[Type | None, Type | None]: """Takes in the current type and a proposed type of an expression. @@ -7494,9 +7477,7 @@ def conditional_types( if not type_range.is_upper_bound ] ) - remaining_type = restrict_subtype_away( - current_type, proposed_precise_type, ignore_type_params=ignore_type_params - ) + remaining_type = restrict_subtype_away(current_type, proposed_precise_type) return proposed_type, remaining_type else: # An isinstance check, but we don't understand the type diff --git a/mypy/subtypes.py b/mypy/subtypes.py index 7130a9187a60..51b09432499d 100644 --- a/mypy/subtypes.py +++ b/mypy/subtypes.py @@ -1910,49 +1910,40 @@ def try_restrict_literal_union(t: UnionType, s: Type) -> list[Type] | None: return new_items -def restrict_subtype_away(t: Type, s: Type, ignore_type_params: bool = True) -> Type: - """Return t minus s for runtime type assertions and TypeIs[]. +def restrict_subtype_away(t: Type, s: Type) -> Type: + """Return t minus s for runtime type assertions. If we can't determine a precise result, return a supertype of the ideal result (just t is a valid result). This is used for type inference of runtime type checks such as - isinstance() or TypeIs[]. Currently, this just removes elements - of a union type. + isinstance(). Currently, this just removes elements of a union type. """ p_t = get_proper_type(t) if isinstance(p_t, UnionType): new_items = try_restrict_literal_union(p_t, s) if new_items is None: new_items = [ - restrict_subtype_away(item, s, ignore_type_params=ignore_type_params) + restrict_subtype_away(item, s) for item in p_t.relevant_items() - if ( - isinstance(get_proper_type(item), AnyType) - or not covers_type(item, s, ignore_type_params) - ) + if not covers_at_runtime(item, s) ] return UnionType.make_union(new_items) - elif covers_type(t, s, ignore_type_params): + elif covers_at_runtime(t, s): return UninhabitedType() else: return t -def covers_type(item: Type, supertype: Type, ignore_type_params: bool = True) -> bool: - """Checks if item is covered by supertype.""" +def covers_at_runtime(item: Type, supertype: Type) -> bool: + """Will isinstance(item, supertype) always return True at runtime?""" item = get_proper_type(item) supertype = get_proper_type(supertype) - if not ignore_type_params: - return is_proper_subtype(item, supertype, ignore_promotions=True) + if isinstance(item, AnyType): + return False - # The following code is used for isinstance(), where ignore the type - # params is important (since this happens at runtime) - supertype = erase_type(supertype) - if is_proper_subtype( - erase_type(item), supertype, ignore_promotions=True, erase_instances=True - ): + if is_subtype(item, supertype, ignore_promotions=True): return True if isinstance(supertype, Instance): if supertype.type.is_protocol: diff --git a/test-data/unit/check-typeis.test b/test-data/unit/check-typeis.test index 71656e54496b..7f02cb2cb67a 100644 --- a/test-data/unit/check-typeis.test +++ b/test-data/unit/check-typeis.test @@ -177,6 +177,18 @@ class C: def is_float(self, a: object) -> TypeIs[float]: pass [builtins fixtures/tuple.pyi] +[case testTypeIsAwaitableAny] +from typing_extensions import TypeIs +from typing import Any, Awaitable, TypeVar, Union +T = TypeVar('T') +def is_awaitable(val: object) -> TypeIs[Awaitable[Any]]: pass +def main(a: Union[Awaitable[T], T]) -> None: + if is_awaitable(a): + reveal_type(a) # N: Revealed type is "Union[typing.Awaitable[T`-1], typing.Awaitable[Any]]" + else: + reveal_type(a) # N: Revealed type is "T`-1" +[builtins fixtures/tuple.pyi] + [case testTypeIsCrossModule] import guard from points import Point From 5c7fe481c38f47c507c1ba34046491a9ad1cad35 Mon Sep 17 00:00:00 2001 From: Jannic Warken Date: Tue, 14 May 2024 16:40:24 +0200 Subject: [PATCH 3/9] Revert changes and only keep removal of type erase --- mypy/subtypes.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/mypy/subtypes.py b/mypy/subtypes.py index 51b09432499d..81132e089b89 100644 --- a/mypy/subtypes.py +++ b/mypy/subtypes.py @@ -1917,7 +1917,8 @@ def restrict_subtype_away(t: Type, s: Type) -> Type: ideal result (just t is a valid result). This is used for type inference of runtime type checks such as - isinstance(). Currently, this just removes elements of a union type. + isinstance() or TypeIs. Currently, this just removes elements + of a union type. """ p_t = get_proper_type(t) if isinstance(p_t, UnionType): @@ -1926,24 +1927,21 @@ def restrict_subtype_away(t: Type, s: Type) -> Type: new_items = [ restrict_subtype_away(item, s) for item in p_t.relevant_items() - if not covers_at_runtime(item, s) + if (isinstance(get_proper_type(item), AnyType) or not covers_type(item, s)) ] return UnionType.make_union(new_items) - elif covers_at_runtime(t, s): + elif covers_type(t, s): return UninhabitedType() else: return t -def covers_at_runtime(item: Type, supertype: Type) -> bool: - """Will isinstance(item, supertype) always return True at runtime?""" +def covers_type(item: Type, supertype: Type) -> bool: + """Returns if item is covered by supertype.""" item = get_proper_type(item) supertype = get_proper_type(supertype) - if isinstance(item, AnyType): - return False - - if is_subtype(item, supertype, ignore_promotions=True): + if is_proper_subtype(item, supertype, ignore_promotions=True, erase_instances=True): return True if isinstance(supertype, Instance): if supertype.type.is_protocol: From 4ee4771ae1f86460e05cb22642a71f4d4c98bc0e Mon Sep 17 00:00:00 2001 From: Jannic Warken Date: Tue, 21 May 2024 08:38:39 +0200 Subject: [PATCH 4/9] Special handling fallback_to_any types --- mypy/subtypes.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/mypy/subtypes.py b/mypy/subtypes.py index 81132e089b89..d11b2e17064d 100644 --- a/mypy/subtypes.py +++ b/mypy/subtypes.py @@ -1927,7 +1927,7 @@ def restrict_subtype_away(t: Type, s: Type) -> Type: new_items = [ restrict_subtype_away(item, s) for item in p_t.relevant_items() - if (isinstance(get_proper_type(item), AnyType) or not covers_type(item, s)) + if not covers_type(item, s) ] return UnionType.make_union(new_items) elif covers_type(t, s): @@ -1941,7 +1941,10 @@ def covers_type(item: Type, supertype: Type) -> bool: item = get_proper_type(item) supertype = get_proper_type(supertype) - if is_proper_subtype(item, supertype, ignore_promotions=True, erase_instances=True): + if isinstance(item, AnyType) or (isinstance(item, Instance) and item.type.fallback_to_any): + return False + + if is_subtype(item, supertype, ignore_promotions=True): return True if isinstance(supertype, Instance): if supertype.type.is_protocol: From 528410c8c392144eb698b11b0dd50247526bdc37 Mon Sep 17 00:00:00 2001 From: Jannic Warken Date: Tue, 21 May 2024 18:00:08 +0200 Subject: [PATCH 5/9] Fix UnionType recursion --- mypy/subtypes.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/mypy/subtypes.py b/mypy/subtypes.py index d11b2e17064d..e13f44830096 100644 --- a/mypy/subtypes.py +++ b/mypy/subtypes.py @@ -1941,9 +1941,18 @@ def covers_type(item: Type, supertype: Type) -> bool: item = get_proper_type(item) supertype = get_proper_type(supertype) - if isinstance(item, AnyType) or (isinstance(item, Instance) and item.type.fallback_to_any): + if isinstance(item, UnionType): return False + if isinstance(item, AnyType) or isinstance(supertype, AnyType): + return False + + if isinstance(item, Instance) and item.type.fallback_to_any: + return is_equivalent(item, supertype) + + if isinstance(supertype, Instance) and supertype.type.fallback_to_any: + return is_equivalent(item, supertype) + if is_subtype(item, supertype, ignore_promotions=True): return True if isinstance(supertype, Instance): From 44449cc66e6c66e1d89c45f2b0ea79f0276cb2dd Mon Sep 17 00:00:00 2001 From: Jannic Warken Date: Fri, 24 May 2024 16:37:15 +0200 Subject: [PATCH 6/9] Make use of is_same_type --- mypy/subtypes.py | 32 ++++++++++++++++++++++++-------- test-data/unit/check-typeis.test | 12 ++++++++++++ 2 files changed, 36 insertions(+), 8 deletions(-) diff --git a/mypy/subtypes.py b/mypy/subtypes.py index e13f44830096..0e8d081b6f6f 100644 --- a/mypy/subtypes.py +++ b/mypy/subtypes.py @@ -1927,7 +1927,7 @@ def restrict_subtype_away(t: Type, s: Type) -> Type: new_items = [ restrict_subtype_away(item, s) for item in p_t.relevant_items() - if not covers_type(item, s) + if isinstance(item, UnionType) or not covers_type(item, s) ] return UnionType.make_union(new_items) elif covers_type(t, s): @@ -1937,24 +1937,40 @@ def restrict_subtype_away(t: Type, s: Type) -> Type: def covers_type(item: Type, supertype: Type) -> bool: - """Returns if item is covered by supertype.""" + """Returns if item is covered by supertype. + + Assumes that item is not a Union type. + Any types (or fallbacks to any) should never cover or be covered. + Examples: + + int covered by int + List[int] covered by List[Any] + A covered by Union[A, Any] + Any NOT covered by int + int NOT covered by Any + """ item = get_proper_type(item) supertype = get_proper_type(supertype) if isinstance(item, UnionType): return False - if isinstance(item, AnyType) or isinstance(supertype, AnyType): + # Handle possible Any types that should not be covered: + if isinstance(item, AnyType): return False + if isinstance(supertype, AnyType): + return False + if (isinstance(item, Instance) and item.type.fallback_to_any) or ( + isinstance(supertype, Instance) and supertype.type.fallback_to_any + ): + return is_same_type(item, supertype) - if isinstance(item, Instance) and item.type.fallback_to_any: - return is_equivalent(item, supertype) - - if isinstance(supertype, Instance) and supertype.type.fallback_to_any: - return is_equivalent(item, supertype) + if isinstance(supertype, UnionType): + return any(covers_type(item, t) for t in supertype.items) if is_subtype(item, supertype, ignore_promotions=True): return True + if isinstance(supertype, Instance): if supertype.type.is_protocol: # TODO: Implement more robust support for runtime isinstance() checks, see issue #3827. diff --git a/test-data/unit/check-typeis.test b/test-data/unit/check-typeis.test index 7f02cb2cb67a..c748b345c09a 100644 --- a/test-data/unit/check-typeis.test +++ b/test-data/unit/check-typeis.test @@ -177,6 +177,18 @@ class C: def is_float(self, a: object) -> TypeIs[float]: pass [builtins fixtures/tuple.pyi] +[case testTypeIsTypeAny] +from typing_extensions import TypeIs +from typing import Any, Type, Union +class A: ... +def is_class(x: object) -> TypeIs[Type[Any]]: ... +def main(a: Union[A, Type[A]]) -> None: + if is_class(a): + reveal_type(a) # N: Revealed type is "Type[Any]" + else: + reveal_type(a) # N: Revealed type is "__main__.A" +[builtins fixtures/tuple.pyi] + [case testTypeIsAwaitableAny] from typing_extensions import TypeIs from typing import Any, Awaitable, TypeVar, Union From 431d722a745ae470f5b324a8506b5d0cc2d39d7d Mon Sep 17 00:00:00 2001 From: Jannic Warken Date: Fri, 24 May 2024 18:16:44 +0200 Subject: [PATCH 7/9] Fix self type check --- mypy/subtypes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy/subtypes.py b/mypy/subtypes.py index 0e8d081b6f6f..8117427d3e3e 100644 --- a/mypy/subtypes.py +++ b/mypy/subtypes.py @@ -1927,7 +1927,7 @@ def restrict_subtype_away(t: Type, s: Type) -> Type: new_items = [ restrict_subtype_away(item, s) for item in p_t.relevant_items() - if isinstance(item, UnionType) or not covers_type(item, s) + if isinstance(get_proper_type(item), UnionType) or not covers_type(item, s) ] return UnionType.make_union(new_items) elif covers_type(t, s): From 81dae26a219c1ca4f99f90c73c6ebd760cc76b6f Mon Sep 17 00:00:00 2001 From: Jannic Warken Date: Sat, 25 May 2024 16:46:20 +0200 Subject: [PATCH 8/9] Code clean-up --- mypy/subtypes.py | 66 +++++++++++++++++------------------------------- 1 file changed, 23 insertions(+), 43 deletions(-) diff --git a/mypy/subtypes.py b/mypy/subtypes.py index aee671f1e61b..cd9834cfb48b 100644 --- a/mypy/subtypes.py +++ b/mypy/subtypes.py @@ -90,7 +90,6 @@ def __init__( ignore_promotions: bool = False, ignore_uninhabited: bool = False, # Proper subtype flags - erase_instances: bool = False, keep_erased_types: bool = False, options: Options | None = None, ) -> None: @@ -99,7 +98,6 @@ def __init__( self.ignore_declared_variance = ignore_declared_variance self.ignore_promotions = ignore_promotions self.ignore_uninhabited = ignore_uninhabited - self.erase_instances = erase_instances self.keep_erased_types = keep_erased_types self.options = options @@ -109,7 +107,7 @@ def check_context(self, proper_subtype: bool) -> None: if proper_subtype: assert not self.ignore_pos_arg_names and not self.ignore_declared_variance else: - assert not self.erase_instances and not self.keep_erased_types + assert not self.keep_erased_types def is_subtype( @@ -187,7 +185,6 @@ def is_proper_subtype( subtype_context: SubtypeContext | None = None, ignore_promotions: bool = False, ignore_uninhabited: bool = False, - erase_instances: bool = False, keep_erased_types: bool = False, ) -> bool: """Is left a proper subtype of right? @@ -195,26 +192,18 @@ def is_proper_subtype( For proper subtypes, there's no need to rely on compatibility due to Any types. Every usable type is a proper subtype of itself. - If erase_instances is True, erase left instance *after* mapping it to supertype - (this is useful for runtime isinstance() checks). If keep_erased_types is True, - do not consider ErasedType a subtype of all types (used by type inference against unions). + If keep_erased_types is True, do not consider ErasedType a subtype + of all types (used by type inference against unions). """ if subtype_context is None: subtype_context = SubtypeContext( ignore_promotions=ignore_promotions, ignore_uninhabited=ignore_uninhabited, - erase_instances=erase_instances, keep_erased_types=keep_erased_types, ) else: assert not any( - { - ignore_promotions, - ignore_uninhabited, - erase_instances, - keep_erased_types, - ignore_uninhabited, - } + {ignore_promotions, ignore_uninhabited, keep_erased_types, ignore_uninhabited} ), "Don't pass both context and individual flags" if type_state.is_assumed_proper_subtype(left, right): return True @@ -405,7 +394,6 @@ def build_subtype_kind(subtype_context: SubtypeContext, proper_subtype: bool) -> subtype_context.ignore_pos_arg_names, subtype_context.ignore_declared_variance, subtype_context.ignore_promotions, - subtype_context.erase_instances, subtype_context.keep_erased_types, ) @@ -533,10 +521,6 @@ def visit_instance(self, left: Instance) -> bool: ) and not self.subtype_context.ignore_declared_variance: # Map left type to corresponding right instances. t = map_instance_to_supertype(left, right.type) - if self.subtype_context.erase_instances: - erased = erase_type(t) - assert isinstance(erased, Instance) - t = erased nominal = True if right.type.has_type_var_tuple_type: # For variadic instances we simply find the correct type argument mappings, @@ -1947,56 +1931,52 @@ def restrict_subtype_away(t: Type, s: Type) -> Type: def covers_type(item: Type, supertype: Type) -> bool: """Returns if item is covered by supertype. - Assumes that item is not a Union type. Any types (or fallbacks to any) should never cover or be covered. - Examples: - int covered by int - List[int] covered by List[Any] - A covered by Union[A, Any] - Any NOT covered by int - int NOT covered by Any + Assumes that item is not a Union type. + + Examples: + int covered by int + List[int] covered by List[Any] + A covered by Union[A, Any] + Any NOT covered by int + int NOT covered by Any """ item = get_proper_type(item) supertype = get_proper_type(supertype) - if isinstance(item, UnionType): - return False + assert not isinstance(item, UnionType) # Handle possible Any types that should not be covered: - if isinstance(item, AnyType): - return False - if isinstance(supertype, AnyType): + if isinstance(item, AnyType) or isinstance(supertype, AnyType): return False - if (isinstance(item, Instance) and item.type.fallback_to_any) or ( + elif (isinstance(item, Instance) and item.type.fallback_to_any) or ( isinstance(supertype, Instance) and supertype.type.fallback_to_any ): return is_same_type(item, supertype) if isinstance(supertype, UnionType): - return any(covers_type(item, t) for t in supertype.items) - - if is_subtype(item, supertype, ignore_promotions=True): - return True - - if isinstance(supertype, Instance): + # Special case that cannot be handled by is_subtype, because it would + # not ignore the Any types: + return any(covers_type(item, t) for t in supertype.relevant_items()) + elif isinstance(supertype, Instance): if supertype.type.is_protocol: # TODO: Implement more robust support for runtime isinstance() checks, see issue #3827. - if is_proper_subtype(item, supertype, ignore_promotions=True): + if is_proper_subtype(item, erase_type(supertype), ignore_promotions=True): return True if isinstance(item, TypedDictType): # Special case useful for selecting TypedDicts from unions using isinstance(x, dict). if supertype.type.fullname == "builtins.dict": return True elif isinstance(item, TypeVarType): - if is_proper_subtype(item.upper_bound, supertype, ignore_promotions=True): + if is_proper_subtype(item.upper_bound, erase_type(supertype), ignore_promotions=True): return True elif isinstance(item, Instance) and supertype.type.fullname == "builtins.int": # "int" covers all native int types if item.type.fullname in MYPYC_NATIVE_INT_NAMES: return True - # TODO: Add more special cases. - return False + + return is_subtype(item, supertype, ignore_promotions=True) def is_more_precise(left: Type, right: Type, *, ignore_promotions: bool = False) -> bool: From c9692c56afd182ac42ea75f1dd8932e30cc720fc Mon Sep 17 00:00:00 2001 From: Jannic Warken Date: Sun, 26 May 2024 14:46:10 +0200 Subject: [PATCH 9/9] Add other test case to document current behaviour --- test-data/unit/check-isinstance.test | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/test-data/unit/check-isinstance.test b/test-data/unit/check-isinstance.test index b7ee38b69d00..ca40f22d283c 100644 --- a/test-data/unit/check-isinstance.test +++ b/test-data/unit/check-isinstance.test @@ -2137,6 +2137,23 @@ else: reveal_type(z) # N: Revealed type is "Any" [builtins fixtures/isinstance.pyi] +[case testIsinstanceSubclassAny] +from typing import Any, Union +X: Any +class BadParent(X): pass +class GoodParent(object): pass +a: Union[GoodParent, BadParent] +if isinstance(a, BadParent): + reveal_type(a) # N: Revealed type is "__main__.BadParent" +else: + reveal_type(a) # N: Revealed type is "Union[__main__.GoodParent, __main__.BadParent]" +b: Union[int, BadParent] +if isinstance(b, (X, GoodParent)): + reveal_type(b) # N: Revealed type is "Union[Any, __main__.BadParent]" +else: + reveal_type(b) # N: Revealed type is "Union[builtins.int, __main__.BadParent]" +[builtins fixtures/isinstance.pyi] + [case testIsInstanceInitialNoneCheckSkipsImpossibleCasesNoStrictOptional] from typing import Optional, Union