[ty] correctly ignore field specifiers when not specified (#20002)

This commit corrects the type checker's behavior when handling
`dataclass_transform` decorators that don't explicitly specify
`field_specifiers`. According to [PEP 681 (Data Class
Transforms)](https://peps.python.org/pep-0681/#dataclass-transform-parameters),
when `field_specifiers` is not provided, it defaults to an empty tuple,
meaning no field specifiers are supported and
`dataclasses.field`/`dataclasses.Field` calls should be ignored.

Fixes https://github.com/astral-sh/ty/issues/980
This commit is contained in:
Leandro Braga 2025-08-20 15:33:23 -03:00 committed by GitHub
parent 1a38831d53
commit 39ee71c2a5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 77 additions and 3 deletions

View file

@ -510,6 +510,10 @@ bitflags! {
const KW_ONLY = 0b0000_1000_0000;
const SLOTS = 0b0001_0000_0000;
const WEAKREF_SLOT = 0b0010_0000_0000;
// This is not an actual argument from `dataclass(...)` but a flag signaling that no
// `field_specifiers` was specified for the `dataclass_transform`, see [1].
// [1]: https://typing.python.org/en/latest/spec/dataclasses.html#dataclass-transform-parameters
const NO_FIELD_SPECIFIERS = 0b0100_0000_0000;
}
}
@ -542,6 +546,11 @@ impl From<DataclassTransformerParams> for DataclassParams {
params.contains(DataclassTransformerParams::FROZEN_DEFAULT),
);
result.set(
Self::NO_FIELD_SPECIFIERS,
!params.contains(DataclassTransformerParams::FIELD_SPECIFIERS),
);
result
}
}

View file

@ -900,7 +900,7 @@ impl<'db> Bindings<'db> {
order_default,
kw_only_default,
frozen_default,
_field_specifiers,
field_specifiers,
_kwargs,
] = overload.parameter_types()
{
@ -919,6 +919,16 @@ impl<'db> Bindings<'db> {
params |= DataclassTransformerParams::FROZEN_DEFAULT;
}
if let Some(field_specifiers_type) = field_specifiers {
// For now, we'll do a simple check: if field_specifiers is not
// None/empty, we assume it might contain dataclasses.field
// TODO: Implement proper parsing to check for
// dataclasses.field/Field specifically
if !field_specifiers_type.is_none(db) {
params |= DataclassTransformerParams::FIELD_SPECIFIERS;
}
}
overload.set_return_type(Type::DataclassTransformer(params));
}
}

View file

@ -2407,8 +2407,18 @@ impl<'db> ClassLiteral<'db> {
let mut kw_only = None;
if let Some(Type::KnownInstance(KnownInstanceType::Field(field))) = default_ty {
default_ty = Some(field.default_type(db));
init = field.init(db);
kw_only = field.kw_only(db);
if self
.dataclass_params(db)
.map(|params| params.contains(DataclassParams::NO_FIELD_SPECIFIERS))
.unwrap_or(false)
{
// This happens when constructing a `dataclass` with a `dataclass_transform`
// without defining the `field_specifiers`, meaning it should ignore
// `dataclasses.field` and `dataclasses.Field`.
} else {
init = field.init(db);
kw_only = field.kw_only(db);
}
}
let mut field = Field {

View file

@ -164,6 +164,7 @@ bitflags! {
const ORDER_DEFAULT = 1 << 1;
const KW_ONLY_DEFAULT = 1 << 2;
const FROZEN_DEFAULT = 1 << 3;
const FIELD_SPECIFIERS= 1 << 4;
}
}