mirror of https://github.com/astral-sh/ruff
[ty] Dataclasses: `__hash__` semantics and `unsafe_hash` (#21470)
## Summary Implement the semantics of `__hash__` for dataclasses and add support for `unsafe_hash` ## Test Plan New Markdown tests.
This commit is contained in:
parent
dbd72480a9
commit
f5fb5c388a
|
|
@ -362,9 +362,71 @@ class AlreadyHasCustomDunderLt:
|
||||||
return False
|
return False
|
||||||
```
|
```
|
||||||
|
|
||||||
### `unsafe_hash`
|
### `__hash__` and `unsafe_hash`
|
||||||
|
|
||||||
To do
|
If `eq` and `frozen` are both `True`, a `__hash__` method is generated by default:
|
||||||
|
|
||||||
|
```py
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
@dataclass(eq=True, frozen=True)
|
||||||
|
class WithHash:
|
||||||
|
x: int
|
||||||
|
|
||||||
|
reveal_type(WithHash.__hash__) # revealed: (self: WithHash) -> int
|
||||||
|
```
|
||||||
|
|
||||||
|
If `eq` is set to `True` and `frozen` is set to `False`, `__hash__` will be set to `None`, to mark
|
||||||
|
is unhashable (because it is mutable):
|
||||||
|
|
||||||
|
```py
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
@dataclass(eq=True, frozen=False)
|
||||||
|
class WithoutHash:
|
||||||
|
x: int
|
||||||
|
|
||||||
|
reveal_type(WithoutHash.__hash__) # revealed: None
|
||||||
|
```
|
||||||
|
|
||||||
|
If `eq` is set to `False`, `__hash__` will inherit from the parent class (which could be `object`).
|
||||||
|
Note that we see a revealed type of `def …` here, because `__hash__` refers to an actual function,
|
||||||
|
not a synthetic method like in the first example.
|
||||||
|
|
||||||
|
```py
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
@dataclass(eq=False, frozen=False)
|
||||||
|
class InheritHash:
|
||||||
|
x: int
|
||||||
|
|
||||||
|
reveal_type(InheritHash.__hash__) # revealed: def __hash__(self) -> int
|
||||||
|
|
||||||
|
class Base:
|
||||||
|
# Type the `self` parameter as `Any` to distinguish it from `object.__hash__`
|
||||||
|
def __hash__(self: Any) -> int:
|
||||||
|
return 42
|
||||||
|
|
||||||
|
@dataclass(eq=False, frozen=False)
|
||||||
|
class InheritHash(Base):
|
||||||
|
x: int
|
||||||
|
|
||||||
|
reveal_type(InheritHash.__hash__) # revealed: def __hash__(self: Any) -> int
|
||||||
|
```
|
||||||
|
|
||||||
|
If `unsafe_hash` is set to `True`, a `__hash__` method will be generated even if the dataclass is
|
||||||
|
mutable:
|
||||||
|
|
||||||
|
```py
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
@dataclass(eq=True, frozen=False, unsafe_hash=True)
|
||||||
|
class WithUnsafeHash:
|
||||||
|
x: int
|
||||||
|
|
||||||
|
reveal_type(WithUnsafeHash.__hash__) # revealed: (self: WithUnsafeHash) -> int
|
||||||
|
```
|
||||||
|
|
||||||
### `frozen`
|
### `frozen`
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2400,6 +2400,28 @@ impl<'db> ClassLiteral<'db> {
|
||||||
|
|
||||||
Some(CallableType::function_like(db, signature))
|
Some(CallableType::function_like(db, signature))
|
||||||
}
|
}
|
||||||
|
(CodeGeneratorKind::DataclassLike(_), "__hash__") => {
|
||||||
|
let unsafe_hash = has_dataclass_param(DataclassFlags::UNSAFE_HASH);
|
||||||
|
let frozen = has_dataclass_param(DataclassFlags::FROZEN);
|
||||||
|
let eq = has_dataclass_param(DataclassFlags::EQ);
|
||||||
|
|
||||||
|
if unsafe_hash || (frozen && eq) {
|
||||||
|
let signature = Signature::new(
|
||||||
|
Parameters::new([Parameter::positional_or_keyword(Name::new_static(
|
||||||
|
"self",
|
||||||
|
))
|
||||||
|
.with_annotated_type(instance_ty)]),
|
||||||
|
Some(KnownClass::Int.to_instance(db)),
|
||||||
|
);
|
||||||
|
|
||||||
|
Some(CallableType::function_like(db, signature))
|
||||||
|
} else if eq && !frozen {
|
||||||
|
Some(Type::none(db))
|
||||||
|
} else {
|
||||||
|
// No `__hash__` is generated, fall back to `object.__hash__`
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
(CodeGeneratorKind::DataclassLike(_), "__match_args__")
|
(CodeGeneratorKind::DataclassLike(_), "__match_args__")
|
||||||
if Program::get(db).python_version(db) >= PythonVersion::PY310 =>
|
if Program::get(db).python_version(db) >= PythonVersion::PY310 =>
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue