2

[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 !

Aleks K.
  • 159
  • 11

2 Answers2

7

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

brockmann
  • 12,613
  • 4
  • 50
  • 93
5

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
batFINGER
  • 84,216
  • 10
  • 108
  • 233