@@ -133,130 +133,6 @@ reveal_type(C.non_data_descriptor) # revealed: Unknown | Literal["non-data"]
133
133
C.data_descriptor = " something else" # This is okay
134
134
```
135
135
136
- ## Possibly unbound descriptor attributes
137
-
138
- ``` py
139
- class DataDescriptor :
140
- def __get__ (self , instance : object , owner : type | None = None ) -> int :
141
- return 1
142
-
143
- def __set__ (self , instance : int , value ) -> None :
144
- pass
145
-
146
- class NonDataDescriptor :
147
- def __get__ (self , instance : object , owner : type | None = None ) -> int :
148
- return 1
149
-
150
- def _ (flag : bool ):
151
- class PossiblyUnbound :
152
- if flag:
153
- non_data: NonDataDescriptor = NonDataDescriptor()
154
- data: DataDescriptor = DataDescriptor()
155
-
156
- # error: [possibly-unbound-attribute] "Attribute `non_data` on type `Literal[PossiblyUnbound]` is possibly unbound"
157
- reveal_type(PossiblyUnbound.non_data) # revealed: int
158
-
159
- # error: [possibly-unbound-attribute] "Attribute `non_data` on type `PossiblyUnbound` is possibly unbound"
160
- reveal_type(PossiblyUnbound().non_data) # revealed: int
161
-
162
- # error: [possibly-unbound-attribute] "Attribute `data` on type `Literal[PossiblyUnbound]` is possibly unbound"
163
- reveal_type(PossiblyUnbound.data) # revealed: int
164
-
165
- # error: [possibly-unbound-attribute] "Attribute `data` on type `PossiblyUnbound` is possibly unbound"
166
- reveal_type(PossiblyUnbound().data) # revealed: int
167
- ```
168
-
169
- ## Unions of descriptors and non-descriptor attributes
170
-
171
- ``` py
172
- from typing import Literal
173
-
174
- def _ (flag1 : bool , flag2 : bool , flag3 : bool , flag4 : bool , flag5 : bool , flag6 : bool , flag7 : bool , flag8 : bool ):
175
- if flag1:
176
- class Descriptor :
177
- if flag2:
178
- def __get__ (self , instance : object , owner : type | None = None ) -> Literal[1 ]:
179
- return 1
180
- else :
181
- def __get__ (self , instance : object , owner : type | None = None ) -> Literal[2 ]:
182
- return 2
183
-
184
- else :
185
- class Descriptor :
186
- def __get__ (self , instance : object , owner : type | None = None ) -> Literal[3 ]:
187
- return 3
188
-
189
- class OtherDescriptor :
190
- def __get__ (self , instance : object , owner : type | None = None ) -> Literal[4 ]:
191
- return 4
192
-
193
- if flag4:
194
- class C :
195
- if flag5:
196
- x: Descriptor | OtherDescriptor = Descriptor() if flag6 else OtherDescriptor()
197
- else :
198
- x: Literal[5 ] = 5
199
-
200
- else :
201
- class C :
202
- if flag7:
203
- x: Literal[6 ] = 6
204
- else :
205
- def f (self ):
206
- self .x: Literal[7 ] = 7
207
-
208
- class OtherC :
209
- x: Literal[8 ] = 8
210
-
211
- c = C() if flag8 else OtherC()
212
-
213
- # Note: the attribute could be possibly-unbound, since we don't know if `f` has been called.
214
-
215
- # error: [possibly-unbound-attribute]
216
- reveal_type(c.x) # revealed: Literal[1, 2, 3, 4, 5, 6, 7, 8]
217
- ```
218
-
219
- ## Recursive descriptors
220
-
221
- The descriptor protocol is recursive, i.e. looking up ` __get__ ` can involve triggering the
222
- descriptor protocol on the callable:
223
-
224
- ``` py
225
- from __future__ import annotations
226
-
227
- class ReturnedCallable2 :
228
- def __call__ (self , descriptor : Descriptor1, instance : None , owner : type[C]) -> int :
229
- return 1
230
-
231
- class ReturnedCallable1 :
232
- def __call__ (self , descriptor : Descriptor2, instance : Callable1, owner : type[Callable1]) -> ReturnedCallable2:
233
- return ReturnedCallable2()
234
-
235
- class Callable3 :
236
- def __call__ (self , descriptor : Descriptor3, instance : Callable2, owner : type[Callable2]) -> ReturnedCallable1:
237
- return ReturnedCallable1()
238
-
239
- class Descriptor3 :
240
- __get__ : Callable3 = Callable3()
241
-
242
- class Callable2 :
243
- __call__ : Descriptor3 = Descriptor3()
244
-
245
- class Descriptor2 :
246
- __get__ : Callable2 = Callable2()
247
-
248
- class Callable1 :
249
- __call__ : Descriptor2 = Descriptor2()
250
-
251
- class Descriptor1 :
252
- __get__ : Callable1 = Callable1()
253
-
254
- class C :
255
- d: Descriptor1 = Descriptor1()
256
-
257
- reveal_type(C.d) # revealed: int
258
- ```
259
-
260
136
## Descriptor protocol for class objects
261
137
262
138
When attributes are accessed on a class object, the following [ precedence chain] is used:
@@ -432,6 +308,55 @@ def _(flag: bool):
432
308
reveal_type(C7.union_of_class_data_descriptor_and_attribute) # revealed: Literal["data", 2]
433
309
```
434
310
311
+ ## Partial fall back
312
+
313
+ Our implementation of the descriptor protocol takes into account that symbols can be possibly
314
+ unbound. In those cases, we fall back to lower precedence steps of the descriptor protocol and union
315
+ all possible results accordingly. We start by defining a data and a non-data descriptor:
316
+
317
+ ``` py
318
+ from typing import Literal
319
+
320
+ class DataDescriptor :
321
+ def __get__ (self , instance : object , owner : type | None = None ) -> Literal[" data" ]:
322
+ return " data"
323
+
324
+ def __set__ (self , instance : object , value : int ) -> None :
325
+ pass
326
+
327
+ class NonDataDescriptor :
328
+ def __get__ (self , instance : object , owner : type | None = None ) -> Literal[" non-data" ]:
329
+ return " non-data"
330
+ ```
331
+
332
+ Then, we demonstrate that we fall back to an instance attribute if a data descriptor is possibly
333
+ unbound:
334
+
335
+ ``` py
336
+ def f1 (flag : bool ):
337
+ class C1 :
338
+ if flag:
339
+ attr = DataDescriptor()
340
+
341
+ def f (self ):
342
+ self .attr = " normal"
343
+
344
+ reveal_type(C1().attr) # revealed: Unknown | Literal["data", "normal"]
345
+ ```
346
+
347
+ We never treat implicit instance attributes as definitely bound, so we fall back to the non-data
348
+ descriptor here:
349
+
350
+ ``` py
351
+ def f2 (flag : bool ):
352
+ class C2 :
353
+ def f (self ):
354
+ self .attr = " normal"
355
+ attr = NonDataDescriptor()
356
+
357
+ reveal_type(C2().attr) # revealed: Unknown | Literal["non-data", "normal"]
358
+ ```
359
+
435
360
## Built-in ` property ` descriptor
436
361
437
362
The built-in ` property ` decorator creates a descriptor. The names for attribute reads/writes are
@@ -624,6 +549,39 @@ reveal_type(C.descriptor) # revealed: Descriptor
624
549
reveal_type(C().descriptor) # revealed: Descriptor
625
550
```
626
551
552
+ ## Possibly unbound descriptor attributes
553
+
554
+ ``` py
555
+ class DataDescriptor :
556
+ def __get__ (self , instance : object , owner : type | None = None ) -> int :
557
+ return 1
558
+
559
+ def __set__ (self , instance : int , value ) -> None :
560
+ pass
561
+
562
+ class NonDataDescriptor :
563
+ def __get__ (self , instance : object , owner : type | None = None ) -> int :
564
+ return 1
565
+
566
+ def _ (flag : bool ):
567
+ class PossiblyUnbound :
568
+ if flag:
569
+ non_data: NonDataDescriptor = NonDataDescriptor()
570
+ data: DataDescriptor = DataDescriptor()
571
+
572
+ # error: [possibly-unbound-attribute] "Attribute `non_data` on type `Literal[PossiblyUnbound]` is possibly unbound"
573
+ reveal_type(PossiblyUnbound.non_data) # revealed: int
574
+
575
+ # error: [possibly-unbound-attribute] "Attribute `non_data` on type `PossiblyUnbound` is possibly unbound"
576
+ reveal_type(PossiblyUnbound().non_data) # revealed: int
577
+
578
+ # error: [possibly-unbound-attribute] "Attribute `data` on type `Literal[PossiblyUnbound]` is possibly unbound"
579
+ reveal_type(PossiblyUnbound.data) # revealed: int
580
+
581
+ # error: [possibly-unbound-attribute] "Attribute `data` on type `PossiblyUnbound` is possibly unbound"
582
+ reveal_type(PossiblyUnbound().data) # revealed: int
583
+ ```
584
+
627
585
## Possibly-unbound ` __get__ ` method
628
586
629
587
``` py
@@ -641,6 +599,47 @@ def _(flag: bool):
641
599
reveal_type(C().descriptor) # revealed: int | MaybeDescriptor
642
600
```
643
601
602
+ ## Descriptors with non-function ` __get__ ` callables that are descriptors themselves
603
+
604
+ The descriptor protocol is recursive, i.e. looking up ` __get__ ` can involve triggering the
605
+ descriptor protocol on the callables ` __call__ ` method:
606
+
607
+ ``` py
608
+ from __future__ import annotations
609
+
610
+ class ReturnedCallable2 :
611
+ def __call__ (self , descriptor : Descriptor1, instance : None , owner : type[C]) -> int :
612
+ return 1
613
+
614
+ class ReturnedCallable1 :
615
+ def __call__ (self , descriptor : Descriptor2, instance : Callable1, owner : type[Callable1]) -> ReturnedCallable2:
616
+ return ReturnedCallable2()
617
+
618
+ class Callable3 :
619
+ def __call__ (self , descriptor : Descriptor3, instance : Callable2, owner : type[Callable2]) -> ReturnedCallable1:
620
+ return ReturnedCallable1()
621
+
622
+ class Descriptor3 :
623
+ __get__ : Callable3 = Callable3()
624
+
625
+ class Callable2 :
626
+ __call__ : Descriptor3 = Descriptor3()
627
+
628
+ class Descriptor2 :
629
+ __get__ : Callable2 = Callable2()
630
+
631
+ class Callable1 :
632
+ __call__ : Descriptor2 = Descriptor2()
633
+
634
+ class Descriptor1 :
635
+ __get__ : Callable1 = Callable1()
636
+
637
+ class C :
638
+ d: Descriptor1 = Descriptor1()
639
+
640
+ reveal_type(C.d) # revealed: int
641
+ ```
642
+
644
643
## Dunder methods
645
644
646
645
Dunder methods are looked up on the meta type, but we still need to invoke the descriptor protocol:
0 commit comments