diff --git a/crates/ty_python_semantic/resources/mdtest/descriptor_protocol.md b/crates/ty_python_semantic/resources/mdtest/descriptor_protocol.md index f4b5854ed7..3b90696928 100644 --- a/crates/ty_python_semantic/resources/mdtest/descriptor_protocol.md +++ b/crates/ty_python_semantic/resources/mdtest/descriptor_protocol.md @@ -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 diff --git a/crates/ty_python_semantic/resources/mdtest/properties.md b/crates/ty_python_semantic/resources/mdtest/properties.md index b4c2abae9f..6f1e2215ef 100644 --- a/crates/ty_python_semantic/resources/mdtest/properties.md +++ b/crates/ty_python_semantic/resources/mdtest/properties.md @@ -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 diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 4284b15278..194bc6db90 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -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