diff --git a/crates/ty_python_semantic/resources/mdtest/protocols.md b/crates/ty_python_semantic/resources/mdtest/protocols.md index 5c7899c7a4..28fd2c9362 100644 --- a/crates/ty_python_semantic/resources/mdtest/protocols.md +++ b/crates/ty_python_semantic/resources/mdtest/protocols.md @@ -355,7 +355,9 @@ And as a corollary, `type[MyProtocol]` can also be called: ```py def f(x: type[MyProtocol]): - reveal_type(x()) # revealed: MyProtocol + # TODO: add a `reveal_type` call here once it's no longer a `Todo` type + # (which doesn't work well with snapshots) + x() ``` ## Members of a protocol @@ -1931,7 +1933,7 @@ def _(r: Recursive): reveal_type(r.t) # revealed: tuple[int, tuple[str, Recursive]] reveal_type(r.callable1) # revealed: (int, /) -> Recursive reveal_type(r.callable2) # revealed: (Recursive, /) -> int - reveal_type(r.subtype_of) # revealed: type[Recursive] + reveal_type(r.subtype_of) # revealed: @Todo(type[T] for protocols) reveal_type(r.generic) # revealed: GenericC[Recursive] reveal_type(r.method(r)) # revealed: Recursive reveal_type(r.nested) # revealed: Recursive | ((Recursive, tuple[Recursive, Recursive], /) -> Recursive) @@ -2069,6 +2071,62 @@ def f(value: Iterator): cast(Iterator, value) # error: [redundant-cast] ``` +## Meta-protocols + +Where `P` is a protocol type, a class object `N` can be said to inhabit the type `type[P]` if: + +- All `ClassVar` members on `P` exist on the class object `N` +- All method members on `P` exist on the class object `N` +- Instantiating `N` creates an object that would satisfy the protocol `P` + +Currently meta-protocols are not fully supported by ty, but we try to keep false positives to a +minimum in the meantime. + +```py +from typing import Protocol, ClassVar +from ty_extensions import static_assert, is_assignable_to, TypeOf, is_subtype_of + +class Foo(Protocol): + x: int + y: ClassVar[str] + def method(self) -> bytes: ... + +def _(f: type[Foo]): + reveal_type(f) # revealed: type[@Todo(type[T] for protocols)] + + # TODO: we should emit `unresolved-attribute` here: although we would accept this for a + # nominal class, we would see any class `N` as inhabiting `Foo` if it had an implicit + # instance attribute `x`, and implicit instance attributes are rarely bound on the class + # object. + reveal_type(f.x) # revealed: @Todo(type[T] for protocols) + + # TODO: should be `str` + reveal_type(f.y) # revealed: @Todo(type[T] for protocols) + f.y = "foo" # fine + + # TODO: should be `Callable[[Foo], bytes]` + reveal_type(f.method) # revealed: @Todo(type[T] for protocols) + +class Bar: ... + +# TODO: these should pass +static_assert(not is_assignable_to(type[Bar], type[Foo])) # error: [static-assert-error] +static_assert(not is_assignable_to(TypeOf[Bar], type[Foo])) # error: [static-assert-error] + +class Baz: + x: int + y: ClassVar[str] = "foo" + def method(self) -> bytes: + return b"foo" + +static_assert(is_assignable_to(type[Baz], type[Foo])) +static_assert(is_assignable_to(TypeOf[Baz], type[Foo])) + +# TODO: these should pass +static_assert(is_subtype_of(type[Baz], type[Foo])) # error: [static-assert-error] +static_assert(is_subtype_of(TypeOf[Baz], type[Foo])) # error: [static-assert-error] +``` + ## TODO Add tests for: diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/protocols.md_-_Protocols_-_Calls_to_protocol_cl…_(288988036f34ddcf).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/protocols.md_-_Protocols_-_Calls_to_protocol_cl…_(288988036f34ddcf).snap index 3553f119c2..923dac0803 100644 --- a/crates/ty_python_semantic/resources/mdtest/snapshots/protocols.md_-_Protocols_-_Calls_to_protocol_cl…_(288988036f34ddcf).snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/protocols.md_-_Protocols_-_Calls_to_protocol_cl…_(288988036f34ddcf).snap @@ -36,7 +36,9 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/protocols.md 22 | 23 | reveal_type(SubclassOfGenericProtocol[int]()) # revealed: SubclassOfGenericProtocol[int] 24 | def f(x: type[MyProtocol]): -25 | reveal_type(x()) # revealed: MyProtocol +25 | # TODO: add a `reveal_type` call here once it's no longer a `Todo` type +26 | # (which doesn't work well with snapshots) +27 | x() ``` # Diagnostics @@ -161,19 +163,7 @@ info[revealed-type]: Revealed type 23 | reveal_type(SubclassOfGenericProtocol[int]()) # revealed: SubclassOfGenericProtocol[int] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `SubclassOfGenericProtocol[int]` 24 | def f(x: type[MyProtocol]): -25 | reveal_type(x()) # revealed: MyProtocol - | - -``` - -``` -info[revealed-type]: Revealed type - --> src/mdtest_snippet.py:25:17 - | -23 | reveal_type(SubclassOfGenericProtocol[int]()) # revealed: SubclassOfGenericProtocol[int] -24 | def f(x: type[MyProtocol]): -25 | reveal_type(x()) # revealed: MyProtocol - | ^^^ `MyProtocol` +25 | # TODO: add a `reveal_type` call here once it's no longer a `Todo` type | ``` diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 3440ee58b0..ac5124a4bc 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -849,6 +849,18 @@ impl<'db> Type<'db> { } } + pub(crate) const fn into_dynamic(self) -> Option { + match self { + Type::Dynamic(dynamic_type) => Some(dynamic_type), + _ => None, + } + } + + pub(crate) const fn expect_dynamic(self) -> DynamicType { + self.into_dynamic() + .expect("Expected a Type::Dynamic variant") + } + #[track_caller] pub(crate) fn expect_class_literal(self) -> ClassLiteral<'db> { self.into_class_literal() diff --git a/crates/ty_python_semantic/src/types/infer.rs b/crates/ty_python_semantic/src/types/infer.rs index 201dcd7500..9d46d16965 100644 --- a/crates/ty_python_semantic/src/types/infer.rs +++ b/crates/ty_python_semantic/src/types/infer.rs @@ -10198,6 +10198,11 @@ impl<'db> TypeInferenceBuilder<'db, '_> { Type::ClassLiteral(class_literal) => { if class_literal.is_known(self.db(), KnownClass::Any) { SubclassOfType::subclass_of_any() + } else if class_literal.is_protocol(self.db()) { + SubclassOfType::from( + self.db(), + todo_type!("type[T] for protocols").expect_dynamic(), + ) } else { SubclassOfType::from( self.db(), diff --git a/crates/ty_python_semantic/src/types/subclass_of.rs b/crates/ty_python_semantic/src/types/subclass_of.rs index c27e92018e..02cb7bbebc 100644 --- a/crates/ty_python_semantic/src/types/subclass_of.rs +++ b/crates/ty_python_semantic/src/types/subclass_of.rs @@ -285,6 +285,12 @@ impl<'db> From> for SubclassOfInner<'db> { } } +impl From for SubclassOfInner<'_> { + fn from(value: DynamicType) -> Self { + SubclassOfInner::Dynamic(value) + } +} + impl<'db> From> for Type<'db> { fn from(value: SubclassOfInner<'db>) -> Self { match value {