@@ -133,130 +133,6 @@ reveal_type(C.non_data_descriptor) # revealed: Unknown | Literal["non-data"]
133133C.data_descriptor = " something else" # This is okay
134134```
135135
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-
260136## Descriptor protocol for class objects
261137
262138When attributes are accessed on a class object, the following [ precedence chain] is used:
@@ -432,6 +308,55 @@ def _(flag: bool):
432308 reveal_type(C7.union_of_class_data_descriptor_and_attribute) # revealed: Literal["data", 2]
433309```
434310
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+
435360## Built-in ` property ` descriptor
436361
437362The built-in ` property ` decorator creates a descriptor. The names for attribute reads/writes are
@@ -624,6 +549,39 @@ reveal_type(C.descriptor) # revealed: Descriptor
624549reveal_type(C().descriptor) # revealed: Descriptor
625550```
626551
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+
627585## Possibly-unbound ` __get__ ` method
628586
629587``` py
@@ -641,6 +599,47 @@ def _(flag: bool):
641599 reveal_type(C().descriptor) # revealed: int | MaybeDescriptor
642600```
643601
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+
644643## Dunder methods
645644
646645Dunder methods are looked up on the meta type, but we still need to invoke the descriptor protocol:
0 commit comments