From bd12b39813f2f314da9327d2631aa6c4178b9ea2 Mon Sep 17 00:00:00 2001 From: TMHSDigital <154358121+TMHSDigital@users.noreply.github.com> Date: Sun, 4 Oct 2026 19:42:26 -0400 Subject: [PATCH] fix(skills): re-register driver functions on file load driver_namespace is reset on every file load. The skill and snippet registered the function only in register(), so the first file a user opens evaluates the driver without it: NameError, the driver is disabled (is_valid False), and it stays disabled even after the function is put back. Measured on 4.5.11 and 5.2.1: z went from 2.4621 to 0.0 across a save and reopen. Both now re-register from a @persistent load_post handler that also resets driver.is_valid, with symmetric register/unregister. The skill's compatibility table and common-mistakes list say why. Closes #387 Signed-off-by: TMHSDigital <154358121+TMHSDigital@users.noreply.github.com> Co-Authored-By: Claude Opus 5.5 --- skills/drivers-and-app-handlers/SKILL.md | 27 ++++++++++++++++++++++-- snippets/driver-with-custom-function.py | 27 ++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 2 deletions(-) diff --git a/skills/drivers-and-app-handlers/SKILL.md b/skills/drivers-and-app-handlers/SKILL.md index 8dd56db4..2e35079b 100644 --- a/skills/drivers-and-app-handlers/SKILL.md +++ b/skills/drivers-and-app-handlers/SKILL.md @@ -114,17 +114,39 @@ var.targets[0].data_path = 'frame_current' fcurve.driver.expression = 'smooth_step((t - 1.0) / 100.0) * 5.0' ``` -The function name in `driver_namespace` must match the call in the expression. Register the function on add-on `register()` and remove it on `unregister()`: +The function name in `driver_namespace` must match the call in the expression. + +`driver_namespace` is **reset on every file load**. Registering only in `register()` works until the user opens a file: the drivers then evaluate with the function missing, raise `NameError: name 'smooth_step' is not defined`, and are disabled (`driver.is_valid = False`). They stay disabled even after the function is added back. Re-register from a `@persistent` `load_post` handler and re-enable the drivers there: ```python +import bpy +from bpy.app.handlers import persistent + + +@persistent +def restore_driver_functions(_filepath=None): + bpy.app.driver_namespace['smooth_step'] = smooth_step + # Drivers that failed during load stay off until re-enabled. + for ids in (bpy.data.objects, bpy.data.shape_keys, bpy.data.materials): + for id_data in ids: + anim = id_data.animation_data + for fcurve in (anim.drivers if anim else ()): + fcurve.driver.is_valid = True + + def register(): bpy.app.driver_namespace['smooth_step'] = smooth_step + bpy.app.handlers.load_post.append(restore_driver_functions) def unregister(): + if restore_driver_functions in bpy.app.handlers.load_post: + bpy.app.handlers.load_post.remove(restore_driver_functions) bpy.app.driver_namespace.pop('smooth_step', None) ``` +Extend the ID collections to wherever your add-on puts drivers (node groups, scenes, cameras). Measured on 4.5.11 and 5.2.1: without the handler, a saved and reopened file leaves the driver dead; with it, the driver evaluates again. + ### Removing drivers ```python @@ -260,6 +282,7 @@ The `exit_pre` handler list is new in Blender 5.1. On 4.5 LTS, fall back to OS-l ## Common AI mistakes - **Calling Python functions in a driver expression without registering them.** Hits the security block. Either rewrite as math, or register via `bpy.app.driver_namespace`. +- **Registering a driver function only in `register()`.** `driver_namespace` is reset on file load, so the driver dies with `NameError` on the first file the user opens and stays disabled. Re-register and reset `driver.is_valid` from a `@persistent` `load_post` handler. - **Forgetting `@persistent` on handlers.** The handler vanishes on the next file load and the user thinks the add-on broke. - **Doing real work inside `depsgraph_update_post`.** This handler fires on every depsgraph evaluation, which is many times per second during playback or interaction. Anything more than O(1) bookkeeping causes user-visible slowdown. - **Recursively modifying the scene from a depsgraph handler.** The modification triggers another depsgraph evaluation, which calls the handler, which modifies the scene. Infinite loop, often manifesting as a hang. @@ -273,7 +296,7 @@ The `exit_pre` handler list is new in Blender 5.1. On 4.5 LTS, fall back to OS-l | --- | --- | --- | | `exit_pre` handler | Not available | New in 5.1; use `atexit` fallback for 4.x | | `save_pre` / `save_post` signature | `(filepath)` — a string | `(filepath)` — a string (unchanged) | -| `driver_namespace` | Available | Same | +| `driver_namespace` | Available; reset on file load | Same (re-register from `load_post`) | | Driver security | Already restrictive | Same | ## See also diff --git a/snippets/driver-with-custom-function.py b/snippets/driver-with-custom-function.py index b3f40f1e..4c55b7dc 100644 --- a/snippets/driver-with-custom-function.py +++ b/snippets/driver-with-custom-function.py @@ -1,10 +1,16 @@ # Driver expression calling a custom Python function via driver_namespace. # Driver expressions block arbitrary Python by default; the namespace is the # whitelisted escape hatch. +# +# driver_namespace is reset on every file load. A driver that evaluates while +# its function is missing raises NameError and is disabled (is_valid False), +# and stays disabled after the function comes back. So re-register from a +# @persistent load_post handler and re-enable the drivers there. # See skill: drivers-and-app-handlers. # Refs: docs.blender.org/api/current/bpy.app.html import bpy +from bpy.app.handlers import persistent def smooth_step(t): @@ -17,6 +23,27 @@ def register_driver_function(): bpy.app.driver_namespace['smooth_step'] = smooth_step +@persistent +def restore_driver_function(_filepath=None): + register_driver_function() + for ids in (bpy.data.objects, bpy.data.shape_keys, bpy.data.materials): + for id_data in ids: + anim = id_data.animation_data + for fcurve in (anim.drivers if anim else ()): + fcurve.driver.is_valid = True + + +def register(): + register_driver_function() + bpy.app.handlers.load_post.append(restore_driver_function) + + +def unregister(): + if restore_driver_function in bpy.app.handlers.load_post: + bpy.app.handlers.load_post.remove(restore_driver_function) + bpy.app.driver_namespace.pop('smooth_step', None) + + def attach_smoothstep_driver(obj, frame_start=1, frame_end=100, peak=5.0): """Drive obj.location.z to smoothstep peak across the given frame range.""" fcurve = obj.driver_add("location", 2)