Skip to content

Commit c3bfa0d

Browse files
ilevkivskyiesarp
authored andcommitted
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 8241059 commit c3bfa0d

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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy