Initializing object data for operator

I have an operator I’m using to export objects to a specialized format. However, this export process involves binding data to different locations in the exported file. This data takes the form of contents of a few CollectionProperty-s. As a result, there’s a lot of data on the object, and I want to allow for a “default” configuration when opening the export operator for a new object so it doesn’t need manually configured.

However, I have been unable to find a way to run a pre-operator initialization. My first thought was to check if it was initialized in the operator’s draw function, then initialize it if it is not - however, Blender disallows this:

AttributeError: Writing to ID classes in this context is not allowed: Cube, Object datablock, error setting Object.

As such, I need some way to run a function on the active object before it gets to draw(). There doesn’t seem to be such a function, unless I am misunderstanding one of the ones I have seen (execute, invoke, poll).

My alternative seems to be to add a button that executes a secondary operator and does the initialization. That’s clunky, though, so I would prefer not to have to.

For reference, here’s a slightly simplified example of my operator:

class CGSExporter(bpy.types.Operator, ExportHelper):
	"""Export the active object to a CGS binary mesh"""
	bl_idname = "cgs.export"
	bl_label = "Export CGS Binary Mesh (.cgs.bm)"
	bl_description = "Export the selected object as a CGS Binary Mesh. Only available if a single object is selected."

	filename_ext = ".cgs.bm"
	filter_glob = StringProperty(default="*.cgs.bm", options={'HIDDEN'})

	@classmethod
	def poll(cls, context):
		return len(bpy.context.selected_objects) == 1

	def draw(self, context):
		layout = self.layout
		obj = context.active_object
		
		if len(obj.cgs_export_attributes) == 0:
			PopulateAttributeExportData(context) # The line that (indirectly) causes the above error
		
		# Do drawing of cgs_export_attributes here

	def execute(self, context):
		cgs_bm_export.export_active_mesh(self.filepath)

		return {'FINISHED'}

data is read only during draw(), you’ll need to put it elsewhere. I don’t know enough about your project to say where but you’ll have to find something earlier in the process to trigger the update. In the past when I’ve needed to do this I’ve had another property or operator that the user would interact with first that I could hook into with an update callback, or worst case scenario run a timer that sits there waiting for something to happen.

I found a solution, though I believe this only works in operators that are popups (like the typical export operator). However, my situation is an export operator, so it works.

The trick is to add an invoke function that does what you need it to, but also calls the base class invoke. If you don’t call the base class, it won’t run correctly.

Here’s the class I put in my original post with these changes:

class CGSExporter(bpy.types.Operator, ExportHelper):
	"""Export the active object to a CGS binary mesh"""
	bl_idname = "cgs.export"
	bl_label = "Export CGS Binary Mesh (.cgs.bm)"
	bl_description = "Export the selected object as a CGS Binary Mesh. Only available if a single object is selected."

	filename_ext = ".cgs.bm"
	filter_glob = StringProperty(default="*.cgs.bm", options={'HIDDEN'})

	def invoke(self, context, event):
		if len(context.active_object.cgs_export_attributes) == 0:
			PopulateAttributeExportData(context)
		
		return super().invoke(context, event)

	@classmethod
	def poll(cls, context):
		return len(bpy.context.selected_objects) == 1

	def draw(self, context):
		layout = self.layout
		obj = context.active_object
		
		# Do drawing of cgs_export_attributes here

	def execute(self, context):
		cgs_bm_export.export_active_mesh(self.filepath)

		return {'FINISHED'}

As a note, the default invoke() for an export operator - ie MyOperator(bpy.types.Operator, ExportHelper) - does the following:


    def invoke(self, context, _event):
        import os
        if not self.filepath:
            blend_filepath = context.blend_data.filepath
            if not blend_filepath:
                blend_filepath = "untitled"
            else:
                blend_filepath = os.path.splitext(blend_filepath)[0]

            self.filepath = blend_filepath + self.filename_ext

        context.window_manager.fileselect_add(self)
        return {'RUNNING_MODAL'}

(Obtained via using Python’s inspect on the function.)