Skip to content

Commit c544e98

Browse files
committed
Generate dataclasses __hash__ accroding to runtime semantics
1 parent bd1f51a commit c544e98

File tree

2 files changed

+58
-0
lines changed

2 files changed

+58
-0
lines changed

mypy/plugins/dataclasses.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,7 @@ def transform(self) -> bool:
247247
"order": self._get_bool_arg("order", self._spec.order_default),
248248
"frozen": self._get_bool_arg("frozen", self._spec.frozen_default),
249249
"slots": self._get_bool_arg("slots", False),
250+
"unsafe_hash": self._get_bool_arg("unsafe_hash", False),
250251
"match_args": self._get_bool_arg("match_args", True),
251252
}
252253
py_version = self._api.options.python_version
@@ -291,6 +292,14 @@ def transform(self) -> bool:
291292
self._api, self._cls, "__init__", args=args, return_type=NoneType()
292293
)
293294

295+
if "__hash__" not in info.names or info.names["__hash__"].plugin_generated:
296+
# Presence of __hash__ usually isn't checked. However, when inheriting from
297+
# an abstract Hashable, we need to override the abstract __hash__ to avoid
298+
# false positives.
299+
self._add_dunder_hash(
300+
is_hashable=decorator_arguments["unsafe_hash"] or decorator_arguments["frozen"]
301+
)
302+
294303
if (
295304
decorator_arguments["eq"]
296305
and info.get("__eq__") is None
@@ -446,6 +455,29 @@ def _add_internal_post_init_method(self, attributes: list[DataclassAttribute]) -
446455
return_type=NoneType(),
447456
)
448457

458+
def _add_dunder_hash(self, is_hashable: bool) -> None:
459+
if is_hashable:
460+
add_method_to_class(
461+
self._api,
462+
self._cls,
463+
"__hash__",
464+
args=[],
465+
return_type=self._api.named_type("builtins.int"),
466+
)
467+
else:
468+
# Python sets `__hash__ = None` otherwise, do the same.
469+
parent_method = self._cls.info.get_method("__hash__")
470+
if parent_method is not None:
471+
# If we inherited `__hash__`, ensure it isn't overridden
472+
self._api.fail(
473+
"Incompatible override of '__hash__': dataclasses without"
474+
" 'frozen' or 'unsafe_hash' have '__hash__' set to None",
475+
self._cls,
476+
)
477+
add_attribute_to_class(
478+
self._api, self._cls, "__hash__", typ=NoneType(), is_classvar=True
479+
)
480+
449481
def add_slots(
450482
self, info: TypeInfo, attributes: list[DataclassAttribute], *, correct_version: bool
451483
) -> None:

test-data/unit/check-dataclasses.test

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2610,3 +2610,29 @@ class B2(B1): # E: A NamedTuple cannot be a dataclass
26102610
pass
26112611

26122612
[builtins fixtures/tuple.pyi]
2613+
2614+
[case testDataclassHash]
2615+
from abc import ABC, abstractmethod
2616+
import dataclasses
2617+
2618+
class Hashable(ABC):
2619+
@abstractmethod
2620+
def __hash__(self) -> int:
2621+
...
2622+
2623+
@dataclasses.dataclass(frozen=True)
2624+
class Good(Hashable):
2625+
a: int
2626+
2627+
@dataclasses.dataclass(unsafe_hash=True)
2628+
class AlsoGood(Hashable):
2629+
a: int
2630+
2631+
@dataclasses.dataclass()
2632+
class Bad(Hashable): # E: Incompatible override of '__hash__': dataclasses without 'frozen' or 'unsafe_hash' have '__hash__' set to None
2633+
a: int
2634+
2635+
Good(4)
2636+
AlsoGood(4)
2637+
Bad(4)
2638+
[builtins fixtures/tuple.pyi]

0 commit comments

Comments
 (0)