#!/usr/bin/env python3 """Generate a deterministic operator/RNA inventory from pinned Blender 5.2.""" import hashlib import json import pathlib import sys import bpy def decode(value): return value.decode("utf-8", errors="replace") if isinstance(value, bytes) else str(value) def binary_sha256(path): digest = hashlib.sha256() with open(path, "rb") as handle: for block in iter(lambda: handle.read(1024 * 1024), b""): digest.update(block) return digest.hexdigest() def property_info(prop): result = {"identifier": prop.identifier, "type": prop.type} if hasattr(prop, "array_length"): result["arrayLength"] = prop.array_length return result def operator_info(module_name, operator_name): operator_path = f"{module_name}.{operator_name}" try: operator = getattr(getattr(bpy.ops, module_name), operator_name) rna = operator.get_rna_type() properties = [property_info(prop) for prop in rna.properties if prop.identifier != "rna_type"] properties.sort(key=lambda prop: prop["identifier"]) try: poll = bool(operator.poll()) poll_error = None except (AttributeError, RuntimeError) as error: poll = None poll_error = type(error).__name__ return { "operator": operator_path, "rnaIdentifier": rna.identifier, "registered": True, "poll": poll, "pollError": poll_error, "properties": properties, } except (AttributeError, KeyError, RuntimeError) as error: return { "operator": operator_path, "rnaIdentifier": None, "registered": False, "poll": None, "pollError": type(error).__name__, "properties": [], } def main(): if "--" not in sys.argv or len(sys.argv[sys.argv.index("--") + 1:]) != 1: raise SystemExit("usage: blender --background --factory-startup --python generate-blender-operator-inventory.py -- OUTPUT.json") output = pathlib.Path(sys.argv[sys.argv.index("--") + 1]).resolve() version = tuple(int(value) for value in bpy.app.version) if version != (5, 2, 0): raise RuntimeError(f"expected Blender 5.2.0, got {version}") operators = [] for module_name in sorted(name for name in dir(bpy.ops) if not name.startswith("_")): try: module = getattr(bpy.ops, module_name) names = sorted(name for name in dir(module) if not name.startswith("_")) except (AttributeError, RuntimeError): continue for operator_name in names: try: value = getattr(module, operator_name) if not hasattr(value, "get_rna_type"): continue except (AttributeError, RuntimeError): continue operators.append(operator_info(module_name, operator_name)) operators.sort(key=lambda entry: entry["operator"]) operator_ids = [entry["operator"] for entry in operators] if len(operator_ids) != len(set(operator_ids)): raise RuntimeError("duplicate Blender operator idname") binary_path = pathlib.Path(bpy.app.binary_path).resolve() inventory = { "schemaVersion": 1, "task": "M15-01B", "operation": "BLENDER_OPERATOR_INVENTORY", "sourceAnchor": "blender-5.2.0/source/blender/editors", "runtime": { "blenderVersion": decode(bpy.app.version_string), "versionTuple": list(version), "buildHash": decode(bpy.app.build_hash), "buildBranch": decode(bpy.app.build_branch), "buildPlatform": decode(bpy.app.build_platform), "buildType": decode(bpy.app.build_type), "buildDate": decode(bpy.app.build_date), "buildTime": decode(bpy.app.build_time), "binarySha256": binary_sha256(binary_path), }, "summary": {"count": len(operators), "registered": sum(1 for entry in operators if entry["registered"]), "pollTrue": sum(1 for entry in operators if entry["poll"] is True), "pollFalse": sum(1 for entry in operators if entry["poll"] is False), "pollUnknown": sum(1 for entry in operators if entry["poll"] is None)}, "operators": operators, "nextTask": "M15-01C", } output.parent.mkdir(parents=True, exist_ok=True) output.write_text(json.dumps(inventory, indent=2, sort_keys=True) + "\n", encoding="utf-8") print(f"blender-operator-inventory-generated count={len(operators)} blender={bpy.app.version_string} output={output}") if __name__ == "__main__": main()