diff --git a/crates/ty_python_semantic/resources/mdtest/descriptor_protocol.md b/crates/ty_python_semantic/resources/mdtest/descriptor_protocol.md index f3617fff74..8a2630d7bb 100644 --- a/crates/ty_python_semantic/resources/mdtest/descriptor_protocol.md +++ b/crates/ty_python_semantic/resources/mdtest/descriptor_protocol.md @@ -193,7 +193,8 @@ reveal_type(C2().attr) # revealed: Unknown | Literal["non-data", "normal"] 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 class C3: @@ -201,11 +202,12 @@ class C3: def f(self): # TODO: we should ideally emit an error here. We are overwriting the - # non-data descriptor with a string, which is not compatible with the - # declared type. - self.attr = "normal" + # non-data descriptor with an integer, which is not compatible with + # the `__get__` return type of `NonDataDescriptor` when called on an + # 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. @@ -221,20 +223,15 @@ class C4: def switch(self): # 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 - # the (implicitly) declared type of `C4.f`, which is a function literal type: - # `def f(self) -> None`. Strictly speaking, this we should also emit an error - # here.. or we should not consider the function definition to be a declaration. + # the return type of `__get__` of `C4.f` (a different bound method). Strictly + # speaking, we should also emit an error in this case. self.f = self.replacement reveal_type(C4.f) # revealed: def f(self) -> None c4 = C4() -# call c4.switch() or not - -# 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 +reveal_type(c4.f) # revealed: bound method C4.f() -> None # As a regression test for https://github.com/astral-sh/ty/issues/350, make sure that no # error is emitted when calling `c4.f()`: diff --git a/crates/ty_python_semantic/resources/mdtest/protocols.md b/crates/ty_python_semantic/resources/mdtest/protocols.md index f14b838b6a..83eebaff5b 100644 --- a/crates/ty_python_semantic/resources/mdtest/protocols.md +++ b/crates/ty_python_semantic/resources/mdtest/protocols.md @@ -1676,7 +1676,9 @@ def _(r: Recursive): reveal_type(r.direct) # revealed: Recursive reveal_type(r.union) # revealed: None | 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.callable1) # revealed: (int, /) -> Recursive reveal_type(r.callable2) # revealed: (Recursive, /) -> int diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index d0db73c75a..8df98dc1c9 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -1771,15 +1771,10 @@ impl<'db> ClassLiteral<'db> { // attribute is a non-data descriptor, it can not possibly be the // correct type of the implicit instance attribute. If there are any // attribute assignments in methods of this class, they would overwrite - // the non-data descriptor. In this case, we just return the type - // inferred from attribute assignments in methods. The descriptor - // protocol implementation in `Type::invoke_descriptor_protocol` will - // take care of unioning with the non-data descriptor type (because we - // account for the fact that the methods containing these assignments - // might never be called). - if !implicit.is_unbound() { - return implicit.into(); - } + // the non-data descriptor. If they do so in a non-compatible way, we + // should emit an error elsewhere. Here, we simply return `Unbound`, + // to signal that there is no instance attribute of this name. + return Symbol::Unbound.into(); } let bindings = use_def.public_bindings(symbol_id);