Compare commits

...

1 Commits

Author SHA1 Message Date
Charlie Marsh
a3394a7167 [ty] Apply @runtime_checkable across ancestors 2026-01-12 08:54:10 -05:00
3 changed files with 62 additions and 4 deletions

View File

@@ -2479,6 +2479,37 @@ def f(arg1: type, arg2: type):
reveal_type(arg2) # revealed: type & ~type[OnlyMethodMembers]
```
Per PEP 544, `@runtime_checkable` propagates to subclasses. A protocol that inherits from a
`@runtime_checkable` protocol is itself runtime-checkable, even without the decorator:
```py
@runtime_checkable
class RuntimeCheckableBase(Protocol):
x: int
class RuntimeCheckableChild(RuntimeCheckableBase, Protocol):
y: str
def g(arg: object):
if isinstance(arg, RuntimeCheckableChild): # no error!
reveal_type(arg) # revealed: RuntimeCheckableChild
else:
reveal_type(arg) # revealed: ~RuntimeCheckableChild
```
This also applies to deeper inheritance hierarchies:
```py
class RuntimeCheckableGrandchild(RuntimeCheckableChild, Protocol):
z: float
def h(arg: object):
if isinstance(arg, RuntimeCheckableGrandchild): # no error!
reveal_type(arg) # revealed: RuntimeCheckableGrandchild
else:
reveal_type(arg) # revealed: ~RuntimeCheckableGrandchild
```
## Truthiness of protocol instances
An instance of a protocol type generally has ambiguous truthiness:

View File

@@ -53,6 +53,26 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/protocols.md
38 | reveal_type(arg2) # revealed: type[OnlyMethodMembers]
39 | else:
40 | reveal_type(arg2) # revealed: type & ~type[OnlyMethodMembers]
41 | @runtime_checkable
42 | class RuntimeCheckableBase(Protocol):
43 | x: int
44 |
45 | class RuntimeCheckableChild(RuntimeCheckableBase, Protocol):
46 | y: str
47 |
48 | def g(arg: object):
49 | if isinstance(arg, RuntimeCheckableChild): # no error!
50 | reveal_type(arg) # revealed: RuntimeCheckableChild
51 | else:
52 | reveal_type(arg) # revealed: ~RuntimeCheckableChild
53 | class RuntimeCheckableGrandchild(RuntimeCheckableChild, Protocol):
54 | z: float
55 |
56 | def h(arg: object):
57 | if isinstance(arg, RuntimeCheckableGrandchild): # no error!
58 | reveal_type(arg) # revealed: RuntimeCheckableGrandchild
59 | else:
60 | reveal_type(arg) # revealed: ~RuntimeCheckableGrandchild
```
# Diagnostics

View File

@@ -76,10 +76,17 @@ impl<'db> ProtocolClass<'db> {
}
pub(super) fn is_runtime_checkable(self, db: &'db dyn Db) -> bool {
self.class_literal(db)
.0
.known_function_decorators(db)
.contains(&KnownFunction::RuntimeCheckable)
// Check if this class or any ancestor protocol is decorated with @runtime_checkable.
// Per PEP 544, @runtime_checkable propagates to subclasses.
self.0.iter_mro(db).any(|base| {
base.into_class().is_some_and(|class| {
class
.class_literal(db)
.0
.known_function_decorators(db)
.contains(&KnownFunction::RuntimeCheckable)
})
})
}
/// Iterate through the body of the protocol class. Check that all definitions