Skip to content

Commit 6b1a872

Browse files
authored
[ty] Validate signatures of dataclass __post_init__ methods (#22730)
1 parent dd45d28 commit 6b1a872

3 files changed

Lines changed: 302 additions & 28 deletions

File tree

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
# Tests for the `__post_init__` method
2+
3+
At runtime, if a dataclass has a `__post_init__` method then all `InitVar`-annotated fields will be
4+
passed as positional arguments to that method as the final statement in the dataclass's generated
5+
`__init__` method. The `__post_init__` signature must therefore be compatible with the fields of the
6+
dataclass:
7+
8+
```py
9+
from dataclasses import dataclass, InitVar
10+
11+
@dataclass
12+
class Empty1:
13+
def __post_init__(self): ... # fine
14+
15+
@dataclass
16+
class Empty2:
17+
def __post_init__(self) -> None: ... # fine
18+
19+
@dataclass
20+
class Empty3:
21+
# The returned value is discarded,
22+
# so arbitrary return annotations are allowed
23+
def __post_init__(self) -> int:
24+
return 42
25+
26+
@dataclass
27+
class Empty4:
28+
def __post_init__(self, *args): ... # fine
29+
30+
@dataclass
31+
class Empty5:
32+
def __post_init__(self, **kwargs): ... # fine
33+
34+
@dataclass
35+
class Empty6:
36+
def __post_init__(self, *args, **kargs): ... # fine
37+
38+
@dataclass
39+
class Empty7:
40+
# no arguments will be passed to this method at runtime,
41+
# because there are no `InitVar` fields on the class,
42+
# so this is an error:
43+
#
44+
# error: [invalid-dataclass]
45+
def __post_init__(self, required_argument: int): ...
46+
47+
@dataclass
48+
class Empty8:
49+
# error: [invalid-dataclass]
50+
def __post_init__(self, *, required_argument): ...
51+
52+
@dataclass
53+
class Empty9:
54+
# error: [invalid-dataclass]
55+
def __post_init__(self, required_argument, /): ...
56+
57+
@dataclass
58+
class SingleField:
59+
x: int
60+
61+
# `x` will not be passed to `__post_init__`,
62+
# because it is not an `InitVar`, so this is an
63+
#
64+
# error: [invalid-dataclass]
65+
def __post_init__(self, x: int) -> None: ...
66+
67+
@dataclass
68+
class SingleFieldGood:
69+
x: int
70+
71+
# this is fine!
72+
def __post_init__(self) -> None: ...
73+
74+
@dataclass
75+
class HasInitVarNoParameter:
76+
x: InitVar[int]
77+
78+
# error: [invalid-dataclass]
79+
def __post_init__(self) -> None: ...
80+
81+
@dataclass
82+
class HasInitVarDifferentParameterName:
83+
x: InitVar[int]
84+
85+
# because arguments are always passed in positionally
86+
# to `__post_init__` methods, we allow a parameter to
87+
# have an arbitrary name as long as it is inferred has
88+
# having a compatible type. So this is fine:
89+
def __post_init__(self, xx) -> None: ...
90+
91+
@dataclass
92+
class HasInitVarBadParameterType:
93+
x: InitVar[int]
94+
95+
# error: [invalid-dataclass]
96+
def __post_init__(self, x: str) -> None: ...
97+
98+
@dataclass
99+
class HasInitVarBadParameterKind:
100+
x: InitVar[int]
101+
102+
# error: [invalid-dataclass]
103+
def __post_init__(self, *, x: int) -> None: ...
104+
105+
@dataclass
106+
class HasInitVarGood:
107+
x: InitVar[int]
108+
109+
def __post_init__(self, x: int) -> None: ...
110+
111+
@dataclass
112+
class HasInitVarGoodPositionalOnly:
113+
x: InitVar[int]
114+
115+
# arguments are always passed to `__post_init__` positionally at runtime,
116+
# so this is fine
117+
def __post_init__(self, x: int, /) -> None: ...
118+
119+
@dataclass
120+
class LotsOfInitVarsBad:
121+
a: int
122+
b: InitVar[str]
123+
c: InitVar[bytes]
124+
d: int
125+
e: int
126+
f: InitVar[range]
127+
g: int
128+
129+
# Only `InitVar` fields are passed in at runtime, so this is an
130+
# error: [invalid-dataclass]
131+
def __post_init__(self, a: int, b: str, c: bytes, d: int, e: int, f: range, g: int): ...
132+
133+
@dataclass
134+
class LotsOfInitVarsOutOfOrder:
135+
a: int
136+
b: InitVar[str]
137+
c: InitVar[bytes]
138+
d: int
139+
e: int
140+
f: InitVar[range]
141+
g: int
142+
143+
# the parameters are in the wrong order, so this is an
144+
# error: [invalid-dataclass]
145+
def __post_init__(self, c: bytes, b: str, f: range): ...
146+
147+
@dataclass
148+
class LotsOfInitVarsGood:
149+
a: int
150+
b: InitVar[str]
151+
c: InitVar[bytes]
152+
d: int
153+
e: int
154+
f: InitVar[range]
155+
g: int
156+
157+
def __post_init__(self, b: str, c: bytes, f: range): ...
158+
159+
@dataclass
160+
class InitVarSubclassGood(LotsOfInitVarsGood):
161+
h: InitVar[list[int]]
162+
i: str
163+
j: InitVar[bool]
164+
165+
def __post_init__(self, b: str, c: bytes, f: range, h: list[int], j: bool): ...
166+
167+
@dataclass
168+
class InitVarSubclassBad(LotsOfInitVarsGood):
169+
h: InitVar[list[int]]
170+
i: str
171+
j: InitVar[bool]
172+
173+
# error: [invalid-dataclass]
174+
def __post_init__(self, h: list[int], j: bool, b: str, c: bytes, f: range): ...
175+
```

crates/ty_python_semantic/resources/mdtest/liskov.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -411,7 +411,7 @@ python-version = "3.13"
411411
```
412412

413413
```pyi
414-
from dataclasses import dataclass
414+
from dataclasses import dataclass, InitVar
415415
from typing_extensions import Self
416416

417417
class Grandparent: ...
@@ -425,14 +425,14 @@ class Child(Parent):
425425

426426
@dataclass(init=False)
427427
class DataSuper:
428-
x: int
428+
x: InitVar[int]
429429

430430
def __post_init__(self, x: int) -> None:
431431
self.x = x
432432

433433
@dataclass(init=False)
434434
class DataSub(DataSuper):
435-
y: str
435+
y: InitVar[str]
436436

437437
def __post_init__(self, x: int, y: str) -> None:
438438
self.y = y

0 commit comments

Comments
 (0)