mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-27 04:19:18 +00:00
[red-knot] Class literal __new__
function callable subtyping (#17533)
## Summary From https://typing.python.org/en/latest/spec/constructors.html#converting-a-constructor-to-callable this covers step 2 and partially step 3 (always respecting the `__new__`) ## Test Plan Update is_subtype_of.md --------- Co-authored-by: Carl Meyer <carl@astral.sh>
This commit is contained in:
parent
aa46047649
commit
e45f23b0ec
3 changed files with 99 additions and 19 deletions
|
@ -1166,6 +1166,56 @@ static_assert(is_subtype_of(TypeOf[C], Callable[[int], int]))
|
||||||
static_assert(is_subtype_of(TypeOf[C], Callable[[], str]))
|
static_assert(is_subtype_of(TypeOf[C], Callable[[], str]))
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### Classes with `__new__`
|
||||||
|
|
||||||
|
```py
|
||||||
|
from typing import Callable
|
||||||
|
from knot_extensions import TypeOf, static_assert, is_subtype_of
|
||||||
|
|
||||||
|
class A:
|
||||||
|
def __new__(cls, a: int) -> int:
|
||||||
|
return a
|
||||||
|
|
||||||
|
static_assert(is_subtype_of(TypeOf[A], Callable[[int], int]))
|
||||||
|
static_assert(not is_subtype_of(TypeOf[A], Callable[[], int]))
|
||||||
|
|
||||||
|
class B: ...
|
||||||
|
class C(B): ...
|
||||||
|
|
||||||
|
class D:
|
||||||
|
def __new__(cls) -> B:
|
||||||
|
return B()
|
||||||
|
|
||||||
|
class E(D):
|
||||||
|
def __new__(cls) -> C:
|
||||||
|
return C()
|
||||||
|
|
||||||
|
static_assert(is_subtype_of(TypeOf[E], Callable[[], C]))
|
||||||
|
static_assert(is_subtype_of(TypeOf[E], Callable[[], B]))
|
||||||
|
static_assert(not is_subtype_of(TypeOf[D], Callable[[], C]))
|
||||||
|
static_assert(is_subtype_of(TypeOf[D], Callable[[], B]))
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Classes with `__call__` and `__new__`
|
||||||
|
|
||||||
|
If `__call__` and `__new__` are both present, `__call__` takes precedence.
|
||||||
|
|
||||||
|
```py
|
||||||
|
from typing import Callable
|
||||||
|
from knot_extensions import TypeOf, static_assert, is_subtype_of
|
||||||
|
|
||||||
|
class MetaWithIntReturn(type):
|
||||||
|
def __call__(cls) -> int:
|
||||||
|
return super().__call__()
|
||||||
|
|
||||||
|
class F(metaclass=MetaWithIntReturn):
|
||||||
|
def __new__(cls) -> str:
|
||||||
|
return super().__new__(cls)
|
||||||
|
|
||||||
|
static_assert(is_subtype_of(TypeOf[F], Callable[[], int]))
|
||||||
|
static_assert(not is_subtype_of(TypeOf[F], Callable[[], str]))
|
||||||
|
```
|
||||||
|
|
||||||
### Bound methods
|
### Bound methods
|
||||||
|
|
||||||
```py
|
```py
|
||||||
|
|
|
@ -1175,25 +1175,9 @@ impl<'db> Type<'db> {
|
||||||
self_subclass_ty.is_subtype_of(db, target_subclass_ty)
|
self_subclass_ty.is_subtype_of(db, target_subclass_ty)
|
||||||
}
|
}
|
||||||
|
|
||||||
(Type::ClassLiteral(_), Type::Callable(_)) => {
|
(Type::ClassLiteral(class_literal), Type::Callable(_)) => {
|
||||||
let metaclass_call_function_symbol = self
|
if let Some(callable) = class_literal.into_callable(db) {
|
||||||
.member_lookup_with_policy(
|
return callable.is_subtype_of(db, target);
|
||||||
db,
|
|
||||||
"__call__".into(),
|
|
||||||
MemberLookupPolicy::NO_INSTANCE_FALLBACK
|
|
||||||
| MemberLookupPolicy::META_CLASS_NO_TYPE_FALLBACK,
|
|
||||||
)
|
|
||||||
.symbol;
|
|
||||||
|
|
||||||
if let Symbol::Type(Type::BoundMethod(metaclass_call_function), _) =
|
|
||||||
metaclass_call_function_symbol
|
|
||||||
{
|
|
||||||
// TODO: this intentionally diverges from step 1 in
|
|
||||||
// https://typing.python.org/en/latest/spec/constructors.html#converting-a-constructor-to-callable
|
|
||||||
// by always respecting the signature of the metaclass `__call__`, rather than
|
|
||||||
// using a heuristic which makes unwarranted assumptions to sometimes ignore it.
|
|
||||||
let metaclass_call_function = metaclass_call_function.into_callable_type(db);
|
|
||||||
return metaclass_call_function.is_subtype_of(db, target);
|
|
||||||
}
|
}
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
@ -5961,6 +5945,15 @@ impl<'db> FunctionType<'db> {
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Convert the `FunctionType` into a [`Type::BoundMethod`].
|
||||||
|
pub(crate) fn into_bound_method_type(
|
||||||
|
self,
|
||||||
|
db: &'db dyn Db,
|
||||||
|
self_instance: Type<'db>,
|
||||||
|
) -> Type<'db> {
|
||||||
|
Type::BoundMethod(BoundMethodType::new(db, self, self_instance))
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns the [`FileRange`] of the function's name.
|
/// Returns the [`FileRange`] of the function's name.
|
||||||
pub fn focus_range(self, db: &dyn Db) -> FileRange {
|
pub fn focus_range(self, db: &dyn Db) -> FileRange {
|
||||||
FileRange::new(
|
FileRange::new(
|
||||||
|
|
|
@ -818,6 +818,43 @@ impl<'db> ClassLiteralType<'db> {
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(super) fn into_callable(self, db: &'db dyn Db) -> Option<Type<'db>> {
|
||||||
|
let self_ty = Type::from(self);
|
||||||
|
let metaclass_call_function_symbol = self_ty
|
||||||
|
.member_lookup_with_policy(
|
||||||
|
db,
|
||||||
|
"__call__".into(),
|
||||||
|
MemberLookupPolicy::NO_INSTANCE_FALLBACK
|
||||||
|
| MemberLookupPolicy::META_CLASS_NO_TYPE_FALLBACK,
|
||||||
|
)
|
||||||
|
.symbol;
|
||||||
|
|
||||||
|
if let Symbol::Type(Type::BoundMethod(metaclass_call_function), _) =
|
||||||
|
metaclass_call_function_symbol
|
||||||
|
{
|
||||||
|
// TODO: this intentionally diverges from step 1 in
|
||||||
|
// https://typing.python.org/en/latest/spec/constructors.html#converting-a-constructor-to-callable
|
||||||
|
// by always respecting the signature of the metaclass `__call__`, rather than
|
||||||
|
// using a heuristic which makes unwarranted assumptions to sometimes ignore it.
|
||||||
|
return Some(metaclass_call_function.into_callable_type(db));
|
||||||
|
}
|
||||||
|
|
||||||
|
let new_function_symbol = self_ty
|
||||||
|
.member_lookup_with_policy(
|
||||||
|
db,
|
||||||
|
"__new__".into(),
|
||||||
|
MemberLookupPolicy::MRO_NO_OBJECT_FALLBACK
|
||||||
|
| MemberLookupPolicy::META_CLASS_NO_TYPE_FALLBACK,
|
||||||
|
)
|
||||||
|
.symbol;
|
||||||
|
|
||||||
|
if let Symbol::Type(Type::FunctionLiteral(new_function), _) = new_function_symbol {
|
||||||
|
return Some(new_function.into_bound_method_type(db, self.into()));
|
||||||
|
}
|
||||||
|
// TODO handle `__init__` also
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns the class member of this class named `name`.
|
/// Returns the class member of this class named `name`.
|
||||||
///
|
///
|
||||||
/// The member resolves to a member on the class itself or any of its proper superclasses.
|
/// The member resolves to a member on the class itself or any of its proper superclasses.
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue