Skip to content

Commit ab76e06

Browse files
[ty] dataclasses: Support dataclasses.KW_ONLY
1 parent 3a430fa commit ab76e06

File tree

3 files changed

+54
-2
lines changed

3 files changed

+54
-2
lines changed

crates/ty_python_semantic/resources/mdtest/dataclasses.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -713,6 +713,31 @@ But calling `asdict` on the class object is not allowed:
713713
asdict(Foo)
714714
```
715715

716+
## `dataclasses.KW_ONLY`
717+
718+
```toml
719+
[environment]
720+
python-version = "3.10"
721+
```
722+
723+
```py
724+
from dataclasses import dataclass, field, KW_ONLY
725+
726+
@dataclass
727+
class C:
728+
x: int
729+
_: KW_ONLY
730+
y: str
731+
732+
# error: [missing-argument]
733+
# error: [too-many-positional-arguments]
734+
C(3, "")
735+
736+
C(3, y="")
737+
```
738+
739+
TODO: more than one use of `KW_ONLY` should be an error.
740+
716741
## Other special cases
717742

718743
### `dataclasses.dataclass`

crates/ty_python_semantic/src/types.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -600,6 +600,11 @@ impl<'db> Type<'db> {
600600
.is_some_and(|instance| instance.class.is_known(db, KnownClass::NotImplementedType))
601601
}
602602

603+
pub fn is_dataclass_kw_only(&self, db: &'db dyn Db) -> bool {
604+
self.into_nominal_instance()
605+
.is_some_and(|instance| instance.class.is_known(db, KnownClass::KwOnly))
606+
}
607+
603608
pub fn is_object(&self, db: &'db dyn Db) -> bool {
604609
self.into_nominal_instance()
605610
.is_some_and(|instance| instance.class.is_object(db))

crates/ty_python_semantic/src/types/class.rs

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1313,9 +1313,17 @@ impl<'db> ClassLiteral<'db> {
13131313
let field_policy = CodeGeneratorKind::from_class(db, self)?;
13141314

13151315
let signature_from_fields = |mut parameters: Vec<_>| {
1316+
let mut kw_only_field_seen = false;
13161317
for (name, (mut attr_ty, mut default_ty)) in
13171318
self.fields(db, specialization, field_policy)
13181319
{
1320+
if attr_ty.is_dataclass_kw_only(db) {
1321+
// These attributes are not presetn in the synthezied __init__ methods, are are
1322+
// only used to indicate that the attributes after this are keyword-only.
1323+
kw_only_field_seen = true;
1324+
continue;
1325+
}
1326+
13191327
// The descriptor handling below is guarded by this fully-static check, because dynamic
13201328
// types like `Any` are valid (data) descriptors: since they have all possible attributes,
13211329
// they also have a (callable) `__set__` method. The problem is that we can't determine
@@ -1360,8 +1368,12 @@ impl<'db> ClassLiteral<'db> {
13601368
}
13611369
}
13621370

1363-
let mut parameter =
1364-
Parameter::positional_or_keyword(name).with_annotated_type(attr_ty);
1371+
let mut parameter = if kw_only_field_seen {
1372+
Parameter::keyword_only(name)
1373+
} else {
1374+
Parameter::positional_or_keyword(name)
1375+
}
1376+
.with_annotated_type(attr_ty);
13651377

13661378
if let Some(default_ty) = default_ty {
13671379
parameter = parameter.with_default_type(default_ty);
@@ -2149,6 +2161,7 @@ pub enum KnownClass {
21492161
NotImplementedType,
21502162
// dataclasses
21512163
Field,
2164+
KwOnly,
21522165
// _typeshed._type_checker_internals
21532166
NamedTupleFallback,
21542167
}
@@ -2234,6 +2247,7 @@ impl<'db> KnownClass {
22342247
| Self::NotImplementedType
22352248
| Self::Classmethod
22362249
| Self::Field
2250+
| Self::KwOnly
22372251
| Self::NamedTupleFallback => Truthiness::Ambiguous,
22382252
}
22392253
}
@@ -2309,6 +2323,7 @@ impl<'db> KnownClass {
23092323
| Self::NotImplementedType
23102324
| Self::UnionType
23112325
| Self::Field
2326+
| Self::KwOnly
23122327
| Self::NamedTupleFallback => false,
23132328
}
23142329
}
@@ -2385,6 +2400,7 @@ impl<'db> KnownClass {
23852400
}
23862401
Self::NotImplementedType => "_NotImplementedType",
23872402
Self::Field => "Field",
2403+
Self::KwOnly => "KW_ONLY",
23882404
Self::NamedTupleFallback => "NamedTupleFallback",
23892405
}
23902406
}
@@ -2615,6 +2631,7 @@ impl<'db> KnownClass {
26152631
| Self::Deque
26162632
| Self::OrderedDict => KnownModule::Collections,
26172633
Self::Field => KnownModule::Dataclasses,
2634+
Self::KwOnly => KnownModule::Dataclasses,
26182635
Self::NamedTupleFallback => KnownModule::TypeCheckerInternals,
26192636
}
26202637
}
@@ -2679,6 +2696,7 @@ impl<'db> KnownClass {
26792696
| Self::NamedTuple
26802697
| Self::NewType
26812698
| Self::Field
2699+
| Self::KwOnly
26822700
| Self::NamedTupleFallback => false,
26832701
}
26842702
}
@@ -2745,6 +2763,7 @@ impl<'db> KnownClass {
27452763
| Self::NamedTuple
27462764
| Self::NewType
27472765
| Self::Field
2766+
| Self::KwOnly
27482767
| Self::NamedTupleFallback => false,
27492768
}
27502769
}
@@ -2818,6 +2837,7 @@ impl<'db> KnownClass {
28182837
}
28192838
"_NotImplementedType" => Self::NotImplementedType,
28202839
"Field" => Self::Field,
2840+
"KW_ONLY" => Self::KwOnly,
28212841
"NamedTupleFallback" => Self::NamedTupleFallback,
28222842
_ => return None,
28232843
};
@@ -2874,6 +2894,7 @@ impl<'db> KnownClass {
28742894
| Self::AsyncGeneratorType
28752895
| Self::WrapperDescriptorType
28762896
| Self::Field
2897+
| Self::KwOnly
28772898
| Self::NamedTupleFallback => module == self.canonical_module(db),
28782899
Self::NoneType => matches!(module, KnownModule::Typeshed | KnownModule::Types),
28792900
Self::SpecialForm
@@ -3079,6 +3100,7 @@ mod tests {
30793100
KnownClass::UnionType => PythonVersion::PY310,
30803101
KnownClass::BaseExceptionGroup | KnownClass::ExceptionGroup => PythonVersion::PY311,
30813102
KnownClass::GenericAlias => PythonVersion::PY39,
3103+
KnownClass::KwOnly => PythonVersion::PY310,
30823104
_ => PythonVersion::PY37,
30833105
};
30843106

0 commit comments

Comments
 (0)