Skip to content

Commit 6937115

Browse files
committed
feat: support init and kw_only fields
1 parent ef6e701 commit 6937115

File tree

2 files changed

+73
-2
lines changed

2 files changed

+73
-2
lines changed

crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,67 @@ class F:
137137
y: str
138138
```
139139

140+
Fields with `init=False` do not participate in the ordering check since they don't appear in
141+
`__init__`:
142+
143+
```py
144+
@dataclass
145+
class GoodWithInitFalse:
146+
x: int = 1
147+
y: str = field(init=False)
148+
z: float = 2.0
149+
150+
@dataclass
151+
class BadWithInitFalse:
152+
x: int = 1
153+
y: str = field(init=False)
154+
# error: [dataclass-field-order] "Required field `z` cannot be defined after fields with default values"
155+
z: float
156+
```
157+
158+
Keyword-only fields (using `kw_only=True`) also don't participate in the positional ordering check:
159+
160+
```toml
161+
[environment]
162+
python-version = "3.10"
163+
```
164+
165+
```py
166+
@dataclass
167+
class GoodWithKwOnly:
168+
x: int = 1
169+
y: str = field(kw_only=True)
170+
z: float = field(kw_only=True, default=2.0)
171+
172+
@dataclass
173+
class BadWithKwOnly:
174+
x: int = 1
175+
y: str = field(kw_only=True)
176+
# error: [dataclass-field-order] "Required field `z` cannot be defined after fields with default values"
177+
z: float
178+
```
179+
180+
Fields after a `KW_ONLY` sentinel are also keyword-only and don't participate in ordering checks:
181+
182+
```py
183+
from dataclasses import KW_ONLY
184+
185+
@dataclass
186+
class GoodWithKwOnlySentinel:
187+
x: int = 1
188+
_: KW_ONLY
189+
y: str
190+
z: float = 2.0
191+
192+
@dataclass
193+
class BadWithKwOnlySentinel:
194+
x: int = 1
195+
# error: [dataclass-field-order] "Required field `y` cannot be defined after fields with default values"
196+
y: str
197+
_: KW_ONLY
198+
z: float
199+
```
200+
140201
Pure class attributes (`ClassVar`) are not included in the signature of `__init__`:
141202

142203
```py

crates/ty_python_semantic/src/types/infer.rs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1365,27 +1365,37 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
13651365
let mut kw_only_field_names = vec![];
13661366
let mut required_after_default_field_names = vec![];
13671367
let mut has_seen_default_field = false;
1368+
let mut kw_only_sentinel_seen = false;
13681369

13691370
for (
13701371
name,
13711372
Field {
13721373
declared_ty: field_ty,
13731374
default_ty,
1375+
init,
1376+
kw_only,
13741377
..
13751378
},
13761379
) in class.fields(self.db(), specialization, field_policy)
13771380
{
1378-
// Check for KW_ONLY
1381+
// Check for fields after KW_ONLY sentinel
13791382
if let Some(instance) = field_ty.into_nominal_instance() {
13801383
if instance
13811384
.class(self.db())
13821385
.is_known(self.db(), KnownClass::KwOnly)
13831386
{
13841387
kw_only_field_names.push(name.clone());
1388+
kw_only_sentinel_seen = true;
13851389
}
13861390
}
13871391

1388-
// Check field ordering
1392+
// Check field ordering - only for fields that participate in positional `__init__` args
1393+
// Skip `init=False` fields (don't appear in `__init__`)
1394+
// Skip `kw_only` fields (go at end of `__init__` signature, not positional)
1395+
// Skip fields that come after a `KW_ONLY` sentinel (they're keyword-only)
1396+
if !init || kw_only == Some(true) || kw_only_sentinel_seen {
1397+
continue;
1398+
}
13891399
if default_ty.is_some() {
13901400
has_seen_default_field = true;
13911401
} else if has_seen_default_field {

0 commit comments

Comments
 (0)