diff --git a/crates/ty_python_semantic/resources/mdtest/dataclasses.md b/crates/ty_python_semantic/resources/mdtest/dataclasses.md index 4616e59e22454..699a624173d8b 100644 --- a/crates/ty_python_semantic/resources/mdtest/dataclasses.md +++ b/crates/ty_python_semantic/resources/mdtest/dataclasses.md @@ -713,6 +713,49 @@ But calling `asdict` on the class object is not allowed: 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 ### `dataclasses.dataclass` diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index 5869411fedabc..708a8e4f40f0f 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -1313,9 +1313,21 @@ impl<'db> ClassLiteral<'db> { let field_policy = CodeGeneratorKind::from_class(db, self)?; let signature_from_fields = |mut parameters: Vec<_>| { + let mut kw_only_field_seen = false; for (name, (mut attr_ty, mut default_ty)) in 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 // 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 @@ -1360,8 +1372,12 @@ impl<'db> ClassLiteral<'db> { } } - let mut parameter = - Parameter::positional_or_keyword(name).with_annotated_type(attr_ty); + let mut parameter = if kw_only_field_seen { + Parameter::keyword_only(name) + } else { + Parameter::positional_or_keyword(name) + } + .with_annotated_type(attr_ty); if let Some(default_ty) = default_ty { parameter = parameter.with_default_type(default_ty); @@ -2149,6 +2165,7 @@ pub enum KnownClass { NotImplementedType, // dataclasses Field, + KwOnly, // _typeshed._type_checker_internals NamedTupleFallback, } @@ -2234,6 +2251,7 @@ impl<'db> KnownClass { | Self::NotImplementedType | Self::Classmethod | Self::Field + | Self::KwOnly | Self::NamedTupleFallback => Truthiness::Ambiguous, } } @@ -2309,6 +2327,7 @@ impl<'db> KnownClass { | Self::NotImplementedType | Self::UnionType | Self::Field + | Self::KwOnly | Self::NamedTupleFallback => false, } } @@ -2385,6 +2404,7 @@ impl<'db> KnownClass { } Self::NotImplementedType => "_NotImplementedType", Self::Field => "Field", + Self::KwOnly => "KW_ONLY", Self::NamedTupleFallback => "NamedTupleFallback", } } @@ -2615,6 +2635,7 @@ impl<'db> KnownClass { | Self::Deque | Self::OrderedDict => KnownModule::Collections, Self::Field => KnownModule::Dataclasses, + Self::KwOnly => KnownModule::Dataclasses, Self::NamedTupleFallback => KnownModule::TypeCheckerInternals, } } @@ -2679,6 +2700,7 @@ impl<'db> KnownClass { | Self::NamedTuple | Self::NewType | Self::Field + | Self::KwOnly | Self::NamedTupleFallback => false, } } @@ -2745,6 +2767,7 @@ impl<'db> KnownClass { | Self::NamedTuple | Self::NewType | Self::Field + | Self::KwOnly | Self::NamedTupleFallback => false, } } @@ -2818,6 +2841,7 @@ impl<'db> KnownClass { } "_NotImplementedType" => Self::NotImplementedType, "Field" => Self::Field, + "KW_ONLY" => Self::KwOnly, "NamedTupleFallback" => Self::NamedTupleFallback, _ => return None, }; @@ -2874,6 +2898,7 @@ impl<'db> KnownClass { | Self::AsyncGeneratorType | Self::WrapperDescriptorType | Self::Field + | Self::KwOnly | Self::NamedTupleFallback => module == self.canonical_module(db), Self::NoneType => matches!(module, KnownModule::Typeshed | KnownModule::Types), Self::SpecialForm @@ -3079,6 +3104,7 @@ mod tests { KnownClass::UnionType => PythonVersion::PY310, KnownClass::BaseExceptionGroup | KnownClass::ExceptionGroup => PythonVersion::PY311, KnownClass::GenericAlias => PythonVersion::PY39, + KnownClass::KwOnly => PythonVersion::PY310, _ => PythonVersion::PY37, };