PyNode updated before fully initialized

I’m trying to define a custom node tree and some nodes. I’m getting stuck pretty early on. In the attached code, I’m trying to make a node that concatenates two input strings.

Assumption: I’m new to PyNodes, and based on my poking around the docs and Blender.SE feedback, I’ve come to the conclusion that the core PyNode system doesn’t really pass information through sockets for you; it’s up to each node to traverse backwards through the socket links, sucking data forward as required.

In the attached script, line 39 triggers a call to update(). Since my update method reaches back through all of the input connections (as per my above assumption) we have a problem - the node is only partially initialized, so not all of the input sockets exist yet!

It seems to me that a system like this ought to be a more automatic. For example, input sockets could be bound to instance attributes, which the system could automatically set. Since this isn’t the case, I’m guessing there’s something substantial in there that I’m misunderstanding.

Could someone explain where my code goes wrong, or where my assumption is wrong?

Thanks,
Andrew


import bpy
from bpy.types import NodeTree, Node, NodeSocket


import pdb


# Derived from the NodeTree base type, similar to Menu, Operator, Panel, etc.
class MyCustomTree(NodeTree):
    # Description string
    '''A custom node tree type that will show up in the node editor header'''
    # Optional identifier string. If not explicitly defined, the python class name is used.
    bl_idname = 'CustomTreeType'
    # Label for nice name display
    bl_label = 'Custom Node Tree'
    # Icon identifier
    bl_icon = 'NODETREE'




# Mix-in class for all custom nodes in this tree type.
# Defines a poll function to enable instantiation.
class MyCustomTreeNode:
    @classmethod
    def poll(cls, ntree):
        return ntree.bl_idname == 'CustomTreeType'


class MyNode(Node, MyCustomTreeNode):
    # Description string
    '''A node that add a prefix or a suffix to a string'''
    # Optional identifier string. If not explicitly defined, the python class name is used.
    bl_idname = 'MyNodeType'
    # Label for nice name display
    bl_label = 'PreSufFix Node'
    # Icon identifier
    bl_icon = 'SOUND'


    in0_val = bpy.props.StringProperty()
    in1_val = bpy.props.StringProperty()


    def init(self, context):
        self.inputs.new('NodeSocketString', "in0")
        self.inputs.new('NodeSocketString', "in1")


        self.outputs.new('NodeSocketString', "out")


        self.inputs['in0'].default_value  = 'foo'
        self.inputs['in1'].default_value = 'bar'
        self.outputs['out'].default_value = 'baz'


        print("INIT DONE")


    def draw_buttons(self, context, layout):
        layout.label(self.outputs['out'].default_value)


    def draw_label(self):
        return "I am a custom text manipulator node"


    def update(self):
        print("Updating node: ", self.name)
        # Update the input sockets
        if self.inputs['in0'].is_linked and self.inputs['in0'].links[0].is_valid:
            self.in0_val = self.inputs['in0'].links[0].from_socket.default_value
        else:
            self.in0_val = 'foo'


        # Update the input sockets
        if self.inputs['in1'].is_linked and self.inputs['in1'].links[0].is_valid:
            self.in1_val = self.inputs['in1'].links[0].from_socket.default_value
        else:
            self.in1_val = 'bar'


        # Update the output socket
        self.outputs['out'].default_value = self.in0_val + self.in1_val
        print(self.outputs['out'].default_value)




### Node Categories ###
# Node categories are a python system for automatically
# extending the Add menu, toolbar panels and search operator.
# For more examples see release/scripts/startup/nodeitems_builtins.py


import nodeitems_utils
from nodeitems_utils import NodeCategory, NodeItem




# our own base class with an appropriate poll function,
# so the categories only show up in our own tree type
class MyNodeCategory(NodeCategory):
    @classmethod
    def poll(cls, context):
        return context.space_data.tree_type == 'CustomTreeType'


# all categories in a list
node_categories = [
    # identifier, label, items list
    MyNodeCategory("MYNODES", "Some Nodes", items=[
        NodeItem("MyNodeType"),
        ]),
    ]




def register():
    bpy.utils.register_class(MyCustomTree)
    bpy.utils.register_class(MyNode)


    nodeitems_utils.register_node_categories("CUSTOM_NODES", node_categories)




def unregister():
    nodeitems_utils.unregister_node_categories("CUSTOM_NODES")


    bpy.utils.unregister_class(MyCustomTree)
    bpy.utils.unregister_class(MyNode)




if __name__ == "__main__":
    register()

PyNodes is better designed for representation of relationships, rather than actually executing code. It limits your ability to separate function from interface, and creates heavily influences your code.

Update is triggered because you’re updating the node’s sockets(During init)
Though I would separate completely from the bpy API, at the very least you must guard your update function until you have a valid state

from contextlib import contextmanager



@contextmanager
def guard(self):
	self.locked = True
	yield
	self.locked = False




def guarded(func):
	def wrapper(self, *args, **kwargs):
		if getattr(self, "locked", False):
			return
		
		return func.__get__(self, type(self))(*args, **kwargs)
	return wrapper




class MyNode(Node, MyCustomTreeNode):
	# Description string
	'''A node that add a prefix or a suffix to a string'''
	# Optional identifier string. If not explicitly defined, the python class name is used.
	bl_idname = 'MyNodeType'
	# Label for nice name display
	bl_label = 'PreSufFix Node'
	# Icon identifier
	bl_icon = 'SOUND'


	in0_val = bpy.props.StringProperty()
	in1_val = bpy.props.StringProperty()


	def init(self, context):
		with guard(self):
			self.inputs.new('NodeSocketString', "in0")
			self.inputs.new('NodeSocketString', "in1")
			self.outputs.new('NodeSocketString', "out")


			self.inputs['in0'].default_value  = 'foo'
			self.inputs['in1'].default_value = 'bar'
			self.outputs['out'].default_value = 'baz'


		print("INIT DONE")


	def draw_buttons(self, context, layout):
		layout.label(self.outputs['out'].default_value)


	def draw_label(self):
		return "I am a custom text manipulator node"


	@guarded
	def update(self):
#        print("Updating node: ", self.name)
		print(self.inputs, self.outputs)
		# Update the input sockets
		if self.inputs['in0'].is_linked and self.inputs['in0'].links[0].is_valid:
			self.in0_val = self.inputs['in0'].links[0].from_socket.default_value
		else:
			self.in0_val = 'foo'


		# Update the input sockets
		if self.inputs['in1'].is_linked and self.inputs['in1'].links[0].is_valid:
			self.in1_val = self.inputs['in1'].links[0].from_socket.default_value
		else:
			self.in1_val = 'bar'


		# Update the output socket
		self.outputs['out'].default_value = self.in0_val + self.in1_val
		print(self.outputs['out'].default_value)

I’ve seen other remarks similar to your “PyNodes is better designed for representation of relationships” but not sure I yet understand what it’s all about. I guess I can’t imagine how the representation of relationships could be at all useful unless the relationships are driving interesting code under the hood.

Perhaps if I described where I’d like to head with this, you could comment on it’s feasibility? I’d like to wrap most/all of scipy.ndimage in PyNodes (which I think will end up looking a lot like the Blender compositor, extended past 2D operations). On a scale from easy to crazy, how well suited is Blender’s PyNode for such a project?