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
18 changes: 9 additions & 9 deletions conformance/third_party/conformance.exp
Original file line number Diff line number Diff line change
Expand Up @@ -3754,8 +3754,8 @@
{
"code": -2,
"column": 12,
"concise_description": "revealed type: (x: list[Unknown], y: list[Unknown]) -> Class8[Unknown]",
"description": "revealed type: (x: list[Unknown], y: list[Unknown]) -> Class8[Unknown]",
"concise_description": "revealed type: [T](x: list[T], y: list[T]) -> Class8[T]",
"description": "revealed type: [T](x: list[T], y: list[T]) -> Class8[T]",
"line": 184,
"name": "reveal-type",
"severity": "info",
Expand All @@ -3764,14 +3764,14 @@
},
{
"code": -2,
"column": 12,
"concise_description": "assert_type(Class8[Unknown], Class8[str]) failed",
"description": "assert_type(Class8[Unknown], Class8[str]) failed",
"line": 185,
"name": "assert-type",
"column": 9,
"concise_description": "Argument `list[str]` is not assignable to parameter `y` with type `list[int]`",
"description": "Argument `list[str]` is not assignable to parameter `y` with type `list[int]`",
"line": 186,
"name": "bad-argument-type",
"severity": "error",
"stop_column": 41,
"stop_line": 185
"stop_column": 13,
"stop_line": 186
},
{
"code": -2,
Expand Down
5 changes: 1 addition & 4 deletions conformance/third_party/conformance.result
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,7 @@
"constructors_call_metaclass.py": [],
"constructors_call_new.py": [],
"constructors_call_type.py": [],
"constructors_callable.py": [
"Line 186: Expected 1 errors",
"Line 185: Unexpected errors ['assert_type(Class8[Unknown], Class8[str]) failed']"
],
"constructors_callable.py": [],
"constructors_consistency.py": [],
"dataclasses_descriptors.py": [
"Line 32: Unexpected errors ['Cannot set field `y` to data descriptor `Desc1` with inconsistent types\\n Return type `Desc1 | int` of `Desc1.__get__` is not assignable to value type `int` of `Desc1.__set__`']",
Expand Down
8 changes: 4 additions & 4 deletions conformance/third_party/results.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
{
"total": 140,
"pass": 134,
"fail": 6,
"pass": 135,
"fail": 5,
"pass_rate": 0.96,
"differences": 13,
"differences": 11,
"passing": [
"aliases_explicit.py",
"aliases_implicit.py",
Expand All @@ -25,6 +25,7 @@
"constructors_call_metaclass.py",
"constructors_call_new.py",
"constructors_call_type.py",
"constructors_callable.py",
"constructors_consistency.py",
"dataclasses_final.py",
"dataclasses_frozen.py",
Expand Down Expand Up @@ -143,7 +144,6 @@
"failing": {
"annotations_forward_refs.py": 2,
"callables_annotation.py": 1,
"constructors_callable.py": 2,
"dataclasses_descriptors.py": 2,
"exceptions_context_managers.py": 2,
"generics_scoping.py": 4
Expand Down
28 changes: 24 additions & 4 deletions pyrefly/lib/alt/call.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1965,6 +1965,22 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> {
}

pub fn constructor_to_callable(&self, cls: &ClassType) -> Type {
let new_attr_ty = self.get_dunder_new(cls, false);
self.constructor_to_callable_impl(cls, new_attr_ty, false)
}

/// Convert a bare class definition while keeping its type parameters generic.
pub fn constructor_to_callable_for_class_def(&self, cls: &ClassType) -> Option<Type> {
let new_attr_ty = self.get_dunder_new_for_class_def(cls)?;
Some(self.constructor_to_callable_impl(cls, Some(new_attr_ty), true))
}

fn constructor_to_callable_impl(
&self,
cls: &ClassType,
new_attr_ty: Option<Type>,
preserve_class_tparams: bool,
) -> Type {
let class_type = self.heap.mk_class_type(cls.clone());
if let Some(metaclass_call_attr_ty) = self.get_metaclass_dunder_call(cls) {
// Use the metaclass __call__ directly (ignoring __new__ and __init__) when either:
Expand All @@ -1989,10 +2005,14 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> {
))
};
// Check the __new__ method and whether it comes from object or has been overridden
let (new_attr_ty, overrides_new) = if let Some(t) = self
.get_dunder_new(cls, false)
.and_then(|t| self.bind_dunder_new(&t, cls.clone()))
{
let bind_new = |t: Type| {
if preserve_class_tparams {
self.bind_dunder_new_for_class_def(&t, cls.clone())
} else {
self.bind_dunder_new(&t, cls.clone())
}
};
let (new_attr_ty, overrides_new) = if let Some(t) = new_attr_ty.and_then(bind_new) {
if t.callable_return_type(self.heap)
.is_some_and(|ret| !self.is_compatible_constructor_return(&ret, cls.class_object()))
{
Expand Down
31 changes: 31 additions & 0 deletions pyrefly/lib/alt/class/class_field.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4846,6 +4846,37 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> {
}
}

/// Get `__new__` through class access when its non-receiver parameters use class type params.
pub fn get_dunder_new_for_class_def(&self, cls: &ClassType) -> Option<Type> {
let new_member =
self.get_class_member_with_defining_class(cls.class_object(), &dunder::NEW)?;
if new_member.is_defined_on("builtins", "object") {
None
} else {
let new_ty = self
.as_class_attribute(
&dunder::NEW,
&new_member.value,
&ClassBase::ClassDef(cls.clone()),
)
.as_instance_method()?;
let class_tparams = self.get_class_tparams(cls.class_object());
let mut uses_class_tparam = false;
new_ty.visit_toplevel_callable(|callable| {
if let Some(callable) = callable.strip_first_param() {
let mut quantifieds = SmallSet::new();
callable
.params
.visit(&mut |ty| ty.collect_quantifieds(&mut quantifieds));
uses_class_tparam |= class_tparams
.iter()
.any(|tparam| quantifieds.contains(tparam));
}
});
uses_class_tparam.then_some(new_ty)
}
}

fn get_dunder_init_helper(&self, instance: &Instance, get_object_init: bool) -> Option<Type> {
let init_method =
self.get_class_member_with_defining_class(instance.class, &dunder::INIT)?;
Expand Down
26 changes: 26 additions & 0 deletions pyrefly/lib/alt/function.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2397,6 +2397,32 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> {
)
}

/// Bind `__new__` while keeping class type parameters used by the constructor callable.
pub fn bind_dunder_new_for_class_def(&self, t: &Type, cls: ClassType) -> Option<Type> {
let mut bound = self.bind_function(
t,
&self.heap.mk_type_of(self.heap.mk_class_type(cls)),
false,
&mut |a, b| self.is_subset_eq(a, b),
)?;
self.expand_mut(&mut bound);
match bound {
Type::Forall(forall) => {
let Forall { tparams, body } = *forall;
let mut used = SmallSet::new();
body.visit(&mut |ty| ty.collect_quantifieds(&mut used));
let tparams = tparams
.iter()
.filter(|tparam| used.contains(*tparam))
.cloned()
.collect();
drop(used);
Some(body.forall(Arc::new(TParams::new(tparams))))
}
_ => Some(bound),
}
}

/// Bind a `__init__` method for constructor callable conversion.
/// Strips the first parameter and sets the return type to the first param's type.
/// Does not instantiate type variables (they should be inferred at the call site).
Expand Down
6 changes: 6 additions & 0 deletions pyrefly/lib/solver/subset.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2332,6 +2332,12 @@ impl<'a, Ans: LookupAnswer> Subset<'a, Ans> {
{
self.is_subset_eq(&self.type_order.constructor_to_callable(got_cls), want)
}
(Type::ClassDef(got), Type::BoundMethod(_) | Type::Callable(_) | Type::Function(_))
if let Some(constructor) =
self.type_order.constructor_to_callable_for_class_def(got) =>
{
self.is_subset_eq(&constructor, want)
}
(Type::ClassDef(got), Type::BoundMethod(_) | Type::Callable(_) | Type::Function(_)) => {
self.is_subset_eq(&Type::type_of(self.type_order.promote_silently(got)), want)
}
Expand Down
5 changes: 5 additions & 0 deletions pyrefly/lib/solver/type_order.rs
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,11 @@ impl<'a, Ans: LookupAnswer> TypeOrder<'a, Ans> {
self.0.constructor_to_callable(cls)
}

pub fn constructor_to_callable_for_class_def(self, cls: &Class) -> Option<Type> {
self.0
.constructor_to_callable_for_class_def(&self.0.as_class_type_unchecked(cls))
}

pub fn instantiate_fresh_forall(self, forall: Forall<Forallable>) -> (QuantifiedHandle, Type) {
self.0.instantiate_fresh_forall(forall)
}
Expand Down
5 changes: 2 additions & 3 deletions pyrefly/lib/test/callable.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1401,7 +1401,6 @@ def test(p4: Proto4[...], p7: Proto7):
);

testcase!(
bug = "conformance: Constructor to Callable conversion issues with overloads and __new__",
test_constructor_callable_conversion,
r#"
from typing import Callable, ParamSpec, TypeVar, Self, assert_type, overload, Generic
Expand Down Expand Up @@ -1435,8 +1434,8 @@ class Class8(Generic[T]):
return super().__new__(cls)

r8 = accepts_callable(Class8)
# pyrefly incorrectly errors on this - should be OK
assert_type(r8([""], [""]), Class8[str]) # E: assert_type(Class8[Unknown], Class8[str]) failed
assert_type(r8([""], [""]), Class8[str])
r8([1], [""]) # E: Argument `list[str]` is not assignable to parameter `y` with type `list[int]`
"#,
);

Expand Down
Loading