[ty] Allow protocols to participate in nominal subtyping as well as structural subtyping (#20314)

This commit is contained in:
Alex Waygood 2025-09-10 12:05:50 +01:00 committed by GitHub
parent 4de7d653bd
commit fd7eb1e22f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 118 additions and 23 deletions

View file

@ -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),

View file

@ -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),
)));
}
}

View file

@ -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)
}
})
}
}

View file

@ -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,