Skip to content

Commit ebf59e2

Browse files
authored
[ty] Rework disjointness of protocol instances vs types with possibly unbound attributes (#19043)
1 parent c6fd11f commit ebf59e2

File tree

4 files changed

+207
-52
lines changed

4 files changed

+207
-52
lines changed

crates/ty_python_semantic/resources/mdtest/protocols.md

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -964,6 +964,121 @@ def _(arg1: Intersection[HasX, NotFinalNominal], arg2: Intersection[HasX, FinalN
964964
reveal_type(arg2) # revealed: Never
965965
```
966966

967+
The disjointness of a single protocol member with the type of an attribute on another type is enough
968+
to make the whole protocol disjoint from the other type, even if all other members on the protocol
969+
are satisfied by the other type. This applies to both `@final` types and non-final types:
970+
971+
```py
972+
class Proto(Protocol):
973+
x: int
974+
y: str
975+
z: bytes
976+
977+
class Foo:
978+
x: int
979+
y: str
980+
z: None
981+
982+
static_assert(is_disjoint_from(Proto, Foo))
983+
984+
@final
985+
class FinalFoo:
986+
x: int
987+
y: str
988+
z: None
989+
990+
static_assert(is_disjoint_from(Proto, FinalFoo))
991+
```
992+
993+
## Intersections of protocols with types that have possibly unbound attributes
994+
995+
Note that if a `@final` class has a possibly unbound attribute corresponding to the protocol member,
996+
instance types and class-literal types referring to that class cannot be a subtype of the protocol
997+
but will also not be disjoint from the protocol:
998+
999+
`a.py`:
1000+
1001+
```py
1002+
from typing import final, ClassVar, Protocol
1003+
from ty_extensions import TypeOf, static_assert, is_subtype_of, is_disjoint_from, is_assignable_to
1004+
1005+
def who_knows() -> bool:
1006+
return False
1007+
1008+
@final
1009+
class Foo:
1010+
if who_knows():
1011+
x: ClassVar[int] = 42
1012+
1013+
class HasReadOnlyX(Protocol):
1014+
@property
1015+
def x(self) -> int: ...
1016+
1017+
static_assert(not is_subtype_of(Foo, HasReadOnlyX))
1018+
static_assert(not is_assignable_to(Foo, HasReadOnlyX))
1019+
static_assert(not is_disjoint_from(Foo, HasReadOnlyX))
1020+
1021+
static_assert(not is_subtype_of(type[Foo], HasReadOnlyX))
1022+
static_assert(not is_assignable_to(type[Foo], HasReadOnlyX))
1023+
static_assert(not is_disjoint_from(type[Foo], HasReadOnlyX))
1024+
1025+
static_assert(not is_subtype_of(TypeOf[Foo], HasReadOnlyX))
1026+
static_assert(not is_assignable_to(TypeOf[Foo], HasReadOnlyX))
1027+
static_assert(not is_disjoint_from(TypeOf[Foo], HasReadOnlyX))
1028+
```
1029+
1030+
A similar principle applies to module-literal types that have possibly unbound attributes:
1031+
1032+
`b.py`:
1033+
1034+
```py
1035+
def who_knows() -> bool:
1036+
return False
1037+
1038+
if who_knows():
1039+
x: int = 42
1040+
```
1041+
1042+
`c.py`:
1043+
1044+
```py
1045+
import b
1046+
from a import HasReadOnlyX
1047+
from ty_extensions import TypeOf, static_assert, is_subtype_of, is_disjoint_from, is_assignable_to
1048+
1049+
static_assert(not is_subtype_of(TypeOf[b], HasReadOnlyX))
1050+
static_assert(not is_assignable_to(TypeOf[b], HasReadOnlyX))
1051+
static_assert(not is_disjoint_from(TypeOf[b], HasReadOnlyX))
1052+
```
1053+
1054+
If the possibly unbound attribute's type is disjoint from the type of the protocol member, though,
1055+
it is still disjoint from the protocol. This applies to both `@final` types and non-final types:
1056+
1057+
`d.py`:
1058+
1059+
```py
1060+
from a import HasReadOnlyX, who_knows
1061+
from typing import final, ClassVar, Protocol
1062+
from ty_extensions import static_assert, is_disjoint_from, TypeOf
1063+
1064+
class Proto(Protocol):
1065+
x: int
1066+
1067+
class Foo:
1068+
def __init__(self):
1069+
if who_knows():
1070+
self.x: None = None
1071+
1072+
@final
1073+
class FinalFoo:
1074+
def __init__(self):
1075+
if who_knows():
1076+
self.x: None = None
1077+
1078+
static_assert(is_disjoint_from(Foo, Proto))
1079+
static_assert(is_disjoint_from(FinalFoo, Proto))
1080+
```
1081+
9671082
## Satisfying a protocol's interface
9681083

9691084
A type does not have to be an `Instance` type in order to be a subtype of a protocol. Other

crates/ty_python_semantic/src/types.rs

Lines changed: 72 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1698,6 +1698,20 @@ impl<'db> Type<'db> {
16981698
/// Note: This function aims to have no false positives, but might return
16991699
/// wrong `false` answers in some cases.
17001700
pub(crate) fn is_disjoint_from(self, db: &'db dyn Db, other: Type<'db>) -> bool {
1701+
fn any_protocol_members_absent_or_disjoint<'db>(
1702+
db: &'db dyn Db,
1703+
protocol: ProtocolInstanceType<'db>,
1704+
other: Type<'db>,
1705+
) -> bool {
1706+
protocol.interface(db).members(db).any(|member| {
1707+
other
1708+
.member(db, member.name())
1709+
.place
1710+
.ignore_possibly_unbound()
1711+
.is_none_or(|attribute_type| member.has_disjoint_type_from(db, attribute_type))
1712+
})
1713+
}
1714+
17011715
match (self, other) {
17021716
(Type::Never, _) | (_, Type::Never) => true,
17031717

@@ -1864,6 +1878,57 @@ impl<'db> Type<'db> {
18641878
Type::SubclassOf(_),
18651879
) => true,
18661880

1881+
(Type::AlwaysTruthy, ty) | (ty, Type::AlwaysTruthy) => {
1882+
// `Truthiness::Ambiguous` may include `AlwaysTrue` as a subset, so it's not guaranteed to be disjoint.
1883+
// Thus, they are only disjoint if `ty.bool() == AlwaysFalse`.
1884+
ty.bool(db).is_always_false()
1885+
}
1886+
(Type::AlwaysFalsy, ty) | (ty, Type::AlwaysFalsy) => {
1887+
// Similarly, they are only disjoint if `ty.bool() == AlwaysTrue`.
1888+
ty.bool(db).is_always_true()
1889+
}
1890+
1891+
(Type::ProtocolInstance(left), Type::ProtocolInstance(right)) => {
1892+
left.is_disjoint_from(db, right)
1893+
}
1894+
1895+
(Type::ProtocolInstance(protocol), Type::SpecialForm(special_form))
1896+
| (Type::SpecialForm(special_form), Type::ProtocolInstance(protocol)) => {
1897+
any_protocol_members_absent_or_disjoint(db, protocol, special_form.instance_fallback(db))
1898+
}
1899+
1900+
(Type::ProtocolInstance(protocol), Type::KnownInstance(known_instance))
1901+
| (Type::KnownInstance(known_instance), Type::ProtocolInstance(protocol)) => {
1902+
any_protocol_members_absent_or_disjoint(db, protocol, known_instance.instance_fallback(db))
1903+
}
1904+
1905+
// The absence of a protocol member on one of these types guarantees
1906+
// that the type will be disjoint from the protocol,
1907+
// but the type will not be disjoint from the protocol if it has a member
1908+
// that is of the correct type but is possibly unbound.
1909+
// If accessing a member on this type returns a possibly unbound `Place`,
1910+
// the type will not be a subtype of the protocol but it will also not be
1911+
// disjoint from the protocol, since there are possible subtypes of the type
1912+
// that could satisfy the protocol.
1913+
//
1914+
// ```py
1915+
// class Foo:
1916+
// if coinflip():
1917+
// X = 42
1918+
//
1919+
// class HasX(Protocol):
1920+
// @property
1921+
// def x(self) -> int: ...
1922+
//
1923+
// # `TypeOf[Foo]` (a class-literal type) is not a subtype of `HasX`,
1924+
// # but `TypeOf[Foo]` & HasX` should not simplify to `Never`,
1925+
// # or this branch would be incorrectly understood to be unreachable,
1926+
// # since we would understand the type of `Foo` in this branch to be
1927+
// # `TypeOf[Foo] & HasX` due to `hasattr()` narrowing.
1928+
//
1929+
// if hasattr(Foo, "X"):
1930+
// print(Foo.X)
1931+
// ```
18671932
(
18681933
ty @ (Type::LiteralString
18691934
| Type::StringLiteral(..)
@@ -1887,51 +1952,24 @@ impl<'db> Type<'db> {
18871952
| Type::ModuleLiteral(..)
18881953
| Type::GenericAlias(..)
18891954
| Type::IntLiteral(..)),
1890-
) => !ty.satisfies_protocol(db, protocol, TypeRelation::Assignability),
1891-
1892-
(Type::AlwaysTruthy, ty) | (ty, Type::AlwaysTruthy) => {
1893-
// `Truthiness::Ambiguous` may include `AlwaysTrue` as a subset, so it's not guaranteed to be disjoint.
1894-
// Thus, they are only disjoint if `ty.bool() == AlwaysFalse`.
1895-
ty.bool(db).is_always_false()
1896-
}
1897-
(Type::AlwaysFalsy, ty) | (ty, Type::AlwaysFalsy) => {
1898-
// Similarly, they are only disjoint if `ty.bool() == AlwaysTrue`.
1899-
ty.bool(db).is_always_true()
1900-
}
1901-
1902-
(Type::ProtocolInstance(left), Type::ProtocolInstance(right)) => {
1903-
left.is_disjoint_from(db, right)
1904-
}
1905-
1906-
(Type::ProtocolInstance(protocol), Type::SpecialForm(special_form))
1907-
| (Type::SpecialForm(special_form), Type::ProtocolInstance(protocol)) => !special_form
1908-
.instance_fallback(db)
1909-
.satisfies_protocol(db, protocol, TypeRelation::Assignability),
1910-
1911-
(Type::ProtocolInstance(protocol), Type::KnownInstance(known_instance))
1912-
| (Type::KnownInstance(known_instance), Type::ProtocolInstance(protocol)) => {
1913-
!known_instance.instance_fallback(db).satisfies_protocol(
1914-
db,
1915-
protocol,
1916-
TypeRelation::Assignability,
1917-
)
1918-
}
1955+
) => any_protocol_members_absent_or_disjoint(db, protocol, ty),
19191956

1957+
// This is the same as the branch above --
1958+
// once guard patterns are stabilised, it could be unified with that branch
1959+
// (<https://github.com/rust-lang/rust/issues/129967>)
19201960
(Type::ProtocolInstance(protocol), nominal @ Type::NominalInstance(n))
19211961
| (nominal @ Type::NominalInstance(n), Type::ProtocolInstance(protocol))
19221962
if n.class.is_final(db) =>
19231963
{
1924-
!nominal.satisfies_protocol(db, protocol, TypeRelation::Assignability)
1964+
any_protocol_members_absent_or_disjoint(db, protocol, nominal)
19251965
}
19261966

19271967
(Type::ProtocolInstance(protocol), other)
19281968
| (other, Type::ProtocolInstance(protocol)) => {
19291969
protocol.interface(db).members(db).any(|member| {
1930-
// TODO: implement disjointness for property/method members as well as attribute members
1931-
member.is_attribute_member()
1932-
&& matches!(
1970+
matches!(
19331971
other.member(db, member.name()).place,
1934-
Place::Type(ty, Boundness::Bound) if ty.is_disjoint_from(db, member.ty())
1972+
Place::Type(attribute_type, _) if member.has_disjoint_type_from(db, attribute_type)
19351973
)
19361974
})
19371975
}

crates/ty_python_semantic/src/types/instance.rs

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use std::marker::PhantomData;
44

55
use super::protocol_class::ProtocolInterface;
66
use super::{ClassType, KnownClass, SubclassOfType, Type, TypeVarVariance};
7-
use crate::place::{Place, PlaceAndQualifiers};
7+
use crate::place::PlaceAndQualifiers;
88
use crate::types::tuple::TupleType;
99
use crate::types::{DynamicType, TypeMapping, TypeRelation, TypeVarInstance, TypeVisitor};
1010
use crate::{Db, FxOrderSet};
@@ -272,14 +272,7 @@ impl<'db> ProtocolInstanceType<'db> {
272272
pub(crate) fn instance_member(self, db: &'db dyn Db, name: &str) -> PlaceAndQualifiers<'db> {
273273
match self.inner {
274274
Protocol::FromClass(class) => class.instance_member(db, name),
275-
Protocol::Synthesized(synthesized) => synthesized
276-
.interface()
277-
.member_by_name(db, name)
278-
.map(|member| PlaceAndQualifiers {
279-
place: Place::bound(member.ty()),
280-
qualifiers: member.qualifiers(),
281-
})
282-
.unwrap_or_else(|| KnownClass::Object.to_instance(db).instance_member(db, name)),
275+
Protocol::Synthesized(synthesized) => synthesized.interface().instance_member(db, name),
283276
}
284277
}
285278

crates/ty_python_semantic/src/types/protocol_class.rs

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use ruff_python_ast::name::Name;
66

77
use crate::{
88
Db, FxOrderSet,
9-
place::{Boundness, Place, place_from_bindings, place_from_declarations},
9+
place::{Boundness, Place, PlaceAndQualifiers, place_from_bindings, place_from_declarations},
1010
semantic_index::{place_table, use_def_map},
1111
types::{
1212
CallableType, ClassBase, ClassLiteral, KnownFunction, PropertyInstanceType, Signature,
@@ -126,18 +126,23 @@ impl<'db> ProtocolInterface<'db> {
126126
})
127127
}
128128

129-
pub(super) fn member_by_name<'a>(
130-
self,
131-
db: &'db dyn Db,
132-
name: &'a str,
133-
) -> Option<ProtocolMember<'a, 'db>> {
129+
fn member_by_name<'a>(self, db: &'db dyn Db, name: &'a str) -> Option<ProtocolMember<'a, 'db>> {
134130
self.inner(db).get(name).map(|data| ProtocolMember {
135131
name,
136132
kind: data.kind,
137133
qualifiers: data.qualifiers,
138134
})
139135
}
140136

137+
pub(super) fn instance_member(self, db: &'db dyn Db, name: &str) -> PlaceAndQualifiers<'db> {
138+
self.member_by_name(db, name)
139+
.map(|member| PlaceAndQualifiers {
140+
place: Place::bound(member.ty()),
141+
qualifiers: member.qualifiers(),
142+
})
143+
.unwrap_or_else(|| Type::object(db).instance_member(db, name))
144+
}
145+
141146
/// Return `true` if if all members on `self` are also members of `other`.
142147
///
143148
/// TODO: this method should consider the types of the members as well as their names.
@@ -328,16 +333,20 @@ impl<'a, 'db> ProtocolMember<'a, 'db> {
328333
self.qualifiers
329334
}
330335

331-
pub(super) fn ty(&self) -> Type<'db> {
336+
fn ty(&self) -> Type<'db> {
332337
match &self.kind {
333338
ProtocolMemberKind::Method(callable) => *callable,
334339
ProtocolMemberKind::Property(property) => Type::PropertyInstance(*property),
335340
ProtocolMemberKind::Other(ty) => *ty,
336341
}
337342
}
338343

339-
pub(super) const fn is_attribute_member(&self) -> bool {
340-
matches!(self.kind, ProtocolMemberKind::Other(_))
344+
pub(super) fn has_disjoint_type_from(&self, db: &'db dyn Db, other: Type<'db>) -> bool {
345+
match &self.kind {
346+
// TODO: implement disjointness for property/method members as well as attribute members
347+
ProtocolMemberKind::Property(_) | ProtocolMemberKind::Method(_) => false,
348+
ProtocolMemberKind::Other(ty) => ty.is_disjoint_from(db, other),
349+
}
341350
}
342351

343352
/// Return `true` if `other` contains an attribute/method/property that satisfies

0 commit comments

Comments
 (0)