mirror of https://github.com/astral-sh/ruff
[red-knot] Add more tests for protocol members (#17550)
This commit is contained in:
parent
99fa850e53
commit
f9c7908bb7
|
|
@ -403,12 +403,37 @@ class Lumberjack(Protocol):
|
||||||
reveal_type(get_protocol_members(Lumberjack)) # revealed: @Todo(specialized non-generic class)
|
reveal_type(get_protocol_members(Lumberjack)) # revealed: @Todo(specialized non-generic class)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
A sub-protocol inherits and extends the members of its superclass protocol(s):
|
||||||
|
|
||||||
|
```py
|
||||||
|
class Bar(Protocol):
|
||||||
|
spam: str
|
||||||
|
|
||||||
|
class Baz(Bar, Protocol):
|
||||||
|
ham: memoryview
|
||||||
|
|
||||||
|
# TODO: `tuple[Literal["spam", "ham"]]` or `frozenset[Literal["spam", "ham"]]`
|
||||||
|
reveal_type(get_protocol_members(Baz)) # revealed: @Todo(specialized non-generic class)
|
||||||
|
|
||||||
|
class Baz2(Bar, Foo, Protocol): ...
|
||||||
|
|
||||||
|
# TODO: either
|
||||||
|
# `tuple[Literal["spam"], Literal["x"], Literal["y"], Literal["z"], Literal["method_member"]]`
|
||||||
|
# or `frozenset[Literal["spam", "x", "y", "z", "method_member"]]`
|
||||||
|
reveal_type(get_protocol_members(Baz2)) # revealed: @Todo(specialized non-generic class)
|
||||||
|
```
|
||||||
|
|
||||||
## Subtyping of protocols with attribute members
|
## Subtyping of protocols with attribute members
|
||||||
|
|
||||||
In the following example, the protocol class `HasX` defines an interface such that any other fully
|
In the following example, the protocol class `HasX` defines an interface such that any other fully
|
||||||
static type can be said to be a subtype of `HasX` if all inhabitants of that other type have a
|
static type can be said to be a subtype of `HasX` if all inhabitants of that other type have a
|
||||||
mutable `x` attribute of type `int`:
|
mutable `x` attribute of type `int`:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[environment]
|
||||||
|
python-version = "3.12"
|
||||||
|
```
|
||||||
|
|
||||||
```py
|
```py
|
||||||
from typing import Protocol
|
from typing import Protocol
|
||||||
from knot_extensions import static_assert, is_assignable_to, is_subtype_of
|
from knot_extensions import static_assert, is_assignable_to, is_subtype_of
|
||||||
|
|
@ -548,6 +573,54 @@ def f(arg: HasXWithDefault):
|
||||||
reveal_type(type(arg).x) # revealed: int
|
reveal_type(type(arg).x) # revealed: int
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Assignments in a class body of a protocol -- of any kind -- are not permitted by red-knot unless the
|
||||||
|
symbol being assigned to is also explicitly declared in the protocol's class body. Note that this is
|
||||||
|
stricter validation of protocol members than many other type checkers currently apply (as of
|
||||||
|
2025/04/21).
|
||||||
|
|
||||||
|
The reason for this strict validation is that undeclared variables in the class body would lead to
|
||||||
|
an ambiguous interface being declared by the protocol.
|
||||||
|
|
||||||
|
```py
|
||||||
|
from typing_extensions import TypeAlias, get_protocol_members
|
||||||
|
|
||||||
|
class MyContext:
|
||||||
|
def __enter__(self) -> int:
|
||||||
|
return 42
|
||||||
|
|
||||||
|
def __exit__(self, *args) -> None: ...
|
||||||
|
|
||||||
|
class LotsOfBindings(Protocol):
|
||||||
|
a: int
|
||||||
|
a = 42 # this is fine, since `a` is declared in the class body
|
||||||
|
b: int = 56 # this is also fine, by the same principle
|
||||||
|
|
||||||
|
type c = str # this is very strange but I can't see a good reason to disallow it
|
||||||
|
d: TypeAlias = bytes # same here
|
||||||
|
|
||||||
|
class Nested: ... # also weird, but we should also probably allow it
|
||||||
|
class NestedProtocol(Protocol): ... # same here...
|
||||||
|
e = 72 # TODO: this should error with `[invalid-protocol]` (`e` is not declared)
|
||||||
|
|
||||||
|
f, g = (1, 2) # TODO: this should error with `[invalid-protocol]` (`f` and `g` are not declared)
|
||||||
|
|
||||||
|
h: int = (i := 3) # TODO: this should error with `[invalid-protocol]` (`i` is not declared)
|
||||||
|
|
||||||
|
for j in range(42): # TODO: this should error with `[invalid-protocol]` (`j` is not declared)
|
||||||
|
pass
|
||||||
|
|
||||||
|
with MyContext() as k: # TODO: this should error with `[invalid-protocol]` (`k` is not declared)
|
||||||
|
pass
|
||||||
|
|
||||||
|
match object():
|
||||||
|
case l: # TODO: this should error with `[invalid-protocol]` (`l` is not declared)
|
||||||
|
...
|
||||||
|
|
||||||
|
# TODO: all bindings in the above class should be understood as protocol members,
|
||||||
|
# even those that we complained about with a diagnostic
|
||||||
|
reveal_type(get_protocol_members(LotsOfBindings)) # revealed: @Todo(specialized non-generic class)
|
||||||
|
```
|
||||||
|
|
||||||
Attribute members are allowed to have assignments in methods on the protocol class, just like
|
Attribute members are allowed to have assignments in methods on the protocol class, just like
|
||||||
non-protocol classes. Unlike other classes, however, *implicit* instance attributes -- those that
|
non-protocol classes. Unlike other classes, however, *implicit* instance attributes -- those that
|
||||||
are not declared in the class body -- are not allowed:
|
are not declared in the class body -- are not allowed:
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue