mirror of
https://github.com/astral-sh/ruff.git
synced 2025-10-01 22:31:23 +00:00
[red-knot] Document current state of attribute assignment diagnostics (#16746)
## Summary A follow-up to https://github.com/astral-sh/ruff/pull/16705 which documents various kinds of diagnostics that can appear when assigning to an attribute. ## Test Plan New snapshot tests.
This commit is contained in:
parent
a467e7c8d3
commit
fe275725e0
9 changed files with 531 additions and 0 deletions
|
@ -0,0 +1,149 @@
|
||||||
|
# Attribute assignment
|
||||||
|
|
||||||
|
<!-- snapshot-diagnostics -->
|
||||||
|
|
||||||
|
This test suite demonstrates various kinds of diagnostics that can be emitted in a
|
||||||
|
`obj.attr = value` assignment.
|
||||||
|
|
||||||
|
## Instance attributes with class-level defaults
|
||||||
|
|
||||||
|
These can be set on instances and on class objects.
|
||||||
|
|
||||||
|
```py
|
||||||
|
class C:
|
||||||
|
attr: int = 0
|
||||||
|
|
||||||
|
instance = C()
|
||||||
|
instance.attr = 1 # fine
|
||||||
|
instance.attr = "wrong" # error: [invalid-assignment]
|
||||||
|
|
||||||
|
C.attr = 1 # fine
|
||||||
|
C.attr = "wrong" # error: [invalid-assignment]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Pure instance attributes
|
||||||
|
|
||||||
|
These can only be set on instances. When trying to set them on class objects, we generate a useful
|
||||||
|
diagnostic that mentions that the attribute is only available on instances.
|
||||||
|
|
||||||
|
```py
|
||||||
|
class C:
|
||||||
|
def __init__(self):
|
||||||
|
self.attr: int = 0
|
||||||
|
|
||||||
|
instance = C()
|
||||||
|
instance.attr = 1 # fine
|
||||||
|
instance.attr = "wrong" # error: [invalid-assignment]
|
||||||
|
|
||||||
|
C.attr = 1 # error: [invalid-attribute-access]
|
||||||
|
```
|
||||||
|
|
||||||
|
## `ClassVar`s
|
||||||
|
|
||||||
|
These can only be set on class objects. When trying to set them on instances, we generate a useful
|
||||||
|
diagnostic that mentions that the attribute is only available on class objects.
|
||||||
|
|
||||||
|
```py
|
||||||
|
from typing import ClassVar
|
||||||
|
|
||||||
|
class C:
|
||||||
|
attr: ClassVar[int] = 0
|
||||||
|
|
||||||
|
C.attr = 1 # fine
|
||||||
|
C.attr = "wrong" # error: [invalid-assignment]
|
||||||
|
|
||||||
|
instance = C()
|
||||||
|
instance.attr = 1 # error: [invalid-attribute-access]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Unknown attributes
|
||||||
|
|
||||||
|
When trying to set an attribute that is not defined, we also emit errors:
|
||||||
|
|
||||||
|
```py
|
||||||
|
class C: ...
|
||||||
|
|
||||||
|
C.non_existent = 1 # error: [unresolved-attribute]
|
||||||
|
|
||||||
|
instance = C()
|
||||||
|
instance.non_existent = 1 # error: [unresolved-attribute]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Possibly-unbound attributes
|
||||||
|
|
||||||
|
When trying to set an attribute that is not defined in all branches, we emit errors:
|
||||||
|
|
||||||
|
```py
|
||||||
|
def _(flag: bool) -> None:
|
||||||
|
class C:
|
||||||
|
if flag:
|
||||||
|
attr: int = 0
|
||||||
|
|
||||||
|
C.attr = 1 # error: [possibly-unbound-attribute]
|
||||||
|
|
||||||
|
instance = C()
|
||||||
|
instance.attr = 1 # error: [possibly-unbound-attribute]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Data descriptors
|
||||||
|
|
||||||
|
When assigning to a data descriptor attribute, we implicitly call the descriptor's `__set__` method.
|
||||||
|
This can lead to various kinds of diagnostics.
|
||||||
|
|
||||||
|
### Invalid argument type
|
||||||
|
|
||||||
|
```py
|
||||||
|
class Descriptor:
|
||||||
|
def __set__(self, instance: object, value: int) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
class C:
|
||||||
|
attr: Descriptor = Descriptor()
|
||||||
|
|
||||||
|
instance = C()
|
||||||
|
instance.attr = 1 # fine
|
||||||
|
|
||||||
|
# TODO: ideally, we would mention why this is an invalid assignment (wrong argument type for `value` parameter)
|
||||||
|
instance.attr = "wrong" # error: [invalid-assignment]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Invalid `__set__` method signature
|
||||||
|
|
||||||
|
```py
|
||||||
|
class WrongDescriptor:
|
||||||
|
def __set__(self, instance: object, value: int, extra: int) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
class C:
|
||||||
|
attr: WrongDescriptor = WrongDescriptor()
|
||||||
|
|
||||||
|
instance = C()
|
||||||
|
|
||||||
|
# TODO: ideally, we would mention why this is an invalid assignment (wrong number of arguments for `__set__`)
|
||||||
|
instance.attr = 1 # error: [invalid-assignment]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Setting attributes on union types
|
||||||
|
|
||||||
|
```py
|
||||||
|
def _(flag: bool) -> None:
|
||||||
|
if flag:
|
||||||
|
class C1:
|
||||||
|
attr: int = 0
|
||||||
|
|
||||||
|
else:
|
||||||
|
class C1:
|
||||||
|
attr: str = ""
|
||||||
|
|
||||||
|
# TODO: The error message here could be improved to explain why the assignment fails.
|
||||||
|
C1.attr = 1 # error: [invalid-assignment]
|
||||||
|
|
||||||
|
class C2:
|
||||||
|
if flag:
|
||||||
|
attr: int = 0
|
||||||
|
else:
|
||||||
|
attr: str = ""
|
||||||
|
|
||||||
|
# TODO: This should be an error
|
||||||
|
C2.attr = 1
|
||||||
|
```
|
|
@ -0,0 +1,39 @@
|
||||||
|
---
|
||||||
|
source: crates/red_knot_test/src/lib.rs
|
||||||
|
expression: snapshot
|
||||||
|
---
|
||||||
|
---
|
||||||
|
mdtest name: attribute_assignment.md - Attribute assignment - Data descriptors - Invalid `__set__` method signature
|
||||||
|
mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/attribute_assignment.md
|
||||||
|
---
|
||||||
|
|
||||||
|
# Python source files
|
||||||
|
|
||||||
|
## mdtest_snippet.py
|
||||||
|
|
||||||
|
```
|
||||||
|
1 | class WrongDescriptor:
|
||||||
|
2 | def __set__(self, instance: object, value: int, extra: int) -> None:
|
||||||
|
3 | pass
|
||||||
|
4 |
|
||||||
|
5 | class C:
|
||||||
|
6 | attr: WrongDescriptor = WrongDescriptor()
|
||||||
|
7 |
|
||||||
|
8 | instance = C()
|
||||||
|
9 |
|
||||||
|
10 | # TODO: ideally, we would mention why this is an invalid assignment (wrong number of arguments for `__set__`)
|
||||||
|
11 | instance.attr = 1 # error: [invalid-assignment]
|
||||||
|
```
|
||||||
|
|
||||||
|
# Diagnostics
|
||||||
|
|
||||||
|
```
|
||||||
|
error: lint:invalid-assignment
|
||||||
|
--> /src/mdtest_snippet.py:11:1
|
||||||
|
|
|
||||||
|
10 | # TODO: ideally, we would mention why this is an invalid assignment (wrong number of arguments for `__set__`)
|
||||||
|
11 | instance.attr = 1 # error: [invalid-assignment]
|
||||||
|
| ^^^^^^^^^^^^^ Invalid assignment to data descriptor attribute `attr` on type `C` with custom `__set__` method
|
||||||
|
|
|
||||||
|
|
||||||
|
```
|
|
@ -0,0 +1,40 @@
|
||||||
|
---
|
||||||
|
source: crates/red_knot_test/src/lib.rs
|
||||||
|
expression: snapshot
|
||||||
|
---
|
||||||
|
---
|
||||||
|
mdtest name: attribute_assignment.md - Attribute assignment - Data descriptors - Invalid argument type
|
||||||
|
mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/attribute_assignment.md
|
||||||
|
---
|
||||||
|
|
||||||
|
# Python source files
|
||||||
|
|
||||||
|
## mdtest_snippet.py
|
||||||
|
|
||||||
|
```
|
||||||
|
1 | class Descriptor:
|
||||||
|
2 | def __set__(self, instance: object, value: int) -> None:
|
||||||
|
3 | pass
|
||||||
|
4 |
|
||||||
|
5 | class C:
|
||||||
|
6 | attr: Descriptor = Descriptor()
|
||||||
|
7 |
|
||||||
|
8 | instance = C()
|
||||||
|
9 | instance.attr = 1 # fine
|
||||||
|
10 |
|
||||||
|
11 | # TODO: ideally, we would mention why this is an invalid assignment (wrong argument type for `value` parameter)
|
||||||
|
12 | instance.attr = "wrong" # error: [invalid-assignment]
|
||||||
|
```
|
||||||
|
|
||||||
|
# Diagnostics
|
||||||
|
|
||||||
|
```
|
||||||
|
error: lint:invalid-assignment
|
||||||
|
--> /src/mdtest_snippet.py:12:1
|
||||||
|
|
|
||||||
|
11 | # TODO: ideally, we would mention why this is an invalid assignment (wrong argument type for `value` parameter)
|
||||||
|
12 | instance.attr = "wrong" # error: [invalid-assignment]
|
||||||
|
| ^^^^^^^^^^^^^ Invalid assignment to data descriptor attribute `attr` on type `C` with custom `__set__` method
|
||||||
|
|
|
||||||
|
|
||||||
|
```
|
|
@ -0,0 +1,51 @@
|
||||||
|
---
|
||||||
|
source: crates/red_knot_test/src/lib.rs
|
||||||
|
expression: snapshot
|
||||||
|
---
|
||||||
|
---
|
||||||
|
mdtest name: attribute_assignment.md - Attribute assignment - Instance attributes with class-level defaults
|
||||||
|
mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/attribute_assignment.md
|
||||||
|
---
|
||||||
|
|
||||||
|
# Python source files
|
||||||
|
|
||||||
|
## mdtest_snippet.py
|
||||||
|
|
||||||
|
```
|
||||||
|
1 | class C:
|
||||||
|
2 | attr: int = 0
|
||||||
|
3 |
|
||||||
|
4 | instance = C()
|
||||||
|
5 | instance.attr = 1 # fine
|
||||||
|
6 | instance.attr = "wrong" # error: [invalid-assignment]
|
||||||
|
7 |
|
||||||
|
8 | C.attr = 1 # fine
|
||||||
|
9 | C.attr = "wrong" # error: [invalid-assignment]
|
||||||
|
```
|
||||||
|
|
||||||
|
# Diagnostics
|
||||||
|
|
||||||
|
```
|
||||||
|
error: lint:invalid-assignment
|
||||||
|
--> /src/mdtest_snippet.py:6:1
|
||||||
|
|
|
||||||
|
4 | instance = C()
|
||||||
|
5 | instance.attr = 1 # fine
|
||||||
|
6 | instance.attr = "wrong" # error: [invalid-assignment]
|
||||||
|
| ^^^^^^^^^^^^^ Object of type `Literal["wrong"]` is not assignable to attribute `attr` of type `int`
|
||||||
|
7 |
|
||||||
|
8 | C.attr = 1 # fine
|
||||||
|
|
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
error: lint:invalid-assignment
|
||||||
|
--> /src/mdtest_snippet.py:9:1
|
||||||
|
|
|
||||||
|
8 | C.attr = 1 # fine
|
||||||
|
9 | C.attr = "wrong" # error: [invalid-assignment]
|
||||||
|
| ^^^^^^ Object of type `Literal["wrong"]` is not assignable to attribute `attr` of type `int`
|
||||||
|
|
|
||||||
|
|
||||||
|
```
|
|
@ -0,0 +1,51 @@
|
||||||
|
---
|
||||||
|
source: crates/red_knot_test/src/lib.rs
|
||||||
|
expression: snapshot
|
||||||
|
---
|
||||||
|
---
|
||||||
|
mdtest name: attribute_assignment.md - Attribute assignment - Possibly-unbound attributes
|
||||||
|
mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/attribute_assignment.md
|
||||||
|
---
|
||||||
|
|
||||||
|
# Python source files
|
||||||
|
|
||||||
|
## mdtest_snippet.py
|
||||||
|
|
||||||
|
```
|
||||||
|
1 | def _(flag: bool) -> None:
|
||||||
|
2 | class C:
|
||||||
|
3 | if flag:
|
||||||
|
4 | attr: int = 0
|
||||||
|
5 |
|
||||||
|
6 | C.attr = 1 # error: [possibly-unbound-attribute]
|
||||||
|
7 |
|
||||||
|
8 | instance = C()
|
||||||
|
9 | instance.attr = 1 # error: [possibly-unbound-attribute]
|
||||||
|
```
|
||||||
|
|
||||||
|
# Diagnostics
|
||||||
|
|
||||||
|
```
|
||||||
|
warning: lint:possibly-unbound-attribute
|
||||||
|
--> /src/mdtest_snippet.py:6:5
|
||||||
|
|
|
||||||
|
4 | attr: int = 0
|
||||||
|
5 |
|
||||||
|
6 | C.attr = 1 # error: [possibly-unbound-attribute]
|
||||||
|
| ------ Attribute `attr` on type `Literal[C]` is possibly unbound
|
||||||
|
7 |
|
||||||
|
8 | instance = C()
|
||||||
|
|
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
warning: lint:possibly-unbound-attribute
|
||||||
|
--> /src/mdtest_snippet.py:9:5
|
||||||
|
|
|
||||||
|
8 | instance = C()
|
||||||
|
9 | instance.attr = 1 # error: [possibly-unbound-attribute]
|
||||||
|
| ------------- Attribute `attr` on type `C` is possibly unbound
|
||||||
|
|
|
||||||
|
|
||||||
|
```
|
|
@ -0,0 +1,52 @@
|
||||||
|
---
|
||||||
|
source: crates/red_knot_test/src/lib.rs
|
||||||
|
expression: snapshot
|
||||||
|
---
|
||||||
|
---
|
||||||
|
mdtest name: attribute_assignment.md - Attribute assignment - Pure instance attributes
|
||||||
|
mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/attribute_assignment.md
|
||||||
|
---
|
||||||
|
|
||||||
|
# Python source files
|
||||||
|
|
||||||
|
## mdtest_snippet.py
|
||||||
|
|
||||||
|
```
|
||||||
|
1 | class C:
|
||||||
|
2 | def __init__(self):
|
||||||
|
3 | self.attr: int = 0
|
||||||
|
4 |
|
||||||
|
5 | instance = C()
|
||||||
|
6 | instance.attr = 1 # fine
|
||||||
|
7 | instance.attr = "wrong" # error: [invalid-assignment]
|
||||||
|
8 |
|
||||||
|
9 | C.attr = 1 # error: [invalid-attribute-access]
|
||||||
|
```
|
||||||
|
|
||||||
|
# Diagnostics
|
||||||
|
|
||||||
|
```
|
||||||
|
error: lint:invalid-assignment
|
||||||
|
--> /src/mdtest_snippet.py:7:1
|
||||||
|
|
|
||||||
|
5 | instance = C()
|
||||||
|
6 | instance.attr = 1 # fine
|
||||||
|
7 | instance.attr = "wrong" # error: [invalid-assignment]
|
||||||
|
| ^^^^^^^^^^^^^ Object of type `Literal["wrong"]` is not assignable to attribute `attr` of type `int`
|
||||||
|
8 |
|
||||||
|
9 | C.attr = 1 # error: [invalid-attribute-access]
|
||||||
|
|
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
error: lint:invalid-attribute-access
|
||||||
|
--> /src/mdtest_snippet.py:9:1
|
||||||
|
|
|
||||||
|
7 | instance.attr = "wrong" # error: [invalid-assignment]
|
||||||
|
8 |
|
||||||
|
9 | C.attr = 1 # error: [invalid-attribute-access]
|
||||||
|
| ^^^^^^ Cannot assign to instance attribute `attr` from the class object `Literal[C]`
|
||||||
|
|
|
||||||
|
|
||||||
|
```
|
|
@ -0,0 +1,50 @@
|
||||||
|
---
|
||||||
|
source: crates/red_knot_test/src/lib.rs
|
||||||
|
expression: snapshot
|
||||||
|
---
|
||||||
|
---
|
||||||
|
mdtest name: attribute_assignment.md - Attribute assignment - Setting attributes on union types
|
||||||
|
mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/attribute_assignment.md
|
||||||
|
---
|
||||||
|
|
||||||
|
# Python source files
|
||||||
|
|
||||||
|
## mdtest_snippet.py
|
||||||
|
|
||||||
|
```
|
||||||
|
1 | def _(flag: bool) -> None:
|
||||||
|
2 | if flag:
|
||||||
|
3 | class C1:
|
||||||
|
4 | attr: int = 0
|
||||||
|
5 |
|
||||||
|
6 | else:
|
||||||
|
7 | class C1:
|
||||||
|
8 | attr: str = ""
|
||||||
|
9 |
|
||||||
|
10 | # TODO: The error message here could be improved to explain why the assignment fails.
|
||||||
|
11 | C1.attr = 1 # error: [invalid-assignment]
|
||||||
|
12 |
|
||||||
|
13 | class C2:
|
||||||
|
14 | if flag:
|
||||||
|
15 | attr: int = 0
|
||||||
|
16 | else:
|
||||||
|
17 | attr: str = ""
|
||||||
|
18 |
|
||||||
|
19 | # TODO: This should be an error
|
||||||
|
20 | C2.attr = 1
|
||||||
|
```
|
||||||
|
|
||||||
|
# Diagnostics
|
||||||
|
|
||||||
|
```
|
||||||
|
error: lint:invalid-assignment
|
||||||
|
--> /src/mdtest_snippet.py:11:5
|
||||||
|
|
|
||||||
|
10 | # TODO: The error message here could be improved to explain why the assignment fails.
|
||||||
|
11 | C1.attr = 1 # error: [invalid-assignment]
|
||||||
|
| ^^^^^^^ Object of type `Literal[1]` is not assignable to attribute `attr` on type `Literal[C1, C1]`
|
||||||
|
12 |
|
||||||
|
13 | class C2:
|
||||||
|
|
|
||||||
|
|
||||||
|
```
|
|
@ -0,0 +1,48 @@
|
||||||
|
---
|
||||||
|
source: crates/red_knot_test/src/lib.rs
|
||||||
|
expression: snapshot
|
||||||
|
---
|
||||||
|
---
|
||||||
|
mdtest name: attribute_assignment.md - Attribute assignment - Unknown attributes
|
||||||
|
mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/attribute_assignment.md
|
||||||
|
---
|
||||||
|
|
||||||
|
# Python source files
|
||||||
|
|
||||||
|
## mdtest_snippet.py
|
||||||
|
|
||||||
|
```
|
||||||
|
1 | class C: ...
|
||||||
|
2 |
|
||||||
|
3 | C.non_existent = 1 # error: [unresolved-attribute]
|
||||||
|
4 |
|
||||||
|
5 | instance = C()
|
||||||
|
6 | instance.non_existent = 1 # error: [unresolved-attribute]
|
||||||
|
```
|
||||||
|
|
||||||
|
# Diagnostics
|
||||||
|
|
||||||
|
```
|
||||||
|
error: lint:unresolved-attribute
|
||||||
|
--> /src/mdtest_snippet.py:3:1
|
||||||
|
|
|
||||||
|
1 | class C: ...
|
||||||
|
2 |
|
||||||
|
3 | C.non_existent = 1 # error: [unresolved-attribute]
|
||||||
|
| ^^^^^^^^^^^^^^ Unresolved attribute `non_existent` on type `Literal[C]`.
|
||||||
|
4 |
|
||||||
|
5 | instance = C()
|
||||||
|
|
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
error: lint:unresolved-attribute
|
||||||
|
--> /src/mdtest_snippet.py:6:1
|
||||||
|
|
|
||||||
|
5 | instance = C()
|
||||||
|
6 | instance.non_existent = 1 # error: [unresolved-attribute]
|
||||||
|
| ^^^^^^^^^^^^^^^^^^^^^ Unresolved attribute `non_existent` on type `C`.
|
||||||
|
|
|
||||||
|
|
||||||
|
```
|
|
@ -0,0 +1,51 @@
|
||||||
|
---
|
||||||
|
source: crates/red_knot_test/src/lib.rs
|
||||||
|
expression: snapshot
|
||||||
|
---
|
||||||
|
---
|
||||||
|
mdtest name: attribute_assignment.md - Attribute assignment - `ClassVar`s
|
||||||
|
mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/attribute_assignment.md
|
||||||
|
---
|
||||||
|
|
||||||
|
# Python source files
|
||||||
|
|
||||||
|
## mdtest_snippet.py
|
||||||
|
|
||||||
|
```
|
||||||
|
1 | from typing import ClassVar
|
||||||
|
2 |
|
||||||
|
3 | class C:
|
||||||
|
4 | attr: ClassVar[int] = 0
|
||||||
|
5 |
|
||||||
|
6 | C.attr = 1 # fine
|
||||||
|
7 | C.attr = "wrong" # error: [invalid-assignment]
|
||||||
|
8 |
|
||||||
|
9 | instance = C()
|
||||||
|
10 | instance.attr = 1 # error: [invalid-attribute-access]
|
||||||
|
```
|
||||||
|
|
||||||
|
# Diagnostics
|
||||||
|
|
||||||
|
```
|
||||||
|
error: lint:invalid-assignment
|
||||||
|
--> /src/mdtest_snippet.py:7:1
|
||||||
|
|
|
||||||
|
6 | C.attr = 1 # fine
|
||||||
|
7 | C.attr = "wrong" # error: [invalid-assignment]
|
||||||
|
| ^^^^^^ Object of type `Literal["wrong"]` is not assignable to attribute `attr` of type `int`
|
||||||
|
8 |
|
||||||
|
9 | instance = C()
|
||||||
|
|
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
error: lint:invalid-attribute-access
|
||||||
|
--> /src/mdtest_snippet.py:10:1
|
||||||
|
|
|
||||||
|
9 | instance = C()
|
||||||
|
10 | instance.attr = 1 # error: [invalid-attribute-access]
|
||||||
|
| ^^^^^^^^^^^^^ Cannot assign to ClassVar `attr` from an instance of type `C`
|
||||||
|
|
|
||||||
|
|
||||||
|
```
|
Loading…
Add table
Add a link
Reference in a new issue