diff --git a/crates/red_knot_python_semantic/resources/mdtest/protocols.md b/crates/red_knot_python_semantic/resources/mdtest/protocols.md index 01b63d95f0..c011cff751 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/protocols.md +++ b/crates/red_knot_python_semantic/resources/mdtest/protocols.md @@ -403,12 +403,37 @@ class Lumberjack(Protocol): 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 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 mutable `x` attribute of type `int`: +```toml +[environment] +python-version = "3.12" +``` + ```py from typing import Protocol 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 ``` +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 non-protocol classes. Unlike other classes, however, *implicit* instance attributes -- those that are not declared in the class body -- are not allowed: