diff --git a/pyrefly/lib/alt/class/class_field.rs b/pyrefly/lib/alt/class/class_field.rs index 744130dff9..5dceb57059 100644 --- a/pyrefly/lib/alt/class/class_field.rs +++ b/pyrefly/lib/alt/class/class_field.rs @@ -4537,10 +4537,50 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> { got_is_classvar, })); } + let cached_property_value_type = |getter: &Type| { + let ty = getter + .callable_return_type(self.heap) + .expect("cached_property getter should have a callable return type"); + if getter.visit_toplevel_func_metadata(&|meta| meta.flags.is_return_inferred) { + ty.promote_implicit_literals(self.stdlib) + } else { + ty + } + }; match (got, want) { (_, ClassAttribute::NoAccess(_)) | (ClassAttribute::NoAccess(_), _) => { unreachable!("handled above") } + (ClassAttribute::Property(got_getter, None, _), ClassAttribute::ReadWrite(want)) + if got_getter.is_cached_property() => + { + let got = cached_property_value_type(got_getter); + let subset_error = is_subset(&got, want) + .map_or_else(Some, |_| is_subset(want, &got).map_or_else(Some, |_| None)); + if let Some(subset_error) = subset_error { + Err(Box::new(AttrSubsetError::Invariant { + got, + want: want.clone(), + subset_error, + })) + } else { + Ok(()) + } + } + (ClassAttribute::Property(got_getter, None, _), ClassAttribute::ReadOnly(want, _)) + if got_getter.is_cached_property() => + { + let got = cached_property_value_type(got_getter); + is_subset(&got, want).map_err(|subset_error| { + Box::new(AttrSubsetError::Covariant { + got, + want: want.clone(), + got_is_property: true, + want_is_property: false, + subset_error, + }) + }) + } ( ClassAttribute::Property(_, _, _), ClassAttribute::ReadOnly(..) | ClassAttribute::ReadWrite(..), diff --git a/pyrefly/lib/test/class_overrides.rs b/pyrefly/lib/test/class_overrides.rs index 753c210fca..7ad39b4e27 100644 --- a/pyrefly/lib/test/class_overrides.rs +++ b/pyrefly/lib/test/class_overrides.rs @@ -698,6 +698,30 @@ class B(A): "#, ); +// Regression test for https://github.com/facebook/pyrefly/issues/3062 +testcase!( + test_cached_property_overrides_unannotated_class_variable, + r#" +from functools import cached_property + +class BaseDatabaseFeatures: + can_introspect_fk = True + update_can_self_select = True + +class MySQLFeatures(BaseDatabaseFeatures): + @cached_property + def can_introspect_fk(self): + return self._check() + + @cached_property + def update_can_self_select(self): + return True + + def _check(self) -> bool: + return False + "#, +); + testcase!( test_inherit_type_attribute, r#"