diff --git a/crates/ty_python_semantic/resources/mdtest/protocols.md b/crates/ty_python_semantic/resources/mdtest/protocols.md index b791c01627..7645ba31c5 100644 --- a/crates/ty_python_semantic/resources/mdtest/protocols.md +++ b/crates/ty_python_semantic/resources/mdtest/protocols.md @@ -2220,6 +2220,65 @@ static_assert(is_subtype_of(TypeOf[satisfies_foo], Foo)) static_assert(is_assignable_to(TypeOf[satisfies_foo], Foo)) ``` +## Nominal subtyping of protocols + +Protocols can participate in nominal subtyping as well as structural subtyping. The main use case +for this is that it allows users an "escape hatch" to force a type checker to consider another type +to be a subtype of a given protocol, even if the other type violates the Liskov Substitution +Principle in some way. + +```py +from typing import Protocol, final +from ty_extensions import static_assert, is_subtype_of, is_disjoint_from + +class X(Protocol): + x: int + +class YProto(X, Protocol): + x: None = None # TODO: we should emit an error here due to the Liskov violation + +@final +class YNominal(X): + x: None = None # TODO: we should emit an error here due to the Liskov violation + +static_assert(is_subtype_of(YProto, X)) +static_assert(is_subtype_of(YNominal, X)) +static_assert(not is_disjoint_from(YProto, X)) +static_assert(not is_disjoint_from(YNominal, X)) +``` + +A common use case for this behaviour is that a lot of ecosystem code depends on type checkers +considering `str` to be a subtype of `Container[str]`. From a structural-subtyping perspective, this +is not the case, since `str.__contains__` only accepts `str`, while the `Container` interface +specifies that a type must have a `__contains__` method which accepts `object` in order for that +type to be considered a subtype of `Container`. Nonetheless, `str` has `Container[str]` in its MRO, +and other type checkers therefore consider it to be a subtype of `Container[str]` -- as such, so do +we: + +```py +from typing import Container + +static_assert(is_subtype_of(str, Container[str])) +static_assert(not is_disjoint_from(str, Container[str])) +``` + +This behaviour can have some counter-intuitive repercussions. For example, one implication of this +is that not all subtype of `Iterable` are necessarily considered iterable by ty if a given subtype +violates the Liskov principle (this also matches the behaviour of other type checkers): + +```py +from typing import Iterable + +class Foo(Iterable[int]): + __iter__ = None + +static_assert(is_subtype_of(Foo, Iterable[int])) + +def _(x: Foo): + for item in x: # error: [not-iterable] + pass +``` + ## Protocols are never singleton types, and are never single-valued types It *might* be possible to have a singleton protocol-instance type...? diff --git a/crates/ty_python_semantic/resources/mdtest/type_compendium/tuple.md b/crates/ty_python_semantic/resources/mdtest/type_compendium/tuple.md index d39ab53246..ab9baee1a4 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_compendium/tuple.md +++ b/crates/ty_python_semantic/resources/mdtest/type_compendium/tuple.md @@ -36,7 +36,11 @@ reveal_type(tuple[int, *tuple[str, ...]]((1,))) # revealed: tuple[int, *tuple[s reveal_type(().__class__()) # revealed: tuple[()] reveal_type((1, 2).__class__((1, 2))) # revealed: tuple[Literal[1], Literal[2]] -def f(x: Iterable[int], y: list[str], z: Never, aa: list[Never]): +class LiskovUncompliantIterable(Iterable[int]): + # TODO we should emit an error here about the Liskov violation + __iter__ = None + +def f(x: Iterable[int], y: list[str], z: Never, aa: list[Never], bb: LiskovUncompliantIterable): reveal_type(tuple(x)) # revealed: tuple[int, ...] reveal_type(tuple(y)) # revealed: tuple[str, ...] reveal_type(tuple(z)) # revealed: tuple[Unknown, ...] @@ -44,6 +48,11 @@ def f(x: Iterable[int], y: list[str], z: Never, aa: list[Never]): # This is correct as the only inhabitants of `list[Never]` can be empty lists reveal_type(tuple(aa)) # revealed: tuple[()] + # `tuple[int, ...] would probably also be fine here since `LiskovUncompliantIterable` + # inherits from `Iterable[int]`. Ultimately all bets are off when the Liskov Principle is + # violated, though -- this test is really just to make sure we don't crash in this situation. + reveal_type(tuple(bb)) # revealed: tuple[Unknown, ...] + reveal_type(tuple((1, 2))) # revealed: tuple[Literal[1], Literal[2]] # TODO: should be `tuple[Literal[1], ...]` diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 3aa89193ed..10f7f96678 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -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), diff --git a/crates/ty_python_semantic/src/types/call/bind.rs b/crates/ty_python_semantic/src/types/call/bind.rs index e08c50b5b8..a93637c452 100644 --- a/crates/ty_python_semantic/src/types/call/bind.rs +++ b/crates/ty_python_semantic/src/types/call/bind.rs @@ -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), ))); } } diff --git a/crates/ty_python_semantic/src/types/instance.rs b/crates/ty_python_semantic/src/types/instance.rs index d37a7ada31..194d047788 100644 --- a/crates/ty_python_semantic/src/types/instance.rs +++ b/crates/ty_python_semantic/src/types/instance.rs @@ -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) + } + }) } } diff --git a/crates/ty_python_semantic/src/types/property_tests.rs b/crates/ty_python_semantic/src/types/property_tests.rs index 77747bcaed..f5a47001de 100644 --- a/crates/ty_python_semantic/src/types/property_tests.rs +++ b/crates/ty_python_semantic/src/types/property_tests.rs @@ -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 type_property_test!( all_type_assignable_to_iterable_are_iterable, db,