Harden to_hashable against cycles

This commit is contained in:
xmarre 2026-03-14 09:46:27 +01:00
parent 4d9516b909
commit 880b51ac4f
2 changed files with 62 additions and 7 deletions

View File

@ -122,8 +122,14 @@ def _sanitize_signature_input(obj, depth=0, max_depth=32):
# the foreign object and risk crashing on custom container semantics.
return Unhashable()
def to_hashable(obj):
def to_hashable(obj, depth=0, max_depth=32, seen=None):
"""Convert prompt-safe built-in values into a stable hashable representation."""
if depth >= max_depth:
return Unhashable()
if seen is None:
seen = set()
# Restrict recursion to plain built-in containers. Some custom nodes insert
# runtime objects into prompt inputs for dynamic graph paths; walking those
# objects as generic Mappings / Sequences is unsafe and can destabilize the
@ -131,16 +137,32 @@ def to_hashable(obj):
obj_type = type(obj)
if obj_type in (int, float, str, bool, bytes, type(None)):
return obj
elif obj_type is dict:
return ("dict", frozenset((to_hashable(k), to_hashable(v)) for k, v in obj.items()))
if obj_type in (dict, list, tuple, set, frozenset):
obj_id = id(obj)
if obj_id in seen:
return Unhashable()
seen = seen | {obj_id}
if obj_type is dict:
return (
"dict",
frozenset(
(
to_hashable(k, depth + 1, max_depth, seen),
to_hashable(v, depth + 1, max_depth, seen),
)
for k, v in obj.items()
),
)
elif obj_type is list:
return ("list", tuple(to_hashable(i) for i in obj))
return ("list", tuple(to_hashable(i, depth + 1, max_depth, seen) for i in obj))
elif obj_type is tuple:
return ("tuple", tuple(to_hashable(i) for i in obj))
return ("tuple", tuple(to_hashable(i, depth + 1, max_depth, seen) for i in obj))
elif obj_type is set:
return ("set", frozenset(to_hashable(i) for i in obj))
return ("set", frozenset(to_hashable(i, depth + 1, max_depth, seen) for i in obj))
elif obj_type is frozenset:
return ("frozenset", frozenset(to_hashable(i) for i in obj))
return ("frozenset", frozenset(to_hashable(i, depth + 1, max_depth, seen) for i in obj))
else:
return Unhashable()

View File

@ -0,0 +1,33 @@
from comfy_execution.caching import Unhashable, to_hashable
def test_to_hashable_returns_unhashable_for_cyclic_builtin_containers():
cyclic_list = []
cyclic_list.append(cyclic_list)
result = to_hashable(cyclic_list)
assert result[0] == "list"
assert len(result[1]) == 1
assert isinstance(result[1][0], Unhashable)
def test_to_hashable_returns_unhashable_when_max_depth_is_reached():
nested = current = []
for _ in range(32):
next_item = []
current.append(next_item)
current = next_item
result = to_hashable(nested)
depth = 0
current = result
while isinstance(current, tuple):
assert current[0] == "list"
assert len(current[1]) == 1
current = current[1][0]
depth += 1
assert depth == 32
assert isinstance(current, Unhashable)