Skip to content

Commit

Permalink
fix: do not overwrite declared hash in subclasses of a model (#2423)
Browse files Browse the repository at this point in the history
  • Loading branch information
PrettyWood committed Feb 27, 2021
1 parent a8d50ae commit f9fe4aa
Show file tree
Hide file tree
Showing 3 changed files with 28 additions and 1 deletion.
1 change: 1 addition & 0 deletions changes/2422-PrettyWood.md
@@ -0,0 +1 @@
do not overwrite declared `__hash__` in subclasses of a model
7 changes: 6 additions & 1 deletion pydantic/main.py
Expand Up @@ -231,6 +231,7 @@ def __new__(mcs, name, bases, namespace, **kwargs): # noqa C901
slots: SetStr = namespace.get('__slots__', ())
slots = {slots} if isinstance(slots, str) else set(slots)
class_vars: SetStr = set()
hash_func: Optional[Callable[[Any], int]] = None

for base in reversed(bases):
if _is_base_model_class_defined and issubclass(base, BaseModel) and base != BaseModel:
Expand All @@ -241,6 +242,7 @@ def __new__(mcs, name, bases, namespace, **kwargs): # noqa C901
post_root_validators += base.__post_root_validators__
private_attributes.update(base.__private_attributes__)
class_vars.update(base.__class_vars__)
hash_func = base.__hash__

config_kwargs = {key: kwargs.pop(key) for key in kwargs.keys() & BaseConfig.__dict__.keys()}
config_from_namespace = namespace.get('Config')
Expand Down Expand Up @@ -332,6 +334,9 @@ def is_untouched(v: Any) -> bool:
json_encoder = pydantic_encoder
pre_rv_new, post_rv_new = extract_root_validators(namespace)

if hash_func is None:
hash_func = generate_hash_function(config.frozen)

exclude_from_namespace = fields | private_attributes.keys() | {'__slots__'}
new_namespace = {
'__config__': config,
Expand All @@ -344,7 +349,7 @@ def is_untouched(v: Any) -> bool:
'__custom_root_type__': _custom_root_type,
'__private_attributes__': private_attributes,
'__slots__': slots | private_attributes.keys(),
'__hash__': generate_hash_function(config.frozen),
'__hash__': hash_func,
'__class_vars__': class_vars,
**{n: v for n, v in namespace.items() if n not in exclude_from_namespace},
}
Expand Down
21 changes: 21 additions & 0 deletions tests/test_main.py
Expand Up @@ -395,6 +395,27 @@ class TestModel(BaseModel):
assert "unhashable type: 'TestModel'" in exc_info.value.args[0]


def test_with_declared_hash():
class Foo(BaseModel):
x: int

def __hash__(self):
return self.x ** 2

class Bar(Foo):
y: int

def __hash__(self):
return self.y ** 3

class Buz(Bar):
z: int

assert hash(Foo(x=2)) == 4
assert hash(Bar(x=2, y=3)) == 27
assert hash(Buz(x=2, y=3, z=4)) == 27


def test_frozen_with_hashable_fields_are_hashable():
class TestModel(BaseModel):
a: int = 10
Expand Down

0 comments on commit f9fe4aa

Please sign in to comment.