Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions mypy/subtypes.py
Original file line number Diff line number Diff line change
Expand Up @@ -2203,9 +2203,47 @@ 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 covers_via_declared_variance(t, s):
return UninhabitedType()
return t


def covers_via_declared_variance(item: Type, supertype: Type) -> bool:
"""Is item covered by supertype when generic arguments follow declared variance?"""
item = get_proper_type(item)
supertype = get_proper_type(supertype)
if not isinstance(item, Instance) or not isinstance(supertype, Instance):
return False
if not item.type.has_base(supertype.type.fullname):
return False

item = map_instance_to_supertype(item, supertype.type)
if len(item.args) != len(supertype.args):
return False

tried_infer = False
type_params = zip(item.args, supertype.args, supertype.type.defn.type_vars)
for item_arg, supertype_arg, tvar in type_params:
if isinstance(tvar, TypeVarType):
if tvar.variance == VARIANCE_NOT_READY and not tried_infer:
infer_class_variances(supertype.type)
tried_infer = True
variance = tvar.variance
else:
variance = COVARIANT

if variance == COVARIANT or variance == VARIANCE_NOT_READY:
if not is_subtype(item_arg, supertype_arg, ignore_promotions=True):
return False
elif variance == CONTRAVARIANT:
if not is_subtype(supertype_arg, item_arg, ignore_promotions=True):
return False
elif not is_same_type(item_arg, supertype_arg):
return False

return True


def covers_at_runtime(item: Type, supertype: Type) -> bool:
"""Will isinstance(item, supertype) always return True at runtime?"""
item = get_proper_type(item)
Expand Down
22 changes: 22 additions & 0 deletions test-data/unit/check-typeis.test
Original file line number Diff line number Diff line change
Expand Up @@ -541,6 +541,28 @@ def test(x: List[Any]) -> None:
g(reveal_type(x)) # N: Revealed type is "builtins.list[builtins.str] | __main__.<subclass of "builtins.list[Any]" and "__main__.A">"
[builtins fixtures/tuple.pyi]

[case testTypeIsNegativeNarrowCovariantGenericAny]
from typing_extensions import TypeIs
from typing import Any, Generic, TypeVar

T = TypeVar("T")
T_co = TypeVar("T_co", covariant=True)

class Box(Generic[T]): ...
class Source(Generic[T_co]): ...

def is_str_box(x: object) -> TypeIs[Box[str]]: ...
def is_source(x: object) -> TypeIs[Source[object]]: ...

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"
[builtins fixtures/tuple.pyi]

[case testTypeIsMultipleCondition]
from typing_extensions import TypeIs
from typing import Any, List
Expand Down
Loading