[ty] Rework disjointness of protocol instances vs types with possibly unbound attributes (#19043)

This commit is contained in:
Alex Waygood 2025-07-01 12:47:27 +01:00 committed by GitHub
parent c6fd11fe36
commit ebf59e2bef
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 207 additions and 52 deletions

View file

@ -964,6 +964,121 @@ def _(arg1: Intersection[HasX, NotFinalNominal], arg2: Intersection[HasX, FinalN
reveal_type(arg2) # revealed: Never
```
The disjointness of a single protocol member with the type of an attribute on another type is enough
to make the whole protocol disjoint from the other type, even if all other members on the protocol
are satisfied by the other type. This applies to both `@final` types and non-final types:
```py
class Proto(Protocol):
x: int
y: str
z: bytes
class Foo:
x: int
y: str
z: None
static_assert(is_disjoint_from(Proto, Foo))
@final
class FinalFoo:
x: int
y: str
z: None
static_assert(is_disjoint_from(Proto, FinalFoo))
```
## Intersections of protocols with types that have possibly unbound attributes
Note that if a `@final` class has a possibly unbound attribute corresponding to the protocol member,
instance types and class-literal types referring to that class cannot be a subtype of the protocol
but will also not be disjoint from the protocol:
`a.py`:
```py
from typing import final, ClassVar, Protocol
from ty_extensions import TypeOf, static_assert, is_subtype_of, is_disjoint_from, is_assignable_to
def who_knows() -> bool:
return False
@final
class Foo:
if who_knows():
x: ClassVar[int] = 42
class HasReadOnlyX(Protocol):
@property
def x(self) -> int: ...
static_assert(not is_subtype_of(Foo, HasReadOnlyX))
static_assert(not is_assignable_to(Foo, HasReadOnlyX))
static_assert(not is_disjoint_from(Foo, HasReadOnlyX))
static_assert(not is_subtype_of(type[Foo], HasReadOnlyX))
static_assert(not is_assignable_to(type[Foo], HasReadOnlyX))
static_assert(not is_disjoint_from(type[Foo], HasReadOnlyX))
static_assert(not is_subtype_of(TypeOf[Foo], HasReadOnlyX))
static_assert(not is_assignable_to(TypeOf[Foo], HasReadOnlyX))
static_assert(not is_disjoint_from(TypeOf[Foo], HasReadOnlyX))
```
A similar principle applies to module-literal types that have possibly unbound attributes:
`b.py`:
```py
def who_knows() -> bool:
return False
if who_knows():
x: int = 42
```
`c.py`:
```py
import b
from a import HasReadOnlyX
from ty_extensions import TypeOf, static_assert, is_subtype_of, is_disjoint_from, is_assignable_to
static_assert(not is_subtype_of(TypeOf[b], HasReadOnlyX))
static_assert(not is_assignable_to(TypeOf[b], HasReadOnlyX))
static_assert(not is_disjoint_from(TypeOf[b], HasReadOnlyX))
```
If the possibly unbound attribute's type is disjoint from the type of the protocol member, though,
it is still disjoint from the protocol. This applies to both `@final` types and non-final types:
`d.py`:
```py
from a import HasReadOnlyX, who_knows
from typing import final, ClassVar, Protocol
from ty_extensions import static_assert, is_disjoint_from, TypeOf
class Proto(Protocol):
x: int
class Foo:
def __init__(self):
if who_knows():
self.x: None = None
@final
class FinalFoo:
def __init__(self):
if who_knows():
self.x: None = None
static_assert(is_disjoint_from(Foo, Proto))
static_assert(is_disjoint_from(FinalFoo, Proto))
```
## Satisfying a protocol's interface
A type does not have to be an `Instance` type in order to be a subtype of a protocol. Other