[red-knot] Extend instance/class attribute tests (#15959)

## Summary

In preparation for creating some (sub) issues for
https://github.com/astral-sh/ruff/issues/14164, I'm trying to document
the current behavior (and a bug) a bit better.
This commit is contained in:
David Peter 2025-02-05 12:45:00 +01:00 committed by GitHub
parent 7ca778f492
commit eb08345fd5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -210,6 +210,8 @@ def get_str() -> str:
return "a" return "a"
class C: class C:
z: int
def __init__(self) -> None: def __init__(self) -> None:
self.x = get_int() self.x = get_int()
self.y: int = 1 self.y: int = 1
@ -220,12 +222,14 @@ class C:
# TODO: this redeclaration should be an error # TODO: this redeclaration should be an error
self.y: str = "a" self.y: str = "a"
# TODO: this redeclaration should be an error
self.z: str = "a"
c_instance = C() c_instance = C()
reveal_type(c_instance.x) # revealed: Unknown | int | str reveal_type(c_instance.x) # revealed: Unknown | int | str
# TODO: We should probably infer `int | str` here.
reveal_type(c_instance.y) # revealed: int reveal_type(c_instance.y) # revealed: int
reveal_type(c_instance.z) # revealed: int
``` ```
#### Attributes defined in tuple unpackings #### Attributes defined in tuple unpackings
@ -354,6 +358,77 @@ class C:
reveal_type(C().declared_and_bound) # revealed: Unknown reveal_type(C().declared_and_bound) # revealed: Unknown
``` ```
#### Static methods do not influence implicitly defined attributes
```py
class Other:
x: int
class C:
@staticmethod
def f(other: Other) -> None:
other.x = 1
# 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]
# This also works if `staticmethod` is aliased:
my_staticmethod = staticmethod
class D:
@my_staticmethod
def f(other: Other) -> None:
other.x = 1
# 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]
```
If `staticmethod` is something else, that should not influence the behavior:
`other.py`:
```py
def staticmethod(f):
return f
class C:
@staticmethod
def f(self) -> None:
self.x = 1
reveal_type(C().x) # revealed: Unknown | Literal[1]
```
And if `staticmethod` is fully qualified, that should also be recognized:
`fully_qualified.py`:
```py
import builtins
class Other:
x: int
class C:
@builtins.staticmethod
def f(other: Other) -> None:
other.x = 1
# 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]
```
#### Attributes defined in statically-known-to-be-false branches #### Attributes defined in statically-known-to-be-false branches
```py ```py
@ -440,12 +515,12 @@ reveal_type(C.pure_class_variable) # revealed: Unknown
C.pure_class_variable = "overwritten on class" C.pure_class_variable = "overwritten on class"
# TODO: should be `Literal["overwritten on class"]` # TODO: should be `Unknown | Literal["value set in class method"]` or
# Literal["overwritten on class"]`, once/if we support local narrowing.
# error: [unresolved-attribute] # error: [unresolved-attribute]
reveal_type(C.pure_class_variable) # revealed: Unknown reveal_type(C.pure_class_variable) # revealed: Unknown
c_instance = C() c_instance = C()
# TODO: should be `Literal["overwritten on class"]`
reveal_type(c_instance.pure_class_variable) # revealed: Unknown | Literal["value set in class method"] reveal_type(c_instance.pure_class_variable) # revealed: Unknown | Literal["value set in class method"]
# TODO: should raise an error. # TODO: should raise an error.