From c701383366cfb46d730d152838b790eee7dca92a Mon Sep 17 00:00:00 2001 From: zilto Date: Mon, 25 Nov 2024 11:06:18 -0500 Subject: [PATCH] fingerprinting base case handles empty __dict__ --- hamilton/caching/fingerprinting.py | 6 +++++- tests/caching/test_fingerprinting.py | 18 +++++++++++++++--- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/hamilton/caching/fingerprinting.py b/hamilton/caching/fingerprinting.py index 1e1ee0ae0..ee5ea96a9 100644 --- a/hamilton/caching/fingerprinting.py +++ b/hamilton/caching/fingerprinting.py @@ -71,7 +71,11 @@ def hash_value(obj, *args, depth=0, **kwargs) -> str: if depth > MAX_DEPTH: return UNHASHABLE - if hasattr(obj, "__dict__"): + # __dict__ attribute contains the instance attributes of the object. + # this is typically sufficient to define the object and its behavior, so it's a good target + # for a hash in the default case. + # Objects that return an empty dict should be skipped (very odd behavior, happens with pandas type) + if getattr(obj, "__dict__", {}) != {}: return hash_value(obj.__dict__, depth=depth + 1) # check if the object comes from a module part of the standard library diff --git a/tests/caching/test_fingerprinting.py b/tests/caching/test_fingerprinting.py index b8bd17111..efc1abc14 100644 --- a/tests/caching/test_fingerprinting.py +++ b/tests/caching/test_fingerprinting.py @@ -23,12 +23,24 @@ def test_hash_no_dict_attribute(): class Foo: __slots__ = () - def __init__(self): - pass + obj = Foo() + assert not hasattr(obj, "__dict__") + + fingerprint = fingerprinting.hash_value(obj) + + assert fingerprint == fingerprinting.UNHASHABLE + + +def test_empty_dict_attr_is_unhashable(): + """Classes with an empty __dict__ can't be hashed during the base case.""" + + class Foo: ... # noqa: E701 obj = Foo() + assert obj.__dict__ == {} + fingerprint = fingerprinting.hash_value(obj) - assert not hasattr(obj, "__dict__") + assert fingerprint == fingerprinting.UNHASHABLE