Skip to content

Commit fe27572

Browse files
authored
[red-knot] Document current state of attribute assignment diagnostics (#16746)
## Summary A follow-up to #16705 which documents various kinds of diagnostics that can appear when assigning to an attribute. ## Test Plan New snapshot tests.
1 parent a467e7c commit fe27572

9 files changed

+531
-0
lines changed
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
# Attribute assignment
2+
3+
<!-- snapshot-diagnostics -->
4+
5+
This test suite demonstrates various kinds of diagnostics that can be emitted in a
6+
`obj.attr = value` assignment.
7+
8+
## Instance attributes with class-level defaults
9+
10+
These can be set on instances and on class objects.
11+
12+
```py
13+
class C:
14+
attr: int = 0
15+
16+
instance = C()
17+
instance.attr = 1 # fine
18+
instance.attr = "wrong" # error: [invalid-assignment]
19+
20+
C.attr = 1 # fine
21+
C.attr = "wrong" # error: [invalid-assignment]
22+
```
23+
24+
## Pure instance attributes
25+
26+
These can only be set on instances. When trying to set them on class objects, we generate a useful
27+
diagnostic that mentions that the attribute is only available on instances.
28+
29+
```py
30+
class C:
31+
def __init__(self):
32+
self.attr: int = 0
33+
34+
instance = C()
35+
instance.attr = 1 # fine
36+
instance.attr = "wrong" # error: [invalid-assignment]
37+
38+
C.attr = 1 # error: [invalid-attribute-access]
39+
```
40+
41+
## `ClassVar`s
42+
43+
These can only be set on class objects. When trying to set them on instances, we generate a useful
44+
diagnostic that mentions that the attribute is only available on class objects.
45+
46+
```py
47+
from typing import ClassVar
48+
49+
class C:
50+
attr: ClassVar[int] = 0
51+
52+
C.attr = 1 # fine
53+
C.attr = "wrong" # error: [invalid-assignment]
54+
55+
instance = C()
56+
instance.attr = 1 # error: [invalid-attribute-access]
57+
```
58+
59+
## Unknown attributes
60+
61+
When trying to set an attribute that is not defined, we also emit errors:
62+
63+
```py
64+
class C: ...
65+
66+
C.non_existent = 1 # error: [unresolved-attribute]
67+
68+
instance = C()
69+
instance.non_existent = 1 # error: [unresolved-attribute]
70+
```
71+
72+
## Possibly-unbound attributes
73+
74+
When trying to set an attribute that is not defined in all branches, we emit errors:
75+
76+
```py
77+
def _(flag: bool) -> None:
78+
class C:
79+
if flag:
80+
attr: int = 0
81+
82+
C.attr = 1 # error: [possibly-unbound-attribute]
83+
84+
instance = C()
85+
instance.attr = 1 # error: [possibly-unbound-attribute]
86+
```
87+
88+
## Data descriptors
89+
90+
When assigning to a data descriptor attribute, we implicitly call the descriptor's `__set__` method.
91+
This can lead to various kinds of diagnostics.
92+
93+
### Invalid argument type
94+
95+
```py
96+
class Descriptor:
97+
def __set__(self, instance: object, value: int) -> None:
98+
pass
99+
100+
class C:
101+
attr: Descriptor = Descriptor()
102+
103+
instance = C()
104+
instance.attr = 1 # fine
105+
106+
# TODO: ideally, we would mention why this is an invalid assignment (wrong argument type for `value` parameter)
107+
instance.attr = "wrong" # error: [invalid-assignment]
108+
```
109+
110+
### Invalid `__set__` method signature
111+
112+
```py
113+
class WrongDescriptor:
114+
def __set__(self, instance: object, value: int, extra: int) -> None:
115+
pass
116+
117+
class C:
118+
attr: WrongDescriptor = WrongDescriptor()
119+
120+
instance = C()
121+
122+
# TODO: ideally, we would mention why this is an invalid assignment (wrong number of arguments for `__set__`)
123+
instance.attr = 1 # error: [invalid-assignment]
124+
```
125+
126+
## Setting attributes on union types
127+
128+
```py
129+
def _(flag: bool) -> None:
130+
if flag:
131+
class C1:
132+
attr: int = 0
133+
134+
else:
135+
class C1:
136+
attr: str = ""
137+
138+
# TODO: The error message here could be improved to explain why the assignment fails.
139+
C1.attr = 1 # error: [invalid-assignment]
140+
141+
class C2:
142+
if flag:
143+
attr: int = 0
144+
else:
145+
attr: str = ""
146+
147+
# TODO: This should be an error
148+
C2.attr = 1
149+
```
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
---
2+
source: crates/red_knot_test/src/lib.rs
3+
expression: snapshot
4+
---
5+
---
6+
mdtest name: attribute_assignment.md - Attribute assignment - Data descriptors - Invalid `__set__` method signature
7+
mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/attribute_assignment.md
8+
---
9+
10+
# Python source files
11+
12+
## mdtest_snippet.py
13+
14+
```
15+
1 | class WrongDescriptor:
16+
2 | def __set__(self, instance: object, value: int, extra: int) -> None:
17+
3 | pass
18+
4 |
19+
5 | class C:
20+
6 | attr: WrongDescriptor = WrongDescriptor()
21+
7 |
22+
8 | instance = C()
23+
9 |
24+
10 | # TODO: ideally, we would mention why this is an invalid assignment (wrong number of arguments for `__set__`)
25+
11 | instance.attr = 1 # error: [invalid-assignment]
26+
```
27+
28+
# Diagnostics
29+
30+
```
31+
error: lint:invalid-assignment
32+
--> /src/mdtest_snippet.py:11:1
33+
|
34+
10 | # TODO: ideally, we would mention why this is an invalid assignment (wrong number of arguments for `__set__`)
35+
11 | instance.attr = 1 # error: [invalid-assignment]
36+
| ^^^^^^^^^^^^^ Invalid assignment to data descriptor attribute `attr` on type `C` with custom `__set__` method
37+
|
38+
39+
```
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
---
2+
source: crates/red_knot_test/src/lib.rs
3+
expression: snapshot
4+
---
5+
---
6+
mdtest name: attribute_assignment.md - Attribute assignment - Data descriptors - Invalid argument type
7+
mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/attribute_assignment.md
8+
---
9+
10+
# Python source files
11+
12+
## mdtest_snippet.py
13+
14+
```
15+
1 | class Descriptor:
16+
2 | def __set__(self, instance: object, value: int) -> None:
17+
3 | pass
18+
4 |
19+
5 | class C:
20+
6 | attr: Descriptor = Descriptor()
21+
7 |
22+
8 | instance = C()
23+
9 | instance.attr = 1 # fine
24+
10 |
25+
11 | # TODO: ideally, we would mention why this is an invalid assignment (wrong argument type for `value` parameter)
26+
12 | instance.attr = "wrong" # error: [invalid-assignment]
27+
```
28+
29+
# Diagnostics
30+
31+
```
32+
error: lint:invalid-assignment
33+
--> /src/mdtest_snippet.py:12:1
34+
|
35+
11 | # TODO: ideally, we would mention why this is an invalid assignment (wrong argument type for `value` parameter)
36+
12 | instance.attr = "wrong" # error: [invalid-assignment]
37+
| ^^^^^^^^^^^^^ Invalid assignment to data descriptor attribute `attr` on type `C` with custom `__set__` method
38+
|
39+
40+
```
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
---
2+
source: crates/red_knot_test/src/lib.rs
3+
expression: snapshot
4+
---
5+
---
6+
mdtest name: attribute_assignment.md - Attribute assignment - Instance attributes with class-level defaults
7+
mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/attribute_assignment.md
8+
---
9+
10+
# Python source files
11+
12+
## mdtest_snippet.py
13+
14+
```
15+
1 | class C:
16+
2 | attr: int = 0
17+
3 |
18+
4 | instance = C()
19+
5 | instance.attr = 1 # fine
20+
6 | instance.attr = "wrong" # error: [invalid-assignment]
21+
7 |
22+
8 | C.attr = 1 # fine
23+
9 | C.attr = "wrong" # error: [invalid-assignment]
24+
```
25+
26+
# Diagnostics
27+
28+
```
29+
error: lint:invalid-assignment
30+
--> /src/mdtest_snippet.py:6:1
31+
|
32+
4 | instance = C()
33+
5 | instance.attr = 1 # fine
34+
6 | instance.attr = "wrong" # error: [invalid-assignment]
35+
| ^^^^^^^^^^^^^ Object of type `Literal["wrong"]` is not assignable to attribute `attr` of type `int`
36+
7 |
37+
8 | C.attr = 1 # fine
38+
|
39+
40+
```
41+
42+
```
43+
error: lint:invalid-assignment
44+
--> /src/mdtest_snippet.py:9:1
45+
|
46+
8 | C.attr = 1 # fine
47+
9 | C.attr = "wrong" # error: [invalid-assignment]
48+
| ^^^^^^ Object of type `Literal["wrong"]` is not assignable to attribute `attr` of type `int`
49+
|
50+
51+
```
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
---
2+
source: crates/red_knot_test/src/lib.rs
3+
expression: snapshot
4+
---
5+
---
6+
mdtest name: attribute_assignment.md - Attribute assignment - Possibly-unbound attributes
7+
mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/attribute_assignment.md
8+
---
9+
10+
# Python source files
11+
12+
## mdtest_snippet.py
13+
14+
```
15+
1 | def _(flag: bool) -> None:
16+
2 | class C:
17+
3 | if flag:
18+
4 | attr: int = 0
19+
5 |
20+
6 | C.attr = 1 # error: [possibly-unbound-attribute]
21+
7 |
22+
8 | instance = C()
23+
9 | instance.attr = 1 # error: [possibly-unbound-attribute]
24+
```
25+
26+
# Diagnostics
27+
28+
```
29+
warning: lint:possibly-unbound-attribute
30+
--> /src/mdtest_snippet.py:6:5
31+
|
32+
4 | attr: int = 0
33+
5 |
34+
6 | C.attr = 1 # error: [possibly-unbound-attribute]
35+
| ------ Attribute `attr` on type `Literal[C]` is possibly unbound
36+
7 |
37+
8 | instance = C()
38+
|
39+
40+
```
41+
42+
```
43+
warning: lint:possibly-unbound-attribute
44+
--> /src/mdtest_snippet.py:9:5
45+
|
46+
8 | instance = C()
47+
9 | instance.attr = 1 # error: [possibly-unbound-attribute]
48+
| ------------- Attribute `attr` on type `C` is possibly unbound
49+
|
50+
51+
```
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
---
2+
source: crates/red_knot_test/src/lib.rs
3+
expression: snapshot
4+
---
5+
---
6+
mdtest name: attribute_assignment.md - Attribute assignment - Pure instance attributes
7+
mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/attribute_assignment.md
8+
---
9+
10+
# Python source files
11+
12+
## mdtest_snippet.py
13+
14+
```
15+
1 | class C:
16+
2 | def __init__(self):
17+
3 | self.attr: int = 0
18+
4 |
19+
5 | instance = C()
20+
6 | instance.attr = 1 # fine
21+
7 | instance.attr = "wrong" # error: [invalid-assignment]
22+
8 |
23+
9 | C.attr = 1 # error: [invalid-attribute-access]
24+
```
25+
26+
# Diagnostics
27+
28+
```
29+
error: lint:invalid-assignment
30+
--> /src/mdtest_snippet.py:7:1
31+
|
32+
5 | instance = C()
33+
6 | instance.attr = 1 # fine
34+
7 | instance.attr = "wrong" # error: [invalid-assignment]
35+
| ^^^^^^^^^^^^^ Object of type `Literal["wrong"]` is not assignable to attribute `attr` of type `int`
36+
8 |
37+
9 | C.attr = 1 # error: [invalid-attribute-access]
38+
|
39+
40+
```
41+
42+
```
43+
error: lint:invalid-attribute-access
44+
--> /src/mdtest_snippet.py:9:1
45+
|
46+
7 | instance.attr = "wrong" # error: [invalid-assignment]
47+
8 |
48+
9 | C.attr = 1 # error: [invalid-attribute-access]
49+
| ^^^^^^ Cannot assign to instance attribute `attr` from the class object `Literal[C]`
50+
|
51+
52+
```

0 commit comments

Comments
 (0)