From 38acdafa555a746cb66b2d1adecc1eaff8e32756 Mon Sep 17 00:00:00 2001 From: Nicolas Predella Date: Fri, 9 Oct 2026 17:03:55 +0300 Subject: [PATCH] better warnings --- infra/blender/blender_slice_nodes_generic.py | 78 ++++++++++++++++++-- tests/test_debug.py | 33 +++++++++ 2 files changed, 105 insertions(+), 6 deletions(-) diff --git a/infra/blender/blender_slice_nodes_generic.py b/infra/blender/blender_slice_nodes_generic.py index 1d998ca..098073b 100644 --- a/infra/blender/blender_slice_nodes_generic.py +++ b/infra/blender/blender_slice_nodes_generic.py @@ -126,13 +126,13 @@ class SliceNode(Node, AutoBlIdName): from ..debug import dump_node_execution return dump_node_execution(self, socket_name, output_val=None, duration_s=0.0) - def resolve(self, fieldname) -> Any: + def resolve(self, fieldname: str) -> Any: cache = getattr(self, "_current_cache", None) visited = getattr(self, "_current_visited", None) inputs = getattr(self, "inputs", None) if inputs is None: - return None - return resolve_input(inputs.get(fieldname), cache=cache, visited=visited) + return resolve_input(None, cache=cache, visited=visited, fieldname=fieldname, node=self) + return resolve_input(inputs.get(fieldname), cache=cache, visited=visited, fieldname=fieldname, node=self) def add_output(self, bl_idname: str, name: str = "", identifier: str = "", hide: bool = False, display_shape: str = "", description: str = ""): bl_idname = NATIVE_SOCKET_MAP.get(bl_idname, bl_idname) @@ -175,12 +175,12 @@ class SliceNode(Node, AutoBlIdName): } self._update_multi_groups() - def resolve_multi_input(self, fieldname) -> Any: + def resolve_multi_input(self, fieldname: str) -> Any: cache = getattr(self, "_current_cache", None) visited = getattr(self, "_current_visited", None) # Search by name or identifier prefix for multi-groups inputs = [i for i in list(self.inputs) if i.name == fieldname or i.identifier.startswith(f"{fieldname}_ms_in_")] - return [resolve_input(r, cache=cache, visited=visited) for r in inputs] + return [resolve_input(r, cache=cache, visited=visited, fieldname=fieldname, node=self) for r in inputs] def _update_multi_groups(self): if not self.get('multi_groups'): return @@ -582,14 +582,80 @@ def _follow_reroute(current: NodeSocket) -> NodeSocket: return current +def _format_node_description(node: Any) -> str: + if node is None: + return "" + name = getattr(node, "name", "") + bl_label = getattr(node, "bl_label", "") + bl_idname = getattr(node, "bl_idname", "") or type(node).__name__ + + label = bl_label or bl_idname + if name and label and name != label: + return f"'{name}' ({label})" + elif name: + return f"'{name}'" + elif label: + return f"'{label}'" + return str(node) + + +def _extract_caller_context() -> tuple[Any, str | None]: + try: + import sys + for depth in range(2, 6): + try: + frame = sys._getframe(depth) + except ValueError: + break + caller_self = frame.f_locals.get("self") + caller_node = None + if caller_self is not None and ( + isinstance(caller_self, Node) or + hasattr(caller_self, "bl_idname") or + hasattr(caller_self, "inputs") + ): + caller_node = caller_self + + caller_fieldname = ( + frame.f_locals.get("fieldname") or + frame.f_locals.get("socket_name") or + frame.f_locals.get("name") + ) + if not isinstance(caller_fieldname, str): + caller_fieldname = None + + if caller_node is not None or caller_fieldname is not None: + return caller_node, caller_fieldname + except Exception: + pass + return None, None + + def resolve_input( socket: SliceNodeSocket | None, *, cache: _EvalCache | None = None, visited: _VisitedSet | None = None, + fieldname: str | None = None, + node: Any = None, ): if socket is None: - raise Exception(f"The queried socket is non-existant") + if node is None or fieldname is None: + caller_node, caller_fieldname = _extract_caller_context() + if node is None: + node = caller_node + if fieldname is None: + fieldname = caller_fieldname + + node_str = _format_node_description(node) + if fieldname and node_str: + raise NodeException(f"The queried socket '{fieldname}' on node {node_str} is non-existent") + elif fieldname: + raise NodeException(f"The queried socket '{fieldname}' is non-existent") + elif node_str: + raise NodeException(f"The queried socket on node {node_str} is non-existent") + else: + raise NodeException("The queried socket is non-existent") assert socket.node diff --git a/tests/test_debug.py b/tests/test_debug.py index f86bb38..68a5469 100644 --- a/tests/test_debug.py +++ b/tests/test_debug.py @@ -321,7 +321,40 @@ class TestNodeDebugSystem(unittest.TestCase): dump = node.dump_inputs("Val") self.assertEqual(dump["inputs"]["Val"]["value_summary"], 42.0) + def test_non_existent_socket_error_message_with_resolve(self): + from NodeSlicer.infra.blender.blender_slice_nodes_generic import NodeException + + class TestNode(SliceNode): + bl_idname = "SLICENODE_Test" + bl_label = "Test Node" + + def __init__(self): + self.name = "My Test Node" + self.inputs = DummySocketCollection([ + DummyNodeSocket("Valid", "Valid", "NodeSocketFloat", value=1.0) + ], node=self) + + node = TestNode() + with self.assertRaises(NodeException) as ctx: + node.resolve("NonExistentSocket") + + err_msg = str(ctx.exception) + self.assertIn("NonExistentSocket", err_msg) + self.assertIn("My Test Node", err_msg) + self.assertIn("Test Node", err_msg) + self.assertIn("non-existent", err_msg) + + def test_non_existent_socket_error_message_isolated(self): + from NodeSlicer.infra.blender.blender_slice_nodes_generic import NodeException, resolve_input + + with self.assertRaises(NodeException) as ctx: + resolve_input(None) + + err_msg = str(ctx.exception) + self.assertIn("non-existent", err_msg) + if __name__ == '__main__': unittest.main() +