[ty] Add a Todo-type branch for type[P] where P is a protocol class (#19947)

This commit is contained in:
Alex Waygood 2025-08-18 21:38:19 +01:00 committed by GitHub
parent 24f6d2dc13
commit e6dcdd29f2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 87 additions and 16 deletions

View file

@ -355,7 +355,9 @@ And as a corollary, `type[MyProtocol]` can also be called:
```py ```py
def f(x: type[MyProtocol]): 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 ## Members of a protocol
@ -1931,7 +1933,7 @@ def _(r: Recursive):
reveal_type(r.t) # revealed: tuple[int, tuple[str, Recursive]] reveal_type(r.t) # revealed: tuple[int, tuple[str, Recursive]]
reveal_type(r.callable1) # revealed: (int, /) -> Recursive reveal_type(r.callable1) # revealed: (int, /) -> Recursive
reveal_type(r.callable2) # revealed: (Recursive, /) -> int 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.generic) # revealed: GenericC[Recursive]
reveal_type(r.method(r)) # revealed: Recursive reveal_type(r.method(r)) # revealed: Recursive
reveal_type(r.nested) # revealed: Recursive | ((Recursive, tuple[Recursive, Recursive], /) -> 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] 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 ## TODO
Add tests for: Add tests for:

View file

@ -36,7 +36,9 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/protocols.md
22 | 22 |
23 | reveal_type(SubclassOfGenericProtocol[int]()) # revealed: SubclassOfGenericProtocol[int] 23 | reveal_type(SubclassOfGenericProtocol[int]()) # revealed: SubclassOfGenericProtocol[int]
24 | def f(x: type[MyProtocol]): 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 # Diagnostics
@ -161,19 +163,7 @@ info[revealed-type]: Revealed type
23 | reveal_type(SubclassOfGenericProtocol[int]()) # revealed: SubclassOfGenericProtocol[int] 23 | reveal_type(SubclassOfGenericProtocol[int]()) # revealed: SubclassOfGenericProtocol[int]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `SubclassOfGenericProtocol[int]` | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `SubclassOfGenericProtocol[int]`
24 | def f(x: type[MyProtocol]): 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
|
```
```
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`
| |
``` ```

View file

@ -849,6 +849,18 @@ impl<'db> Type<'db> {
} }
} }
pub(crate) const fn into_dynamic(self) -> Option<DynamicType> {
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] #[track_caller]
pub(crate) fn expect_class_literal(self) -> ClassLiteral<'db> { pub(crate) fn expect_class_literal(self) -> ClassLiteral<'db> {
self.into_class_literal() self.into_class_literal()

View file

@ -10198,6 +10198,11 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
Type::ClassLiteral(class_literal) => { Type::ClassLiteral(class_literal) => {
if class_literal.is_known(self.db(), KnownClass::Any) { if class_literal.is_known(self.db(), KnownClass::Any) {
SubclassOfType::subclass_of_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 { } else {
SubclassOfType::from( SubclassOfType::from(
self.db(), self.db(),

View file

@ -285,6 +285,12 @@ impl<'db> From<ClassType<'db>> for SubclassOfInner<'db> {
} }
} }
impl From<DynamicType> for SubclassOfInner<'_> {
fn from(value: DynamicType) -> Self {
SubclassOfInner::Dynamic(value)
}
}
impl<'db> From<SubclassOfInner<'db>> for Type<'db> { impl<'db> From<SubclassOfInner<'db>> for Type<'db> {
fn from(value: SubclassOfInner<'db>) -> Self { fn from(value: SubclassOfInner<'db>) -> Self {
match value { match value {