Skip to content

Commit 39ee71c

Browse files
[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 astral-sh/ty#980
1 parent 1a38831 commit 39ee71c

File tree

5 files changed

+77
-3
lines changed

5 files changed

+77
-3
lines changed

crates/ty_python_semantic/resources/mdtest/dataclasses/fields.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,3 +84,47 @@ reveal_type(field(default=1)) # revealed: dataclasses.Field[Literal[1]]
8484
reveal_type(field(default=None)) # revealed: dataclasses.Field[None]
8585
reveal_type(field(default_factory=get_default)) # revealed: dataclasses.Field[str]
8686
```
87+
88+
## dataclass_transform field_specifiers
89+
90+
If `field_specifiers` is not specified, it defaults to an empty tuple, meaning no field specifiers
91+
are supported and `dataclasses.field` and `dataclasses.Field` should not be accepted by default.
92+
93+
```py
94+
from typing_extensions import dataclass_transform
95+
from dataclasses import field, dataclass
96+
from typing import TypeVar
97+
98+
T = TypeVar("T")
99+
100+
@dataclass_transform()
101+
def create_model(*, init: bool = True):
102+
def deco(cls: type[T]) -> type[T]:
103+
return cls
104+
return deco
105+
106+
@create_model()
107+
class A:
108+
name: str = field(init=False)
109+
110+
# field(init=False) should be ignored for dataclass_transform without explicit field_specifiers
111+
reveal_type(A.__init__) # revealed: (self: A, name: str = Unknown) -> None
112+
113+
@dataclass
114+
class B:
115+
name: str = field(init=False)
116+
117+
# Regular @dataclass should respect field(init=False)
118+
reveal_type(B.__init__) # revealed: (self: B) -> None
119+
```
120+
121+
Test constructor calls:
122+
123+
```py
124+
# This should NOT error because field(init=False) is ignored for A
125+
A(name="foo")
126+
127+
# This should error because field(init=False) is respected for B
128+
# error: [unknown-argument]
129+
B(name="foo")
130+
```

crates/ty_python_semantic/src/types.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -510,6 +510,10 @@ bitflags! {
510510
const KW_ONLY = 0b0000_1000_0000;
511511
const SLOTS = 0b0001_0000_0000;
512512
const WEAKREF_SLOT = 0b0010_0000_0000;
513+
// This is not an actual argument from `dataclass(...)` but a flag signaling that no
514+
// `field_specifiers` was specified for the `dataclass_transform`, see [1].
515+
// [1]: https://typing.python.org/en/latest/spec/dataclasses.html#dataclass-transform-parameters
516+
const NO_FIELD_SPECIFIERS = 0b0100_0000_0000;
513517
}
514518
}
515519

@@ -542,6 +546,11 @@ impl From<DataclassTransformerParams> for DataclassParams {
542546
params.contains(DataclassTransformerParams::FROZEN_DEFAULT),
543547
);
544548

549+
result.set(
550+
Self::NO_FIELD_SPECIFIERS,
551+
!params.contains(DataclassTransformerParams::FIELD_SPECIFIERS),
552+
);
553+
545554
result
546555
}
547556
}

crates/ty_python_semantic/src/types/call/bind.rs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -900,7 +900,7 @@ impl<'db> Bindings<'db> {
900900
order_default,
901901
kw_only_default,
902902
frozen_default,
903-
_field_specifiers,
903+
field_specifiers,
904904
_kwargs,
905905
] = overload.parameter_types()
906906
{
@@ -919,6 +919,16 @@ impl<'db> Bindings<'db> {
919919
params |= DataclassTransformerParams::FROZEN_DEFAULT;
920920
}
921921

922+
if let Some(field_specifiers_type) = field_specifiers {
923+
// For now, we'll do a simple check: if field_specifiers is not
924+
// None/empty, we assume it might contain dataclasses.field
925+
// TODO: Implement proper parsing to check for
926+
// dataclasses.field/Field specifically
927+
if !field_specifiers_type.is_none(db) {
928+
params |= DataclassTransformerParams::FIELD_SPECIFIERS;
929+
}
930+
}
931+
922932
overload.set_return_type(Type::DataclassTransformer(params));
923933
}
924934
}

crates/ty_python_semantic/src/types/class.rs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2407,8 +2407,18 @@ impl<'db> ClassLiteral<'db> {
24072407
let mut kw_only = None;
24082408
if let Some(Type::KnownInstance(KnownInstanceType::Field(field))) = default_ty {
24092409
default_ty = Some(field.default_type(db));
2410-
init = field.init(db);
2411-
kw_only = field.kw_only(db);
2410+
if self
2411+
.dataclass_params(db)
2412+
.map(|params| params.contains(DataclassParams::NO_FIELD_SPECIFIERS))
2413+
.unwrap_or(false)
2414+
{
2415+
// This happens when constructing a `dataclass` with a `dataclass_transform`
2416+
// without defining the `field_specifiers`, meaning it should ignore
2417+
// `dataclasses.field` and `dataclasses.Field`.
2418+
} else {
2419+
init = field.init(db);
2420+
kw_only = field.kw_only(db);
2421+
}
24122422
}
24132423

24142424
let mut field = Field {

crates/ty_python_semantic/src/types/function.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,7 @@ bitflags! {
164164
const ORDER_DEFAULT = 1 << 1;
165165
const KW_ONLY_DEFAULT = 1 << 2;
166166
const FROZEN_DEFAULT = 1 << 3;
167+
const FIELD_SPECIFIERS= 1 << 4;
167168
}
168169
}
169170

0 commit comments

Comments
 (0)