mirror of https://github.com/astral-sh/ruff
[ty] Respect `kw_only` from parent class (#21820)
## Summary Closes https://github.com/astral-sh/ty/issues/1769. --------- Co-authored-by: Carl Meyer <carl@astral.sh>
This commit is contained in:
parent
8293afe2ae
commit
d2aabeaaa2
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
@ -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);
|
||||
|
|
@ -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))
|
||||
};
|
||||
|
||||
|
|
@ -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 =
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue