@@ -403,12 +403,37 @@ class Lumberjack(Protocol):
403403reveal_type(get_protocol_members(Lumberjack)) # revealed: @Todo(specialized non-generic class)
404404```
405405
406+ A sub-protocol inherits and extends the members of its superclass protocol(s):
407+
408+ ``` py
409+ class Bar (Protocol ):
410+ spam: str
411+
412+ class Baz (Bar , Protocol ):
413+ ham: memoryview
414+
415+ # TODO : `tuple[Literal["spam", "ham"]]` or `frozenset[Literal["spam", "ham"]]`
416+ reveal_type(get_protocol_members(Baz)) # revealed: @Todo(specialized non-generic class)
417+
418+ class Baz2 (Bar , Foo , Protocol ): ...
419+
420+ # TODO : either
421+ # `tuple[Literal["spam"], Literal["x"], Literal["y"], Literal["z"], Literal["method_member"]]`
422+ # or `frozenset[Literal["spam", "x", "y", "z", "method_member"]]`
423+ reveal_type(get_protocol_members(Baz2)) # revealed: @Todo(specialized non-generic class)
424+ ```
425+
406426## Subtyping of protocols with attribute members
407427
408428In the following example, the protocol class ` HasX ` defines an interface such that any other fully
409429static type can be said to be a subtype of ` HasX ` if all inhabitants of that other type have a
410430mutable ` x ` attribute of type ` int ` :
411431
432+ ``` toml
433+ [environment ]
434+ python-version = " 3.12"
435+ ```
436+
412437``` py
413438from typing import Protocol
414439from knot_extensions import static_assert, is_assignable_to, is_subtype_of
@@ -548,6 +573,54 @@ def f(arg: HasXWithDefault):
548573 reveal_type(type (arg).x) # revealed: int
549574```
550575
576+ Assignments in a class body of a protocol -- of any kind -- are not permitted by red-knot unless the
577+ symbol being assigned to is also explicitly declared in the protocol's class body. Note that this is
578+ stricter validation of protocol members than many other type checkers currently apply (as of
579+ 2025/04/21).
580+
581+ The reason for this strict validation is that undeclared variables in the class body would lead to
582+ an ambiguous interface being declared by the protocol.
583+
584+ ``` py
585+ from typing_extensions import TypeAlias, get_protocol_members
586+
587+ class MyContext :
588+ def __enter__ (self ) -> int :
589+ return 42
590+
591+ def __exit__ (self , * args ) -> None : ...
592+
593+ class LotsOfBindings (Protocol ):
594+ a: int
595+ a = 42 # this is fine, since `a` is declared in the class body
596+ b: int = 56 # this is also fine, by the same principle
597+
598+ type c = str # this is very strange but I can't see a good reason to disallow it
599+ d: TypeAlias = bytes # same here
600+
601+ class Nested : ... # also weird, but we should also probably allow it
602+ class NestedProtocol (Protocol ): ... # same here...
603+ e = 72 # TODO : this should error with `[invalid-protocol]` (`e` is not declared)
604+
605+ f, g = (1 , 2 ) # TODO : this should error with `[invalid-protocol]` (`f` and `g` are not declared)
606+
607+ h: int = (i := 3 ) # TODO : this should error with `[invalid-protocol]` (`i` is not declared)
608+
609+ for j in range (42 ): # TODO : this should error with `[invalid-protocol]` (`j` is not declared)
610+ pass
611+
612+ with MyContext() as k: # TODO : this should error with `[invalid-protocol]` (`k` is not declared)
613+ pass
614+
615+ match object ():
616+ case l: # TODO : this should error with `[invalid-protocol]` (`l` is not declared)
617+ ...
618+
619+ # TODO : all bindings in the above class should be understood as protocol members,
620+ # even those that we complained about with a diagnostic
621+ reveal_type(get_protocol_members(LotsOfBindings)) # revealed: @Todo(specialized non-generic class)
622+ ```
623+
551624Attribute members are allowed to have assignments in methods on the protocol class, just like
552625non-protocol classes. Unlike other classes, however, * implicit* instance attributes -- those that
553626are not declared in the class body -- are not allowed:
0 commit comments