diff --git a/mypy/subtypes.py b/mypy/subtypes.py index 259bb3791dea..fbbae90b1f14 100644 --- a/mypy/subtypes.py +++ b/mypy/subtypes.py @@ -101,6 +101,9 @@ def __init__( # Proper subtype flags erase_instances: bool = False, keep_erased_types: bool = False, + # Use ordinary compatibility for variant type arguments while preserving + # exact/proper checks for invariant arguments. + allow_any_in_variant_positions: bool = False, options: Options | None = None, ) -> None: self.ignore_type_params = ignore_type_params @@ -110,15 +113,30 @@ def __init__( self.ignore_promotions = ignore_promotions self.erase_instances = erase_instances self.keep_erased_types = keep_erased_types + self.allow_any_in_variant_positions = allow_any_in_variant_positions self.options = options + def non_proper_context(self) -> SubtypeContext: + return SubtypeContext( + ignore_type_params=self.ignore_type_params, + ignore_pos_arg_names=self.ignore_pos_arg_names, + ignore_declared_variance=self.ignore_declared_variance, + always_covariant=self.always_covariant, + ignore_promotions=self.ignore_promotions, + options=self.options, + ) + def check_context(self, proper_subtype: bool) -> None: # Historically proper and non-proper subtypes were defined using different helpers # and different visitors. Check if flag values are such that we definitely support. 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.erase_instances + and not self.keep_erased_types + and not self.allow_any_in_variant_positions + ) def is_subtype( @@ -197,6 +215,7 @@ def is_proper_subtype( ignore_promotions: bool = False, erase_instances: bool = False, keep_erased_types: bool = False, + allow_any_in_variant_positions: bool = False, ) -> bool: """Is left a proper subtype of right? @@ -206,6 +225,8 @@ def is_proper_subtype( 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 allow_any_in_variant_positions is True, variant type arguments use ordinary subtype + compatibility while invariant arguments still use proper/exact checks. """ if left == right: return True @@ -214,10 +235,14 @@ def is_proper_subtype( ignore_promotions=ignore_promotions, erase_instances=erase_instances, keep_erased_types=keep_erased_types, + allow_any_in_variant_positions=allow_any_in_variant_positions, ) else: assert ( - not ignore_promotions and not erase_instances and not keep_erased_types + not ignore_promotions + and not erase_instances + and not keep_erased_types + and not allow_any_in_variant_positions ), "Don't pass both context and individual flags" if type_state.is_assumed_proper_subtype(left, right): return True @@ -390,11 +415,19 @@ def check_type_parameter( # avoid these cases altogether. if variance == COVARIANT or variance == VARIANCE_NOT_READY: if proper_subtype: + if subtype_context.allow_any_in_variant_positions: + return is_subtype( + left, right, subtype_context=subtype_context.non_proper_context() + ) return is_proper_subtype(left, right, subtype_context=subtype_context) else: return is_subtype(left, right, subtype_context=subtype_context) elif variance == CONTRAVARIANT: if proper_subtype: + if subtype_context.allow_any_in_variant_positions: + return is_subtype( + right, left, subtype_context=subtype_context.non_proper_context() + ) return is_proper_subtype(right, left, subtype_context=subtype_context) else: return is_subtype(right, left, subtype_context=subtype_context) @@ -440,6 +473,7 @@ def build_subtype_kind(subtype_context: SubtypeContext, proper_subtype: bool) -> subtype_context.ignore_promotions, subtype_context.erase_instances, subtype_context.keep_erased_types, + subtype_context.allow_any_in_variant_positions, ) def _is_subtype(self, left: Type, right: Type) -> bool: @@ -2203,6 +2237,8 @@ def restrict_subtype_away(t: Type, s: Type, *, consider_runtime_isinstance: bool return UninhabitedType() if is_proper_subtype(t, s, ignore_promotions=True, erase_instances=True): return UninhabitedType() + if is_proper_subtype(t, s, ignore_promotions=True, allow_any_in_variant_positions=True): + return UninhabitedType() return t diff --git a/test-data/unit/check-typeis.test b/test-data/unit/check-typeis.test index 65ee837452f5..9cea4df0522d 100644 --- a/test-data/unit/check-typeis.test +++ b/test-data/unit/check-typeis.test @@ -541,6 +541,35 @@ def test(x: List[Any]) -> None: g(reveal_type(x)) # N: Revealed type is "builtins.list[builtins.str] | __main__." [builtins fixtures/tuple.pyi] +[case testTypeIsNegativeNarrowCovariantGenericAny] +from typing_extensions import Never, TypeIs +from typing import Any, Generic, TypeVar + +T = TypeVar("T") +T_co = TypeVar("T_co", covariant=True) +T_contra = TypeVar("T_contra", contravariant=True) + +class Box(Generic[T]): ... +class Source(Generic[T_co]): ... +class Sink(Generic[T_contra]): ... + +def is_str_box(x: object) -> TypeIs[Box[str]]: ... +def is_source(x: object) -> TypeIs[Source[object]]: ... +def is_sink(x: object) -> TypeIs[Sink[Never]]: ... + +def keep_invariant(x: Box[Any]) -> None: + assert not is_str_box(x) + reveal_type(x) # N: Revealed type is "__main__.Box[Any]" + +def narrow_covariant(x: int | Source[Any]) -> None: + assert not is_source(x) + reveal_type(x) # N: Revealed type is "builtins.int" + +def narrow_contravariant(x: int | Sink[Any]) -> None: + assert not is_sink(x) + reveal_type(x) # N: Revealed type is "builtins.int" +[builtins fixtures/tuple.pyi] + [case testTypeIsMultipleCondition] from typing_extensions import TypeIs from typing import Any, List