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
Original file line number Diff line number Diff line change
Expand Up @@ -701,6 +701,111 @@ Employee("Alice", e_id=1)
Employee("Alice", 1) # error: [too-many-positional-arguments]
```

### Inherited fields with class-level `kw_only`

When a child dataclass uses `@dataclass(kw_only=True)`, the `kw_only` setting should only apply to
fields defined in the child class, not to inherited fields from parent classes.

This is a regression test for <https://github.com/astral-sh/ty/issues/1769>.

```toml
[environment]
python-version = "3.10"
```

```py
from dataclasses import dataclass

@dataclass
class Inner:
inner: int

@dataclass(kw_only=True)
class Outer(Inner):
outer: int

# Inherited field `inner` is positional, new field `outer` is keyword-only
reveal_type(Outer.__init__) # revealed: (self: Outer, inner: int, *, outer: int) -> None

Outer(0, outer=5) # OK
Outer(inner=0, outer=5) # Also OK
# error: [missing-argument]
# error: [too-many-positional-arguments]
Outer(0, 5)
```

This also works when the parent class uses the `KW_ONLY` sentinel:

```py
from dataclasses import dataclass, KW_ONLY

@dataclass
class Parent:
a: int
_: KW_ONLY
b: str

@dataclass(kw_only=True)
class Child(Parent):
c: bytes

# `a` is positional (from parent), `b` is keyword-only (from parent's KW_ONLY),
# `c` is keyword-only (from child's kw_only=True)
reveal_type(Child.__init__) # revealed: (self: Child, a: int, *, b: str, c: bytes) -> None

Child(1, b="hello", c=b"world") # OK
# error: [missing-argument] "No arguments provided for required parameters `b`, `c`"
# error: [too-many-positional-arguments]
Child(1, "hello", b"world")
```

And when the child class uses the `KW_ONLY` sentinel while inheriting from a parent:

```py
from dataclasses import dataclass, KW_ONLY

@dataclass
class Base:
x: int

@dataclass
class Derived(Base):
y: str
_: KW_ONLY
z: bytes

# `x` and `y` are positional, `z` is keyword-only (from Derived's KW_ONLY)
reveal_type(Derived.__init__) # revealed: (self: Derived, x: int, y: str, *, z: bytes) -> None

Derived(1, "hello", z=b"world") # OK
# error: [missing-argument]
# error: [too-many-positional-arguments]
Derived(1, "hello", b"world")
```

The reverse case also works: when a parent has `kw_only=True` but the child doesn't, the parent's
fields stay keyword-only while the child's fields are positional:

```py
from dataclasses import dataclass

@dataclass(kw_only=True)
class KwOnlyParent:
parent_field: int

@dataclass
class PositionalChild(KwOnlyParent):
child_field: str

# `child_field` is positional (child's default), `parent_field` stays keyword-only
reveal_type(PositionalChild.__init__) # revealed: (self: PositionalChild, child_field: str, *, parent_field: int) -> None

PositionalChild("hello", parent_field=1) # OK
# error: [missing-argument]
# error: [too-many-positional-arguments]
PositionalChild("hello", 1)
```

### `slots`

If a dataclass is defined with `slots=True`, the `__slots__` attribute is generated as a tuple. It
Expand Down
32 changes: 29 additions & 3 deletions crates/ty_python_semantic/src/types/class.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ use crate::types::constraints::{ConstraintSet, IteratorConstraintsExtension};
use crate::types::context::InferContext;
use crate::types::diagnostic::{INVALID_TYPE_ALIAS_TYPE, SUPER_CALL_IN_NAMED_TUPLE_METHOD};
use crate::types::enums::enum_metadata;
use crate::types::function::{DataclassTransformerParams, KnownFunction};
use crate::types::function::{
DataclassTransformerFlags, DataclassTransformerParams, KnownFunction,
};
use crate::types::generics::{
GenericContext, InferableTypeVars, Specialization, walk_generic_context, walk_specialization,
};
Expand Down Expand Up @@ -2347,6 +2349,8 @@ impl<'db> ClassLiteral<'db> {
};

// Dataclass transformer flags can be overwritten using class arguments.
// TODO this should be done more generally, not just in `own_synthesized_member`, so that
// `dataclass_params` always reflects the transformer params.
if let Some(transformer_params) = transformer_params.as_mut() {
if let Some(class_def) = self.definition(db).kind(db).as_class() {
let module = parsed_module(db, self.file(db)).load(db);
Expand Down Expand Up @@ -2376,6 +2380,8 @@ impl<'db> ClassLiteral<'db> {

let has_dataclass_param = |param| {
dataclass_params.is_some_and(|params| params.flags(db).contains(param))
// TODO if we were correctly initializing `dataclass_params` from the
// transformer params, this fallback shouldn't be needed here.
|| transformer_params.is_some_and(|params| params.flags(db).contains(param))
};

Expand Down Expand Up @@ -2455,8 +2461,7 @@ impl<'db> ClassLiteral<'db> {
}
}

let is_kw_only = name == "__replace__"
|| kw_only.unwrap_or(has_dataclass_param(DataclassFlags::KW_ONLY));
let is_kw_only = name == "__replace__" || kw_only.unwrap_or(false);

// Use the alias name if provided, otherwise use the field name
let parameter_name =
Expand Down Expand Up @@ -3175,6 +3180,27 @@ impl<'db> ClassLiteral<'db> {
}
}

// Resolve the kw_only to the class-level default. This ensures that when fields
// are inherited by child classes, they use their defining class's kw_only default.
if let FieldKind::Dataclass {
kw_only: ref mut kw @ None,
..
} = field.kind
{
let class_kw_only_default = self
.dataclass_params(db)
.is_some_and(|params| params.flags(db).contains(DataclassFlags::KW_ONLY))
// TODO this next part should not be necessary, if we were properly
// initializing `dataclass_params` from the dataclass-transform params, for
// metaclass and base-class-based dataclass-transformers.
|| matches!(
field_policy,
CodeGeneratorKind::DataclassLike(Some(transformer_params))
if transformer_params.flags(db).contains(DataclassTransformerFlags::KW_ONLY_DEFAULT)
);
*kw = Some(class_kw_only_default);
}

attributes.insert(symbol.name().clone(), field);
}
}
Expand Down
Loading