Skip to content

Commit 1f0ad67

Browse files
sharkdpmishamsk
andauthored
[red-knot] Initial set of descriptor protocol tests (#15972)
## Summary This is a first step towards creating a test suite for [descriptors](https://docs.python.org/3/howto/descriptor.html). It does not (yet) aim to be exhaustive. relevant ticket: #15966 ## Test Plan Compared desired behavior with the runtime behavior and the behavior of existing type checkers. --------- Co-authored-by: Mike Perlov <[email protected]>
1 parent a84b27e commit 1f0ad67

File tree

1 file changed

+199
-0
lines changed

1 file changed

+199
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
# Descriptor protocol
2+
3+
[Descriptors] let objects customize attribute lookup, storage, and deletion.
4+
5+
A descriptor is an attribute value that has one of the methods in the descriptor protocol. Those
6+
methods are `__get__()`, `__set__()`, and `__delete__()`. If any of those methods are defined for an
7+
attribute, it is said to be a descriptor.
8+
9+
## Basic example
10+
11+
An introductory example, modeled after a [simple example] in the primer on descriptors, involving a
12+
descriptor that returns a constant value:
13+
14+
```py
15+
from typing import Literal
16+
17+
class Ten:
18+
def __get__(self, instance: object, owner: type | None = None) -> Literal[10]:
19+
return 10
20+
21+
def __set__(self, instance: object, value: Literal[10]) -> None:
22+
pass
23+
24+
class C:
25+
ten = Ten()
26+
27+
c = C()
28+
29+
# TODO: this should be `Literal[10]`
30+
reveal_type(c.ten) # revealed: Unknown | Ten
31+
32+
# TODO: This should `Literal[10]`
33+
reveal_type(C.ten) # revealed: Unknown | Ten
34+
35+
# These are fine:
36+
c.ten = 10
37+
C.ten = 10
38+
39+
# TODO: Both of these should be errors
40+
c.ten = 11
41+
C.ten = 11
42+
```
43+
44+
## Different types for `__get__` and `__set__`
45+
46+
The return type of `__get__` and the value type of `__set__` can be different:
47+
48+
```py
49+
class FlexibleInt:
50+
def __init__(self):
51+
self._value: int | None = None
52+
53+
def __get__(self, instance: object, owner: type | None = None) -> int | None:
54+
return self._value
55+
56+
def __set__(self, instance: object, value: int | str) -> None:
57+
self._value = int(value)
58+
59+
class C:
60+
flexible_int = FlexibleInt()
61+
62+
c = C()
63+
64+
# TODO: should be `int | None`
65+
reveal_type(c.flexible_int) # revealed: Unknown | FlexibleInt
66+
67+
c.flexible_int = 42 # okay
68+
c.flexible_int = "42" # also okay!
69+
70+
# TODO: should be `int | None`
71+
reveal_type(c.flexible_int) # revealed: Unknown | FlexibleInt
72+
73+
# TODO: should be an error
74+
c.flexible_int = None # not okay
75+
76+
# TODO: should be `int | None`
77+
reveal_type(c.flexible_int) # revealed: Unknown | FlexibleInt
78+
```
79+
80+
## Built-in `property` descriptor
81+
82+
The built-in `property` decorator creates a descriptor. The names for attribute reads/writes are
83+
determined by the return type of the `name` method and the parameter type of the setter,
84+
respectively.
85+
86+
```py
87+
class C:
88+
_name: str | None = None
89+
90+
@property
91+
def name(self) -> str:
92+
return self._name or "Unset"
93+
# TODO: No diagnostic should be emitted here
94+
# error: [unresolved-attribute] "Type `Literal[name]` has no attribute `setter`"
95+
@name.setter
96+
def name(self, value: str | None) -> None:
97+
self._value = value
98+
99+
c = C()
100+
101+
reveal_type(c._name) # revealed: str | None
102+
103+
# Should be `str`
104+
reveal_type(c.name) # revealed: @Todo(bound method)
105+
106+
# Should be `builtins.property`
107+
reveal_type(C.name) # revealed: Literal[name]
108+
109+
# This is fine:
110+
c.name = "new"
111+
112+
c.name = None
113+
114+
# TODO: this should be an error
115+
c.name = 42
116+
```
117+
118+
## Built-in `classmethod` descriptor
119+
120+
Similarly to `property`, `classmethod` decorator creates an implicit descriptor that binds the first
121+
argument to the class instead of the instance.
122+
123+
```py
124+
class C:
125+
def __init__(self, value: str) -> None:
126+
self._name: str = value
127+
128+
@classmethod
129+
def factory(cls, value: str) -> "C":
130+
return cls(value)
131+
132+
@classmethod
133+
def get_name(cls) -> str:
134+
return cls.__name__
135+
136+
c1 = C.factory("test") # okay
137+
138+
# TODO: should be `C`
139+
reveal_type(c1) # revealed: @Todo(return type)
140+
141+
# TODO: should be `str`
142+
reveal_type(C.get_name()) # revealed: @Todo(return type)
143+
144+
# TODO: should be `str`
145+
reveal_type(C("42").get_name()) # revealed: @Todo(bound method)
146+
```
147+
148+
## Descriptors only work when used as class variables
149+
150+
From the descriptor guide:
151+
152+
> Descriptors only work when used as class variables. When put in instances, they have no effect.
153+
154+
```py
155+
from typing import Literal
156+
157+
class Ten:
158+
def __get__(self, instance: object, owner: type | None = None) -> Literal[10]:
159+
return 10
160+
161+
class C:
162+
def __init__(self):
163+
self.ten = Ten()
164+
165+
reveal_type(C().ten) # revealed: Unknown | Ten
166+
```
167+
168+
## Descriptors distinguishing between class and instance access
169+
170+
Overloads can be used to distinguish between when a descriptor is accessed on a class object and
171+
when it is accessed on an instance. A real-world example of this is the `__get__` method on
172+
`types.FunctionType`.
173+
174+
```py
175+
from typing_extensions import Literal, LiteralString, overload
176+
177+
class Descriptor:
178+
@overload
179+
def __get__(self, instance: None, owner: type, /) -> Literal["called on class object"]: ...
180+
@overload
181+
def __get__(self, instance: object, owner: type | None = None, /) -> Literal["called on instance"]: ...
182+
def __get__(self, instance, owner=None, /) -> LiteralString:
183+
if instance:
184+
return "called on instance"
185+
else:
186+
return "called on class object"
187+
188+
class C:
189+
d = Descriptor()
190+
191+
# TODO: should be `Literal["called on class object"]
192+
reveal_type(C.d) # revealed: Unknown | Descriptor
193+
194+
# TODO: should be `Literal["called on instance"]
195+
reveal_type(C().d) # revealed: Unknown | Descriptor
196+
```
197+
198+
[descriptors]: https://docs.python.org/3/howto/descriptor.html
199+
[simple example]: https://docs.python.org/3/howto/descriptor.html#simple-example-a-descriptor-that-returns-a-constant

0 commit comments

Comments
 (0)