From ce05e377a805199e3513ffcaa1fa37816582304d Mon Sep 17 00:00:00 2001 From: xmarre Date: Mon, 16 Mar 2026 16:48:42 +0100 Subject: [PATCH] Stop canonicalizing dict keys --- comfy_execution/caching.py | 20 +++++++++++++---- tests-unit/execution_test/caching_test.py | 6 +++--- tests/execution/test_caching.py | 26 ++++++++++++++++++++--- 3 files changed, 42 insertions(+), 10 deletions(-) diff --git a/comfy_execution/caching.py b/comfy_execution/caching.py index dc785d21f..6610150aa 100644 --- a/comfy_execution/caching.py +++ b/comfy_execution/caching.py @@ -467,9 +467,15 @@ class CacheKeySetInputSignature(CacheKeySet): """Build the full cache signature for a node and its ordered ancestors.""" signature = [] ancestors, order_mapping = self.get_ordered_ancestry(dynprompt, node_id) - signature.append(await self.get_immediate_node_signature(dynprompt, node_id, order_mapping)) + immediate = await self.get_immediate_node_signature(dynprompt, node_id, order_mapping) + if type(immediate) is Unhashable: + return immediate + signature.append(immediate) for ancestor_id in ancestors: - signature.append(await self.get_immediate_node_signature(dynprompt, ancestor_id, order_mapping)) + immediate = await self.get_immediate_node_signature(dynprompt, ancestor_id, order_mapping) + if type(immediate) is Unhashable: + return immediate + signature.append(immediate) return tuple(signature) async def get_immediate_node_signature(self, dynprompt, node_id, ancestor_order_mapping): @@ -485,7 +491,10 @@ class CacheKeySetInputSignature(CacheKeySet): node = dynprompt.get_node(node_id) class_type = node["class_type"] class_def = nodes.NODE_CLASS_MAPPINGS[class_type] - signature = [class_type, _shallow_is_changed_signature(await self.is_changed_cache.get(node_id))] + is_changed_signature = _shallow_is_changed_signature(await self.is_changed_cache.get(node_id)) + if type(is_changed_signature) is Unhashable: + return is_changed_signature + signature = [class_type, is_changed_signature] if self.include_node_id_in_input() or (hasattr(class_def, "NOT_IDEMPOTENT") and class_def.NOT_IDEMPOTENT) or include_unique_id_in_input(class_type): signature.append(node_id) inputs = node["inputs"] @@ -495,7 +504,10 @@ class CacheKeySetInputSignature(CacheKeySet): ancestor_index = ancestor_order_mapping[ancestor_id] signature.append((key,("ANCESTOR", ancestor_index, ancestor_socket))) else: - signature.append((key, to_hashable(inputs[key]))) + value_signature = to_hashable(inputs[key]) + if type(value_signature) is Unhashable: + return value_signature + signature.append((key, value_signature)) return tuple(signature) # This function returns a list of all ancestors of the given node. The order of the list is diff --git a/tests-unit/execution_test/caching_test.py b/tests-unit/execution_test/caching_test.py index 2c63f68c8..6d9d38bce 100644 --- a/tests-unit/execution_test/caching_test.py +++ b/tests-unit/execution_test/caching_test.py @@ -386,8 +386,8 @@ def test_shallow_is_changed_signature_fails_closed_for_opaque_payload(caching_mo assert isinstance(sanitized, caching.Unhashable) -def test_get_immediate_node_signature_marks_recursive_is_changed_unhashable(caching_module, monkeypatch): - """Recursive `is_changed` payloads should be cut off before signature canonicalization.""" +def test_get_immediate_node_signature_fails_closed_for_unhashable_is_changed(caching_module, monkeypatch): + """Recursive `is_changed` payloads should fail the full fragment closed.""" caching, nodes_module = caching_module monkeypatch.setitem(nodes_module.NODE_CLASS_MAPPINGS, "UnitTestNode", _DummyNode) @@ -409,4 +409,4 @@ def test_get_immediate_node_signature_marks_recursive_is_changed_unhashable(cach signature = asyncio.run(key_set.get_immediate_node_signature(dynprompt, "node", {})) - assert isinstance(signature[1], caching.Unhashable) + assert isinstance(signature, caching.Unhashable) diff --git a/tests/execution/test_caching.py b/tests/execution/test_caching.py index a92a8a416..569bf5bd8 100644 --- a/tests/execution/test_caching.py +++ b/tests/execution/test_caching.py @@ -84,9 +84,29 @@ def test_get_immediate_node_signature_fails_closed_for_opaque_non_link_input(mon keyset = caching.CacheKeySetInputSignature(dynprompt, [], _StubIsChangedCache()) signature = asyncio.run(keyset.get_immediate_node_signature(dynprompt, "1", {})) - assert signature[:2] == ("TestCacheNode", None) - assert signature[2][0] == "value" - assert type(signature[2][1]) is caching.Unhashable + assert isinstance(signature, caching.Unhashable) + + +def test_get_node_signature_propagates_unhashable_immediate_fragment(monkeypatch): + class OpaqueRuntimeValue: + pass + + dynprompt = _StubDynPrompt( + { + "1": { + "class_type": "TestCacheNode", + "inputs": {"value": OpaqueRuntimeValue()}, + } + } + ) + + monkeypatch.setitem(caching.nodes.NODE_CLASS_MAPPINGS, "TestCacheNode", _StubNode) + monkeypatch.setattr(caching, "NODE_CLASS_CONTAINS_UNIQUE_ID", {}) + + keyset = caching.CacheKeySetInputSignature(dynprompt, [], _StubIsChangedCache()) + signature = asyncio.run(keyset.get_node_signature(dynprompt, "1")) + + assert isinstance(signature, caching.Unhashable) def test_get_node_signature_never_visits_raw_non_link_input(monkeypatch):