diff --git a/docs/source/protocols.rst b/docs/source/protocols.rst index ed8d94f62ef1..258cd4b0de56 100644 --- a/docs/source/protocols.rst +++ b/docs/source/protocols.rst @@ -352,6 +352,53 @@ the parameters are positional-only. Example (using the legacy syntax for generic copy_a = copy_b # OK copy_b = copy_a # Also OK +Binding of types in protocol attributes +*************************************** + +All protocol attributes annotations are treated as externally visible types +of those attributes. This means that for example callables are not bound, +and descriptors are not invoked: + +.. code-block:: python + + from typing import Callable, Protocol, overload + + class Integer: + @overload + def __get__(self, instance: None, owner: object) -> Integer: ... + @overload + def __get__(self, instance: object, owner: object) -> int: ... + # + + class Example(Protocol): + foo: Callable[[object], int] + bar: Integer + + ex: Example + reveal_type(ex.foo) # Revealed type is Callable[[object], int] + reveal_type(ex.bar) # Revealed type is Integer + +In other words, protocol attribute types are handled as they would appear in a +``self`` attribute annotation in a regular class. If you want some protocol +attributes to be handled as though they were defined at class level, you should +declare them explicitly using ``ClassVar[...]``. Continuing previous example: + +.. code-block:: python + + from typing import ClassVar + + class OtherExample(Protocol): + # This style is *not recommended*, but may be needed to reuse + # some complex callable types. Otherwise use regular methods. + foo: ClassVar[Callable[[object], int]] + # This may be needed to mimic descriptor access on Type[...] types, + # otherwise use a plain "bar: int" style. + bar: ClassVar[Integer] + + ex2: OtherExample + reveal_type(ex2.foo) # Revealed type is Callable[[], int] + reveal_type(ex2.bar) # Revealed type is int + .. _predefined_protocols_reference: Predefined protocol reference diff --git a/mypy/subtypes.py b/mypy/subtypes.py index acb41609fdc5..a5e6938615e7 100644 --- a/mypy/subtypes.py +++ b/mypy/subtypes.py @@ -1457,7 +1457,8 @@ def get_member_flags(name: str, itype: Instance, class_obj: bool = False) -> set flags = {IS_VAR} if not v.is_final: flags.add(IS_SETTABLE) - if v.is_classvar: + # TODO: define cleaner rules for class vs instance variables. + if v.is_classvar and not is_descriptor(v.type): flags.add(IS_CLASSVAR) if class_obj and v.is_inferred: flags.add(IS_CLASSVAR) @@ -1465,6 +1466,15 @@ def get_member_flags(name: str, itype: Instance, class_obj: bool = False) -> set return set() +def is_descriptor(typ: Type | None) -> bool: + typ = get_proper_type(typ) + if isinstance(typ, Instance): + return typ.type.get("__get__") is not None + if isinstance(typ, UnionType): + return all(is_descriptor(item) for item in typ.relevant_items()) + return False + + def find_node_type( node: Var | FuncBase, itype: Instance, diff --git a/test-data/unit/check-protocols.test b/test-data/unit/check-protocols.test index f330aa4ecc02..c6c2c5f8da98 100644 --- a/test-data/unit/check-protocols.test +++ b/test-data/unit/check-protocols.test @@ -4602,3 +4602,47 @@ def deco(fn: Callable[[], T]) -> Callable[[], list[T]]: ... @deco def defer() -> int: ... [builtins fixtures/list.pyi] + +[case testProtocolClassValDescriptor] +from typing import Any, Protocol, overload, ClassVar, Type + +class Desc: + @overload + def __get__(self, instance: None, owner: object) -> Desc: ... + @overload + def __get__(self, instance: object, owner: object) -> int: ... + def __get__(self, instance, owner): + pass + +class P(Protocol): + x: ClassVar[Desc] + +class C: + x = Desc() + +t: P = C() +reveal_type(t.x) # N: Revealed type is "builtins.int" +tt: Type[P] = C +reveal_type(tt.x) # N: Revealed type is "__main__.Desc" + +bad: P = C # E: Incompatible types in assignment (expression has type "type[C]", variable has type "P") \ + # N: Following member(s) of "C" have conflicts: \ + # N: x: expected "int", got "Desc" + +[case testProtocolClassValCallable] +from typing import Any, Protocol, overload, ClassVar, Type, Callable + +class P(Protocol): + foo: Callable[[object], int] + bar: ClassVar[Callable[[object], int]] + +class C: + foo: Callable[[object], int] + bar: ClassVar[Callable[[object], int]] + +t: P = C() +reveal_type(t.foo) # N: Revealed type is "def (builtins.object) -> builtins.int" +reveal_type(t.bar) # N: Revealed type is "def () -> builtins.int" +tt: Type[P] = C +reveal_type(tt.foo) # N: Revealed type is "def (builtins.object) -> builtins.int" +reveal_type(tt.bar) # N: Revealed type is "def (builtins.object) -> builtins.int"