[ty] Respect __new__ and metaclass __call__ types

This commit is contained in:
Charlie Marsh
2025-12-31 11:42:57 -05:00
parent 758926eecd
commit 817336ad35
4 changed files with 294 additions and 17 deletions

View File

@@ -1,5 +1,63 @@
# Class definitions
## `__new__` return type
Python's `__new__` method can return any type, not just an instance of the class. When `__new__`
returns a type that is not assignable to the class instance type, we use that return type directly.
### `__new__` returning a different type
```py
class ReturnsInt:
def __new__(cls) -> int:
return 42
reveal_type(ReturnsInt()) # revealed: int
x: int = ReturnsInt() # OK
y: ReturnsInt = ReturnsInt() # error: [invalid-assignment]
```
### `__new__` returning a union type
```py
class MaybeInt:
def __new__(cls, value: str) -> "int | MaybeInt":
try:
return int(value)
except ValueError:
return object.__new__(cls)
reveal_type(MaybeInt("42")) # revealed: int | MaybeInt
a: int | MaybeInt = MaybeInt("42") # OK
b: int = MaybeInt("42") # error: [invalid-assignment]
```
### `__new__` returning the class type
When `__new__` returns the class type (or `Self`), the normal instance type is used.
```py
class Normal:
def __new__(cls) -> "Normal":
return object.__new__(cls)
reveal_type(Normal()) # revealed: Normal
```
### `__new__` with no return type annotation
When `__new__` has no return type annotation, we fall back to the instance type.
```py
class NoAnnotation:
def __new__(cls):
return object.__new__(cls)
reveal_type(NoAnnotation()) # revealed: NoAnnotation
```
## Deferred resolution of bases
### Only the stringified name is deferred

View File

