1
$\begingroup$

[Intro]

I'm looking for the Python function (if it exists at all) that saves the .blend file in order to modify it. If it doesn't exist - Please continue reading. I'm opened to any suggestions!

[Reason]

I want to expand it so that every time a .blend file is being saved, a Statistics.txt file with project info is saved along with it.

[Workaround]

I think the whole thing is written in C instead of Python so I can't really modify it. The workaround I came up with is every time the wm.save_mainfile() or the wm.save_as_mainfile() is being called, a custom function gets called instead. That custom function simply runs the wm.save_mainfile() and then executes the code for the Stats.txt file.

[N/B]

The reason I want to avoid the workaround I came up with is that this method requires modifying a number of (5-10) .py files that actually run the wm.save_mainfile() function

[P.S]

So, thanks to @brockmann I got my hands on this example file (written by brockmann):

import bpy
import os
from bpy.app.handlers import persistent

bl_info = {
    "name": "Stats Add-on",
    "author": "brockmann",
    "version": (0, 1),
    "blender": (2, 82, 0),
    "category": "System"
}


class STATS_AP_preferences(bpy.types.AddonPreferences):
    # this must match the add-on name, use '__package__'
    # when defining this in a submodule of a python package.
    bl_idname = __name__

    state: bpy.props.EnumProperty(
        name="switch",
        default = 'ON',
        items=(
            ("ON", "on", "", 1),
            ("OFF", "off", "", 2)))

    def draw(self, context):
        layout = self.layout
        layout.prop(self, "state", expand=True)
    

class STATS_PT_panel(bpy.types.Panel):
    """Creates a Panel in the scene context of the properties editor"""
    bl_label = "Layout Demo"
    bl_idname = "SCENE_PT_layout"
    bl_space_type = 'PROPERTIES'
    bl_region_type = 'WINDOW'
    bl_context = "scene"

    def draw(self, context):
        layout = self.layout
        prefs = context.preferences.addons[__name__].preferences
        layout.prop(prefs, "state", expand=True)

@persistent
def my_save_handler(dummy):
    if bpy.context.preferences.addons[__name__].preferences.state == 'ON':
        stats = bpy.context.scene.statistics(bpy.context.view_layer).split("|")
        text_file = os.path.join(bpy.path.abspath("//"), "stats.txt")
        with open(text_file, mode='wt', encoding='utf-8') as myfile:
            myfile.write('\n'.join(stats))


classes = (
    STATS_AP_preferences,
    STATS_PT_panel
)

def register():
    from bpy.utils import register_class
    for cls in classes:
        register_class(cls)

    bpy.app.handlers.save_post.append(my_save_handler)

def unregister():
    from bpy.utils import unregister_class
    for cls in reversed(classes):
        unregister_class(cls)
    
    bpy.app.handlers.save_post.remove(my_save_handler)

if __name__ == "__main__":
    register()

[The new problem]

Everything works great when I load the script and then save the project file after that. The problem is that after closing Blender and opening it some time later - the script no longer works for some reason, even though it's "ON". Any ideas why this is happening?

[FINAL]

Everything works great! I updated the code snipet above so that anyone could use this tamplate. Thanks again to @brockmann !

$\endgroup$
7
$\begingroup$

You can use application handlers, specifically save_post or save_pre handlers. When you're saving the file, the handler kicks in and allows to execute whatever you want after/before saving the file:

import bpy
import os

def my_save_handler(dummy):
    stats = bpy.context.scene.statistics(bpy.context.view_layer).split("|")
    text_file = os.path.join(bpy.path.abspath("//"), "stats.txt")
    with open(text_file, mode='wt', encoding='utf-8') as myfile:
        myfile.write('\n'.join(stats))

def register():
    bpy.app.handlers.save_post.append(my_save_handler)

def unregister():
    bpy.app.handlers.save_post.remove(my_save_handler)

if __name__ == "__main__":
    register()

Demo above creates a file called stats.txt when saving the blend file:

