From b925ae5061bb16cba276b6a4dd92663ea5f55abf Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Mon, 29 Dec 2025 22:28:03 -0500 Subject: [PATCH] [ty] Avoid including `property` in subclasses properties (#22088) ## Summary As-is, the following rejects `return self.value` in `def other` in the subclass ([link](https://play.ty.dev/f55b47b2-313e-45d1-ba45-fde410bed32e)) because `self.value` is resolving to `Unknown | int | float | property`: ```python class Base: _value: float = 0.0 @property def value(self) -> float: return self._value @value.setter def value(self, v: float) -> None: self._value = v @property def other(self) -> float: return self.value @other.setter def other(self, v: float) -> None: self.value = v class Derived(Base): @property def other(self) -> float: return self.value @other.setter def other(self, v: float) -> None: reveal_type(self.value) # revealed: int | float self.value = v ``` I believe the root cause is that we're not excluding properties when searching for class methods, so we're treating the `other` setter as a classmethod. I don't fully understand how that ends up materializing as `| property` on the union though. --- .../resources/mdtest/descriptor_protocol.md | 36 +++++++++++++++++++ crates/ty_python_semantic/src/types/class.rs | 27 +++++++++----- 2 files changed, 55 insertions(+), 8 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/descriptor_protocol.md b/crates/ty_python_semantic/resources/mdtest/descriptor_protocol.md index 03110eac70..ce59da02c5 100644 --- a/crates/ty_python_semantic/resources/mdtest/descriptor_protocol.md +++ b/crates/ty_python_semantic/resources/mdtest/descriptor_protocol.md @@ -525,6 +525,42 @@ c.name = None c.name = 42 ``` +### Overriding properties in subclasses + +When a subclass overrides a property, accessing other inherited properties from within the +overriding property methods should still work correctly. + +```py +class Base: + _value: float = 0.0 + + @property + def value(self) -> float: + return self._value + + @value.setter + def value(self, v: float) -> None: + self._value = v + + @property + def other(self) -> float: + return self.value + + @other.setter + def other(self, v: float) -> None: + self.value = v + +class Derived(Base): + @property + def other(self) -> float: + return self.value + + @other.setter + def other(self, v: float) -> None: + reveal_type(self.value) # revealed: int | float + self.value = v +``` + ### Properties with no setters diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index 691dbf9e75..7b89f0828b 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -3479,16 +3479,27 @@ impl<'db> ClassLiteral<'db> { let is_valid_scope = |method_scope: &Scope| { if let Some(method_def) = method_scope.node().as_function() { let method_name = method_def.node(&module).name.as_str(); - if let Some(Type::FunctionLiteral(method_type)) = - class_member(db, class_body_scope, method_name) - .inner - .place - .ignore_possibly_undefined() + match class_member(db, class_body_scope, method_name) + .inner + .place + .ignore_possibly_undefined() { - let method_decorator = MethodDecorator::try_from_fn_type(db, method_type); - if method_decorator != Ok(target_method_decorator) { - return false; + Some(Type::FunctionLiteral(method_type)) => { + let method_decorator = MethodDecorator::try_from_fn_type(db, method_type); + if method_decorator != Ok(target_method_decorator) { + return false; + } } + Some(Type::PropertyInstance(_)) => { + // Property getters and setters have their own scopes. They take `self` + // as the first parameter (like regular instance methods), so they're + // included when looking for `MethodDecorator::None`. However, they're + // not classmethods or staticmethods, so exclude them for those cases. + if target_method_decorator != MethodDecorator::None { + return false; + } + } + _ => {} } } true