mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-29 13:24:57 +00:00
[ty] Treat Hashable
, and similar protocols, equivalently to object
for subtyping/assignability (#20284)
This commit is contained in:
parent
9cb37db510
commit
4de7d653bd
5 changed files with 154 additions and 35 deletions
|
@ -165,7 +165,9 @@ from does_not_exist import DoesNotExist # error: [unresolved-import]
|
|||
reveal_type(DoesNotExist) # revealed: Unknown
|
||||
|
||||
if hasattr(DoesNotExist, "__mro__"):
|
||||
reveal_type(DoesNotExist) # revealed: Unknown & <Protocol with members '__mro__'>
|
||||
# TODO: this should be `Unknown & <Protocol with members '__mro__'>` or similar
|
||||
# (The second part of the intersection is incorrectly simplified to `object` due to https://github.com/astral-sh/ty/issues/986)
|
||||
reveal_type(DoesNotExist) # revealed: Unknown
|
||||
|
||||
class Foo(DoesNotExist): ... # no error!
|
||||
reveal_type(Foo.__mro__) # revealed: tuple[<class 'Foo'>, Unknown, <class 'object'>]
|
||||
|
|
|
@ -985,6 +985,13 @@ from ty_extensions import is_equivalent_to
|
|||
static_assert(is_equivalent_to(UniversalSet, object))
|
||||
```
|
||||
|
||||
and that therefore `Any` is a subtype of `UniversalSet` (in general, `Any` can *only* ever be a
|
||||
subtype of `object` and types that are equivalent to `object`):
|
||||
|
||||
```py
|
||||
static_assert(is_subtype_of(Any, UniversalSet))
|
||||
```
|
||||
|
||||
`object` is a subtype of certain other protocols too. Since all fully static types (whether nominal
|
||||
or structural) are subtypes of `object`, these protocols are also subtypes of `object`; and this
|
||||
means that these protocols are also equivalent to `UniversalSet` and `object`:
|
||||
|
@ -995,6 +1002,10 @@ class SupportsStr(Protocol):
|
|||
|
||||
static_assert(is_equivalent_to(SupportsStr, UniversalSet))
|
||||
static_assert(is_equivalent_to(SupportsStr, object))
|
||||
static_assert(is_subtype_of(SupportsStr, UniversalSet))
|
||||
static_assert(is_subtype_of(UniversalSet, SupportsStr))
|
||||
static_assert(is_assignable_to(UniversalSet, SupportsStr))
|
||||
static_assert(is_assignable_to(SupportsStr, UniversalSet))
|
||||
|
||||
class SupportsClass(Protocol):
|
||||
@property
|
||||
|
@ -1003,6 +1014,11 @@ class SupportsClass(Protocol):
|
|||
static_assert(is_equivalent_to(SupportsClass, UniversalSet))
|
||||
static_assert(is_equivalent_to(SupportsClass, SupportsStr))
|
||||
static_assert(is_equivalent_to(SupportsClass, object))
|
||||
|
||||
static_assert(is_subtype_of(SupportsClass, SupportsStr))
|
||||
static_assert(is_subtype_of(SupportsStr, SupportsClass))
|
||||
static_assert(is_assignable_to(SupportsStr, SupportsClass))
|
||||
static_assert(is_assignable_to(SupportsClass, SupportsStr))
|
||||
```
|
||||
|
||||
If a protocol contains members that are not defined on `object`, then that protocol will (like all
|
||||
|
@ -1024,6 +1040,47 @@ static_assert(not is_assignable_to(HasX, Foo))
|
|||
static_assert(not is_subtype_of(HasX, Foo))
|
||||
```
|
||||
|
||||
Since `object` defines a `__hash__` method, this means that the standard-library `Hashable` protocol
|
||||
is currently understood by ty as being equivalent to `object`, much like `SupportsStr` and
|
||||
`UniversalSet` above:
|
||||
|
||||
```py
|
||||
from typing import Hashable
|
||||
|
||||
static_assert(is_equivalent_to(object, Hashable))
|
||||
static_assert(is_assignable_to(object, Hashable))
|
||||
static_assert(is_subtype_of(object, Hashable))
|
||||
```
|
||||
|
||||
This means that any type considered assignable to `object` (which is all types) is considered by ty
|
||||
to be assignable to `Hashable`. This avoids false positives on code like this:
|
||||
|
||||
```py
|
||||
from typing import Sequence
|
||||
from ty_extensions import is_disjoint_from
|
||||
|
||||
def takes_hashable_or_sequence(x: Hashable | list[Hashable]): ...
|
||||
|
||||
takes_hashable_or_sequence(["foo"]) # fine
|
||||
takes_hashable_or_sequence(None) # fine
|
||||
|
||||
static_assert(not is_disjoint_from(list[str], Hashable | list[Hashable]))
|
||||
static_assert(not is_disjoint_from(list[str], Sequence[Hashable]))
|
||||
|
||||
static_assert(is_subtype_of(list[Hashable], Sequence[Hashable]))
|
||||
static_assert(is_subtype_of(list[str], Sequence[Hashable]))
|
||||
```
|
||||
|
||||
but means that ty currently does not detect errors on code like this, which is flagged by other type
|
||||
checkers:
|
||||
|
||||
```py
|
||||
def needs_something_hashable(x: Hashable):
|
||||
hash(x)
|
||||
|
||||
needs_something_hashable([])
|
||||
```
|
||||
|
||||
## Diagnostics for protocols with invalid attribute members
|
||||
|
||||
This is a short appendix to the previous section with the `snapshot-diagnostics` directive enabled
|
||||
|
@ -2553,6 +2610,48 @@ class E[T: B](Protocol): ...
|
|||
x: E[D]
|
||||
```
|
||||
|
||||
### Recursive supertypes of `object`
|
||||
|
||||
A recursive protocol can be a supertype of `object` (though it is hard to create such a protocol
|
||||
without violating the Liskov Substitution Principle, since all protocols are also subtypes of
|
||||
`object`):
|
||||
|
||||
```py
|
||||
from typing import Protocol
|
||||
from ty_extensions import static_assert, is_subtype_of, is_equivalent_to, is_disjoint_from
|
||||
|
||||
class HasRepr(Protocol):
|
||||
# TODO: we should emit a diagnostic here complaining about a Liskov violation
|
||||
# (it incompatibly overrides `__repr__` from `object`, a supertype of `HasRepr`)
|
||||
def __repr__(self) -> object: ...
|
||||
|
||||
class HasReprRecursive(Protocol):
|
||||
# TODO: we should emit a diagnostic here complaining about a Liskov violation
|
||||
# (it incompatibly overrides `__repr__` from `object`, a supertype of `HasReprRecursive`)
|
||||
def __repr__(self) -> "HasReprRecursive": ...
|
||||
|
||||
class HasReprRecursiveAndFoo(Protocol):
|
||||
# TODO: we should emit a diagnostic here complaining about a Liskov violation
|
||||
# (it incompatibly overrides `__repr__` from `object`, a supertype of `HasReprRecursiveAndFoo`)
|
||||
def __repr__(self) -> "HasReprRecursiveAndFoo": ...
|
||||
foo: int
|
||||
|
||||
static_assert(is_subtype_of(object, HasRepr))
|
||||
static_assert(is_subtype_of(HasRepr, object))
|
||||
static_assert(is_equivalent_to(object, HasRepr))
|
||||
static_assert(not is_disjoint_from(HasRepr, object))
|
||||
|
||||
static_assert(is_subtype_of(object, HasReprRecursive))
|
||||
static_assert(is_subtype_of(HasReprRecursive, object))
|
||||
static_assert(is_equivalent_to(object, HasReprRecursive))
|
||||
static_assert(not is_disjoint_from(HasReprRecursive, object))
|
||||
|
||||
static_assert(not is_subtype_of(object, HasReprRecursiveAndFoo))
|
||||
static_assert(is_subtype_of(HasReprRecursiveAndFoo, object))
|
||||
static_assert(not is_equivalent_to(object, HasReprRecursiveAndFoo))
|
||||
static_assert(not is_disjoint_from(HasReprRecursiveAndFoo, object))
|
||||
```
|
||||
|
||||
## Meta-protocols
|
||||
|
||||
Where `P` is a protocol type, a class object `N` can be said to inhabit the type `type[P]` if:
|
||||
|
|
|
@ -1397,6 +1397,9 @@ impl<'db> Type<'db> {
|
|||
(_, Type::NominalInstance(instance)) if instance.is_object(db) => {
|
||||
C::always_satisfiable(db)
|
||||
}
|
||||
(_, Type::ProtocolInstance(target)) if target.is_equivalent_to_object(db) => {
|
||||
C::always_satisfiable(db)
|
||||
}
|
||||
|
||||
// `Never` is the bottom type, the empty set.
|
||||
// It is a subtype of all other types.
|
||||
|
@ -1610,9 +1613,10 @@ impl<'db> Type<'db> {
|
|||
callable.has_relation_to_impl(db, target, relation, visitor)
|
||||
}),
|
||||
|
||||
(Type::ProtocolInstance(left), Type::ProtocolInstance(right)) => {
|
||||
left.has_relation_to_impl(db, right, relation, visitor)
|
||||
}
|
||||
(Type::ProtocolInstance(left), Type::ProtocolInstance(right)) => left
|
||||
.interface(db)
|
||||
.extends_interface_of(db, right.interface(db), relation, visitor),
|
||||
|
||||
// A protocol instance can never be a subtype of a nominal type, with the *sole* exception of `object`.
|
||||
(Type::ProtocolInstance(_), _) => C::unsatisfiable(db),
|
||||
(_, Type::ProtocolInstance(protocol)) => {
|
||||
|
|
|
@ -482,6 +482,43 @@ impl<'db> ProtocolInstanceType<'db> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Return `true` if this protocol is a supertype of `object`.
|
||||
///
|
||||
/// This indicates that the protocol represents the same set of possible runtime objects
|
||||
/// as `object` (since `object` is the universal set of *all* possible runtime objects!).
|
||||
/// Such a protocol is therefore an equivalent type to `object`, which would in fact be
|
||||
/// normalised to `object`.
|
||||
pub(super) fn is_equivalent_to_object(self, db: &'db dyn Db) -> bool {
|
||||
#[salsa::tracked(cycle_fn=recover, cycle_initial=initial, heap_size=ruff_memory_usage::heap_size)]
|
||||
fn inner<'db>(db: &'db dyn Db, protocol: ProtocolInstanceType<'db>, _: ()) -> bool {
|
||||
Type::object(db)
|
||||
.satisfies_protocol(
|
||||
db,
|
||||
protocol,
|
||||
TypeRelation::Subtyping,
|
||||
&HasRelationToVisitor::new(ConstraintSet::always_satisfiable(db)),
|
||||
)
|
||||
.is_always_satisfied(db)
|
||||
}
|
||||
|
||||
#[expect(clippy::trivially_copy_pass_by_ref)]
|
||||
fn recover<'db>(
|
||||
_db: &'db dyn Db,
|
||||
_result: &bool,
|
||||
_count: u32,
|
||||
_value: ProtocolInstanceType<'db>,
|
||||
_: (),
|
||||
) -> salsa::CycleRecoveryAction<bool> {
|
||||
salsa::CycleRecoveryAction::Iterate
|
||||
}
|
||||
|
||||
fn initial<'db>(_db: &'db dyn Db, _value: ProtocolInstanceType<'db>, _: ()) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
inner(db, self, ())
|
||||
}
|
||||
|
||||
/// Return a "normalized" version of this `Protocol` type.
|
||||
///
|
||||
/// See [`Type::normalized`] for more details.
|
||||
|
@ -497,17 +534,8 @@ impl<'db> ProtocolInstanceType<'db> {
|
|||
db: &'db dyn Db,
|
||||
visitor: &NormalizedVisitor<'db>,
|
||||
) -> Type<'db> {
|
||||
let object = Type::object(db);
|
||||
if object
|
||||
.satisfies_protocol(
|
||||
db,
|
||||
self,
|
||||
TypeRelation::Subtyping,
|
||||
&HasRelationToVisitor::new(ConstraintSet::always_satisfiable(db)),
|
||||
)
|
||||
.is_always_satisfied(db)
|
||||
{
|
||||
return object;
|
||||
if self.is_equivalent_to_object(db) {
|
||||
return Type::object(db);
|
||||
}
|
||||
match self.inner {
|
||||
Protocol::FromClass(_) => Type::ProtocolInstance(Self::synthesized(
|
||||
|
@ -517,22 +545,6 @@ impl<'db> ProtocolInstanceType<'db> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Return `true` if this protocol type has the given type relation to the protocol `other`.
|
||||
///
|
||||
/// TODO: consider the types of the members as well as their existence
|
||||
pub(super) fn has_relation_to_impl<C: Constraints<'db>>(
|
||||
self,
|
||||
db: &'db dyn Db,
|
||||
other: Self,
|
||||
_relation: TypeRelation,
|
||||
visitor: &HasRelationToVisitor<'db, C>,
|
||||
) -> C {
|
||||
other
|
||||
.inner
|
||||
.interface(db)
|
||||
.is_sub_interface_of(db, self.inner.interface(db), visitor)
|
||||
}
|
||||
|
||||
/// Return `true` if this protocol type is equivalent to the protocol `other`.
|
||||
///
|
||||
/// TODO: consider the types of the members as well as their existence
|
||||
|
|
|
@ -230,19 +230,21 @@ impl<'db> ProtocolInterface<'db> {
|
|||
.unwrap_or_else(|| Type::object(db).member(db, name))
|
||||
}
|
||||
|
||||
/// Return `true` if if all members on `self` are also members of `other`.
|
||||
/// Return `true` if `self` extends the interface of `other`, i.e.,
|
||||
/// all members on `other` are also members of `self`.
|
||||
///
|
||||
/// TODO: this method should consider the types of the members as well as their names.
|
||||
pub(super) fn is_sub_interface_of<C: Constraints<'db>>(
|
||||
pub(super) fn extends_interface_of<C: Constraints<'db>>(
|
||||
self,
|
||||
db: &'db dyn Db,
|
||||
other: Self,
|
||||
_relation: TypeRelation,
|
||||
_visitor: &HasRelationToVisitor<'db, C>,
|
||||
) -> C {
|
||||
// TODO: This could just return a bool as written, but this form is what will be needed to
|
||||
// combine the constraints when we do assignability checks on each member.
|
||||
self.inner(db).keys().when_all(db, |member_name| {
|
||||
C::from_bool(db, other.inner(db).contains_key(member_name))
|
||||
other.inner(db).keys().when_all(db, |member_name| {
|
||||
C::from_bool(db, self.inner(db).contains_key(member_name))
|
||||
})
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue