Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: do not overwrite declared __hash__ in subclasses of a model #2423

Merged
merged 1 commit into from Feb 27, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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