@@ -155,7 +155,9 @@ reveal_type(c_instance.declared_in_body_and_init) # revealed: str | None
155
155
156
156
reveal_type(c_instance.declared_in_body_defined_in_init) # revealed: str | None
157
157
158
- reveal_type(c_instance.bound_in_body_declared_in_init) # revealed: str | None
158
+ # TODO : This should be `str | None`. Fixing this requires an overhaul of the `Symbol` API,
159
+ # which is planned in https://github.com/astral-sh/ruff/issues/14297
160
+ reveal_type(c_instance.bound_in_body_declared_in_init) # revealed: Unknown | str | None
159
161
160
162
reveal_type(c_instance.bound_in_body_and_init) # revealed: Unknown | None | Literal["a"]
161
163
```
@@ -704,8 +706,91 @@ reveal_type(Derived().declared_in_body) # revealed: int | None
704
706
reveal_type(Derived().defined_in_init) # revealed: str | None
705
707
```
706
708
709
+ ## Accessing attributes on class objects
710
+
711
+ When accessing attributes on class objects, they are always looked up on the type of the class
712
+ object first, i.e. on the meta class:
713
+
714
+ ``` py
715
+ from typing import Literal
716
+
717
+ class Meta1 :
718
+ attr: Literal[" meta class value" ] = " meta class value"
719
+
720
+ class C1 (metaclass = Meta1 ): ...
721
+
722
+ reveal_type(C1.attr) # revealed: Literal["meta class value"]
723
+ ```
724
+
725
+ However, the meta class attribute only takes precedence over a class-level attribute if it is a data
726
+ descriptor. If it is a non-data descriptor or a normal attribute, the class-level attribute is used
727
+ instead (see the [ descriptor protocol tests] for data/non-data descriptor attributes):
728
+
729
+ ``` py
730
+ class Meta2 :
731
+ attr: str = " meta class value"
732
+
733
+ class C2 (metaclass = Meta2 ):
734
+ attr: Literal[" class value" ] = " class value"
735
+
736
+ reveal_type(C2.attr) # revealed: Literal["class value"]
737
+ ```
738
+
739
+ If the class-level attribute is only partially defined, we union the meta class attribute with the
740
+ class-level attribute:
741
+
742
+ ``` py
743
+ def _ (flag : bool ):
744
+ class Meta3 :
745
+ attr1 = " meta class value"
746
+ attr2: Literal[" meta class value" ] = " meta class value"
747
+
748
+ class C3 (metaclass = Meta3 ):
749
+ if flag:
750
+ attr1 = " class value"
751
+ # TODO : Neither mypy nor pyright show an error here, but we could consider emitting a conflicting-declaration diagnostic here.
752
+ attr2: Literal[" class value" ] = " class value"
753
+
754
+ reveal_type(C3.attr1) # revealed: Unknown | Literal["meta class value", "class value"]
755
+ reveal_type(C3.attr2) # revealed: Literal["meta class value", "class value"]
756
+ ```
757
+
758
+ If the * meta class* attribute is only partially defined, we emit a ` possibly-unbound-attribute `
759
+ diagnostic:
760
+
761
+ ``` py
762
+ def _ (flag : bool ):
763
+ class Meta4 :
764
+ if flag:
765
+ attr1: str = " meta class value"
766
+
767
+ class C4 (metaclass = Meta4 ): ...
768
+ # error: [possibly-unbound-attribute]
769
+ reveal_type(C4.attr1) # revealed: str
770
+ ```
771
+
772
+ Finally, if both the meta class attribute and the class-level attribute are only partially defined,
773
+ we union them and emit a ` possibly-unbound-attribute ` diagnostic:
774
+
775
+ ``` py
776
+ def _ (flag1 : bool , flag2 : bool ):
777
+ class Meta5 :
778
+ if flag1:
779
+ attr1 = " meta class value"
780
+
781
+ class C5 (metaclass = Meta5 ):
782
+ if flag2:
783
+ attr1 = " class value"
784
+
785
+ # error: [possibly-unbound-attribute]
786
+ reveal_type(C5.attr1) # revealed: Unknown | Literal["meta class value", "class value"]
787
+ ```
788
+
707
789
## Union of attributes
708
790
791
+ If the (meta) class is a union type or if the attribute on the (meta) class has a union type, we
792
+ infer those union types accordingly:
793
+
709
794
``` py
710
795
def _ (flag : bool ):
711
796
if flag:
@@ -716,14 +801,35 @@ def _(flag: bool):
716
801
class C1 :
717
802
x = 2
718
803
804
+ reveal_type(C1.x) # revealed: Unknown | Literal[1, 2]
805
+
719
806
class C2 :
720
807
if flag:
721
808
x = 3
722
809
else :
723
810
x = 4
724
811
725
- reveal_type(C1.x) # revealed: Unknown | Literal[1, 2]
726
812
reveal_type(C2.x) # revealed: Unknown | Literal[3, 4]
813
+
814
+ if flag:
815
+ class Meta3 (type ):
816
+ x = 5
817
+
818
+ else :
819
+ class Meta3 (type ):
820
+ x = 6
821
+
822
+ class C3 (metaclass = Meta3 ): ...
823
+ reveal_type(C3.x) # revealed: Unknown | Literal[5, 6]
824
+
825
+ class Meta4 (type ):
826
+ if flag:
827
+ x = 7
828
+ else :
829
+ x = 8
830
+
831
+ class C4 (metaclass = Meta4 ): ...
832
+ reveal_type(C4.x) # revealed: Unknown | Literal[7, 8]
727
833
```
728
834
729
835
## Inherited class attributes
@@ -883,7 +989,7 @@ def _(flag: bool):
883
989
self .x = 1
884
990
885
991
# error: [possibly-unbound-attribute]
886
- reveal_type(Foo().x) # revealed: int
992
+ reveal_type(Foo().x) # revealed: int | Unknown
887
993
```
888
994
889
995
#### Possibly unbound
@@ -1105,8 +1211,8 @@ Most attribute accesses on bool-literal types are delegated to `builtins.bool`,
1105
1211
bools are instances of that class:
1106
1212
1107
1213
``` py
1108
- reveal_type(True .__and__ ) # revealed: @Todo(overloaded method)
1109
- reveal_type(False .__or__ ) # revealed: @Todo(overloaded method)
1214
+ reveal_type(True .__and__ ) # revealed: <bound method `__and__` of `Literal[True]`>
1215
+ reveal_type(False .__or__ ) # revealed: <bound method `__or__` of `Literal[False]`>
1110
1216
```
1111
1217
1112
1218
Some attributes are special-cased, however:
@@ -1262,6 +1368,7 @@ reveal_type(C.a_none) # revealed: None
1262
1368
Some of the tests in the * Class and instance variables* section draw inspiration from
1263
1369
[ pyright's documentation] on this topic.
1264
1370
1371
+ [ descriptor protocol tests ] : descriptor_protocol.md
1265
1372
[ pyright's documentation ] : https://microsoft.github.io/pyright/#/type-concepts-advanced?id=class-and-instance-variables
1266
1373
[ typing spec on `classvar` ] : https://typing.readthedocs.io/en/latest/spec/class-compat.html#classvar
1267
1374
[ `typing.classvar` ] : https://docs.python.org/3/library/typing.html#typing.ClassVar
0 commit comments