[ty] Support dataclasses.KW_ONLY (#18677)

This commit is contained in:
Abhijeet Prasad Bodas 2025-06-16 22:57:55 +05:30 committed by GitHub
parent c3aa965546
commit 2b15f1d240
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 71 additions and 2 deletions

View file

@ -713,6 +713,49 @@ But calling `asdict` on the class object is not allowed:
asdict(Foo) asdict(Foo)
``` ```
## `dataclasses.KW_ONLY`
If an attribute is annotated with `dataclasses.KW_ONLY`, it is not added to the synthesized
`__init__` of the class. Instead, this special marker annotation causes Python at runtime to ensure
that all annotations following it have keyword-only parameters generated for them in the class's
synthesized `__init__` method.
```toml
[environment]
python-version = "3.10"
```
```py
from dataclasses import dataclass, field, KW_ONLY
@dataclass
class C:
x: int
_: KW_ONLY
y: str
# error: [missing-argument]
# error: [too-many-positional-arguments]
C(3, "")
C(3, y="")
```
Using `KW_ONLY` to annotate more than one field in a dataclass causes a `TypeError` to be raised at
runtime:
```py
@dataclass
class Fails:
a: int
b: KW_ONLY
c: str
# TODO: we should emit an error here
# (two different names with `KW_ONLY` annotations in the same dataclass means the class fails at runtime)
d: KW_ONLY
```
## Other special cases ## Other special cases
### `dataclasses.dataclass` ### `dataclasses.dataclass`

View file

@ -1313,9 +1313,21 @@ impl<'db> ClassLiteral<'db> {
let field_policy = CodeGeneratorKind::from_class(db, self)?; let field_policy = CodeGeneratorKind::from_class(db, self)?;
let signature_from_fields = |mut parameters: Vec<_>| { let signature_from_fields = |mut parameters: Vec<_>| {
let mut kw_only_field_seen = false;
for (name, (mut attr_ty, mut default_ty)) in for (name, (mut attr_ty, mut default_ty)) in
self.fields(db, specialization, field_policy) self.fields(db, specialization, field_policy)
{ {
if attr_ty
.into_nominal_instance()
.is_some_and(|instance| instance.class.is_known(db, KnownClass::KwOnly))
{
// Attributes annotated with `dataclass.KW_ONLY` are not present in the synthesized
// `__init__` method, ; they only used to indicate that the parameters after this are
// keyword-only.
kw_only_field_seen = true;
continue;
}
// The descriptor handling below is guarded by this fully-static check, because dynamic // The descriptor handling below is guarded by this fully-static check, because dynamic
// types like `Any` are valid (data) descriptors: since they have all possible attributes, // types like `Any` are valid (data) descriptors: since they have all possible attributes,
// they also have a (callable) `__set__` method. The problem is that we can't determine // they also have a (callable) `__set__` method. The problem is that we can't determine
@ -1360,8 +1372,12 @@ impl<'db> ClassLiteral<'db> {
} }
} }
let mut parameter = let mut parameter = if kw_only_field_seen {
Parameter::positional_or_keyword(name).with_annotated_type(attr_ty); Parameter::keyword_only(name)
} else {
Parameter::positional_or_keyword(name)
}
.with_annotated_type(attr_ty);
if let Some(default_ty) = default_ty { if let Some(default_ty) = default_ty {
parameter = parameter.with_default_type(default_ty); parameter = parameter.with_default_type(default_ty);
@ -2149,6 +2165,7 @@ pub enum KnownClass {
NotImplementedType, NotImplementedType,
// dataclasses // dataclasses
Field, Field,
KwOnly,
// _typeshed._type_checker_internals // _typeshed._type_checker_internals
NamedTupleFallback, NamedTupleFallback,
} }
@ -2234,6 +2251,7 @@ impl<'db> KnownClass {
| Self::NotImplementedType | Self::NotImplementedType
| Self::Classmethod | Self::Classmethod
| Self::Field | Self::Field
| Self::KwOnly
| Self::NamedTupleFallback => Truthiness::Ambiguous, | Self::NamedTupleFallback => Truthiness::Ambiguous,
} }
} }
@ -2309,6 +2327,7 @@ impl<'db> KnownClass {
| Self::NotImplementedType | Self::NotImplementedType
| Self::UnionType | Self::UnionType
| Self::Field | Self::Field
| Self::KwOnly
| Self::NamedTupleFallback => false, | Self::NamedTupleFallback => false,
} }
} }
@ -2385,6 +2404,7 @@ impl<'db> KnownClass {
} }
Self::NotImplementedType => "_NotImplementedType", Self::NotImplementedType => "_NotImplementedType",
Self::Field => "Field", Self::Field => "Field",
Self::KwOnly => "KW_ONLY",
Self::NamedTupleFallback => "NamedTupleFallback", Self::NamedTupleFallback => "NamedTupleFallback",
} }
} }
@ -2615,6 +2635,7 @@ impl<'db> KnownClass {
| Self::Deque | Self::Deque
| Self::OrderedDict => KnownModule::Collections, | Self::OrderedDict => KnownModule::Collections,
Self::Field => KnownModule::Dataclasses, Self::Field => KnownModule::Dataclasses,
Self::KwOnly => KnownModule::Dataclasses,
Self::NamedTupleFallback => KnownModule::TypeCheckerInternals, Self::NamedTupleFallback => KnownModule::TypeCheckerInternals,
} }
} }
@ -2679,6 +2700,7 @@ impl<'db> KnownClass {
| Self::NamedTuple | Self::NamedTuple
| Self::NewType | Self::NewType
| Self::Field | Self::Field
| Self::KwOnly
| Self::NamedTupleFallback => false, | Self::NamedTupleFallback => false,
} }
} }
@ -2745,6 +2767,7 @@ impl<'db> KnownClass {
| Self::NamedTuple | Self::NamedTuple
| Self::NewType | Self::NewType
| Self::Field | Self::Field
| Self::KwOnly
| Self::NamedTupleFallback => false, | Self::NamedTupleFallback => false,
} }
} }
@ -2818,6 +2841,7 @@ impl<'db> KnownClass {
} }
"_NotImplementedType" => Self::NotImplementedType, "_NotImplementedType" => Self::NotImplementedType,
"Field" => Self::Field, "Field" => Self::Field,
"KW_ONLY" => Self::KwOnly,
"NamedTupleFallback" => Self::NamedTupleFallback, "NamedTupleFallback" => Self::NamedTupleFallback,
_ => return None, _ => return None,
}; };
@ -2874,6 +2898,7 @@ impl<'db> KnownClass {
| Self::AsyncGeneratorType | Self::AsyncGeneratorType
| Self::WrapperDescriptorType | Self::WrapperDescriptorType
| Self::Field | Self::Field
| Self::KwOnly
| Self::NamedTupleFallback => module == self.canonical_module(db), | Self::NamedTupleFallback => module == self.canonical_module(db),
Self::NoneType => matches!(module, KnownModule::Typeshed | KnownModule::Types), Self::NoneType => matches!(module, KnownModule::Typeshed | KnownModule::Types),
Self::SpecialForm Self::SpecialForm
@ -3079,6 +3104,7 @@ mod tests {
KnownClass::UnionType => PythonVersion::PY310, KnownClass::UnionType => PythonVersion::PY310,
KnownClass::BaseExceptionGroup | KnownClass::ExceptionGroup => PythonVersion::PY311, KnownClass::BaseExceptionGroup | KnownClass::ExceptionGroup => PythonVersion::PY311,
KnownClass::GenericAlias => PythonVersion::PY39, KnownClass::GenericAlias => PythonVersion::PY39,
KnownClass::KwOnly => PythonVersion::PY310,
_ => PythonVersion::PY37, _ => PythonVersion::PY37,
}; };