From efe6ed7cb096c5ef893b81a3057161b2df08732b Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Wed, 11 Jun 2025 15:27:15 +0100 Subject: [PATCH 1/3] Handle corner case: protocol vs classvar vs descriptor --- mypy/subtypes.py | 12 +++++++++++- test-data/unit/check-protocols.test | 26 ++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/mypy/subtypes.py b/mypy/subtypes.py index acb41609fdc55..a5e6938615e75 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 f330aa4ecc028..8b3d70e00140f 100644 --- a/test-data/unit/check-protocols.test +++ b/test-data/unit/check-protocols.test @@ -4602,3 +4602,29 @@ 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: Any) -> Desc: ... + @overload + def __get__(self, instance: object, owner: Any) -> 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" From 0173fbb1b915c4c887cab19203fb1633e5943551 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Fri, 13 Jun 2025 18:09:45 +0100 Subject: [PATCH 2/3] Add docs (also one test just in case) --- docs/source/protocols.rst | 47 +++++++++++++++++++++++++++++ test-data/unit/check-protocols.test | 22 ++++++++++++-- 2 files changed, 67 insertions(+), 2 deletions(-) diff --git a/docs/source/protocols.rst b/docs/source/protocols.rst index ed8d94f62ef15..61ace9518cdb0 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 re-use + # 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/test-data/unit/check-protocols.test b/test-data/unit/check-protocols.test index 8b3d70e00140f..c6c2c5f8da980 100644 --- a/test-data/unit/check-protocols.test +++ b/test-data/unit/check-protocols.test @@ -4608,9 +4608,9 @@ from typing import Any, Protocol, overload, ClassVar, Type class Desc: @overload - def __get__(self, instance: None, owner: Any) -> Desc: ... + def __get__(self, instance: None, owner: object) -> Desc: ... @overload - def __get__(self, instance: object, owner: Any) -> int: ... + def __get__(self, instance: object, owner: object) -> int: ... def __get__(self, instance, owner): pass @@ -4628,3 +4628,21 @@ 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" From e629bf4586d05dac8ef9f9214f419f37d53374d4 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Fri, 13 Jun 2025 18:23:04 +0100 Subject: [PATCH 3/3] Codespell --- docs/source/protocols.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/protocols.rst b/docs/source/protocols.rst index 61ace9518cdb0..258cd4b0de564 100644 --- a/docs/source/protocols.rst +++ b/docs/source/protocols.rst @@ -388,7 +388,7 @@ declare them explicitly using ``ClassVar[...]``. Continuing previous example: from typing import ClassVar class OtherExample(Protocol): - # This style is *not recommended*, but may be needed to re-use + # 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,