-
Notifications
You must be signed in to change notification settings - Fork 1.6k
[ty] diagnostic for dataclass field order #19825
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
base: main
Are you sure you want to change the base?
Changes from all commits
b41ad42
8ed995f
00c073b
6ecd7c6
a4fe617
ef6e701
6937115
b6f4dbe
26aef75
22b8bac
24ecdef
97cc18b
d59fb46
cfa5a90
1c5c1fb
f2bc54a
b1df25a
0bd66e1
32fae78
c2c37b0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -110,6 +110,35 @@ CustomerModel(id=1, name="Test") | |
| CustomerModel() | ||
| ``` | ||
|
|
||
| ### Metaclass with `kw_only_default=True` | ||
|
|
||
| ```py | ||
| from typing import dataclass_transform | ||
|
|
||
| @dataclass_transform(kw_only_default=True) | ||
| class ModelMeta(type): | ||
| pass | ||
|
|
||
| class WithMeta(metaclass=ModelMeta): | ||
| x: int | ||
|
|
||
| # error: [missing-argument] | ||
| # error: [too-many-positional-arguments] | ||
| WithMeta(3) | ||
|
|
||
| @dataclass_transform(kw_only_default=True) | ||
| def mydc(): | ||
| return lambda func: func | ||
|
|
||
| @mydc | ||
| class WithDeco: | ||
| x: int | ||
|
|
||
| # error: [missing-argument] | ||
| # error: [too-many-positional-arguments] | ||
| WithDeco(3) | ||
| ``` | ||
|
|
||
| ### Decorating a base class | ||
|
|
||
| ```py | ||
|
|
@@ -231,6 +260,28 @@ class CustomerModel: | |
| c = CustomerModel(1, "Harry") | ||
|
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. The test immediately above here I thought should be fixed by logic you added in this PR, but then I realized why it isn't -- commented below. |
||
| ``` | ||
|
|
||
| When `kw_only_default=True` is set in the `dataclass_transform` decorator, all fields become | ||
| keyword-only by default and should not participate in positional field ordering checks: | ||
|
|
||
| ```py | ||
| from typing import dataclass_transform | ||
|
|
||
| @dataclass_transform(kw_only_default=True) | ||
| def create_model(): ... | ||
| @create_model() | ||
| class ModelGood: | ||
| x: int = 1 | ||
| y: str | ||
|
|
||
| @create_model() | ||
| class ModelAlsoGood: | ||
| x: int | ||
| y: str = "default" | ||
| z: float | ||
| ``` | ||
|
|
||
| Overwrite with field specifiers are not yet supported. | ||
|
|
||
| ### `field_specifiers` | ||
|
|
||
| To do | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -2378,17 +2378,25 @@ impl<'db> ClassLiteral<'db> { | |||||||||||||||||||||||||||||||||||||||||
| .collect() | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| /// Returns a list of all annotated attributes defined in the body of this class. This is similar | ||||||||||||||||||||||||||||||||||||||||||
| /// to the `__annotations__` attribute at runtime, but also contains default values. | ||||||||||||||||||||||||||||||||||||||||||
| /// Returns a map of all annotated attributes defined in the body of this class. | ||||||||||||||||||||||||||||||||||||||||||
| /// This extends the `__annotations__` attribute at runtime by also including default values | ||||||||||||||||||||||||||||||||||||||||||
| /// and computed field properties. | ||||||||||||||||||||||||||||||||||||||||||
| /// | ||||||||||||||||||||||||||||||||||||||||||
| /// For a class body like | ||||||||||||||||||||||||||||||||||||||||||
| /// ```py | ||||||||||||||||||||||||||||||||||||||||||
| /// @dataclass | ||||||||||||||||||||||||||||||||||||||||||
| /// @dataclass(kw_only=True) | ||||||||||||||||||||||||||||||||||||||||||
| /// class C: | ||||||||||||||||||||||||||||||||||||||||||
| /// x: int | ||||||||||||||||||||||||||||||||||||||||||
| /// y: str = "a" | ||||||||||||||||||||||||||||||||||||||||||
| /// y: str = "hello" | ||||||||||||||||||||||||||||||||||||||||||
| /// z: float = field(kw_only=False, default=1.0) | ||||||||||||||||||||||||||||||||||||||||||
| /// ``` | ||||||||||||||||||||||||||||||||||||||||||
| /// we return a map `{"x": (int, None), "y": (str, Some(Literal["a"]))}`. | ||||||||||||||||||||||||||||||||||||||||||
| /// we return a map `{"x": Field, "y": Field, "z": Field}` where each `Field` contains | ||||||||||||||||||||||||||||||||||||||||||
| /// the annotated type, default value (if any), and field properties. | ||||||||||||||||||||||||||||||||||||||||||
| /// | ||||||||||||||||||||||||||||||||||||||||||
| /// **Important**: The returned `Field` objects represent our full understanding of the fields, | ||||||||||||||||||||||||||||||||||||||||||
| /// including properties inherited from class-level dataclass parameters (like `kw_only=True`) | ||||||||||||||||||||||||||||||||||||||||||
| /// and dataclass-transform parameters (like `kw_only_default=True`). They do not represent | ||||||||||||||||||||||||||||||||||||||||||
| /// only what is explicitly specified in each field definition. | ||||||||||||||||||||||||||||||||||||||||||
| pub(super) fn own_fields( | ||||||||||||||||||||||||||||||||||||||||||
| self, | ||||||||||||||||||||||||||||||||||||||||||
| db: &'db dyn Db, | ||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -2440,7 +2448,6 @@ impl<'db> ClassLiteral<'db> { | |||||||||||||||||||||||||||||||||||||||||
| let mut init = true; | ||||||||||||||||||||||||||||||||||||||||||
| let mut kw_only = None; | ||||||||||||||||||||||||||||||||||||||||||
| if let Some(Type::KnownInstance(KnownInstanceType::Field(field))) = default_ty { | ||||||||||||||||||||||||||||||||||||||||||
| default_ty = Some(field.default_type(db)); | ||||||||||||||||||||||||||||||||||||||||||
| if self | ||||||||||||||||||||||||||||||||||||||||||
| .dataclass_params(db) | ||||||||||||||||||||||||||||||||||||||||||
| .map(|params| params.contains(DataclassParams::NO_FIELD_SPECIFIERS)) | ||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -2449,7 +2456,9 @@ impl<'db> ClassLiteral<'db> { | |||||||||||||||||||||||||||||||||||||||||
| // This happens when constructing a `dataclass` with a `dataclass_transform` | ||||||||||||||||||||||||||||||||||||||||||
| // without defining the `field_specifiers`, meaning it should ignore | ||||||||||||||||||||||||||||||||||||||||||
| // `dataclasses.field` and `dataclasses.Field`. | ||||||||||||||||||||||||||||||||||||||||||
| default_ty = Some(Type::unknown()); | ||||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||||
| default_ty = field.default_type(db); | ||||||||||||||||||||||||||||||||||||||||||
| init = field.init(db); | ||||||||||||||||||||||||||||||||||||||||||
| kw_only = field.kw_only(db); | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -2468,9 +2477,24 @@ impl<'db> ClassLiteral<'db> { | |||||||||||||||||||||||||||||||||||||||||
| kw_only_sentinel_field_seen = true; | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| // If no explicit kw_only setting and we've seen KW_ONLY sentinel, mark as keyword-only | ||||||||||||||||||||||||||||||||||||||||||
| if field.kw_only.is_none() && kw_only_sentinel_field_seen { | ||||||||||||||||||||||||||||||||||||||||||
| field.kw_only = Some(true); | ||||||||||||||||||||||||||||||||||||||||||
| // If no explicit field-level "kw_only" setting, | ||||||||||||||||||||||||||||||||||||||||||
| // we check for KW_ONLY sentinel or class-level settings | ||||||||||||||||||||||||||||||||||||||||||
| if field.kw_only.is_none() { | ||||||||||||||||||||||||||||||||||||||||||
| if kw_only_sentinel_field_seen { | ||||||||||||||||||||||||||||||||||||||||||
| // KW_ONLY sentinel makes subsequent fields keyword-only | ||||||||||||||||||||||||||||||||||||||||||
| field.kw_only = Some(true); | ||||||||||||||||||||||||||||||||||||||||||
| } else if self | ||||||||||||||||||||||||||||||||||||||||||
| .dataclass_params(db) | ||||||||||||||||||||||||||||||||||||||||||
| .is_some_and(|params| params.contains(DataclassParams::KW_ONLY)) | ||||||||||||||||||||||||||||||||||||||||||
| || self.try_metaclass(db).is_ok_and(|(_, transformer_params)| { | ||||||||||||||||||||||||||||||||||||||||||
| transformer_params.is_some_and(|params| { | ||||||||||||||||||||||||||||||||||||||||||
| params.contains(DataclassTransformerParams::KW_ONLY_DEFAULT) | ||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||
| // `@dataclass(kw_only=True)` or `@dataclass_transform(kw_only_default=True)` | ||||||||||||||||||||||||||||||||||||||||||
| field.kw_only = Some(true); | ||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+2488
to
+2496
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. There's a test (I commented above) that I would expect this logic to fix, but it doesn't fix it. And I think I understand why. Currently I think this doesn't need to be fixed in this PR, but it does impact the correctness of this code, so I think we should add a TODO comment here.
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| attributes.insert(symbol.name().clone(), field); | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
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 test looks great. Nit: I think this section is to show basic usage, and arguments are tested below. I think we should move this down with the rest of the
kw_only_defaulttests below, and just add an example with metaclass there. (I think the example here with decorator is probably redundant with the test we already have down there, and we don't need both?)