Skip to content
Draft
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 29 additions & 3 deletions pyrefly/lib/alt/attr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ 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;
use crate::types::types::Type;
Expand Down Expand Up @@ -434,6 +436,9 @@ enum AttributeBase1 {
TypedDict(TypedDict),
/// Attribute lookup on a base as part of a subset check against a protocol.
ProtocolSubset(Box<AttributeBase1>),
/// 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 {
Expand Down Expand Up @@ -1234,6 +1239,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) {
Expand Down Expand Up @@ -1725,9 +1739,14 @@ 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(),
));
acc.push(AttributeBase1::BoundMethodFunction(
bound_method.func.clone(),
));
}
Type::Ellipsis => {
if let Some(cls) = self.stdlib.ellipsis_type() {
acc.push(AttributeBase1::ClassInstance(cls.clone()))
Expand Down Expand Up @@ -2152,6 +2171,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,
Expand Down
35 changes: 35 additions & 0 deletions pyrefly/lib/test/descriptors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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#"
Expand Down