Skip to content

Commit bfbac5e

Browse files
authored
stubgen: Fix generated dataclass __init__ signature (#16906)
Fixes #16811 stubgen was swallowing default values for `__init__` methods generated by the dataclass plugin making their signature incorrect. This is because the plugin does not include the argument's initializer in the generated signature. I changed it to include a dummy ellipsis so that stubgen can generate correct code. I also fixed arguments added by the dataclass plugin with the invalid names `*` and `**` to have the valid and unique names `*generated_args` and `**generated_kwargs` (with extra underscores to make them unique if necessary). This removes the need for the hack to special case them in stubgen and is less confusing for someone looking at them in a stub file.
1 parent 2e5174c commit bfbac5e

File tree

4 files changed

+50
-31
lines changed

4 files changed

+50
-31
lines changed

mypy/plugins/dataclasses.py

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
Context,
2525
DataclassTransformSpec,
2626
Decorator,
27+
EllipsisExpr,
2728
Expression,
2829
FuncDef,
2930
FuncItem,
@@ -149,13 +150,13 @@ def to_argument(
149150
return Argument(
150151
variable=self.to_var(current_info),
151152
type_annotation=self.expand_type(current_info),
152-
initializer=None,
153+
initializer=EllipsisExpr() if self.has_default else None, # Only used by stubgen
153154
kind=arg_kind,
154155
)
155156

156157
def expand_type(self, current_info: TypeInfo) -> Type | None:
157158
if self.type is not None and self.info.self_type is not None:
158-
# In general, it is not safe to call `expand_type()` during semantic analyzis,
159+
# In general, it is not safe to call `expand_type()` during semantic analysis,
159160
# however this plugin is called very late, so all types should be fully ready.
160161
# Also, it is tricky to avoid eager expansion of Self types here (e.g. because
161162
# we serialize attributes).
@@ -269,11 +270,17 @@ def transform(self) -> bool:
269270
if arg.kind == ARG_POS:
270271
arg.kind = ARG_OPT
271272

272-
nameless_var = Var("")
273+
existing_args_names = {arg.variable.name for arg in args}
274+
gen_args_name = "generated_args"
275+
while gen_args_name in existing_args_names:
276+
gen_args_name += "_"
277+
gen_kwargs_name = "generated_kwargs"
278+
while gen_kwargs_name in existing_args_names:
279+
gen_kwargs_name += "_"
273280
args = [
274-
Argument(nameless_var, AnyType(TypeOfAny.explicit), None, ARG_STAR),
281+
Argument(Var(gen_args_name), AnyType(TypeOfAny.explicit), None, ARG_STAR),
275282
*args,
276-
Argument(nameless_var, AnyType(TypeOfAny.explicit), None, ARG_STAR2),
283+
Argument(Var(gen_kwargs_name), AnyType(TypeOfAny.explicit), None, ARG_STAR2),
277284
]
278285

279286
add_method_to_class(

mypy/stubgen.py

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -537,17 +537,6 @@ def _get_func_args(self, o: FuncDef, ctx: FunctionContext) -> list[ArgSig]:
537537
if new_args is not None:
538538
args = new_args
539539

540-
is_dataclass_generated = (
541-
self.analyzed and self.processing_dataclass and o.info.names[o.name].plugin_generated
542-
)
543-
if o.name == "__init__" and is_dataclass_generated and "**" in [a.name for a in args]:
544-
# The dataclass plugin generates invalid nameless "*" and "**" arguments
545-
new_name = "".join(a.name.strip("*") for a in args)
546-
for arg in args:
547-
if arg.name == "*":
548-
arg.name = f"*{new_name}_" # this name is guaranteed to be unique
549-
elif arg.name == "**":
550-
arg.name = f"**{new_name}__" # same here
551540
return args
552541

553542
def _get_func_return(self, o: FuncDef, ctx: FunctionContext) -> str | None:

test-data/unit/check-dataclasses.test

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1610,10 +1610,16 @@ B: Any
16101610
@dataclass
16111611
class A(B):
16121612
a: int
1613+
@dataclass
1614+
class C(B):
1615+
generated_args: int
1616+
generated_kwargs: int
16131617

16141618
A(a=1, b=2)
16151619
A(1)
16161620
A(a="foo") # E: Argument "a" to "A" has incompatible type "str"; expected "int"
1621+
C(generated_args="foo", generated_kwargs="bar") # E: Argument "generated_args" to "C" has incompatible type "str"; expected "int" \
1622+
# E: Argument "generated_kwargs" to "C" has incompatible type "str"; expected "int"
16171623
[builtins fixtures/dataclasses.pyi]
16181624

16191625
[case testDataclassesCallableFrozen]

test-data/unit/stubgen.test

Lines changed: 32 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4083,20 +4083,21 @@ class W: ...
40834083
class V: ...
40844084

40854085
[case testDataclass_semanal]
4086-
from dataclasses import dataclass, InitVar
4086+
from dataclasses import InitVar, dataclass, field
40874087
from typing import ClassVar
40884088

40894089
@dataclass
40904090
class X:
40914091
a: int
4092-
b: str = "hello"
4093-
c: ClassVar
4094-
d: ClassVar = 200
4092+
b: InitVar[str]
4093+
c: str = "hello"
4094+
d: ClassVar
4095+
e: ClassVar = 200
40954096
f: list[int] = field(init=False, default_factory=list)
40964097
g: int = field(default=2, kw_only=True)
40974098
h: int = 1
4098-
i: InitVar[str]
4099-
j: InitVar = 100
4099+
i: InitVar = 100
4100+
j: list[int] = field(default_factory=list)
41004101
non_field = None
41014102

41024103
@dataclass(init=False, repr=False, frozen=True)
@@ -4109,23 +4110,24 @@ from typing import ClassVar
41094110
@dataclass
41104111
class X:
41114112
a: int
4112-
b: str = ...
4113-
c: ClassVar
4114-
d: ClassVar = ...
4113+
b: InitVar[str]
4114+
c: str = ...
4115+
d: ClassVar
4116+
e: ClassVar = ...
41154117
f: list[int] = ...
41164118
g: int = ...
41174119
h: int = ...
4118-
i: InitVar[str]
4119-
j: InitVar = ...
4120+
i: InitVar = ...
4121+
j: list[int] = ...
41204122
non_field = ...
4121-
def __init__(self, a, b, f, g, h, i, j) -> None: ...
4123+
def __init__(self, a, b, c=..., *, g=..., h=..., i=..., j=...) -> None: ...
41224124

41234125
@dataclass(init=False, repr=False, frozen=True)
41244126
class Y: ...
41254127

41264128
[case testDataclassWithKwOnlyField_semanal]
41274129
# flags: --python-version=3.10
4128-
from dataclasses import dataclass, InitVar, KW_ONLY
4130+
from dataclasses import dataclass, field, InitVar, KW_ONLY
41294131
from typing import ClassVar
41304132

41314133
@dataclass
@@ -4162,7 +4164,7 @@ class X:
41624164
i: InitVar[str]
41634165
j: InitVar = ...
41644166
non_field = ...
4165-
def __init__(self, a, b, f, g, *, h, i, j) -> None: ...
4167+
def __init__(self, a, b=..., *, g=..., h=..., i, j=...) -> None: ...
41664168

41674169
@dataclass(init=False, repr=False, frozen=True)
41684170
class Y: ...
@@ -4193,14 +4195,29 @@ import missing
41934195
class X(missing.Base):
41944196
a: int
41954197

4198+
@dataclass
4199+
class Y(missing.Base):
4200+
generated_args: str
4201+
generated_args_: str
4202+
generated_kwargs: float
4203+
generated_kwargs_: float
4204+
41964205
[out]
41974206
import missing
41984207
from dataclasses import dataclass
41994208

42004209
@dataclass
42014210
class X(missing.Base):
42024211
a: int
4203-
def __init__(self, *selfa_, a, **selfa__) -> None: ...
4212+
def __init__(self, *generated_args, a, **generated_kwargs) -> None: ...
4213+
4214+
@dataclass
4215+
class Y(missing.Base):
4216+
generated_args: str
4217+
generated_args_: str
4218+
generated_kwargs: float
4219+
generated_kwargs_: float
4220+
def __init__(self, *generated_args__, generated_args, generated_args_, generated_kwargs, generated_kwargs_, **generated_kwargs__) -> None: ...
42044221

42054222
[case testAlwaysUsePEP604Union]
42064223
import typing

0 commit comments

Comments
 (0)