Scene Collection 
 Icing 
 Verts:995,340 
 Faces:995,397 
 Tris:1,990,650 
 Objects:0/1,011 
 Memory: 593.1 MiB 
 2.93.4

Handlers are freed when loading new files by default. If you want to keep the handler alive when loading new files add the @persistent decorator to the handler function:

@persistent
def my_save_handler(dummy):
    ...

Further reading: https://docs.blender.org/api/current/bpy.app.handlers.html?highlight=save_pre#persistent-handler-example

$\endgroup$
6
5
$\begingroup$

Use the handlers.

There are two handlers available, save_pre and save_post. All handlers have two arguments, the scene and the depsgraph. Since the load and save handlers are not specific to one scene, rather all, any or all of these arguments can be None, the case here. The scene argument is often named dummy in examples when this is the case

The persistent decorator @persistent is used to keep the handler alive, should the user load a file.

Instead will iterate over all scenes and print the statistics on save.

import bpy

from bpy.app.handlers import persistent, save_post


def stats():
    from pathlib import Path
    fp = Path(bpy.data.filepath)
    name = fp.stem
    fp = fp.parent / f"{name}_stats.txt"
    buffer = [name]
    for scene in bpy.data.scenes:
        buffer.append(scene.name)
        buffer.extend(
            [
                f"\t{ob.name} : {ob.type}"
                for ob in scene.objects
            ]
        )
    fp.write_text("\n".join(buffer))

@persistent
def save_handler(scene, depsgraph):
    stats()

save_post.append(save_handler)

Note: tried looping over scenes and producing scene stats with scene.statistics(scene.view_layers[i]) as also outlined in @brockmann's excellent answer. (added example file IO using pathlib) As a general rule I suggest never mix context with handlers... especially operator calls. A detailed stats report can be gleaned from the data

Printing a hierarchy of objects.

Finding Vertices, Edges, Faces, and Tris using Python

Sample output, blend file snork.blend

snork_stats.txt

snork
Scene
    Armature : ARMATURE
    Cube : MESH
    Lamp : LIGHT
    Camera : CAMERA
    Cube.001 : MESH
Scene.001
    Cube.002 : MESH

Re pre and post.

If we make a handler for both, and simply

def save_pre_handler(dummy):
    print("PRE", bpy.data.filepath)

def save_post_handler(dummy):
    print("POST", bpy.data.filepath)

On opening snork.blend and save,

Read blend: /home/batfinger/Desktop/snork.blend
PRE /home/batfinger/Desktop/snork.blend
POST /home/batfinger/Desktop/snork.blend
Info: Saved "snork.blend"

then save as grunt.blend

PRE /home/batfinger/Desktop/snork.blend
POST /home/batfinger/Desktop/grunt.blend

could generate some report (make buffer global for example) from pre handler to use in post to inform grunt was previously snork.

Stat the file.

There is a python script available to read data from blend files without launching blender.

How to extract render info from a .blend file without launching Blender?

Is there a way to acces external .blend file's data with python script?

How to search through multiple Blender files to find a specific object?

Iterate over CollectionProperty with blendfile.py

Manipulating .blend file text blocks with blendfile.py (CRUD)

also mentioned in links is running blender in background mode

blender -b snork.py -P stats.py

Instead of writing file on save, could add a python or shell script to generate the info from a saved blend file.

blendstats snork.py
$\endgroup$
3
  • 2
    $\begingroup$ Very nice...... 30 seconds Mr. Holmes :D $\endgroup$
    – brockmann
    Sep 9 at 8:37
  • $\begingroup$ Works in this case and is a simple demo, OP messing around with a cool C binary anyway. If you have any serious concerns, don't hesitate and suggest an edit @batFINGER. $\endgroup$
    – brockmann
    Sep 9 at 8:54
  • 1
    $\begingroup$ Gotta do something to keep up brockers.. $\endgroup$
    – batFINGER
    Sep 9 at 10:44

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.