mirror of https://github.com/astral-sh/ruff
[ty] Ignore descriptor class-level declarations for purposes of finding instance attributes, variant 3
This commit is contained in:
parent
2c717c9f5e
commit
cdafb3d81c
|
|
@ -193,7 +193,8 @@ reveal_type(C2().attr) # revealed: Unknown | Literal["non-data", "normal"]
|
||||||
C2().attr = 1
|
C2().attr = 1
|
||||||
```
|
```
|
||||||
|
|
||||||
This situation does not change if the attribute is declared on the class body:
|
This situation changes if the class attribute is declared. Here, we should error if we see an
|
||||||
|
incompatible attribute assignment, but otherwise just follow the declared type:
|
||||||
|
|
||||||
```py
|
```py
|
||||||
class C3:
|
class C3:
|
||||||
|
|
@ -201,11 +202,12 @@ class C3:
|
||||||
|
|
||||||
def f(self):
|
def f(self):
|
||||||
# TODO: we should ideally emit an error here. We are overwriting the
|
# TODO: we should ideally emit an error here. We are overwriting the
|
||||||
# non-data descriptor with a string, which is not compatible with the
|
# non-data descriptor with an integer, which is not compatible with
|
||||||
# declared type.
|
# the `__get__` return type of `NonDataDescriptor` when called on an
|
||||||
self.attr = "normal"
|
# instance.
|
||||||
|
self.attr = 1
|
||||||
|
|
||||||
reveal_type(C3().attr) # revealed: Literal["non-data", "normal"] | Unknown
|
reveal_type(C3().attr) # revealed: Literal["non-data"]
|
||||||
```
|
```
|
||||||
|
|
||||||
The scenario above is similar to a use case where a method on a class is dynamically replaced.
|
The scenario above is similar to a use case where a method on a class is dynamically replaced.
|
||||||
|
|
@ -221,20 +223,15 @@ class C4:
|
||||||
def switch(self):
|
def switch(self):
|
||||||
# Similar to the `C3` example, we are overwriting a non-data descriptor (the
|
# Similar to the `C3` example, we are overwriting a non-data descriptor (the
|
||||||
# function `C4.f`) with something (a bound method) that is not compatible with
|
# function `C4.f`) with something (a bound method) that is not compatible with
|
||||||
# the (implicitly) declared type of `C4.f`, which is a function literal type:
|
# the return type of `__get__` of `C4.f` (a different bound method). Strictly
|
||||||
# `def f(self) -> None`. Strictly speaking, this we should also emit an error
|
# speaking, we should also emit an error in this case.
|
||||||
# here.. or we should not consider the function definition to be a declaration.
|
|
||||||
self.f = self.replacement
|
self.f = self.replacement
|
||||||
|
|
||||||
reveal_type(C4.f) # revealed: def f(self) -> None
|
reveal_type(C4.f) # revealed: def f(self) -> None
|
||||||
|
|
||||||
c4 = C4()
|
c4 = C4()
|
||||||
|
|
||||||
# call c4.switch() or not
|
reveal_type(c4.f) # revealed: bound method C4.f() -> None
|
||||||
|
|
||||||
# TODO: This should reveal the following type, as soon as we understand the type of self:
|
|
||||||
# `(bound method C4.f() -> None) | (bound method C4.replacement() -> None) | Unknown`
|
|
||||||
reveal_type(c4.f) # revealed: (bound method C4.f() -> None) | Unknown
|
|
||||||
|
|
||||||
# As a regression test for https://github.com/astral-sh/ty/issues/350, make sure that no
|
# As a regression test for https://github.com/astral-sh/ty/issues/350, make sure that no
|
||||||
# error is emitted when calling `c4.f()`:
|
# error is emitted when calling `c4.f()`:
|
||||||
|
|
|
||||||
|
|
@ -1676,7 +1676,9 @@ def _(r: Recursive):
|
||||||
reveal_type(r.direct) # revealed: Recursive
|
reveal_type(r.direct) # revealed: Recursive
|
||||||
reveal_type(r.union) # revealed: None | Recursive
|
reveal_type(r.union) # revealed: None | Recursive
|
||||||
reveal_type(r.intersection1) # revealed: C & Recursive
|
reveal_type(r.intersection1) # revealed: C & Recursive
|
||||||
reveal_type(r.intersection2) # revealed: C & ~Recursive
|
# TODO: no error, and a revealed type of `C & ~Recursive`
|
||||||
|
# error: [unresolved-attribute] "Type `Recursive` has no attribute `intersection2`"
|
||||||
|
reveal_type(r.intersection2) # revealed: Unknown
|
||||||
reveal_type(r.t) # revealed: tuple[int, tuple[str, Recursive]]
|
reveal_type(r.t) # revealed: tuple[int, tuple[str, Recursive]]
|
||||||
reveal_type(r.callable1) # revealed: (int, /) -> Recursive
|
reveal_type(r.callable1) # revealed: (int, /) -> Recursive
|
||||||
reveal_type(r.callable2) # revealed: (Recursive, /) -> int
|
reveal_type(r.callable2) # revealed: (Recursive, /) -> int
|
||||||
|
|
|
||||||
|
|
@ -1771,15 +1771,10 @@ impl<'db> ClassLiteral<'db> {
|
||||||
// attribute is a non-data descriptor, it can not possibly be the
|
// attribute is a non-data descriptor, it can not possibly be the
|
||||||
// correct type of the implicit instance attribute. If there are any
|
// correct type of the implicit instance attribute. If there are any
|
||||||
// attribute assignments in methods of this class, they would overwrite
|
// attribute assignments in methods of this class, they would overwrite
|
||||||
// the non-data descriptor. In this case, we just return the type
|
// the non-data descriptor. If they do so in a non-compatible way, we
|
||||||
// inferred from attribute assignments in methods. The descriptor
|
// should emit an error elsewhere. Here, we simply return `Unbound`,
|
||||||
// protocol implementation in `Type::invoke_descriptor_protocol` will
|
// to signal that there is no instance attribute of this name.
|
||||||
// take care of unioning with the non-data descriptor type (because we
|
return Symbol::Unbound.into();
|
||||||
// account for the fact that the methods containing these assignments
|
|
||||||
// might never be called).
|
|
||||||
if !implicit.is_unbound() {
|
|
||||||
return implicit.into();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let bindings = use_def.public_bindings(symbol_id);
|
let bindings = use_def.public_bindings(symbol_id);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue