diff --git a/pyrefly/lib/solver/solver.rs b/pyrefly/lib/solver/solver.rs index 5654db0d25..8d902236db 100644 --- a/pyrefly/lib/solver/solver.rs +++ b/pyrefly/lib/solver/solver.rs @@ -2645,6 +2645,9 @@ pub enum SubsetError { /// A function without **kwargs is not assignable to a function with Unpack-ed TypedDict **kwargs /// unless the TypedDict is closed. OpenTypedDictKwargs(Name), + /// A bound method cannot be converted to a callable because there is no + /// receiver parameter to bind. + BoundMethodMissingSelf(Name), // TODO(rechen): replace this with specific reasons Other, } @@ -2683,6 +2686,9 @@ impl SubsetError { SubsetError::OpenTypedDictKwargs(td) => Some(format!( "Callable without `**kwargs` cannot be assigned to callable with `**kwargs: Unpack[{td}]`, because `{td}` is not closed and may have additional unknown keys" )), + SubsetError::BoundMethodMissingSelf(function) => Some(format!( + "Function `{function}` is treated as a method when accessed from an instance, but its signature does not accept a bound `self` argument" + )), SubsetError::Other => None, } } diff --git a/pyrefly/lib/solver/subset.rs b/pyrefly/lib/solver/subset.rs index d9a9b73866..bc8c0b6b14 100644 --- a/pyrefly/lib/solver/subset.rs +++ b/pyrefly/lib/solver/subset.rs @@ -1843,21 +1843,31 @@ impl<'a, Ans: LookupAnswer> Subset<'a, Ans> { Ok(()) } (Type::Overload(overload), want) => self.is_subset_overload(overload, want), - (Type::BoundMethod(method), Type::Callable(_) | Type::Function(_)) + (Type::BoundMethod(method), Type::Callable(_) | Type::Function(_)) => { if let Some(l_no_self) = self.type_order.bind_boundmethod(method, &mut |got, want| { self.is_subset_eq(got, want).is_ok() - }) => - { - self.is_subset_eq(&l_no_self, want) + }) + { + self.is_subset_eq(&l_no_self, want) + } else { + Err(SubsetError::BoundMethodMissingSelf( + method.func.metadata().kind.function_name().into_owned(), + )) + } } - (Type::Callable(_) | Type::Function(_), Type::BoundMethod(method)) + (Type::Callable(_) | Type::Function(_), Type::BoundMethod(method)) => { if let Some(u_no_self) = self.type_order.bind_boundmethod(method, &mut |got, want| { self.is_subset_eq(got, want).is_ok() - }) => - { - self.is_subset_eq(got, &u_no_self) + }) + { + self.is_subset_eq(got, &u_no_self) + } else { + Err(SubsetError::BoundMethodMissingSelf( + method.func.metadata().kind.function_name().into_owned(), + )) + } } (Type::BoundMethod(l), Type::BoundMethod(u)) if let Some(l_no_self) = self diff --git a/pyrefly/lib/test/attributes.rs b/pyrefly/lib/test/attributes.rs index a346991eaa..1c0ec0b768 100644 --- a/pyrefly/lib/test/attributes.rs +++ b/pyrefly/lib/test/attributes.rs @@ -319,6 +319,23 @@ def foo(x: Callable[[int], str], c: C, c2: C2, c3: C3): "#, ); +testcase!( + test_classvar_callable_return_bound_method_error, + r#" +from typing import Callable, ClassVar + +def integer_factory() -> int: + return 1 + +class A: + _factory: ClassVar[Callable[[], int]] = integer_factory + + @property + def factory(self) -> Callable[[], int]: + return self._factory # E: Function `_factory` is treated as a method when accessed from an instance, but its signature does not accept a bound `self` argument + "#, +); + testcase!( test_bound_classmethod_explicit_targs, r#"