[ty] Skip eagerly evaluated scopes for attribute storing (#20856)
## Summary Fix https://github.com/astral-sh/ty/issues/664 This PR adds support for storing attributes in comprehension scopes (any eager scope.) For example in the following code we infer type of `z` correctly: ```py class C: def __init__(self): [None for self.z in range(1)] reveal_type(C().z) # previously [unresolved-attribute] but now shows Unknown | int ``` The fix works by adjusting the following logics: To identify if an attriute is an assignment to self or cls we need to check the scope is a method. To allow comprehension scopes here we skip any eager scope in the check. Also at this stage the code checks if self or the first method argument is shadowed by another binding that eager scope to prevent this: ```py class D: g: int class C: def __init__(self): [[None for self.g in range(1)] for self in [D()]] reveal_type(C().g) # [unresolved-attribute] ``` When determining scopes that attributes might be defined after collecting all the methods of the class the code also returns any decendant scope that is eager and only has eager parents until the method scope. When checking reachability of a attribute definition if the attribute is defined in an eager scope we use the reachability of the first non eager scope which must be a method. This allows attributes to be marked as reachable and be seen. There are also which I didn't add support for: ```py class C: def __init__(self): def f(): [None for self.z in range(1)] f() reveal_type(C().z) # [unresolved-attribute] ``` In the above example we will not even return the comprehension scope as an attribute scope because there is a non eager scope (`f` function) between the comprehension and the `__init__` method --------- Co-authored-by: Carl Meyer <carl@astral.sh>
This commit is contained in:
committed by
GitHub
parent
164c2a6cc6
commit
988c38c013
@@ -369,6 +369,11 @@ reveal_type(c_instance.y) # revealed: Unknown | int
|
||||
|
||||
#### Attributes defined in comprehensions
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.12"
|
||||
```
|
||||
|
||||
```py
|
||||
class TupleIterator:
|
||||
def __next__(self) -> tuple[int, str]:
|
||||
@@ -380,19 +385,9 @@ class TupleIterable:
|
||||
|
||||
class C:
|
||||
def __init__(self) -> None:
|
||||
# TODO: Should not emit this diagnostic
|
||||
# error: [unresolved-attribute]
|
||||
[... for self.a in range(3)]
|
||||
# TODO: Should not emit this diagnostic
|
||||
# error: [unresolved-attribute]
|
||||
# error: [unresolved-attribute]
|
||||
[... for (self.b, self.c) in TupleIterable()]
|
||||
# TODO: Should not emit this diagnostic
|
||||
# error: [unresolved-attribute]
|
||||
# error: [unresolved-attribute]
|
||||
[... for self.d in range(3) for self.e in range(3)]
|
||||
# TODO: Should not emit this diagnostic
|
||||
# error: [unresolved-attribute]
|
||||
[[... for self.f in range(3)] for _ in range(3)]
|
||||
[[... for self.g in range(3)] for self in [D()]]
|
||||
|
||||
@@ -401,35 +396,74 @@ class D:
|
||||
|
||||
c_instance = C()
|
||||
|
||||
# TODO: no error, reveal Unknown | int
|
||||
# error: [unresolved-attribute]
|
||||
reveal_type(c_instance.a) # revealed: Unknown
|
||||
reveal_type(c_instance.a) # revealed: Unknown | int
|
||||
|
||||
# TODO: no error, reveal Unknown | int
|
||||
# error: [unresolved-attribute]
|
||||
reveal_type(c_instance.b) # revealed: Unknown
|
||||
reveal_type(c_instance.b) # revealed: Unknown | int
|
||||
|
||||
# TODO: no error, reveal Unknown | str
|
||||
# error: [unresolved-attribute]
|
||||
reveal_type(c_instance.c) # revealed: Unknown
|
||||
reveal_type(c_instance.c) # revealed: Unknown | str
|
||||
|
||||
# TODO: no error, reveal Unknown | int
|
||||
# error: [unresolved-attribute]
|
||||
reveal_type(c_instance.d) # revealed: Unknown
|
||||
reveal_type(c_instance.d) # revealed: Unknown | int
|
||||
|
||||
# TODO: no error, reveal Unknown | int
|
||||
# error: [unresolved-attribute]
|
||||
reveal_type(c_instance.e) # revealed: Unknown
|
||||
reveal_type(c_instance.e) # revealed: Unknown | int
|
||||
|
||||
# TODO: no error, reveal Unknown | int
|
||||
# error: [unresolved-attribute]
|
||||
reveal_type(c_instance.f) # revealed: Unknown
|
||||
reveal_type(c_instance.f) # revealed: Unknown | int
|
||||
|
||||
# This one is correctly not resolved as an attribute:
|
||||
# error: [unresolved-attribute]
|
||||
reveal_type(c_instance.g) # revealed: Unknown
|
||||
```
|
||||
|
||||
It does not matter how much the comprehension is nested.
|
||||
|
||||
Similarly attributes defined by the comprehension in a generic method are recognized.
|
||||
|
||||
```py
|
||||
class C:
|
||||
def f[T](self):
|
||||
[... for self.a in [1]]
|
||||
[[... for self.b in [1]] for _ in [1]]
|
||||
|
||||
c_instance = C()
|
||||
|
||||
reveal_type(c_instance.a) # revealed: Unknown | int
|
||||
reveal_type(c_instance.b) # revealed: Unknown | int
|
||||
```
|
||||
|
||||
If the comprehension is inside another scope like function then that attribute is not inferred.
|
||||
|
||||
```py
|
||||
class C:
|
||||
def __init__(self):
|
||||
def f():
|
||||
# error: [unresolved-attribute]
|
||||
[... for self.a in [1]]
|
||||
|
||||
def g():
|
||||
# error: [unresolved-attribute]
|
||||
[... for self.b in [1]]
|
||||
g()
|
||||
|
||||
c_instance = C()
|
||||
|
||||
# This attribute is in the function f and is not reachable
|
||||
# error: [unresolved-attribute]
|
||||
reveal_type(c_instance.a) # revealed: Unknown
|
||||
|
||||
# error: [unresolved-attribute]
|
||||
reveal_type(c_instance.b) # revealed: Unknown
|
||||
```
|
||||
|
||||
If the comprehension is nested in any other eager scope it still can assign attributes.
|
||||
|
||||
```py
|
||||
class C:
|
||||
def __init__(self):
|
||||
class D:
|
||||
[[... for self.a in [1]] for _ in [1]]
|
||||
|
||||
reveal_type(C().a) # revealed: Unknown | int
|
||||
```
|
||||
|
||||
#### Conditionally declared / bound attributes
|
||||
|
||||
We currently treat implicit instance attributes to be bound, even if they are only conditionally
|
||||
|
||||
Reference in New Issue
Block a user