mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-30 05:44:56 +00:00
[ty] Add decorator check for implicit attribute assignments (#18587)
## Summary Previously, the checks for implicit attribute assignments didn't properly account for method decorators. This PR fixes that by: - Adding a decorator check in `implicit_instance_attribute`. This allows it to filter out methods with mismatching decorators when analyzing attribute assignments. - Adding attribute search for implicit class attributes: if an attribute can't be found directly in the class body, the `ClassLiteral::own_class_member` function will now search in classmethods. - Adding `staticmethod`: it has been added into `KnownClass` and together with the new decorator check, it will no longer expose attributes when the assignment target name is the same as the first method name. If accepted, it should fix https://github.com/astral-sh/ty/issues/205 and https://github.com/astral-sh/ty/issues/207. ## Test Plan This is tested with existing mdtest suites and is able to get most of the TODO marks for implicit assignments in classmethods and staticmethods removed. However, there's one specific test case I failed to figure out how to correctly resolve:b279508bdc/crates/ty_python_semantic/resources/mdtest/attributes.md (L754-L755)
I tried to add `instance_member().is_unbound()` check in this [else branch](b279508bdc/crates/ty_python_semantic/src/types/infer.rs (L3299-L3301)
) but it causes tests with class attributes defined in class body to fail. While it's possible to implicitly add `ClassVar` to qualifiers to make this assignment fail and keep everything else passing, it doesn't feel like the right solution.
This commit is contained in:
parent
ca7933804e
commit
fd2cc37f90
4 changed files with 190 additions and 33 deletions
|
@ -522,8 +522,8 @@ class C:
|
|||
# error: [unresolved-attribute]
|
||||
reveal_type(C.x) # revealed: Unknown
|
||||
|
||||
# TODO: this should raise `unresolved-attribute` as well, and the type should be `Unknown`
|
||||
reveal_type(C().x) # revealed: Unknown | Literal[1]
|
||||
# error: [unresolved-attribute]
|
||||
reveal_type(C().x) # revealed: Unknown
|
||||
|
||||
# This also works if `staticmethod` is aliased:
|
||||
|
||||
|
@ -537,8 +537,8 @@ class D:
|
|||
# error: [unresolved-attribute]
|
||||
reveal_type(D.x) # revealed: Unknown
|
||||
|
||||
# TODO: this should raise `unresolved-attribute` as well, and the type should be `Unknown`
|
||||
reveal_type(D().x) # revealed: Unknown | Literal[1]
|
||||
# error: [unresolved-attribute]
|
||||
reveal_type(D().x) # revealed: Unknown
|
||||
```
|
||||
|
||||
If `staticmethod` is something else, that should not influence the behavior:
|
||||
|
@ -571,8 +571,8 @@ class C:
|
|||
# error: [unresolved-attribute]
|
||||
reveal_type(C.x) # revealed: Unknown
|
||||
|
||||
# TODO: this should raise `unresolved-attribute` as well, and the type should be `Unknown`
|
||||
reveal_type(C().x) # revealed: Unknown | Literal[1]
|
||||
# error: [unresolved-attribute]
|
||||
reveal_type(C().x) # revealed: Unknown
|
||||
```
|
||||
|
||||
#### Attributes defined in statically-known-to-be-false branches
|
||||
|
@ -742,17 +742,9 @@ class C:
|
|||
# for a more realistic example, let's actually call the method
|
||||
C.class_method()
|
||||
|
||||
# TODO: We currently plan to support this and show no error here.
|
||||
# mypy shows an error here, pyright does not.
|
||||
# error: [unresolved-attribute]
|
||||
reveal_type(C.pure_class_variable) # revealed: Unknown
|
||||
reveal_type(C.pure_class_variable) # revealed: Unknown | Literal["value set in class method"]
|
||||
|
||||
# TODO: should be no error when descriptor protocol is supported
|
||||
# and the assignment is properly attributed to the class method.
|
||||
# error: [invalid-attribute-access] "Cannot assign to instance attribute `pure_class_variable` from the class object `<class 'C'>`"
|
||||
C.pure_class_variable = "overwritten on class"
|
||||
# TODO: should be no error
|
||||
# error: [unresolved-attribute] "Attribute `pure_class_variable` can only be accessed on instances, not on the class object `<class 'C'>` itself."
|
||||
reveal_type(C.pure_class_variable) # revealed: Literal["overwritten on class"]
|
||||
|
||||
c_instance = C()
|
||||
|
@ -2150,6 +2142,25 @@ class C:
|
|||
reveal_type(C().x) # revealed: int
|
||||
```
|
||||
|
||||
### Attributes defined in methods with unknown decorators
|
||||
|
||||
When an attribute is defined in a method that is decorated with an unknown decorator, we consider it
|
||||
to be accessible on both the class itself and instances of that class. This is consistent with the
|
||||
gradual guarantee, because the unknown decorator *could* be an alias for `builtins.classmethod`.
|
||||
|
||||
```py
|
||||
# error: [unresolved-import]
|
||||
from unknown_library import unknown_decorator
|
||||
|
||||
class C:
|
||||
@unknown_decorator
|
||||
def f(self):
|
||||
self.x: int = 1
|
||||
|
||||
reveal_type(C.x) # revealed: int
|
||||
reveal_type(C().x) # revealed: int
|
||||
```
|
||||
|
||||
## Enum classes
|
||||
|
||||
Enums are not supported yet; attribute access on an enum class is inferred as `Todo`.
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue