mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-30 05:44:56 +00:00
[ty] Allow protocols to participate in nominal subtyping as well as structural subtyping (#20314)
This commit is contained in:
parent
4de7d653bd
commit
fd7eb1e22f
6 changed files with 118 additions and 23 deletions
|
@ -1613,16 +1613,13 @@ impl<'db> Type<'db> {
|
|||
callable.has_relation_to_impl(db, target, 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)) => {
|
||||
self.satisfies_protocol(db, protocol, 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),
|
||||
|
||||
// All `StringLiteral` types are a subtype of `LiteralString`.
|
||||
(Type::StringLiteral(_), Type::LiteralString) => C::always_satisfiable(db),
|
||||
|
||||
|
|
|
@ -1058,17 +1058,15 @@ impl<'db> Bindings<'db> {
|
|||
// `tuple(range(42))` => `tuple[int, ...]`
|
||||
// BUT `tuple((1, 2))` => `tuple[Literal[1], Literal[2]]` rather than `tuple[Literal[1, 2], ...]`
|
||||
if let [Some(argument)] = overload.parameter_types() {
|
||||
let Ok(tuple_spec) = argument.try_iterate(db) else {
|
||||
tracing::debug!(
|
||||
"type" = %argument.display(db),
|
||||
"try_iterate() should not fail on a type \
|
||||
assignable to `Iterable`",
|
||||
);
|
||||
continue;
|
||||
};
|
||||
// We deliberately use `.iterate()` here (falling back to `Unknown` if it isn't iterable)
|
||||
// rather than `.try_iterate().expect()`. Even though we know at this point that the input
|
||||
// type is assignable to `Iterable`, that doesn't mean that the input type is *actually*
|
||||
// iterable (it could be a Liskov-uncompliant subtype of the `Iterable` class that sets
|
||||
// `__iter__ = None`, for example). That would be badly written Python code, but we still
|
||||
// need to be able to handle it without crashing.
|
||||
overload.set_return_type(Type::tuple(TupleType::new(
|
||||
db,
|
||||
tuple_spec.as_ref(),
|
||||
&argument.iterate(db),
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -107,13 +107,36 @@ impl<'db> Type<'db> {
|
|||
relation: TypeRelation,
|
||||
visitor: &HasRelationToVisitor<'db, C>,
|
||||
) -> C {
|
||||
protocol
|
||||
.inner
|
||||
.interface(db)
|
||||
.members(db)
|
||||
.when_all(db, |member| {
|
||||
member.is_satisfied_by(db, self, relation, visitor)
|
||||
})
|
||||
let structurally_satisfied = if let Type::ProtocolInstance(self_protocol) = self {
|
||||
self_protocol.interface(db).extends_interface_of(
|
||||
db,
|
||||
protocol.interface(db),
|
||||
relation,
|
||||
visitor,
|
||||
)
|
||||
} else {
|
||||
protocol
|
||||
.inner
|
||||
.interface(db)
|
||||
.members(db)
|
||||
.when_all(db, |member| {
|
||||
member.is_satisfied_by(db, self, relation, visitor)
|
||||
})
|
||||
};
|
||||
|
||||
// Even if `self` does not satisfy the protocol from a structural perspective,
|
||||
// we may still need to consider it as satisfying the protocol if `protocol` is
|
||||
// a class-based protocol and `self` has the protocol class in its MRO.
|
||||
//
|
||||
// This matches the behaviour of other type checkers, and is required for us to
|
||||
// recognise `str` as a subtype of `Container[str]`.
|
||||
structurally_satisfied.or(db, || {
|
||||
if let Protocol::FromClass(class) = protocol.inner {
|
||||
self.has_relation_to_impl(db, Type::non_tuple_instance(class), relation, visitor)
|
||||
} else {
|
||||
C::unsatisfiable(db)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -320,6 +320,15 @@ mod flaky {
|
|||
// iteration protocol as well as the new-style iteration protocol: not all objects that
|
||||
// we consider iterable are assignable to `Iterable[object]`.
|
||||
//
|
||||
// Note also that (like other property tests in this module),
|
||||
// this invariant will only hold true for Liskov-compliant types assignable to `Iterable`.
|
||||
// Since protocols can participate in nominal assignability/subtyping as well as
|
||||
// structural assignability/subtyping, it is possible to construct types that a type
|
||||
// checker must consider to be subtypes of `Iterable` even though they are not in fact
|
||||
// iterable (as long as the user `type: ignore`s any type-checker errors stemming from
|
||||
// the Liskov violation). All you need to do is to create a class that subclasses
|
||||
// `Iterable` but assigns `__iter__ = None` in the class body (or similar).
|
||||
//
|
||||
// Currently flaky due to <https://github.com/astral-sh/ty/issues/889>
|
||||
type_property_test!(
|
||||
all_type_assignable_to_iterable_are_iterable, db,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue