Special-case type return
This commit is contained in:
@@ -115,6 +115,40 @@ Foo() # error: [missing-argument]
|
||||
reveal_type(Foo(1)) # revealed: Foo
|
||||
```
|
||||
|
||||
### Metaclass `__call__` returning bare `type`
|
||||
|
||||
When the metaclass `__call__` is annotated as returning `type`, this is typically a mistake in
|
||||
singleton patterns where the programmer intended to return an instance. Both mypy and pyright handle
|
||||
this specially by ignoring the `type` return annotation and using the instance type instead.
|
||||
|
||||
```py
|
||||
from typing import Any
|
||||
|
||||
class Singleton(type):
|
||||
_instances: dict["Singleton", object] = {}
|
||||
|
||||
def __call__(cls, *args: Any, **kwargs: Any) -> type:
|
||||
if cls not in cls._instances:
|
||||
cls._instances[cls] = super().__call__(*args, **kwargs)
|
||||
# error: [invalid-return-type]
|
||||
return cls._instances[cls]
|
||||
|
||||
class MyConfig(metaclass=Singleton):
|
||||
def __init__(self, x: int) -> None:
|
||||
pass
|
||||
|
||||
def get(self, key: str) -> str:
|
||||
return key
|
||||
|
||||
# Despite the `-> type` annotation, we treat this as returning an instance.
|
||||
# This matches mypy and pyright behavior for this common pattern.
|
||||
MyConfig() # error: [missing-argument]
|
||||
reveal_type(MyConfig(1)) # revealed: MyConfig
|
||||
|
||||
# Instance methods work correctly.
|
||||
MyConfig(1).get("key")
|
||||
```
|
||||
|
||||
## Default
|
||||
|
||||
```py
|
||||
|
||||
@@ -7349,11 +7349,23 @@ impl<'db> Type<'db> {
|
||||
.to_instance(db)
|
||||
.expect("type should be convertible to instance type");
|
||||
|
||||
// Special case: if the return type is exactly `type`, treat it as returning the
|
||||
// instance type. This is a common pattern in singleton metaclasses where `__call__`
|
||||
// is annotated as `-> type` but actually returns an instance. Both mypy and pyright
|
||||
// handle this specially to avoid false positives.
|
||||
// Note: `-> type` in annotations becomes `NominalInstance(type)`.
|
||||
let returns_bare_type = matches!(
|
||||
metaclass_return_type,
|
||||
Type::NominalInstance(instance) if instance.class(db).is_known(db, KnownClass::Type)
|
||||
);
|
||||
|
||||
// Check if we should skip `__new__`/`__init__` evaluation.
|
||||
// Skip if: return type is not assignable to instance, is Never, or contains Any.
|
||||
let skip_new_init = !metaclass_return_type.is_assignable_to(db, instance_ty)
|
||||
|| metaclass_return_type.is_never()
|
||||
|| matches!(metaclass_return_type, Type::Dynamic(DynamicType::Any));
|
||||
// But don't skip if the return type is exactly `type` (common singleton pattern).
|
||||
let skip_new_init = !returns_bare_type
|
||||
&& (!metaclass_return_type.is_assignable_to(db, instance_ty)
|
||||
|| metaclass_return_type.is_never()
|
||||
|| matches!(metaclass_return_type, Type::Dynamic(DynamicType::Any)));
|
||||
|
||||
// If there are argument errors or we should skip `__new__`/`__init__`, return metaclass result.
|
||||
if call_result.is_err() || skip_new_init {
|
||||
|
||||
Reference in New Issue
Block a user