Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions crates/ty_python_semantic/resources/mdtest/dataclasses.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
30 changes: 28 additions & 2 deletions crates/ty_python_semantic/src/types/class.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -2149,6 +2165,7 @@ pub enum KnownClass {
NotImplementedType,
// dataclasses
Field,
KwOnly,
// _typeshed._type_checker_internals
NamedTupleFallback,
}
Expand Down Expand Up @@ -2234,6 +2251,7 @@ impl<'db> KnownClass {
| Self::NotImplementedType
| Self::Classmethod
| Self::Field
| Self::KwOnly
| Self::NamedTupleFallback => Truthiness::Ambiguous,
}
}
Expand Down Expand Up @@ -2309,6 +2327,7 @@ impl<'db> KnownClass {
| Self::NotImplementedType
| Self::UnionType
| Self::Field
| Self::KwOnly
| Self::NamedTupleFallback => false,
}
}
Expand Down Expand Up @@ -2385,6 +2404,7 @@ impl<'db> KnownClass {
}
Self::NotImplementedType => "_NotImplementedType",
Self::Field => "Field",
Self::KwOnly => "KW_ONLY",
Self::NamedTupleFallback => "NamedTupleFallback",
}
}
Expand Down Expand Up @@ -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,
}
}
Expand Down Expand Up @@ -2679,6 +2700,7 @@ impl<'db> KnownClass {
| Self::NamedTuple
| Self::NewType
| Self::Field
| Self::KwOnly
| Self::NamedTupleFallback => false,
}
}
Expand Down Expand Up @@ -2745,6 +2767,7 @@ impl<'db> KnownClass {
| Self::NamedTuple
| Self::NewType
| Self::Field
| Self::KwOnly
| Self::NamedTupleFallback => false,
}
}
Expand Down Expand Up @@ -2818,6 +2841,7 @@ impl<'db> KnownClass {
}
"_NotImplementedType" => Self::NotImplementedType,
"Field" => Self::Field,
"KW_ONLY" => Self::KwOnly,
"NamedTupleFallback" => Self::NamedTupleFallback,
_ => return None,
};
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
};

Expand Down
Loading