-
Notifications
You must be signed in to change notification settings - Fork 2.3k
[ty] Add subtyping between Callable types and class literals with __init__
#17638
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 4 commits
6569913
a648a13
9bb69c4
4b2bbe4
e106e27
5248d09
fc15783
35e7efa
36455c0
89c2fd0
0df5734
f6055f3
35d5b14
b7fc290
504bfd4
c1bb91d
8466781
084a338
3c416c3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,7 @@ | ||
| use std::hash::BuildHasherDefault; | ||
| use std::sync::{LazyLock, Mutex}; | ||
|
|
||
| use super::FunctionSignature; | ||
| use super::{ | ||
| class_base::ClassBase, infer_expression_type, infer_unpack_types, IntersectionBuilder, | ||
| KnownFunction, MemberLookupPolicy, Mro, MroError, MroIterator, SubclassOfType, Truthiness, | ||
|
|
@@ -872,9 +873,9 @@ impl<'db> ClassLiteral<'db> { | |
| )) | ||
| } | ||
|
|
||
| pub(super) fn into_callable(self, db: &'db dyn Db) -> Option<Type<'db>> { | ||
| pub(super) fn into_callable(self, db: &'db dyn Db) -> Type<'db> { | ||
| let self_ty = Type::from(self); | ||
| let metaclass_call_function_symbol = self_ty | ||
| let metaclass_dunder_call_function_symbol = self_ty | ||
| .member_lookup_with_policy( | ||
| db, | ||
| "__call__".into(), | ||
|
|
@@ -883,27 +884,117 @@ impl<'db> ClassLiteral<'db> { | |
| ) | ||
| .symbol; | ||
|
|
||
| if let Symbol::Type(Type::BoundMethod(metaclass_call_function), _) = | ||
| metaclass_call_function_symbol | ||
| if let Symbol::Type(Type::BoundMethod(metaclass__dunder_call_function), _) = | ||
| metaclass_dunder_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)); | ||
| return metaclass__dunder_call_function.into_callable_type(db); | ||
| } | ||
|
|
||
| let dunder_new_method = self_ty | ||
| .find_name_in_mro(db, "__new__") | ||
| .expect("find_name_in_mro always succeeds for class literals") | ||
| .symbol | ||
| .try_call_dunder_get(db, self_ty); | ||
| let dunder_new_function_symbol = self_ty | ||
| .member_lookup_with_policy( | ||
| db, | ||
| "__new__".into(), | ||
| MemberLookupPolicy::MRO_NO_OBJECT_FALLBACK | ||
| | MemberLookupPolicy::META_CLASS_NO_TYPE_FALLBACK, | ||
| ) | ||
| .symbol; | ||
|
|
||
| let dunder_new_function = | ||
| if let Symbol::Type(Type::FunctionLiteral(dunder_new_function), _) = | ||
| dunder_new_function_symbol | ||
| { | ||
| // Step 3: If the return type of the `__new__` evaluates to a type that is not a subclass of this class, | ||
| // then we should ignore the `__init__` and just return the `__new__` method. | ||
| let new_return_ty = match dunder_new_function.signature(db) { | ||
| FunctionSignature::Single(signature) | ||
| | FunctionSignature::Overloaded(_, Some(signature)) => signature.return_ty, | ||
| FunctionSignature::Overloaded(..) => None, | ||
| }; | ||
|
|
||
| if let Some(new_return_ty) = new_return_ty { | ||
| if !new_return_ty.to_meta_type(db).is_subtype_of(db, self_ty) { | ||
|
MatthewMckee4 marked this conversation as resolved.
Outdated
|
||
| return dunder_new_function.into_bound_method_type(db, self_ty); | ||
| } | ||
| } | ||
| Some(dunder_new_function.into_bound_method_type(db, self_ty)) | ||
| } else { | ||
| None | ||
| }; | ||
|
|
||
| let dunder_init_function_symbol = self_ty | ||
| .member_lookup_with_policy( | ||
| db, | ||
| "__init__".into(), | ||
| MemberLookupPolicy::MRO_NO_OBJECT_FALLBACK | ||
| | MemberLookupPolicy::META_CLASS_NO_TYPE_FALLBACK, | ||
| ) | ||
| .symbol; | ||
|
|
||
| if let Symbol::Type(Type::FunctionLiteral(dunder_new_method), _) = dunder_new_method { | ||
| return Some(dunder_new_method.into_bound_method_type(db, self.into())); | ||
| // TODO: should be the concrete value of `Self` | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not actually sure what the spec means by "the concrete value of |
||
| let correct_return_type = Type::instance(db, ClassType::NonGeneric(self)); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What about generic classes? Can we add a test for that case? Do we need a TODO? Should this just be
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think generic cases need some more work, do you think |
||
|
|
||
| // If the class defines an `__init__` method, then we synthesize a `__init__` method | ||
| // that has the same parameters as the `__init__` method after it is bound, and with the return type | ||
| // of the concrete value of `Self`. | ||
| let synthesized_dunder_init_callable = | ||
| if let Symbol::Type(Type::FunctionLiteral(dunder_init_function), _) = | ||
| dunder_init_function_symbol | ||
| { | ||
| let synthesized_signature = |signature: Signature<'db>| { | ||
| Signature::new(signature.parameters().clone(), Some(correct_return_type)) | ||
| .bind_self() | ||
| }; | ||
|
|
||
| let synthesized_dunder_init_signature = match dunder_init_function.signature(db) { | ||
| FunctionSignature::Single(signature) => { | ||
| CallableType::single(db, synthesized_signature(signature.clone())) | ||
| } | ||
| FunctionSignature::Overloaded(overloads, _) => CallableType::from_overloads( | ||
| db, | ||
| overloads | ||
| .iter() | ||
| .map(Clone::clone) | ||
| .map(synthesized_signature), | ||
| ), | ||
| }; | ||
| Some(Type::Callable(synthesized_dunder_init_signature)) | ||
| } else { | ||
| None | ||
| }; | ||
|
|
||
| let concrete_dunder_functions: Vec<_> = | ||
| [dunder_new_function, synthesized_dunder_init_callable] | ||
| .into_iter() | ||
| .flatten() | ||
| .collect(); | ||
|
|
||
| if concrete_dunder_functions.is_empty() { | ||
| // If no `__new__` or `__init__` method is found, then we fall back to looking for | ||
| // an `object.__new__` method. | ||
| let new_function_symbol = self_ty | ||
| .member_lookup_with_policy( | ||
| db, | ||
| "__new__".into(), | ||
| MemberLookupPolicy::META_CLASS_NO_TYPE_FALLBACK, | ||
| ) | ||
| .symbol; | ||
|
|
||
| if let Symbol::Type(Type::FunctionLiteral(new_function), _) = new_function_symbol { | ||
| new_function.into_bound_method_type(db, self_ty) | ||
| } else { | ||
| // Fallback if no `object.__new__` is found. | ||
| Type::Callable(CallableType::single( | ||
| db, | ||
| Signature::new(Parameters::empty(), Some(correct_return_type)), | ||
| )) | ||
| } | ||
| } else { | ||
| UnionType::from_elements(db, concrete_dunder_functions) | ||
| } | ||
| // TODO handle `__init__` also | ||
| None | ||
| } | ||
|
|
||
| /// Returns the class member of this class named `name`. | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As per the spec this "shouldn't" pass, but there's maybe an argument that it should pass
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(As is the test without "not" should pass)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why does this not pass according to the spec? It seems like the spec would say in this case that the callable type of
Cwould be the union of the__new__signature, and the__init__bound-method signature with replaced return type. That union seems like it should be a subtype ofCallable[[int], C]?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, so yeah you're right, but what is happening here (after i added ":Any" to args and kwargs)
is we end up calling is_subtype with
self: (*args: Any, **kwargs: Any) -> C
target: (int, /) -> C
which returns false because self isnt fully static