Skip to content

Commit 2d4e769

Browse files
TMHSDigitalclaude
andcommitted
fix(skills): headless guidance matches what --background actually runs
Measured on .scratch/blender-4.5.11-windows-x64 and .scratch/blender-5.2.1-windows-x64 (--background --factory-startup), same results on both: - bpy.ops.object.select_all / object.delete / object.transform_apply -> {'FINISHED'} with no override. The skill called select_all a failure ("fails: no view3d") and said the only true-headless option was a bpy.data equivalent. - window_manager.windows has 1 window under --background (the loaded file's); its screen has a VIEW_3D area. view3d.snap_cursor_to_selected fails poll with no override and returns {'FINISHED'} with the rule's window/area/region override. The skill said --background has "no windows at all". So the issue's IndexError premise for the rule's windows[0] example was wrong; the example works. What was wrong was the skill. Changes: - headless-batch-scripting: new "Which operators run headless" section (object/data operators run as-is, narrow with an object/selection override; editor operators need window/area/region borrowed from the loaded file); select_all reframed as fragile/slow, not failing; table rows and the common-mistake example use view3d.snap_cursor_to_selected; the helper reads bpy.context.window_manager and demos an editor operator. - prefer-temp-override-over-context-copy: the window example now guards the VIEW_3D lookup (a file whose layout has no 3D viewport has nothing to borrow) and says why it works under --background. Closes #329 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 0583b45 commit 2d4e769

2 files changed

Lines changed: 36 additions & 17 deletions

File tree

‎rules/prefer-temp-override-over-context-copy.mdc‎

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -73,13 +73,17 @@ similar operators act on `selected_editable_objects`, so
7373

7474
## When you also need a window override
7575

76-
Headless scripts often need `window`, `area`, `region`, or `screen`
77-
overrides because no UI exists by default. Pass them all to
78-
`temp_override`:
76+
Editor operators (`view3d.*`, `screen.*`) poll on an area and region,
77+
which a script's context does not provide. Object operators do not need
78+
this. Under `--background` the loaded file still has its window and screen,
79+
so borrow the 3D viewport from it, and guard the lookup: a file whose
80+
layout has no 3D viewport has nothing to borrow.
7981

8082
```python
8183
window = bpy.context.window_manager.windows[0]
82-
area = next(a for a in window.screen.areas if a.type == 'VIEW_3D')
84+
area = next((a for a in window.screen.areas if a.type == 'VIEW_3D'), None)
85+
if area is None:
86+
raise RuntimeError("loaded file has no 3D viewport to override")
8387
region = next(r for r in area.regions if r.type == 'WINDOW')
8488

8589
with bpy.context.temp_override(window=window, area=area, region=region):

‎skills/headless-batch-scripting/SKILL.md‎

Lines changed: 28 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -43,24 +43,26 @@ The Blender API is the same in headless mode but several context-dependent behav
4343
| --- | --- |
4444
| `bpy.context.window`, `bpy.context.area`, `bpy.context.region` | Often `None` |
4545
| `bpy.context.active_object` | `None` if no scene is loaded or no active object set |
46-
| `bpy.context.selected_objects` | Empty by default |
47-
| Most `bpy.ops.<editor>.*` operators | Fail with "context is incorrect" because they expect an editor that does not exist |
46+
| `bpy.context.selected_objects` | Whatever the loaded file saved (the factory startup selects its cube); never assume it |
47+
| Editor operators (`view3d.*`, `screen.*`) | Fail with "context is incorrect": the script's context has no area/region. Override one from the loaded file's screen (below) |
4848
| Modal operators | Cannot be used; no event loop |
4949
| Drag and drop, file dialogs, keymaps | All gone |
5050
| Scene rendering (`bpy.ops.render.render`) | Works fine, this is the standard headless render path |
5151

52-
The headless API surface is essentially "everything that operates on `bpy.data` directly" plus a small whitelist of operators that do not depend on UI state (most `render.*`, most `wm.*` save/load, scene-level edits).
52+
The headless API surface is "everything that operates on `bpy.data` directly" plus every operator that polls on objects or data rather than on an editor (`object.*`, import/export, `render.*`, `wm.*` save/load). Editor operators need the override described below.
5353

5454
## Rule of thumb: `bpy.data` is your friend
5555

5656
The single most important pattern: **prefer `bpy.data.*` over `bpy.ops.*`** in batch scripts.
5757

5858
```python
59-
# WRONG: requires a 3D viewport context to even poll.
59+
# FRAGILE: object operators do run under --background, but they act on
60+
# whatever the selection happens to be, and each call is a full operator
61+
# round-trip (slow in a loop).
6062
bpy.ops.object.select_all(action='DESELECT')
6163
bpy.ops.object.delete()
6264

63-
# RIGHT: works headless.
65+
# RIGHT: explicit, selection-independent, fast.
6466
for obj in list(bpy.data.objects):
6567
bpy.data.objects.remove(obj, do_unlink=True)
6668
```
@@ -75,15 +77,28 @@ bpy.ops.wm.obj_export(filepath="/tmp/out.obj", export_selected_objects=False)
7577

7678
The `mesh-editing-and-bmesh` skill covers the rest of the `bpy.data` and `bmesh` patterns. This skill is about **when** the headless context forces you toward those patterns.
7779

78-
## When you must use a `bpy.ops` that wants UI: `temp_override`
80+
## Which operators run headless
81+
82+
Two kinds of operator behave differently under `--background`:
83+
84+
- **Object and data operators** (`object.*`, `mesh.*` outside edit-mode UI, `export_scene.*`, `wm.*_export`, `render.render`): they poll on `context.object` / selection, not on an editor, so they run headless. `bpy.ops.object.select_all(action='DESELECT')` and `bpy.ops.object.transform_apply(...)` both return `{'FINISHED'}` under `--background` on 4.5 LTS and 5.2 LTS. Narrow them with an object/selection override, no window needed:
85+
86+
```python
87+
with bpy.context.temp_override(object=obj, active_object=obj, selected_editable_objects=[obj]):
88+
bpy.ops.object.transform_apply(location=False, rotation=True, scale=True)
89+
```
90+
91+
- **Editor operators** (`view3d.*`, `screen.*`, `uv.*` that read the editor): they poll on an area/region and fail with `poll() failed, context is incorrect` unless you override one. A file loaded under `--background` still has its window and screen, so you can borrow the 3D viewport area from it.
92+
93+
## When you must use an editor operator: `temp_override` with window/area/region
7994

8095
A few `bpy.ops` calls have no `bpy.data` equivalent and genuinely need an editor context. For these, use `bpy.context.temp_override`:
8196

8297
```python
8398
import bpy
8499

85100
def find_window_and_area():
86-
for window in bpy.data.window_managers[0].windows:
101+
for window in bpy.context.window_manager.windows:
87102
for area in window.screen.areas:
88103
if area.type == 'VIEW_3D':
89104
return window, area
@@ -100,12 +115,10 @@ def run_in_view3d_context(callable_, *args, **kwargs):
100115
return callable_(*args, **kwargs)
101116

102117

103-
run_in_view3d_context(bpy.ops.object.transform_apply, location=True, rotation=True, scale=True)
118+
run_in_view3d_context(bpy.ops.view3d.snap_cursor_to_selected)
104119
```
105120

106-
In **pure** headless mode (`--background`), there are no windows at all, so this pattern only works if you have a "GUI but headless" setup (e.g. `blender --python` without `--background`, running on a virtual framebuffer).
107-
108-
For true headless, your only option is to find a `bpy.data` equivalent or pre-bake the operation into the .blend file.
121+
This works under `--background` too: the window and screen come from the loaded file (the factory startup has one window whose screen includes a `VIEW_3D` area), so `bpy.ops.view3d.snap_cursor_to_selected()` returns `{'FINISHED'}` with the override and fails its poll without it. It fails only when the loaded file's screen has no 3D viewport, which is why the helper raises instead of assuming one. Prefer a `bpy.data` equivalent whenever one exists; it does not depend on what the file's UI layout happened to be.
109122

110123
## Argument parsing after `--`
111124

@@ -222,12 +235,14 @@ except Exception as e:
222235

223236
## Common AI mistakes
224237

225-
1. **Calling UI-dependent `bpy.ops` in headless** without a `temp_override`:
238+
1. **Calling an editor operator in headless** without a window/area/region `temp_override`:
226239

227240
```python
228-
bpy.ops.object.select_all(action='DESELECT') # fails: no view3d
241+
bpy.ops.view3d.snap_cursor_to_selected() # poll() failed, context is incorrect
229242
```
230243

244+
Object operators such as `object.select_all` and `object.transform_apply` do not need this; they run under `--background` as-is.
245+
231246
2. **Using `bpy.context.scene` before a scene is loaded**. After Blender starts, the default startup file is loaded, so `context.scene` works. But if you've called `bpy.ops.wm.read_factory_settings(use_empty=True)`, dereferencing `context.scene.collection` may surprise you.
232247

233248
3. **Forgetting the `--` argv split**. Without it, `argparse` sees Blender's own flags and rejects them.

0 commit comments

Comments
 (0)