Skip to content

Commit e1d8310

Browse files
TMHSDigitalclaude
andauthored
fix(skills): re-register driver functions on file load (#411)
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 <noreply@anthropic.com>
1 parent ae57f0d commit e1d8310

2 files changed

Lines changed: 52 additions & 2 deletions

File tree

‎skills/drivers-and-app-handlers/SKILL.md‎

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -114,17 +114,39 @@ var.targets[0].data_path = 'frame_current'
114114
fcurve.driver.expression = 'smooth_step((t - 1.0) / 100.0) * 5.0'
115115
```
116116

117-
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()`:
117+
The function name in `driver_namespace` must match the call in the expression.
118+
119+
`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:
118120

119121
```python
122+
import bpy
123+
from bpy.app.handlers import persistent
124+
125+
126+
@persistent
127+
def restore_driver_functions(_filepath=None):
128+
bpy.app.driver_namespace['smooth_step'] = smooth_step
129+
# Drivers that failed during load stay off until re-enabled.
130+
for ids in (bpy.data.objects, bpy.data.shape_keys, bpy.data.materials):
131+
for id_data in ids:
132+
anim = id_data.animation_data
133+
for fcurve in (anim.drivers if anim else ()):
134+
fcurve.driver.is_valid = True
135+
136+
120137
def register():
121138
bpy.app.driver_namespace['smooth_step'] = smooth_step
139+
bpy.app.handlers.load_post.append(restore_driver_functions)
122140

123141

124142
def unregister():
143+
if restore_driver_functions in bpy.app.handlers.load_post:
144+
bpy.app.handlers.load_post.remove(restore_driver_functions)
125145
bpy.app.driver_namespace.pop('smooth_step', None)
126146
```
127147

148+
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.
149+
128150
### Removing drivers
129151

130152
```python
@@ -263,6 +285,7 @@ The `exit_pre` handler list is new in Blender 5.1. On 4.5 LTS, fall back to OS-l
263285
## Common AI mistakes
264286

265287
- **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`.
288+
- **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.
266289
- **Forgetting `@persistent` on handlers.** The handler vanishes on the next file load and the user thinks the add-on broke.
267290
- **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.
268291
- **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.
@@ -276,7 +299,7 @@ The `exit_pre` handler list is new in Blender 5.1. On 4.5 LTS, fall back to OS-l
276299
| --- | --- | --- |
277300
| `exit_pre` handler | Not available | New in 5.1; use `atexit` fallback for 4.x |
278301
| `save_pre` / `save_post` signature | `(filepath)` — a string | `(filepath)` — a string (unchanged) |
279-
| `driver_namespace` | Available | Same |
302+
| `driver_namespace` | Available; reset on file load | Same (re-register from `load_post`) |
280303
| Driver security | Already restrictive | Same |
281304

282305
## See also

‎snippets/driver-with-custom-function.py‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
11
# Driver expression calling a custom Python function via driver_namespace.
22
# Driver expressions block arbitrary Python by default; the namespace is the
33
# whitelisted escape hatch.
4+
#
5+
# driver_namespace is reset on every file load. A driver that evaluates while
6+
# its function is missing raises NameError and is disabled (is_valid False),
7+
# and stays disabled after the function comes back. So re-register from a
8+
# @persistent load_post handler and re-enable the drivers there.
49
# See skill: drivers-and-app-handlers.
510
# Refs: docs.blender.org/api/current/bpy.app.html
611

712
import bpy
13+
from bpy.app.handlers import persistent
814

915

1016
def smooth_step(t):
@@ -17,6 +23,27 @@ def register_driver_function():
1723
bpy.app.driver_namespace['smooth_step'] = smooth_step
1824

1925

26+
@persistent
27+
def restore_driver_function(_filepath=None):
28+
register_driver_function()
29+
for ids in (bpy.data.objects, bpy.data.shape_keys, bpy.data.materials):
30+
for id_data in ids:
31+
anim = id_data.animation_data
32+
for fcurve in (anim.drivers if anim else ()):
33+
fcurve.driver.is_valid = True
34+
35+
36+
def register():
37+
register_driver_function()
38+
bpy.app.handlers.load_post.append(restore_driver_function)
39+
40+
41+
def unregister():
42+
if restore_driver_function in bpy.app.handlers.load_post:
43+
bpy.app.handlers.load_post.remove(restore_driver_function)
44+
bpy.app.driver_namespace.pop('smooth_step', None)
45+
46+
2047
def attach_smoothstep_driver(obj, frame_start=1, frame_end=100, peak=5.0):
2148
"""Drive obj.location.z to smoothstep peak across the given frame range."""
2249
fcurve = obj.driver_add("location", 2)

0 commit comments

Comments
 (0)