Skip to content

Commit d03b12e

Browse files
authored
[red-knot] Assignments to attributes (#16705)
## Summary This changeset adds proper support for assignments to attributes: ```py obj.attr = value ``` In particular, the following new features are now available: * We previously didn't raise any errors if you tried to assign to a non-existing attribute `attr`. This is now fixed. * If `type(obj).attr` is a data descriptor, we now call its `__set__` method instead of trying to assign to the load-context type of `obj.attr`, which can be different for data descriptors. * An initial attempt was made to support unions and intersections, as well as possibly-unbound situations. There are some remaining TODOs in tests, but they only affect edge cases. Having nested diagnostics would be one way that could help solve the remaining cases, I believe. ## Follow ups The following things are planned as follow-ups: - Write a test suite with snapshot diagnostics for various attribute assignment errors - Improve the diagnostics. An easy improvement would be to highlight the right hand side of the assignment as a secondary span (with the rhs type as additional information). Some other ideas are mentioned in TODO comments in this PR. - Improve the union/intersection/possible-unboundness handling - Add support for calling custom `__setattr__` methods (see new false positive in the ecosystem results) ## Ecosystem changes Some changes are related to assignments on attributes with a custom `__setattr__` method (see above). Since we didn't notice missing attributes at all in store context previously, these are new. The other changes are related to properties. We previously used their read-context type to test the assignment. That results in weird error messages, as we often see assignments to `self.property` and then we think that those are instance attributes *and* descriptors, leading to union types. Now we properly look them up on the meta type, see the decorated function, and try to overwrite it with the new value (as we don't understand decorators yet). Long story short: the errors are still weird, we need to understand decorators to make them go away. ## Test Plan New Markdown tests
1 parent 14c5ed5 commit d03b12e

File tree

4 files changed

+570
-103
lines changed

4 files changed

+570
-103
lines changed

crates/red_knot_python_semantic/resources/mdtest/attributes.md

+103
Original file line numberDiff line numberDiff line change
@@ -818,40 +818,74 @@ def _(flag: bool):
818818
if flag:
819819
class C1:
820820
x = 1
821+
y: int = 1
821822

822823
else:
823824
class C1:
824825
x = 2
826+
y: int | str = "b"
825827

826828
reveal_type(C1.x) # revealed: Unknown | Literal[1, 2]
829+
reveal_type(C1.y) # revealed: int | str
830+
831+
C1.y = 100
832+
# error: [invalid-assignment] "Object of type `Literal["problematic"]` is not assignable to attribute `y` on type `Literal[C1, C1]`"
833+
C1.y = "problematic"
827834

828835
class C2:
829836
if flag:
830837
x = 3
838+
y: int = 3
831839
else:
832840
x = 4
841+
y: int | str = "d"
833842

834843
reveal_type(C2.x) # revealed: Unknown | Literal[3, 4]
844+
reveal_type(C2.y) # revealed: int | str
845+
846+
C2.y = 100
847+
# error: [invalid-assignment] "Object of type `None` is not assignable to attribute `y` of type `int | str`"
848+
C2.y = None
849+
# TODO: should be an error, needs more sophisticated union handling in `validate_attribute_assignment`
850+
C2.y = "problematic"
835851

836852
if flag:
837853
class Meta3(type):
838854
x = 5
855+
y: int = 5
839856

840857
else:
841858
class Meta3(type):
842859
x = 6
860+
y: int | str = "f"
843861

844862
class C3(metaclass=Meta3): ...
845863
reveal_type(C3.x) # revealed: Unknown | Literal[5, 6]
864+
reveal_type(C3.y) # revealed: int | str
865+
866+
C3.y = 100
867+
# error: [invalid-assignment] "Object of type `None` is not assignable to attribute `y` of type `int | str`"
868+
C3.y = None
869+
# TODO: should be an error, needs more sophisticated union handling in `validate_attribute_assignment`
870+
C3.y = "problematic"
846871

847872
class Meta4(type):
848873
if flag:
849874
x = 7
875+
y: int = 7
850876
else:
851877
x = 8
878+
y: int | str = "h"
852879

853880
class C4(metaclass=Meta4): ...
854881
reveal_type(C4.x) # revealed: Unknown | Literal[7, 8]
882+
reveal_type(C4.y) # revealed: int | str
883+
884+
C4.y = 100
885+
# error: [invalid-assignment] "Object of type `None` is not assignable to attribute `y` of type `int | str`"
886+
C4.y = None
887+
# TODO: should be an error, needs more sophisticated union handling in `validate_attribute_assignment`
888+
C4.y = "problematic"
855889
```
856890

857891
## Unions with possibly unbound paths
@@ -875,8 +909,14 @@ def _(flag1: bool, flag2: bool):
875909
# error: [possibly-unbound-attribute] "Attribute `x` on type `Literal[C1, C2, C3]` is possibly unbound"
876910
reveal_type(C.x) # revealed: Unknown | Literal[1, 3]
877911

912+
# error: [invalid-assignment] "Object of type `Literal[100]` is not assignable to attribute `x` on type `Literal[C1, C2, C3]`"
913+
C.x = 100
914+
878915
# error: [possibly-unbound-attribute] "Attribute `x` on type `C1 | C2 | C3` is possibly unbound"
879916
reveal_type(C().x) # revealed: Unknown | Literal[1, 3]
917+
918+
# error: [invalid-assignment] "Object of type `Literal[100]` is not assignable to attribute `x` on type `C1 | C2 | C3`"
919+
C().x = 100
880920
```
881921

882922
### Possibly-unbound within a class
@@ -901,10 +941,16 @@ def _(flag: bool, flag1: bool, flag2: bool):
901941
# error: [possibly-unbound-attribute] "Attribute `x` on type `Literal[C1, C2, C3]` is possibly unbound"
902942
reveal_type(C.x) # revealed: Unknown | Literal[1, 2, 3]
903943

944+
# error: [possibly-unbound-attribute]
945+
C.x = 100
946+
904947
# Note: we might want to consider ignoring possibly-unbound diagnostics for instance attributes eventually,
905948
# see the "Possibly unbound/undeclared instance attribute" section below.
906949
# error: [possibly-unbound-attribute] "Attribute `x` on type `C1 | C2 | C3` is possibly unbound"
907950
reveal_type(C().x) # revealed: Unknown | Literal[1, 2, 3]
951+
952+
# error: [possibly-unbound-attribute]
953+
C().x = 100
908954
```
909955

910956
### Possibly-unbound within gradual types
@@ -922,6 +968,9 @@ def _(flag: bool):
922968
x: int
923969

924970
reveal_type(Derived().x) # revealed: int | Any
971+
972+
Derived().x = 1
973+
Derived().x = "a"
925974
```
926975

927976
### Attribute possibly unbound on a subclass but not on a superclass
@@ -936,8 +985,10 @@ def _(flag: bool):
936985
x = 2
937986

938987
reveal_type(Bar.x) # revealed: Unknown | Literal[2, 1]
988+
Bar.x = 3
939989

940990
reveal_type(Bar().x) # revealed: Unknown | Literal[2, 1]
991+
Bar().x = 3
941992
```
942993

943994
### Attribute possibly unbound on a subclass and on a superclass
@@ -955,8 +1006,14 @@ def _(flag: bool):
9551006
# error: [possibly-unbound-attribute]
9561007
reveal_type(Bar.x) # revealed: Unknown | Literal[2, 1]
9571008

1009+
# error: [possibly-unbound-attribute]
1010+
Bar.x = 3
1011+
9581012
# error: [possibly-unbound-attribute]
9591013
reveal_type(Bar().x) # revealed: Unknown | Literal[2, 1]
1014+
1015+
# error: [possibly-unbound-attribute]
1016+
Bar().x = 3
9601017
```
9611018

9621019
### Possibly unbound/undeclared instance attribute
@@ -975,6 +1032,9 @@ def _(flag: bool):
9751032

9761033
# error: [possibly-unbound-attribute]
9771034
reveal_type(Foo().x) # revealed: int | Unknown
1035+
1036+
# error: [possibly-unbound-attribute]
1037+
Foo().x = 1
9781038
```
9791039

9801040
#### Possibly unbound
@@ -989,6 +1049,9 @@ def _(flag: bool):
9891049
# Emitting a diagnostic in a case like this is not something we support, and it's unclear
9901050
# if we ever will (or want to)
9911051
reveal_type(Foo().x) # revealed: Unknown | Literal[1]
1052+
1053+
# Same here
1054+
Foo().x = 2
9921055
```
9931056

9941057
### Unions with all paths unbound
@@ -1003,6 +1066,11 @@ def _(flag: bool):
10031066

10041067
# error: [unresolved-attribute] "Type `Literal[C1, C2]` has no attribute `x`"
10051068
reveal_type(C.x) # revealed: Unknown
1069+
1070+
# TODO: This should ideally be a `unresolved-attribute` error. We need better union
1071+
# handling in `validate_attribute_assignment` for this.
1072+
# error: [invalid-assignment] "Object of type `Literal[1]` is not assignable to attribute `x` on type `Literal[C1, C2]`"
1073+
C.x = 1
10061074
```
10071075

10081076
## Inherited class attributes
@@ -1017,6 +1085,8 @@ class B(A): ...
10171085
class C(B): ...
10181086

10191087
reveal_type(C.X) # revealed: Unknown | Literal["foo"]
1088+
1089+
C.X = "bar"
10201090
```
10211091

10221092
### Multiple inheritance
@@ -1040,6 +1110,8 @@ reveal_type(A.__mro__)
10401110

10411111
# `E` is earlier in the MRO than `F`, so we should use the type of `E.X`
10421112
reveal_type(A.X) # revealed: Unknown | Literal[42]
1113+
1114+
A.X = 100
10431115
```
10441116

10451117
## Intersections of attributes
@@ -1057,9 +1129,13 @@ class B: ...
10571129
def _(a_and_b: Intersection[A, B]):
10581130
reveal_type(a_and_b.x) # revealed: int
10591131

1132+
a_and_b.x = 2
1133+
10601134
# Same for class objects
10611135
def _(a_and_b: Intersection[type[A], type[B]]):
10621136
reveal_type(a_and_b.x) # revealed: int
1137+
1138+
a_and_b.x = 2
10631139
```
10641140

10651141
### Attribute available on both elements
@@ -1069,6 +1145,7 @@ from knot_extensions import Intersection
10691145

10701146
class P: ...
10711147
class Q: ...
1148+
class R(P, Q): ...
10721149

10731150
class A:
10741151
x: P = P()
@@ -1078,10 +1155,12 @@ class B:
10781155

10791156
def _(a_and_b: Intersection[A, B]):
10801157
reveal_type(a_and_b.x) # revealed: P & Q
1158+
a_and_b.x = R()
10811159

10821160
# Same for class objects
10831161
def _(a_and_b: Intersection[type[A], type[B]]):
10841162
reveal_type(a_and_b.x) # revealed: P & Q
1163+
a_and_b.x = R()
10851164
```
10861165

10871166
### Possible unboundness
@@ -1091,6 +1170,7 @@ from knot_extensions import Intersection
10911170

10921171
class P: ...
10931172
class Q: ...
1173+
class R(P, Q): ...
10941174

10951175
def _(flag: bool):
10961176
class A1:
@@ -1102,11 +1182,17 @@ def _(flag: bool):
11021182
def inner1(a_and_b: Intersection[A1, B1]):
11031183
# error: [possibly-unbound-attribute]
11041184
reveal_type(a_and_b.x) # revealed: P
1185+
1186+
# error: [possibly-unbound-attribute]
1187+
a_and_b.x = R()
11051188
# Same for class objects
11061189
def inner1_class(a_and_b: Intersection[type[A1], type[B1]]):
11071190
# error: [possibly-unbound-attribute]
11081191
reveal_type(a_and_b.x) # revealed: P
11091192

1193+
# error: [possibly-unbound-attribute]
1194+
a_and_b.x = R()
1195+
11101196
class A2:
11111197
if flag:
11121198
x: P = P()
@@ -1116,6 +1202,11 @@ def _(flag: bool):
11161202

11171203
def inner2(a_and_b: Intersection[A2, B1]):
11181204
reveal_type(a_and_b.x) # revealed: P & Q
1205+
1206+
# TODO: this should not be an error, we need better intersection
1207+
# handling in `validate_attribute_assignment` for this
1208+
# error: [possibly-unbound-attribute]
1209+
a_and_b.x = R()
11191210
# Same for class objects
11201211
def inner2_class(a_and_b: Intersection[type[A2], type[B1]]):
11211212
reveal_type(a_and_b.x) # revealed: P & Q
@@ -1131,21 +1222,33 @@ def _(flag: bool):
11311222
def inner3(a_and_b: Intersection[A3, B3]):
11321223
# error: [possibly-unbound-attribute]
11331224
reveal_type(a_and_b.x) # revealed: P & Q
1225+
1226+
# error: [possibly-unbound-attribute]
1227+
a_and_b.x = R()
11341228
# Same for class objects
11351229
def inner3_class(a_and_b: Intersection[type[A3], type[B3]]):
11361230
# error: [possibly-unbound-attribute]
11371231
reveal_type(a_and_b.x) # revealed: P & Q
11381232

1233+
# error: [possibly-unbound-attribute]
1234+
a_and_b.x = R()
1235+
11391236
class A4: ...
11401237
class B4: ...
11411238

11421239
def inner4(a_and_b: Intersection[A4, B4]):
11431240
# error: [unresolved-attribute]
11441241
reveal_type(a_and_b.x) # revealed: Unknown
1242+
1243+
# error: [invalid-assignment]
1244+
a_and_b.x = R()
11451245
# Same for class objects
11461246
def inner4_class(a_and_b: Intersection[type[A4], type[B4]]):
11471247
# error: [unresolved-attribute]
11481248
reveal_type(a_and_b.x) # revealed: Unknown
1249+
1250+
# error: [invalid-assignment]
1251+
a_and_b.x = R()
11491252
```
11501253

11511254
### Intersection of implicit instance attributes

0 commit comments

Comments
 (0)