Wm.save_mainfile errors out when executed from python on unsaved file

Hi,

I’ve recently lost hours of work due to Blender not saving modified images when saving the .blend file. When texture painting with Eevee, Blender is very unstable and crashes extremely often. That would be fine if manual saving and autosaving worked correctly, so that packed image datablocks would be treated like all the other datablocks, and saved with the blend file, instead of explicitly.

This means that any time Blender crashes, you will lose all your texture painting work despite having autosave enabled and pressing Ctrl+S after pretty much every other brush stroke.

My hopes that this will get addressed soon are not high, so I decided to work around it by creating my own simple save operator to save all modified packed image datablocks too.

class WINDOW_OT_SavFileWithImages(bpy.types.Operator):
"""Saves current Blender file and all modified images"""
bl_idname = "wm.save_with_images"
bl_label = "Save with Images"

def execute(self, context):
    bpy.ops.image.save_all_modified()
    bpy.ops.wm.save_mainfile(check_existing=False)
    return {'FINISHED'}

For some reason, when I run the operator from the menu on an empty unsaved file, it opens the file save dialog:


When I execute exactly the same operator in an unsaved file from within the python, I get this:

How can I solve this?
Thanks

When a blend file has never been saved before, you need to specify a complete filepath with the bpy.ops.wm.save_mainfile command. For instance, bpy.ops.wm.save_mainfile(filepath = "C:\\my_blend.blend") should work. If you don’t provide the filepath, Blender does not know where to save. In the first case, the dialogue comes up, in the second case Blender tries to create a file untitled.blend but errors out because it lacks write permissions on the path it guessed (most likely the path is invalid).

Simplest way to solve it is to disallow your operator to be called in case the .blend file hasn’t been saved yet. You can do this in the poll function:

@classmethod
def poll(cls, context):
    return bpy.data.filepath is not None

Thank you very much. I just realized I could check out what the File menu does, and it seems to indeed perform the same check and run Save As operator instead, if the file was not saved:

    layout.operator_context = 'EXEC_AREA' if context.blend_data.is_saved else 'INVOKE_AREA'
    layout.operator("wm.save_mainfile", text="Save", icon='FILE_TICK')

    layout.operator_context = 'INVOKE_AREA'
    layout.operator("wm.save_as_mainfile", text="Save As...")
    layout.operator_context = 'INVOKE_AREA'
    layout.operator("wm.save_as_mainfile", text="Save Copy...").copy = True

So I have updated my script to this:

class WINDOW_OT_SaveFileWithImages(bpy.types.Operator):
    """Saves current Blender file and all modified images"""
    bl_idname = "wm.save_with_images"
    bl_label = "Save with Images"

    def execute(self, context):
        try:
            bpy.ops.image.save_all_modified()
        except Exception:
            self.report({'INFO'}, 'No modified images were saved')

        if context.blend_data.is_saved:
            bpy.ops.wm.save_mainfile(check_existing=False)
        else:
            bpy.ops.wm.save_as_mainfile()
        return {'FINISHED'}

But even save_as_mainfile errors out:

I don’t get this. In the space_topbar.py, there is nothing supplying any filepath to the save_as_mainfile()


yet it does not error out, but instead opens in the most recent last known file browser location:

Supplying the save_as_mainfile() with filepath doesn’t make the error go away:


But furthermore, I would like to reproduce the File menu behavior where executing the operator in an unsaved file opens Save As window in the most recent last known folder, instead of fixed location.

Nevermind, got it :slight_smile:

class WINDOW_OT_SaveFileWithImages(bpy.types.Operator):
    """Saves current Blender file and all modified images"""
    bl_idname = "wm.save_with_images"
    bl_label = "Save with Images"

    def execute(self, context):
        try:
            bpy.ops.image.save_all_modified()
        except Exception:
            self.report({'INFO'}, 'No modified images were saved')

        if context.blend_data.is_saved:
            bpy.ops.wm.save_mainfile('EXEC_AREA', check_existing=False)
        else:
            bpy.ops.wm.save_mainfile('INVOKE_AREA', check_existing=False)

        return {'FINISHED'}

I had no idea that the Execution context is related to the operator, not the layout. I thought it controls the layout state, but it it’s actually relevant to the operator itself.

1 Like