From 48324403bb92164faa3624e579295d5db93930d9 Mon Sep 17 00:00:00 2001 From: Andras Schmelczer Date: Tue, 27 Oct 2026 20:09:29 +0000 Subject: [PATCH] Hash nested model fields by immutable value --- great_ai/views/hashable_base_model.py | 4 +++- tests/test_output_hashing.py | 26 ++++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) create mode 100644 tests/test_output_hashing.py diff --git a/great_ai/views/hashable_base_model.py b/great_ai/views/hashable_base_model.py index 9680321..514d21d 100644 --- a/great_ai/views/hashable_base_model.py +++ b/great_ai/views/hashable_base_model.py @@ -3,4 +3,6 @@ from pydantic import BaseModel class HashableBaseModel(BaseModel): def __hash__(self) -> int: - return hash((type(self),) + tuple(self.__dict__.values())) + from ..helper.frozen_call import make_hashable + + return hash(make_hashable(self)) diff --git a/tests/test_output_hashing.py b/tests/test_output_hashing.py new file mode 100644 index 0000000..8ac0a47 --- /dev/null +++ b/tests/test_output_hashing.py @@ -0,0 +1,26 @@ +from great_ai import ClassificationOutput, MultiLabelClassificationOutput, Trace + + +def test_output_models_hash_nested_mutable_fields_by_value() -> None: + first = MultiLabelClassificationOutput( + labels=[ + ClassificationOutput( + label="a", confidence=0.5, explanation={"tokens": ["a", "b"]} + ) + ] + ) + second = first.model_copy(deep=True) + assert hash(first) == hash(second) + assert len({first, second}) == 1 + + +def test_trace_hash_supports_its_dictionary_and_list_fields() -> None: + trace: Trace = Trace( + trace_id="id", + created="2026-01-01", + original_execution_time_ms=0, + logged_values={"x": [1, 2]}, + models=[], + tags=["online"], + ) + assert hash(trace) == hash(trace.model_copy(deep=True))