[ty] Use the return type of `__get__` for descriptor lookups even when `__get__` is called with incorrect arguments (#21424)

This commit is contained in:
Alex Waygood 2025-11-13 12:05:10 +00:00 committed by GitHub
parent eb1957cd17
commit cd183c5e1f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 36 additions and 9 deletions

View File

@ -696,8 +696,16 @@ reveal_type(C().instance_access) # revealed: str
reveal_type(C.metaclass_access) # revealed: bytes
# TODO: These should emit a diagnostic
reveal_type(C().class_object_access) # revealed: TailoredForClassObjectAccess
reveal_type(C.instance_access) # revealed: TailoredForInstanceAccess
#
# However, we use the return-type of `__get__` as the inferred type anyway:
# the way to specify that the descriptor object itself is returned when the
# attribute is accessed on the instance or the class is by overloading `__get__`.
#
# Using the return type of `__get__` even for `__get__` calls that have invalid
# arguments passed to them avoids false positives in situations where there are
# `__get__` calls that we don't sufficiently understand.
reveal_type(C().class_object_access) # revealed: int
reveal_type(C.instance_access) # revealed: str
```
### Descriptors with incorrect `__get__` signature
@ -712,10 +720,28 @@ class C:
descriptor: Descriptor = Descriptor()
# TODO: This should be an error
reveal_type(C.descriptor) # revealed: Descriptor
reveal_type(C.descriptor) # revealed: int
# TODO: This should be an error
reveal_type(C().descriptor) # revealed: Descriptor
reveal_type(C().descriptor) # revealed: int
```
### "Descriptors" with non-callable `__get__` attributes
If `__get__` is not callable at all, the interpreter will still attempt to call the method at
runtime, and this will raise an exception. As such, even for `__get__ = None`, we still "attempt to
call `__get__`" on the descriptor object (leading us to infer `Unknown`):
```py
class BrokenDescriptor:
__get__: None = None
class Foo:
desc: BrokenDescriptor = BrokenDescriptor()
# TODO: this raises `TypeError` at runtime due to the implicit call to `__get__`;
# we should emit a diagnostic
reveal_type(Foo().desc) # revealed: Unknown
```
### Undeclared descriptor arguments

View File

@ -167,10 +167,9 @@ class C:
c = C()
c.attr = 1
# TODO: An error should be emitted here, and the type should be `Unknown`
# or `Never`. See https://github.com/astral-sh/ruff/issues/16298 for more
# details.
reveal_type(c.attr) # revealed: Unknown | property
# TODO: An error should be emitted here.
# See https://github.com/astral-sh/ruff/issues/16298 for more details.
reveal_type(c.attr) # revealed: Unknown
```
### Wrong setter signature

View File

@ -3963,7 +3963,9 @@ impl<'db> Type<'db> {
UnionType::from_elements(db, [bindings.return_type(db), self])
}
})
.ok()?;
// TODO: an error when calling `__get__` will lead to a `TypeError` or similar at runtime;
// we should emit a diagnostic here instead of silently ignoring the error.
.unwrap_or_else(|CallError(_, bindings)| bindings.return_type(db));
let descriptor_kind = if self.is_data_descriptor(db) {
AttributeKind::DataDescriptor