From 835ff18f817c4382c51064067c5b0cfe82612835 Mon Sep 17 00:00:00 2001 From: Asuka Minato Date: Fri, 7 Nov 2025 21:15:22 +0900 Subject: [PATCH 1/4] fix --- pyrefly/lib/alt/attr.rs | 11 ++++++++--- pyrefly/lib/test/descriptors.rs | 35 +++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 3 deletions(-) diff --git a/pyrefly/lib/alt/attr.rs b/pyrefly/lib/alt/attr.rs index e69c1dbcab..3978f1dd9c 100644 --- a/pyrefly/lib/alt/attr.rs +++ b/pyrefly/lib/alt/attr.rs @@ -50,6 +50,7 @@ use crate::types::read_only::ReadOnlyReason; use crate::types::type_var::Restriction; use crate::types::typed_dict::TypedDict; use crate::types::types::AnyStyle; +use crate::types::types::BoundMethod; use crate::types::types::Overload; use crate::types::types::SuperObj; use crate::types::types::Type; @@ -1725,9 +1726,13 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> { self.stdlib.function_type().clone() }, )), - Type::BoundMethod(_) => acc.push(AttributeBase1::ClassInstance( - self.stdlib.method_type().clone(), - )), + Type::BoundMethod(bound_method) => { + acc.push(AttributeBase1::ClassInstance( + self.stdlib.method_type().clone(), + )); + let BoundMethod { obj: _, func } = *bound_method; + self.as_attribute_base1(func.as_type(), acc); + } Type::Ellipsis => { if let Some(cls) = self.stdlib.ellipsis_type() { acc.push(AttributeBase1::ClassInstance(cls.clone())) diff --git a/pyrefly/lib/test/descriptors.rs b/pyrefly/lib/test/descriptors.rs index 147bf6ea5e..c7b44aa073 100644 --- a/pyrefly/lib/test/descriptors.rs +++ b/pyrefly/lib/test/descriptors.rs @@ -206,6 +206,41 @@ C().d = "42" "#, ); +testcase!( + test_bound_method_preserves_function_attributes_from_descriptor, + r#" +from __future__ import annotations + +from typing import Callable + + +class CachedMethod: + def __init__(self, fn: Callable[[Constraint], int]) -> None: + self._fn = fn + + def __get__(self, obj: Constraint | None, owner: type[Constraint]) -> CachedMethod: + return self + + def __call__(self, obj: Constraint) -> int: + return self._fn(obj) + + def clear_cache(self, obj: Constraint) -> None: ... + + +def cache_on_self(fn: Callable[[Constraint], int]) -> CachedMethod: + return CachedMethod(fn) + + +class Constraint: + @cache_on_self + def pointwise_read_writes(self) -> int: + return 0 + + def clear_cache(self) -> None: + self.pointwise_read_writes.clear_cache(self) + "#, +); + testcase!( test_class_property_descriptor, r#" From 34d8fc2635cf59c5915dd3f0b5ad1e08951ef458 Mon Sep 17 00:00:00 2001 From: Asuka Minato Date: Sat, 8 Nov 2025 05:13:42 +0900 Subject: [PATCH 2/4] fix --- pyrefly/lib/alt/attr.rs | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/pyrefly/lib/alt/attr.rs b/pyrefly/lib/alt/attr.rs index 3978f1dd9c..903fd79448 100644 --- a/pyrefly/lib/alt/attr.rs +++ b/pyrefly/lib/alt/attr.rs @@ -435,6 +435,9 @@ enum AttributeBase1 { TypedDict(TypedDict), /// Attribute lookup on a base as part of a subset check against a protocol. ProtocolSubset(Box), + /// Treat methods decorated with descriptors as if their underlying function were directly accessible. + /// Missing attributes on this variant are ignored – it only augments lookup results when present. + BoundMethodFunction(BoundMethodType), } impl AttributeBase1 { @@ -1235,6 +1238,15 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> { acc.not_found(NotFoundOn::ClassObject(class.class_object().dupe(), base)); } } + AttributeBase1::BoundMethodFunction(bound_func) => { + let mut func_bases = Vec::new(); + self.as_attribute_base1(bound_func.clone().as_type(), &mut func_bases); + for base1 in func_bases { + let not_found_len = acc.not_found.len(); + self.lookup_attr_from_attribute_base1(base1, attr_name, acc); + acc.not_found.truncate(not_found_len); + } + } AttributeBase1::ClassObject(class) => { match self.get_class_attribute(class, attr_name) { @@ -1730,8 +1742,7 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> { acc.push(AttributeBase1::ClassInstance( self.stdlib.method_type().clone(), )); - let BoundMethod { obj: _, func } = *bound_method; - self.as_attribute_base1(func.as_type(), acc); + acc.push(AttributeBase1::BoundMethodFunction(bound_method.func.clone())); } Type::Ellipsis => { if let Some(cls) = self.stdlib.ellipsis_type() { @@ -2157,6 +2168,13 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> { AttributeBase1::TypeQuantified(_, class) => { self.completions_class(class.class_object(), expected_attribute_name, res) } + AttributeBase1::BoundMethodFunction(bound_func) => { + let mut func_bases = Vec::new(); + self.as_attribute_base1(bound_func.clone().as_type(), &mut func_bases); + for base1 in func_bases { + self.completions_inner1(&base1, expected_attribute_name, res); + } + } AttributeBase1::TypeAny(_) | AttributeBase1::TypeNever => self.completions_class_type( self.stdlib.builtins_type(), expected_attribute_name, From b3c00f206910061b89f13d7dfc5e5ed56b8d115a Mon Sep 17 00:00:00 2001 From: Asuka Minato Date: Sat, 8 Nov 2025 05:18:36 +0900 Subject: [PATCH 3/4] fix --- pyrefly/lib/alt/attr.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pyrefly/lib/alt/attr.rs b/pyrefly/lib/alt/attr.rs index 903fd79448..1820daf503 100644 --- a/pyrefly/lib/alt/attr.rs +++ b/pyrefly/lib/alt/attr.rs @@ -51,6 +51,7 @@ use crate::types::type_var::Restriction; use crate::types::typed_dict::TypedDict; use crate::types::types::AnyStyle; use crate::types::types::BoundMethod; +use crate::types::types::BoundMethodType; use crate::types::types::Overload; use crate::types::types::SuperObj; use crate::types::types::Type; @@ -1742,7 +1743,9 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> { acc.push(AttributeBase1::ClassInstance( self.stdlib.method_type().clone(), )); - acc.push(AttributeBase1::BoundMethodFunction(bound_method.func.clone())); + acc.push(AttributeBase1::BoundMethodFunction( + bound_method.func.clone(), + )); } Type::Ellipsis => { if let Some(cls) = self.stdlib.ellipsis_type() { From 505236b0db1918d52bd3449d98ddffa6447dc468 Mon Sep 17 00:00:00 2001 From: Asuka Minato Date: Wed, 12 Nov 2025 18:43:31 +0900 Subject: [PATCH 4/4] fix based on comment --- pyrefly/lib/alt/attr.rs | 55 +++++++++++++++++++++++++---------------- 1 file changed, 34 insertions(+), 21 deletions(-) diff --git a/pyrefly/lib/alt/attr.rs b/pyrefly/lib/alt/attr.rs index 1820daf503..25eafacd8d 100644 --- a/pyrefly/lib/alt/attr.rs +++ b/pyrefly/lib/alt/attr.rs @@ -50,7 +50,6 @@ use crate::types::read_only::ReadOnlyReason; use crate::types::type_var::Restriction; use crate::types::typed_dict::TypedDict; use crate::types::types::AnyStyle; -use crate::types::types::BoundMethod; use crate::types::types::BoundMethodType; use crate::types::types::Overload; use crate::types::types::SuperObj; @@ -436,9 +435,9 @@ enum AttributeBase1 { TypedDict(TypedDict), /// Attribute lookup on a base as part of a subset check against a protocol. ProtocolSubset(Box), - /// Treat methods decorated with descriptors as if their underlying function were directly accessible. - /// Missing attributes on this variant are ignored – it only augments lookup results when present. - BoundMethodFunction(BoundMethodType), + /// Bound methods prefer exposing builtin `types.MethodType` attributes but fall back to the + /// underlying function's attributes when the builtin ones are missing. + BoundMethod(BoundMethodType), } impl AttributeBase1 { @@ -1239,13 +1238,24 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> { acc.not_found(NotFoundOn::ClassObject(class.class_object().dupe(), base)); } } - AttributeBase1::BoundMethodFunction(bound_func) => { - let mut func_bases = Vec::new(); - self.as_attribute_base1(bound_func.clone().as_type(), &mut func_bases); - for base1 in func_bases { - let not_found_len = acc.not_found.len(); - self.lookup_attr_from_attribute_base1(base1, attr_name, acc); + AttributeBase1::BoundMethod(bound_func) => { + let method_type_base = + AttributeBase1::ClassInstance(self.stdlib.method_type().clone()); + let found_len = acc.found.len(); + let not_found_len = acc.not_found.len(); + let error_len = acc.internal_error.len(); + self.lookup_attr_from_attribute_base1(method_type_base, attr_name, acc); + if acc.found.len() == found_len { acc.not_found.truncate(not_found_len); + acc.internal_error.truncate(error_len); + let mut func_bases = Vec::new(); + self.as_attribute_base1(bound_func.clone().as_type(), &mut func_bases); + for base1 in func_bases { + self.lookup_attr_from_attribute_base1(base1, attr_name, acc); + } + } else { + acc.not_found.truncate(not_found_len); + acc.internal_error.truncate(error_len); } } @@ -1740,12 +1750,7 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> { }, )), Type::BoundMethod(bound_method) => { - acc.push(AttributeBase1::ClassInstance( - self.stdlib.method_type().clone(), - )); - acc.push(AttributeBase1::BoundMethodFunction( - bound_method.func.clone(), - )); + acc.push(AttributeBase1::BoundMethod(bound_method.func.clone())); } Type::Ellipsis => { if let Some(cls) = self.stdlib.ellipsis_type() { @@ -2171,11 +2176,19 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> { AttributeBase1::TypeQuantified(_, class) => { self.completions_class(class.class_object(), expected_attribute_name, res) } - AttributeBase1::BoundMethodFunction(bound_func) => { - let mut func_bases = Vec::new(); - self.as_attribute_base1(bound_func.clone().as_type(), &mut func_bases); - for base1 in func_bases { - self.completions_inner1(&base1, expected_attribute_name, res); + AttributeBase1::BoundMethod(bound_func) => { + let before = res.len(); + self.completions_class_type( + self.stdlib.method_type(), + expected_attribute_name, + res, + ); + if res.len() == before { + let mut func_bases = Vec::new(); + self.as_attribute_base1(bound_func.clone().as_type(), &mut func_bases); + for base1 in func_bases { + self.completions_inner1(&base1, expected_attribute_name, res); + } } } AttributeBase1::TypeAny(_) | AttributeBase1::TypeNever => self.completions_class_type(