Skip to content

Commit

Permalink
Fix inheritance of hash function for frozen models (#6789)
Browse files Browse the repository at this point in the history
  • Loading branch information
dmontagu committed Jul 21, 2023
1 parent 9fb218b commit 3279756
Show file tree
Hide file tree
Showing 2 changed files with 47 additions and 6 deletions.
28 changes: 22 additions & 6 deletions pydantic/_internal/_model_construction.py
Expand Up @@ -116,12 +116,8 @@ def wrapped_model_post_init(self: BaseModel, __context: Any) -> None:
namespace['__class_vars__'] = class_vars
namespace['__private_attributes__'] = {**base_private_attributes, **private_attributes}

if '__hash__' not in namespace and config_wrapper.frozen:

def hash_func(self: Any) -> int:
return hash(self.__class__) + hash(tuple(self.__dict__.values()))

namespace['__hash__'] = hash_func
if config_wrapper.frozen:
set_default_hash_func(namespace, bases)

cls: type[BaseModel] = super().__new__(mcs, cls_name, bases, namespace, **kwargs) # type: ignore

Expand Down Expand Up @@ -359,6 +355,26 @@ def inspect_namespace( # noqa C901
return private_attributes


def set_default_hash_func(namespace: dict[str, Any], bases: tuple[type[Any], ...]) -> None:
if '__hash__' in namespace:
return

base_hash_func = None
for base in bases:
base_hash_func = getattr(base, '__hash__', PydanticUndefined)
if base_hash_func is not PydanticUndefined:
break

if base_hash_func is None:
# This will be the case for `BaseModel` since it defines `__eq__` but not `__hash__`.
# In this case, we generate a standard hash function, generally for use with frozen models.

def hash_func(self: Any) -> int:
return hash(self.__class__) + hash(tuple(self.__dict__.values()))

namespace['__hash__'] = hash_func


def set_model_fields(
cls: type[BaseModel], bases: tuple[type[Any], ...], config_wrapper: ConfigWrapper, types_namespace: dict[str, Any]
) -> None:
Expand Down
25 changes: 25 additions & 0 deletions tests/test_main.py
Expand Up @@ -574,6 +574,31 @@ class TestModel(BaseModel):
assert hash(m) != hash(m4)


def test_hash_method_is_inherited_for_frozen_models():
from functools import lru_cache

class MyBaseModel(BaseModel):
"""A base model with sensible configurations."""

model_config = ConfigDict(frozen=True)

def __hash__(self):
return hash(id(self))

class MySubClass(MyBaseModel):
x: Dict[str, int]

@lru_cache(maxsize=None)
def cached_method(self):
return len(self.x)

my_instance = MySubClass(x={'a': 1, 'b': 2})
assert my_instance.cached_method() == 2

object.__setattr__(my_instance, 'x', {}) # can't change the "normal" way due to frozen
assert my_instance.cached_method() == 2


@pytest.fixture(name='ValidateAssignmentModel', scope='session')
def validate_assignment_fixture():
class ValidateAssignmentModel(BaseModel):
Expand Down

0 comments on commit 3279756

Please sign in to comment.