@@ -1,3 +1,96 @@
## Custom `__call__` on metaclass
When a metaclass defines a custom `__call__` method, it controls what happens when the class is
called. The return type and parameter types of the metaclass `__call__` are used instead of the
class's `__new__` and `__init__` methods.
### Basic metaclass `__call__`
```py
class Meta(type):
def __call__(cls, x: int, y: str) -> str:
return y
class Foo(metaclass=Meta): ...
reveal_type(Foo(1, "hello")) # revealed: str
a: str = Foo(1, "hello") # OK
```
### Metaclass `__call__` with wrong arguments
```py
class Meta(type):
def __call__(cls, x: int) -> int:
return x
class Foo(metaclass=Meta): ...
Foo("wrong") # error: [invalid-argument-type]
Foo() # error: [missing-argument]
Foo(1, 2) # error: [too-many-positional-arguments]
```
### Metaclass `__call__` takes precedence over `__init__`
```py
class Meta(type):
def __call__(cls) -> str:
return "hello"
class Foo(metaclass=Meta):
def __init__(self, x: int, y: int) -> None:
pass
# The metaclass __call__ takes precedence, so no arguments are needed
# and the return type is str, not Foo.
reveal_type(Foo()) # revealed: str
```
### Metaclass `__call__` with TypeVar return type
When the metaclass `__call__` returns a TypeVar bound to the class type, it's essentially a
pass-through to the normal constructor machinery. In this case, we should still check the `__new__`
and `__init__` signatures.
```py
from typing import TypeVar
T = TypeVar("T")
class Meta(type):
def __call__(cls: type[T], *args, **kwargs) -> T:
return object.__new__(cls)
class Foo(metaclass=Meta):
def __init__(self, x: int) -> None:
pass
# The metaclass __call__ returns T (bound to Foo), so we check __init__ parameters.
Foo() # error: [missing-argument]
reveal_type(Foo(1)) # revealed: Foo
```
### Metaclass `__call__` with no return type annotation
When the metaclass `__call__` has no return type annotation (returns `Unknown`), we should still
check the `__new__` and `__init__` signatures.
```py
class Meta(type):
def __call__(cls, *args, **kwargs):
return object.__new__(cls)
class Foo(metaclass=Meta):
def __init__(self, x: int) -> None:
pass
# No return type annotation means we fall through to check __init__ parameters.
Foo() # error: [missing-argument]
reveal_type(Foo(1)) # revealed: Foo
```
## Default
```py

View File

@@ -7211,8 +7211,77 @@ impl<'db> Type<'db> {
_ => (None, None, self),
};
// As of now we do not model custom `__call__` on meta-classes, so the code below
// only deals with interplay between `__new__` and `__init__` methods.
// Check for a custom `__call__` on the metaclass. If present and it returns a type
// that is not assignable to the class instance type, it takes precedence over the
// `__new__`/`__init__` logic (e.g., returning `int | Meta2` instead of `Class2`).
//
// However, if the metaclass `__call__` returns the class instance type (or a type
// variable that resolves to it), we still need to check `__new__`/`__init__` parameters,
// since the metaclass `__call__` is likely just delegating to `super().__call__()`.
let metaclass_dunder_call = self_type.member_lookup_with_policy(
db,
"__call__".into(),
MemberLookupPolicy::NO_INSTANCE_FALLBACK
| MemberLookupPolicy::META_CLASS_NO_TYPE_FALLBACK,
);
if let Place::Defined(Type::BoundMethod(metaclass_dunder_call), _, boundness, _) =
metaclass_dunder_call.place
{
// Get the declared return type directly from the function signature.
// This avoids needing to perform argument matching just to determine the return type.
let signature = metaclass_dunder_call.function(db).signature(db);
let declared_return_type = signature
.overloads
.first()
.and_then(|sig| sig.return_ty)
.map(|ty| ty.filter_union(db, |elem| !elem.is_dynamic()))
.unwrap_or(Type::unknown());
// Get the instance type for comparison.
let instance_ty = self
.to_instance(db)
.expect("type should be convertible to instance type");
// Only use the metaclass `__call__` directly if:
// 1. The return type is not dynamic (has an explicit annotation).
// 2. The return type doesn't contain unresolved type variables (like `T`).
// 3. The return type is not assignable to the instance type.
//
// This handles cases like `Meta2.__call__` returning `int | Meta2`.
// If the return type has typevars (like `T` which would resolve to the instance),
// is dynamic, or is assignable to the instance, fall through to `__new__`/`__init__`.
if !declared_return_type.is_dynamic()
&& !declared_return_type.has_typevar(db)
&& !declared_return_type.is_assignable_to(db, instance_ty)
{
// Use the metaclass `__call__` for argument checking.
let bindings = Type::BoundMethod(metaclass_dunder_call).bindings(db);
let argument_types = infer_argument_types(Some(bindings.clone()));
let call_result = bindings
.match_parameters(db, &argument_types)
.check_types(db, &argument_types, tcx, &[])
.map_err(CallDunderError::from)
.and_then(|bindings| {
if boundness == Definedness::PossiblyUndefined {
Err(CallDunderError::PossiblyUnbound(Box::new(bindings)))
} else {
Ok(bindings)
}
});
return match call_result {
Ok(bindings) => Ok(bindings.return_type(db)),
Err(error) => Err(ConstructorCallError::MetaclassCall(
error.fallback_return_type(db),
error,
)),
};
}
}
// The code below deals with interplay between `__new__` and `__init__` methods.
// The logic is roughly as follows:
// 1. If `__new__` is defined anywhere in the MRO (except for `object`, since it is always
// present), we call it and analyze outcome. We then analyze `__init__` call, but only
@@ -7223,10 +7292,8 @@ impl<'db> Type<'db> {
// the way to `object` (single `self` argument call). This time it is correct to
// fallback to `object.__init__`, since it will indeed check that no arguments are
// passed.
//
// Note that we currently ignore `__new__` return type, since we do not yet support `Self`
// and most builtin classes use it as return type annotation. We always return the instance
// type.
// 3. If `__new__` returns a type that is not assignable to the class instance type
// (e.g., `int` or `int | A`), we use that return type instead of the instance type.
// Lookup `__new__` method in the MRO up to, but not including, `object`. Also, we must
// avoid `__new__` on `type` since per descriptor protocol, if `__new__` is not defined on
@@ -7307,6 +7374,7 @@ impl<'db> Type<'db> {
}
});
// Call `__init__` if `__new__` was not called, or if `__init__` is explicitly defined.
let init_call_outcome = if new_call_outcome.is_none() || !init_method.is_undefined() {
let call_result = match init_ty
.member_lookup_with_policy(
@@ -7347,6 +7415,28 @@ impl<'db> Type<'db> {
.to_instance(db)
.expect("type should be convertible to instance type");
// Extract the return type from `__new__` if it was called successfully.
// Filter out:
// 1. Dynamic types (`Unknown`, `Any`) since they indicate the `__new__` method doesn't
// have an explicit return type annotation (e.g., synthesized methods like tuple's
// `__new__`) or is inherited from `Any`.
// 2. Types containing unresolved type variables (like `Self`) since they need to be
// resolved by the constructor machinery, not used directly.
let new_return_type = new_call_outcome
.as_ref()
.and_then(|result| result.as_ref().ok())
.map(|bindings| bindings.return_type(db))
.map(|ty| ty.filter_union(db, |elem| !elem.is_dynamic()))
.filter(|ty| !ty.is_dynamic() && !ty.has_typevar(db));
// Determine the return type based on `__new__` return type.
// Only use the `__new__` return type if it returns something that is not a subclass of
// the class being constructed (e.g., `int | A`). Otherwise, use the instance type so that
// proper generic specialization can be applied.
let return_ty = new_return_type
.filter(|return_ty| !return_ty.is_assignable_to(db, instance_ty))
.unwrap_or(instance_ty);
match (generic_origin, new_call_outcome, init_call_outcome) {
// All calls are successful or not called at all
(
@@ -7388,26 +7478,25 @@ impl<'db> Type<'db> {
generic_origin.apply_specialization(db, |_| specialization),
)
})
.unwrap_or(instance_ty);
// Use the `__new__` return type if available, otherwise use instance_ty.
.unwrap_or(return_ty);
Ok(specialized)
}
(None, None | Some(Ok(_)), None | Some(Ok(_))) => Ok(instance_ty),
(None, None | Some(Ok(_)), None | Some(Ok(_))) => Ok(return_ty),
(_, None | Some(Ok(_)), Some(Err(error))) => {
// no custom `__new__` or it was called and succeeded, but `__init__` failed.
Err(ConstructorCallError::Init(instance_ty, error))
// No custom `__new__` or it was called and succeeded, but `__init__` failed.
Err(ConstructorCallError::Init(return_ty, error))
}
(_, Some(Err(error)), None | Some(Ok(_))) => {
// custom `__new__` was called and failed, but init is ok
Err(ConstructorCallError::New(instance_ty, error))
// Custom `__new__` was called and failed, but init is ok.
Err(ConstructorCallError::New(return_ty, error))
}
(_, Some(Err(new_error)), Some(Err(init_error))) => {
// custom `__new__` was called and failed, and `__init__` is also not ok
// Custom `__new__` was called and failed, and `__init__` is also not ok.
Err(ConstructorCallError::NewAndInit(
instance_ty,
new_error,
init_error,
return_ty, new_error, init_error,
))
}
}
@@ -12040,6 +12129,7 @@ enum ConstructorCallError<'db> {
Init(Type<'db>, CallDunderError<'db>),
New(Type<'db>, DunderNewCallError<'db>),
NewAndInit(Type<'db>, DunderNewCallError<'db>, CallDunderError<'db>),
MetaclassCall(Type<'db>, CallDunderError<'db>),
}
impl<'db> ConstructorCallError<'db> {
@@ -12048,6 +12138,7 @@ impl<'db> ConstructorCallError<'db> {
Self::Init(ty, _) => *ty,
Self::New(ty, _) => *ty,
Self::NewAndInit(ty, _, _) => *ty,
Self::MetaclassCall(ty, _) => *ty,
}
}
@@ -12109,6 +12200,30 @@ impl<'db> ConstructorCallError<'db> {
}
};
let report_metaclass_call_error =
|call_dunder_error: &CallDunderError<'db>| match call_dunder_error {
CallDunderError::MethodNotAvailable => {
unreachable!(
"MethodNotAvailable should not occur when metaclass `__call__` was found"
)
}
CallDunderError::PossiblyUnbound(bindings) => {
if let Some(builder) = context
.report_lint(&POSSIBLY_MISSING_IMPLICIT_CALL, context_expression_node)
{
builder.into_diagnostic(format_args!(
"Method `__call__` on type `{}` may be missing.",
context_expression_type.display(context.db()),
));
}
bindings.report_diagnostics(context, context_expression_node);
}
CallDunderError::CallError(_, bindings) => {
bindings.report_diagnostics(context, context_expression_node);
}
};
match self {
Self::Init(_, init_call_dunder_error) => {
report_init_error(init_call_dunder_error);
@@ -12120,6 +12235,9 @@ impl<'db> ConstructorCallError<'db> {
report_new_error(new_call_error);
report_init_error(init_call_dunder_error);
}
Self::MetaclassCall(_, metaclass_call_error) => {
report_metaclass_call_error(metaclass_call_error);
}
}
}
}

View File

@@ -2568,7 +2568,15 @@ impl<'db> ClassLiteral<'db> {
(CodeGeneratorKind::NamedTuple, "__new__") => {
let cls_parameter = Parameter::positional_or_keyword(Name::new_static("cls"))
.with_annotated_type(KnownClass::Type.to_instance(db));
signature_from_fields(vec![cls_parameter], Some(Type::none(db)))
// Use `Self` type variable as return type so that subclasses get the correct
// return type when calling `__new__`. For example, if `IntBox` inherits from
// `Box[int]` (a NamedTuple), then `IntBox(1)` should return `IntBox`, not `Box[int]`.
let self_ty = Type::TypeVar(BoundTypeVarInstance::synthetic_self(
db,
instance_ty,
BindingContext::Synthetic,
));
signature_from_fields(vec![cls_parameter], Some(self_ty))
}
(CodeGeneratorKind::NamedTuple, "_replace" | "__replace__") => {
if name == "__replace__"