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
8 changes: 7 additions & 1 deletion pyrefly/lib/alt/call.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1091,6 +1091,7 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> {
// Tracks whether we've already recorded a trace for IDE features.
// Priority: metaclass __call__ > overridden __new__ > __init__.
let mut recorded_trace = false;
let prefer_init_trace = self.constructor_prefers_init_over_inherited_new(&cls);
let errors = self.error_collector();
if let Some(ret) = self.call_metaclass(
&cls,
Expand Down Expand Up @@ -1187,7 +1188,7 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> {
AttributeReferenceKind::ConstructorCall,
);
}
if !recorded_trace {
if !recorded_trace && !prefer_init_trace {
self.record_resolved_trace(arguments_range, &new_method);
recorded_trace = true;
}
Expand Down Expand Up @@ -2269,6 +2270,11 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> {
} else {
(default_constructor(), false)
};
if overrides_init && self.constructor_prefers_init_over_inherited_new(cls) {
// An inherited catch-all `__new__` should not obscure a more useful `__init__`
// signature. Direct construction still checks both methods independently.
return init_attr_ty;
}
if !overrides_new && overrides_init {
// If `__init__` is overridden and `__new__` is inherited from object, use `__init__`
init_attr_ty
Expand Down
19 changes: 19 additions & 0 deletions pyrefly/lib/alt/class/class_field.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4945,6 +4945,25 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> {
}
}

/// Whether an inherited permissive `__new__` should yield to an overridden `__init__`
/// when presenting the class as a callable.
pub(crate) fn constructor_prefers_init_over_inherited_new(&self, cls: &ClassType) -> bool {
let Some(new_member) =
self.get_class_member_with_defining_class(cls.class_object(), &dunder::NEW)
else {
return false;
};
self.get_dunder_init(cls, false).is_some()
&& new_member.defining_class != *cls.class_object()
&& new_member.value.is_function_without_return_annotation()
&& new_member
.value
.ty()
.visit_toplevel_func_metadata::<bool>(&|meta| {
meta.flags.has_gradual_variadic_params
})
}

/// 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 =
Expand Down
38 changes: 38 additions & 0 deletions pyrefly/lib/test/lsp/completion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1666,6 +1666,44 @@ Completion Results:
);
}

#[test]
fn kwargs_completion_pydantic_constructor_ignores_inherited_unannotated_new() {
let sqlmodel = r#"
from typing import Any
from pydantic import BaseModel

class SQLModel(BaseModel):
def __new__(cls, *args: Any, **kwargs: Any):
return object.__new__(cls)

def __init__(self, **data: Any) -> None: ...
"#;
let main = r#"
from sqlmodel import SQLModel

class A(SQLModel):
a: int
b: str

A(
# ^
"#;
let pydantic_path =
std::env::var("PYDANTIC_TEST_PATH").expect("PYDANTIC_TEST_PATH must be set");
let mut test_env = TestEnv::new_with_site_package_paths(&[&pydantic_path]);
test_env.add("sqlmodel", sqlmodel);
test_env.add("main", main);
let (state, handle) = test_env
.with_default_require_level(Require::Exports)
.to_state();
let report =
get_default_test_report()(&state, &handle("main"), extract_cursors_for_test(main)[0]);
assert!(report.contains("- (Variable) a=:"), "{report}");
assert!(report.contains("- (Variable) b=:"), "{report}");
assert!(!report.contains("args="), "{report}");
assert!(!report.contains("kwargs="), "{report}");
}

#[test]
fn kwargs_completion_dunder_call_metaclass_constructor() {
let code = r#"
Expand Down
45 changes: 45 additions & 0 deletions pyrefly/lib/test/lsp/hover.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1994,6 +1994,51 @@ Person("Alice", 25)
);
}

#[test]
fn hover_on_pydantic_constructor_ignores_inherited_unannotated_new() {
let sqlmodel = r#"
from typing import Any
from pydantic import BaseModel

class SQLModel(BaseModel):
def __new__(cls, *args: Any, **kwargs: Any):
return object.__new__(cls)

def __init__(self, **data: Any) -> None: ...
"#;
let main = r#"
from sqlmodel import SQLModel

class A(SQLModel):
a: int
b: str

value = A
# ^
A(a=1, b="")
#^
"#;
let pydantic_path =
std::env::var("PYDANTIC_TEST_PATH").expect("PYDANTIC_TEST_PATH must be set");
let mut test_env = TestEnv::new_with_site_package_paths(&[&pydantic_path]);
test_env.add("sqlmodel", sqlmodel);
test_env.add("main", main);
let (state, handle) = test_env
.with_default_require_level(Require::Exports)
.to_state();
for position in extract_cursors_for_test(main) {
let report = get_test_report(&state, &handle("main"), position);
assert!(
report.contains("a:") && report.contains("b:"),
"Expected Pydantic constructor hover to show synthesized fields, got: {report}"
);
assert!(
!report.contains("*args: Any") && !report.contains("**kwargs: Any"),
"Expected Pydantic constructor hover to hide inherited broad __new__, got: {report}"
);
}
}

#[test]
fn hover_on_namedtuple_constructor_shows_field_signature() {
let code = r#"
Expand Down
Loading