Skip to content

Commit 5081c59

Browse files
authored
Handle corner case: protocol vs classvar vs descriptor (#19277)
Ref #19274 This is a bit ugly. But I propose to have this "hot-fix" until we have a proper overhaul of instance vs class variables. To be clear: attribute access already works correctly (on both `P` and `Type[P]`), but subtyping returns false because of ```python elif (IS_CLASSVAR in subflags) != (IS_CLASSVAR in superflags): return False ```
1 parent 4373e05 commit 5081c59

File tree

3 files changed

+102
-1
lines changed

3 files changed

+102
-1
lines changed

docs/source/protocols.rst

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -352,6 +352,53 @@ the parameters are positional-only. Example (using the legacy syntax for generic
352352
copy_a = copy_b # OK
353353
copy_b = copy_a # Also OK
354354
355+
Binding of types in protocol attributes
356+
***************************************
357+
358+
All protocol attributes annotations are treated as externally visible types
359+
of those attributes. This means that for example callables are not bound,
360+
and descriptors are not invoked:
361+
362+
.. code-block:: python
363+
364+
from typing import Callable, Protocol, overload
365+
366+
class Integer:
367+
@overload
368+
def __get__(self, instance: None, owner: object) -> Integer: ...
369+
@overload
370+
def __get__(self, instance: object, owner: object) -> int: ...
371+
# <some implementation>
372+
373+
class Example(Protocol):
374+
foo: Callable[[object], int]
375+
bar: Integer
376+
377+
ex: Example
378+
reveal_type(ex.foo) # Revealed type is Callable[[object], int]
379+
reveal_type(ex.bar) # Revealed type is Integer
380+
381+
In other words, protocol attribute types are handled as they would appear in a
382+
``self`` attribute annotation in a regular class. If you want some protocol
383+
attributes to be handled as though they were defined at class level, you should
384+
declare them explicitly using ``ClassVar[...]``. Continuing previous example:
385+
386+
.. code-block:: python
387+
388+
from typing import ClassVar
389+
390+
class OtherExample(Protocol):
391+
# This style is *not recommended*, but may be needed to reuse
392+
# some complex callable types. Otherwise use regular methods.
393+
foo: ClassVar[Callable[[object], int]]
394+
# This may be needed to mimic descriptor access on Type[...] types,
395+
# otherwise use a plain "bar: int" style.
396+
bar: ClassVar[Integer]
397+
398+
ex2: OtherExample
399+
reveal_type(ex2.foo) # Revealed type is Callable[[], int]
400+
reveal_type(ex2.bar) # Revealed type is int
401+
355402
.. _predefined_protocols_reference:
356403

357404
Predefined protocol reference

mypy/subtypes.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1457,14 +1457,24 @@ def get_member_flags(name: str, itype: Instance, class_obj: bool = False) -> set
14571457
flags = {IS_VAR}
14581458
if not v.is_final:
14591459
flags.add(IS_SETTABLE)
1460-
if v.is_classvar:
1460+
# TODO: define cleaner rules for class vs instance variables.
1461+
if v.is_classvar and not is_descriptor(v.type):
14611462
flags.add(IS_CLASSVAR)
14621463
if class_obj and v.is_inferred:
14631464
flags.add(IS_CLASSVAR)
14641465
return flags
14651466
return set()
14661467

14671468

1469+
def is_descriptor(typ: Type | None) -> bool:
1470+
typ = get_proper_type(typ)
1471+
if isinstance(typ, Instance):
1472+
return typ.type.get("__get__") is not None
1473+
if isinstance(typ, UnionType):
1474+
return all(is_descriptor(item) for item in typ.relevant_items())
1475+
return False
1476+
1477+
14681478
def find_node_type(
14691479
node: Var | FuncBase,
14701480
itype: Instance,

test-data/unit/check-protocols.test

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4602,3 +4602,47 @@ def deco(fn: Callable[[], T]) -> Callable[[], list[T]]: ...
46024602
@deco
46034603
def defer() -> int: ...
46044604
[builtins fixtures/list.pyi]
4605+
4606+
[case testProtocolClassValDescriptor]
4607+
from typing import Any, Protocol, overload, ClassVar, Type
4608+
4609+
class Desc:
4610+
@overload
4611+
def __get__(self, instance: None, owner: object) -> Desc: ...
4612+
@overload
4613+
def __get__(self, instance: object, owner: object) -> int: ...
4614+
def __get__(self, instance, owner):
4615+
pass
4616+
4617+
class P(Protocol):
4618+
x: ClassVar[Desc]
4619+
4620+
class C:
4621+
x = Desc()
4622+
4623+
t: P = C()
4624+
reveal_type(t.x) # N: Revealed type is "builtins.int"
4625+
tt: Type[P] = C
4626+
reveal_type(tt.x) # N: Revealed type is "__main__.Desc"
4627+
4628+
bad: P = C # E: Incompatible types in assignment (expression has type "type[C]", variable has type "P") \
4629+
# N: Following member(s) of "C" have conflicts: \
4630+
# N: x: expected "int", got "Desc"
4631+
4632+
[case testProtocolClassValCallable]
4633+
from typing import Any, Protocol, overload, ClassVar, Type, Callable
4634+
4635+
class P(Protocol):
4636+
foo: Callable[[object], int]
4637+
bar: ClassVar[Callable[[object], int]]
4638+
4639+
class C:
4640+
foo: Callable[[object], int]
4641+
bar: ClassVar[Callable[[object], int]]
4642+
4643+
t: P = C()
4644+
reveal_type(t.foo) # N: Revealed type is "def (builtins.object) -> builtins.int"
4645+
reveal_type(t.bar) # N: Revealed type is "def () -> builtins.int"
4646+
tt: Type[P] = C
4647+
reveal_type(tt.foo) # N: Revealed type is "def (builtins.object) -> builtins.int"
4648+
reveal_type(tt.bar) # N: Revealed type is "def (builtins.object) -> builtins.int"

0 commit comments

Comments
 (0)