Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
14 changes: 12 additions & 2 deletions pyrefly/lib/alt/call.rs
Original file line number Diff line number Diff line change
Expand Up @@ -301,8 +301,18 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> {
self.as_call_target_impl(ty, Some(quantified), dunder_call)
})
} else if dunder_call {
// Avoid infinite recursion
CallTargetLookup::Error(vec![])
self.instance_as_dunder_call(&cls).map_or(
CallTargetLookup::Error(vec![]),
|ty| {
let is_self_recursive = matches!(&ty, Type::ClassType(inner) if inner == &cls)
|| matches!(&ty, Type::SelfType(inner) if inner.class_object() == cls.class_object());
if is_self_recursive {
CallTargetLookup::Error(vec![])
} else {
self.as_call_target_impl(ty, quantified, /* dunder_call */ true)
}
},
)
} else {
self.instance_as_dunder_call(&cls).map_or(
CallTargetLookup::Error(vec![]),
Expand Down
19 changes: 13 additions & 6 deletions pyrefly/lib/alt/class/class_field.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4228,14 +4228,14 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> {
/// Return `__call__` as a bound method if instances of `cls` have `__call__`.
/// This is what the runtime automatically does when we try to call an instance.
pub fn instance_as_dunder_call(&self, cls: &ClassType) -> Option<Type> {
self.get_instance_attribute(cls, &dunder::CALL)
.and_then(|attr| attr.as_instance_method())
let attr = self.get_instance_attribute(cls, &dunder::CALL)?;
self.resolve_dunder_call_attr(attr)
}

/// Return `__call__` as bound method when called on `Self`.
pub fn self_as_dunder_call(&self, cls: &ClassType) -> Option<Type> {
self.get_self_attribute(cls, &dunder::CALL)
.and_then(|attr| attr.as_instance_method())
let attr = self.get_self_attribute(cls, &dunder::CALL)?;
self.resolve_dunder_call_attr(attr)
}

/// Return `__call__` as a bound method if instances of `type_var` have `__call__`.
Expand All @@ -4246,8 +4246,8 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> {
quantified: Quantified,
upper_bound: &ClassType,
) -> Option<Type> {
self.get_bounded_quantified_attribute(quantified, upper_bound, &dunder::CALL)
.and_then(|attr| attr.as_instance_method())
let attr = self.get_bounded_quantified_attribute(quantified, upper_bound, &dunder::CALL)?;
self.resolve_dunder_call_attr(attr)
}

fn callable_params_and_flags(mut ty: Type) -> Option<(ParamList, FuncFlags)> {
Expand All @@ -4264,4 +4264,11 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> {
}?;
Some((params, flags))
}

fn resolve_dunder_call_attr(&self, attr: ClassAttribute) -> Option<Type> {
let errors = self.error_swallower();
let fake_range = TextRange::default();
self.resolve_get_class_attr(&dunder::CALL, attr, fake_range, &errors, None)
.ok()
}
}
17 changes: 17 additions & 0 deletions pyrefly/lib/test/descriptors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,23 @@ C().d = 42 # E: Attribute `d` of class `C` is a read-only descriptor with no `
"#,
);

testcase!(
test_descriptor_dunder_call,
r#"
from typing import assert_type
class SomeCallable:
def __call__(self, x: int) -> str:
return "a"
class Descriptor:
def __get__(self, instance: object, owner: type | None = None) -> SomeCallable:
return SomeCallable()
class B:
__call__: Descriptor = Descriptor()
b_instance = B()
assert_type(b_instance(1), str)
"#,
);

// Test that instance-only attributes with descriptor types are not treated as descriptors.
// Descriptor protocol only applies to class-body initialized attributes; both annotation-only
// and method-initialized attributes should allow assignment.
Expand Down
Loading