-
Notifications
You must be signed in to change notification settings - Fork 1.6k
[red-knot] Dataclasses: synthesize __init__ with proper signature
#17428
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
Conversation
|
2c6c1df to
ef6e875
Compare
182d0a1 to
2a31576
Compare
bdd3212 to
c589560
Compare
c589560 to
0370df5
Compare
| if !declarations | ||
| .clone() | ||
| .all(|DeclarationWithConstraint { declaration, .. }| { | ||
| declaration.is_some_and(|declaration| { | ||
| matches!( | ||
| declaration.kind(db), | ||
| DefinitionKind::AnnotatedAssignment(..) | ||
| ) | ||
| }) | ||
| }) | ||
| { | ||
| continue; | ||
| } |
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.
This is a bit of a hack to avoid things like function definitions and nested class definitions showing up as dataclass fields. The filtering by AnnotatedAssignment is correct, I think, but we do not correctly handle weird things like
class C:
if flag():
def attr(): ...
else:
attr: int = 1Another option to solve this might be to pass down some kind of definition-kind-filter to symbol_from_declarations? Thoughts?
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.
A filter for symbol_from_declarations seems reasonable -- or it could even be a filter in the use-def map fetching?
The even trickier part about supporting something like this is that I think it would mean we'd have to generate a union of __init__ methods with different signatures? It seems closely-related to the possibly-unbound handling you mention below, in that sense.
I definitely think this can be a TODO for now, I don't think any other type checker supports it.
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.
I added a comment for now.
| continue; | ||
| } | ||
|
|
||
| if let Some(attr_ty) = attr.symbol.ignore_possibly_unbound() { |
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.
In this PR, I did not attempt to model any sort of possibly-unbound handling. It's also possible that there are edge cases with unions of dataclasses or dataclasses with unions of attributes that we don't handle correctly yet. I would like to postpone that to a post-alpha follow up, if that sounds okay. It's certainly not a problem on any of the ecosystem projects, because it would have shown up in new diagnostics otherwise.
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.
Yes I think it's fine for this to be post-alpha, even post-beta probably.
|
|
||
| ## Signature of `__init__` | ||
|
|
||
| TODO: All of the following tests are missing the `self` argument in the `__init__` signature. |
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.
This is still not solved. Turning FunctionType into an enum sounds painful 🙃, but I'll look into it eventually.
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.
I think it won't be too bad? It's really more like putting a new wrapper enum around the existing FunctionType (though the new wrapper enum should probably get the name FunctionType.) Some APIs of FunctionType will be easy to proxy (e.g. signature), and some are easy because we can just give up (no way to provide a definition location to support goto-type-definition for a synthetic function). Not sure if there are some that may be tricky.
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.
Thanks. I think I will I'll postpone this until after @dhruvmanila's overload work has been merged, as it looks to me like that would introduce conflicts, that we can easily avoid by just waiting a bit.
2a31576 to
eedb81a
Compare
0370df5 to
bb5c049
Compare
I don't think so? It looks to me like they depend on missing support for dataclass inheritance. The dataclass EDIT: sorry, ignore this! Should have read the PR first. Clearly it does support dataclass fields inheritance, so I think you're right that the issue here is support for inheriting from a generic class. I think maybe #17434 will fix this? |
carljm
left a comment
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.
Impeccable work as usual.
| if !declarations | ||
| .clone() | ||
| .all(|DeclarationWithConstraint { declaration, .. }| { | ||
| declaration.is_some_and(|declaration| { | ||
| matches!( | ||
| declaration.kind(db), | ||
| DefinitionKind::AnnotatedAssignment(..) | ||
| ) | ||
| }) | ||
| }) | ||
| { | ||
| continue; | ||
| } |
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.
A filter for symbol_from_declarations seems reasonable -- or it could even be a filter in the use-def map fetching?
The even trickier part about supporting something like this is that I think it would mean we'd have to generate a union of __init__ methods with different signatures? It seems closely-related to the possibly-unbound handling you mention below, in that sense.
I definitely think this can be a TODO for now, I don't think any other type checker supports it.
| continue; | ||
| } | ||
|
|
||
| if let Some(attr_ty) = attr.symbol.ignore_possibly_unbound() { |
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.
Yes I think it's fine for this to be post-alpha, even post-beta probably.
| let dunder_set = attr_ty.class_member(db, "__set__".into()); | ||
| if let Some(dunder_set) = dunder_set.symbol.ignore_possibly_unbound() { | ||
| // This type of this attribute is a data descriptor. Instead of overwriting the | ||
| // descriptor attribute, data-classes will (implicitly) call the `__set__` method | ||
| // of the descriptor. This means that the synthesized `__init__` parameter for | ||
| // this attribute is determined by possible `value` parameter types with which | ||
| // the `__set__` method can be called. We build a union of all possible options | ||
| // to account for possible overloads. | ||
| let mut value_types = UnionBuilder::new(db); | ||
| for signature in &dunder_set.signatures(db) { | ||
| for overload in signature { | ||
| if let Some(value_param) = overload.parameters().get_positional(2) { | ||
| value_types = value_types.add( | ||
| value_param.annotated_type().unwrap_or_else(Type::unknown), | ||
| ); | ||
| } else if overload.parameters().is_gradual() { | ||
| value_types = value_types.add(Type::unknown()); | ||
| } | ||
| } | ||
| } | ||
| attr_ty = value_types.build(); | ||
|
|
||
| // The default value of the attribute is *not* determined by the right hand side | ||
| // of the class-body assignment. Instead, the runtime invokes `__get__` on the | ||
| // descriptor, as if it had been called on the class itself, i.e. it passes `None` | ||
| // for the `instance` argument. | ||
|
|
||
| if let Some(ref mut default_ty) = default_ty { | ||
| *default_ty = default_ty | ||
| .try_call_dunder_get(db, Type::none(db), Type::ClassLiteral(self)) | ||
| .map(|(return_ty, _)| return_ty) | ||
| .unwrap_or_else(Type::unknown); | ||
| } | ||
| } |
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.
Amazing.
bb5c049 to
a3abde4
Compare
* main: [red-knot] Detect version-related syntax errors (#16379) [`pyflakes`] Add fix safety section (`F841`) (#17410) [red-knot] Add `KnownFunction` variants for `is_protocol`, `get_protocol_members` and `runtime_checkable` (#17450) Bump 0.11.6 (#17449) Auto generate `visit_source_order` (#17180) [red-knot] Initial tests for protocols (#17436) [red-knot] Dataclasses: synthesize `__init__` with proper signature (#17428) [red-knot] Dataclasses: support `order=True` (#17406)
* main: (123 commits) [red-knot] Handle explicit class specialization in type expressions (#17434) [red-knot] allow assignment expression in call compare narrowing (#17461) [red-knot] fix building unions with literals and AlwaysTruthy/AlwaysFalsy (#17451) [red-knot] Type narrowing for assertions (take 2) (#17345) [red-knot] class bases are not affected by __future__.annotations (#17456) [red-knot] Add support for overloaded functions (#17366) [`pyupgrade`] Add fix safety section to docs (`UP036`) (#17444) [red-knot] more type-narrowing in match statements (#17302) [red-knot] Add some narrowing for assignment expressions (#17448) [red-knot] Understand `typing.Protocol` and `typing_extensions.Protocol` as equivalent (#17446) Server: Use `min` instead of `max` to limit the number of threads (#17421) [red-knot] Detect version-related syntax errors (#16379) [`pyflakes`] Add fix safety section (`F841`) (#17410) [red-knot] Add `KnownFunction` variants for `is_protocol`, `get_protocol_members` and `runtime_checkable` (#17450) Bump 0.11.6 (#17449) Auto generate `visit_source_order` (#17180) [red-knot] Initial tests for protocols (#17436) [red-knot] Dataclasses: synthesize `__init__` with proper signature (#17428) [red-knot] Dataclasses: support `order=True` (#17406) [red-knot] Super-basic generic inference at call sites (#17301) ...
Summary
This changeset allows us to generate the signature of synthesized
__init__functions in dataclasses by analyzing the fields on the class (and its superclasses). There are certain things that I have not yet attempted to model in this PR, likekw_only,dataclasses.KW_ONLYor functionality arounddataclasses.field.depends on #17406
ticket: #16651
Ecosystem analysis
These two seem to depend on missing features in generics (see relevant code here):
These two are true positives 🥳. See relevant code here.
This one depends on
**unpacking of dictionaries, which we don't support yet:Test Plan
New Markdown tests.