diff --git a/crates/ty_python_semantic/resources/mdtest/classes.md b/crates/ty_python_semantic/resources/mdtest/classes.md index 4d92cb9cdf..4ebfb773a0 100644 --- a/crates/ty_python_semantic/resources/mdtest/classes.md +++ b/crates/ty_python_semantic/resources/mdtest/classes.md @@ -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 diff --git a/crates/ty_python_semantic/resources/mdtest/metaclass.md b/crates/ty_python_semantic/resources/mdtest/metaclass.md index 3c4762fe8f..cb2a18efe2 100644 --- a/crates/ty_python_semantic/resources/mdtest/metaclass.md +++ b/crates/ty_python_semantic/resources/mdtest/metaclass.md @@ -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 diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 129af6d7e7..211a6876f4 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -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); + } } } } diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index 869c82c90f..a2b1905687 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -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__"