|
2 | 2 |
|
3 | 3 | from __future__ import annotations
|
4 | 4 |
|
5 |
| -from typing import Iterator, Optional |
| 5 | +from typing import TYPE_CHECKING, Iterator, Optional |
6 | 6 | from typing_extensions import Final
|
7 | 7 |
|
8 | 8 | from mypy import errorcodes, message_registry
|
|
26 | 26 | DataclassTransformSpec,
|
27 | 27 | Expression,
|
28 | 28 | FuncDef,
|
| 29 | + FuncItem, |
29 | 30 | IfStmt,
|
30 | 31 | JsonDict,
|
31 | 32 | NameExpr,
|
|
55 | 56 | from mypy.types import (
|
56 | 57 | AnyType,
|
57 | 58 | CallableType,
|
| 59 | + FunctionLike, |
58 | 60 | Instance,
|
59 | 61 | LiteralType,
|
60 | 62 | NoneType,
|
|
69 | 71 | )
|
70 | 72 | from mypy.typevars import fill_typevars
|
71 | 73 |
|
| 74 | +if TYPE_CHECKING: |
| 75 | + from mypy.checker import TypeChecker |
| 76 | + |
72 | 77 | # The set of decorators that generate dataclasses.
|
73 | 78 | dataclass_makers: Final = {"dataclass", "dataclasses.dataclass"}
|
74 | 79 |
|
75 | 80 |
|
76 | 81 | SELF_TVAR_NAME: Final = "_DT"
|
77 |
| -_TRANSFORM_SPEC_FOR_DATACLASSES = DataclassTransformSpec( |
| 82 | +_TRANSFORM_SPEC_FOR_DATACLASSES: Final = DataclassTransformSpec( |
78 | 83 | eq_default=True,
|
79 | 84 | order_default=False,
|
80 | 85 | kw_only_default=False,
|
81 | 86 | frozen_default=False,
|
82 | 87 | field_specifiers=("dataclasses.Field", "dataclasses.field"),
|
83 | 88 | )
|
84 |
| -_INTERNAL_REPLACE_SYM_NAME = "__mypy-replace" |
| 89 | +_INTERNAL_REPLACE_SYM_NAME: Final = "__mypy-replace" |
| 90 | +_INTERNAL_POST_INIT_SYM_NAME: Final = "__mypy-__post_init__" |
85 | 91 |
|
86 | 92 |
|
87 | 93 | class DataclassAttribute:
|
@@ -350,6 +356,8 @@ def transform(self) -> bool:
|
350 | 356 |
|
351 | 357 | if self._spec is _TRANSFORM_SPEC_FOR_DATACLASSES:
|
352 | 358 | self._add_internal_replace_method(attributes)
|
| 359 | + if "__post_init__" in info.names: |
| 360 | + self._add_internal_post_init_method(attributes) |
353 | 361 |
|
354 | 362 | info.metadata["dataclass"] = {
|
355 | 363 | "attributes": [attr.serialize() for attr in attributes],
|
@@ -385,7 +393,47 @@ def _add_internal_replace_method(self, attributes: list[DataclassAttribute]) ->
|
385 | 393 | fallback=self._api.named_type("builtins.function"),
|
386 | 394 | )
|
387 | 395 |
|
388 |
| - self._cls.info.names[_INTERNAL_REPLACE_SYM_NAME] = SymbolTableNode( |
| 396 | + info.names[_INTERNAL_REPLACE_SYM_NAME] = SymbolTableNode( |
| 397 | + kind=MDEF, node=FuncDef(typ=signature), plugin_generated=True |
| 398 | + ) |
| 399 | + |
| 400 | + def _add_internal_post_init_method(self, attributes: list[DataclassAttribute]) -> None: |
| 401 | + arg_types: list[Type] = [fill_typevars(self._cls.info)] |
| 402 | + arg_kinds = [ARG_POS] |
| 403 | + arg_names: list[str | None] = ["self"] |
| 404 | + |
| 405 | + info = self._cls.info |
| 406 | + for attr in attributes: |
| 407 | + if not attr.is_init_var: |
| 408 | + continue |
| 409 | + attr_type = attr.expand_type(info) |
| 410 | + assert attr_type is not None |
| 411 | + arg_types.append(attr_type) |
| 412 | + # We always use `ARG_POS` without a default value, because it is practical. |
| 413 | + # Consider this case: |
| 414 | + # |
| 415 | + # @dataclass |
| 416 | + # class My: |
| 417 | + # y: dataclasses.InitVar[str] = 'a' |
| 418 | + # def __post_init__(self, y: str) -> None: ... |
| 419 | + # |
| 420 | + # We would be *required* to specify `y: str = ...` if default is added here. |
| 421 | + # But, most people won't care about adding default values to `__post_init__`, |
| 422 | + # because it is not designed to be called directly, and duplicating default values |
| 423 | + # for the sake of type-checking is unpleasant. |
| 424 | + arg_kinds.append(ARG_POS) |
| 425 | + arg_names.append(attr.name) |
| 426 | + |
| 427 | + signature = CallableType( |
| 428 | + arg_types=arg_types, |
| 429 | + arg_kinds=arg_kinds, |
| 430 | + arg_names=arg_names, |
| 431 | + ret_type=NoneType(), |
| 432 | + fallback=self._api.named_type("builtins.function"), |
| 433 | + name="__post_init__", |
| 434 | + ) |
| 435 | + |
| 436 | + info.names[_INTERNAL_POST_INIT_SYM_NAME] = SymbolTableNode( |
389 | 437 | kind=MDEF, node=FuncDef(typ=signature), plugin_generated=True
|
390 | 438 | )
|
391 | 439 |
|
@@ -1052,3 +1100,33 @@ def replace_function_sig_callback(ctx: FunctionSigContext) -> CallableType:
|
1052 | 1100 | fallback=ctx.default_signature.fallback,
|
1053 | 1101 | name=f"{ctx.default_signature.name} of {inst_type_str}",
|
1054 | 1102 | )
|
| 1103 | + |
| 1104 | + |
| 1105 | +def is_processed_dataclass(info: TypeInfo | None) -> bool: |
| 1106 | + return info is not None and "dataclass" in info.metadata |
| 1107 | + |
| 1108 | + |
| 1109 | +def check_post_init(api: TypeChecker, defn: FuncItem, info: TypeInfo) -> None: |
| 1110 | + if defn.type is None: |
| 1111 | + return |
| 1112 | + |
| 1113 | + ideal_sig = info.get_method(_INTERNAL_POST_INIT_SYM_NAME) |
| 1114 | + if ideal_sig is None or ideal_sig.type is None: |
| 1115 | + return |
| 1116 | + |
| 1117 | + # We set it ourself, so it is always fine: |
| 1118 | + assert isinstance(ideal_sig.type, ProperType) |
| 1119 | + assert isinstance(ideal_sig.type, FunctionLike) |
| 1120 | + # Type of `FuncItem` is always `FunctionLike`: |
| 1121 | + assert isinstance(defn.type, FunctionLike) |
| 1122 | + |
| 1123 | + api.check_override( |
| 1124 | + override=defn.type, |
| 1125 | + original=ideal_sig.type, |
| 1126 | + name="__post_init__", |
| 1127 | + name_in_super="__post_init__", |
| 1128 | + supertype="dataclass", |
| 1129 | + original_class_or_static=False, |
| 1130 | + override_class_or_static=False, |
| 1131 | + node=defn, |
| 1132 | + ) |
0 commit comments