Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
7 changes: 3 additions & 4 deletions crates/ty_python_semantic/resources/mdtest/properties.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 3 additions & 1 deletion crates/ty_python_semantic/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading