Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 0d750c7

Browse files
committed
[ty] Dataclasses: __hash__ semantics and unsafe_hash
1 parent 29acc1e commit 0d750c7

2 files changed

Lines changed: 86 additions & 2 deletions

File tree

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

Lines changed: 64 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -362,9 +362,71 @@ class AlreadyHasCustomDunderLt:
362362
return False
363363
```
364364

365-
### `unsafe_hash`
365+
### `__hash__` and `unsafe_hash`
366366

367-
To do
367+
If `eq` and `frozen` are both `True`, a `__hash__` method is generated by default:
368+
369+
```py
370+
from dataclasses import dataclass
371+
372+
@dataclass(eq=True, frozen=True)
373+
class WithHash:
374+
x: int
375+
376+
reveal_type(WithHash.__hash__) # revealed: (self: WithHash) -> int
377+
```
378+
379+
If `eq` is set to `True` and `frozen` is set to `False`, `__hash__` will be set to `None`, to mark
380+
is unhashable (because it is mutable):
381+
382+
```py
383+
from dataclasses import dataclass
384+
385+
@dataclass(eq=True, frozen=False)
386+
class WithoutHash:
387+
x: int
388+
389+
reveal_type(WithoutHash.__hash__) # revealed: None
390+
```
391+
392+
If `eq` is set to `False`, `__hash__` will inherit from the parent class (which could be `object`).
393+
Note that we see a revealed type of `def …` here, because `__hash__` refers to an actual function,
394+
not a synthetic method like in the first example.
395+
396+
```py
397+
from dataclasses import dataclass
398+
from typing import Any
399+
400+
@dataclass(eq=False, frozen=False)
401+
class InheritHash:
402+
x: int
403+
404+
reveal_type(InheritHash.__hash__) # revealed: def __hash__(self) -> int
405+
406+
class Base:
407+
# Type the `self` parameter as `Any` to distinguish it from `object.__hash__`
408+
def __hash__(self: Any) -> int:
409+
return 42
410+
411+
@dataclass(eq=False, frozen=False)
412+
class InheritHash(Base):
413+
x: int
414+
415+
reveal_type(InheritHash.__hash__) # revealed: def __hash__(self: Any) -> int
416+
```
417+
418+
If `unsafe_hash` is set to `True`, a `__hash__` method will be generated even if the dataclass is
419+
mutable:
420+
421+
```py
422+
from dataclasses import dataclass
423+
424+
@dataclass(eq=True, frozen=False, unsafe_hash=True)
425+
class WithUnsafeHash:
426+
x: int
427+
428+
reveal_type(WithUnsafeHash.__hash__) # revealed: (self: WithUnsafeHash) -> int
429+
```
368430

369431
### `frozen`
370432

crates/ty_python_semantic/src/types/class.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2402,6 +2402,28 @@ impl<'db> ClassLiteral<'db> {
24022402

24032403
Some(CallableType::function_like(db, signature))
24042404
}
2405+
(CodeGeneratorKind::DataclassLike(_), "__hash__") => {
2406+
let unsafe_hash = has_dataclass_param(DataclassFlags::UNSAFE_HASH);
2407+
let frozen = has_dataclass_param(DataclassFlags::FROZEN);
2408+
let eq = has_dataclass_param(DataclassFlags::EQ);
2409+
2410+
if unsafe_hash || (frozen && eq) {
2411+
let signature = Signature::new(
2412+
Parameters::new([Parameter::positional_or_keyword(Name::new_static(
2413+
"self",
2414+
))
2415+
.with_annotated_type(instance_ty)]),
2416+
Some(KnownClass::Int.to_instance(db)),
2417+
);
2418+
2419+
Some(CallableType::function_like(db, signature))
2420+
} else if eq && !frozen {
2421+
Some(Type::none(db))
2422+
} else {
2423+
// No `__hash__` is generated, fall back to `object.__hash__`
2424+
None
2425+
}
2426+
}
24052427
(CodeGeneratorKind::DataclassLike(_), "__match_args__")
24062428
if Program::get(db).python_version(db) >= PythonVersion::PY310 =>
24072429
{

0 commit comments

Comments
 (0)