Skip to content

Commit 26e1cc8

Browse files
committed
non-instance metaclass __call__ bypasses __new__ and __init__
1 parent acfc7fa commit 26e1cc8

2 files changed

Lines changed: 63 additions & 23 deletions

File tree

crates/ty_python_semantic/resources/mdtest/metaclass.md

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -384,7 +384,9 @@ class Meta(type):
384384
def __call__(cls, x: int) -> int: ...
385385
@overload
386386
def __call__(cls, x: str) -> "D": ...
387-
def __call__(cls, x: int | str) -> Any:
387+
@overload
388+
def __call__(cls, x: float) -> "D": ...
389+
def __call__(cls, x: int | str | float) -> Any:
388390
raise NotImplementedError
389391

390392
class D(metaclass=Meta):
@@ -398,17 +400,21 @@ class D(metaclass=Meta):
398400
def __init__(self, x: Literal["ok"]) -> None:
399401
pass
400402

401-
# `__new__` rejects `int` even though metaclass `__call__` accepts it.
403+
# Non-instance metaclass `__call__` path bypasses `__new__` and `__init__`.
404+
# The `int -> int` metaclass overload is selected.
405+
reveal_type(D(1)) # revealed: int
406+
407+
# `__new__` rejects `float` even though metaclass `__call__` accepts it and returns `D`.
402408
# error: [no-matching-overload]
403-
D(1)
409+
D(1.5)
404410

405411
# `__init__` rejects the argument after both `__call__` and `__new__` match `str -> D`.
406412
# error: [invalid-argument-type]
407413
D("bad")
408414

409415
# Metaclass `__call__` rejects `bytes` even though `__new__` accepts it.
410416
# error: [no-matching-overload]
411-
reveal_type(D(b"hello")) # revealed: int
417+
D(b"hello")
412418

413419
reveal_type(D("ok")) # revealed: D
414420
```

crates/ty_python_semantic/src/types.rs

Lines changed: 53 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4983,13 +4983,60 @@ impl<'db> Type<'db> {
49834983
(Place::Undefined, true) => None,
49844984
};
49854985

4986+
// Three-layer constructor handling for mixed metaclass `__call__`:
4987+
// - Non-instance-returning metaclass overloads bypass `__new__`/`__init__`.
4988+
// - Instance-returning metaclass overloads must satisfy downstream constructor checks.
4989+
if let Some(metaclass_mixed_sigs) = metaclass_mixed_non_instance_sigs {
4990+
let downstream_bindings = if let Some(bindings) = missing_init_bindings {
4991+
Some(bindings)
4992+
} else if new_is_all_non_instance {
4993+
// `__new__` is still relevant for instance-returning metaclass paths, but
4994+
// `__init__` is skipped when `__new__` is all non-instance.
4995+
new_bindings.map(|(new_bindings, _)| new_bindings)
4996+
} else if let Some(new_mixed_sigs) = new_mixed_non_instance_sigs {
4997+
let mut bindings: Bindings<'db> =
4998+
CallableBinding::from_overloads(self_type, Vec::from(new_mixed_sigs)).into();
4999+
if let Some((init_bindings, _)) = init_bindings.as_ref() {
5000+
bindings.set_mixed_constructor_init(class.class_literal(db), init_bindings);
5001+
}
5002+
Some(bindings)
5003+
} else {
5004+
let mut all_bindings: SmallVec<[Bindings<'db>; 2]> = SmallVec::new();
5005+
let mut callable_type_builder = UnionBuilder::new(db);
5006+
5007+
if let Some((new_bindings, new_callable)) = new_bindings {
5008+
all_bindings.push(new_bindings);
5009+
callable_type_builder = callable_type_builder.add(new_callable);
5010+
}
5011+
if let Some((init_bindings, init_callable)) = init_bindings {
5012+
all_bindings.push(init_bindings);
5013+
callable_type_builder = callable_type_builder.add(init_callable);
5014+
}
5015+
5016+
match all_bindings.len() {
5017+
0 => None,
5018+
1 => Some(all_bindings.into_iter().next().unwrap()),
5019+
_ => {
5020+
let callable_type = callable_type_builder.build();
5021+
Some(Bindings::from_union(callable_type, all_bindings))
5022+
}
5023+
}
5024+
};
5025+
5026+
let mut bindings: Bindings<'db> =
5027+
CallableBinding::from_overloads(self, Vec::from(metaclass_mixed_sigs)).into();
5028+
if let Some(downstream_bindings) = downstream_bindings.as_ref() {
5029+
bindings.set_mixed_constructor_init(class.class_literal(db), downstream_bindings);
5030+
}
5031+
return bindings
5032+
.with_generic_context(db, class_generic_context)
5033+
.with_constructor_instance_type(constructor_instance_ty);
5034+
}
5035+
49865036
// Preserve legacy behavior when `__new__` always returns a non-instance and there are no
49875037
// metaclass constraints to additionally enforce: skip constructor synthesis and use the
49885038
// raw `__new__` return type directly.
4989-
if new_is_all_non_instance
4990-
&& metaclass_instance_bindings.is_none()
4991-
&& metaclass_mixed_non_instance_sigs.is_none()
4992-
{
5039+
if new_is_all_non_instance && metaclass_instance_bindings.is_none() {
49935040
let (new_bindings, _) = new_bindings.unwrap();
49945041
return new_bindings.with_generic_context(db, class_generic_context);
49955042
}
@@ -5004,28 +5051,15 @@ impl<'db> Type<'db> {
50045051
} else {
50055052
// Collect all bindings that must accept the constructor call.
50065053
// This may include `__new__`, `__init__`, and/or a metaclass `__call__`.
5007-
let has_mixed_constructor_paths = new_mixed_non_instance_sigs.is_some()
5008-
|| metaclass_mixed_non_instance_sigs.is_some();
5009-
let mut all_bindings: SmallVec<[Bindings<'db>; 4]> = SmallVec::new();
5054+
let has_mixed_constructor_paths = new_mixed_non_instance_sigs.is_some();
5055+
let mut all_bindings: SmallVec<[Bindings<'db>; 3]> = SmallVec::new();
50105056
let mut callable_type_builder = UnionBuilder::new(db);
50115057

50125058
if let Some((metaclass_bindings, metaclass_ty)) = metaclass_instance_bindings {
50135059
all_bindings.push(metaclass_bindings);
50145060
callable_type_builder = callable_type_builder.add(metaclass_ty);
50155061
}
50165062

5017-
// If `__new__` or metaclass `__call__` had mixed return types (some non-instance,
5018-
// some instance), keep all overloads with per-overload return types and attach
5019-
// deferred `__init__` validation. If both are mixed, both constraint sets apply.
5020-
if let Some(combined_sigs) = metaclass_mixed_non_instance_sigs {
5021-
let mut bindings: Bindings<'db> =
5022-
CallableBinding::from_overloads(self, Vec::from(combined_sigs)).into();
5023-
if let Some((init_bindings, _)) = init_bindings.as_ref() {
5024-
bindings.set_mixed_constructor_init(class.class_literal(db), init_bindings);
5025-
}
5026-
all_bindings.push(bindings);
5027-
callable_type_builder = callable_type_builder.add(self);
5028-
}
50295063
if let Some(combined_sigs) = new_mixed_non_instance_sigs {
50305064
let mut bindings: Bindings<'db> =
50315065
CallableBinding::from_overloads(self_type, Vec::from(combined_sigs)).into();

0 commit comments

Comments
 (0)