Temp Override Join

examples/temp-override-join/

Join seven lantern parts into one object under bpy.context.temp_override — the supported replacement for the removed context.copy() dict-pass form.

Rendered headless by the example itself. Select it to enlarge.

witnesses temp_override actually applies: join consumes all six sources, exactly one mesh remains, no geometry is lost, the five part materials merge into one slot each with every face still on its own material, and local Z spans foot to grip.

tags operators context

blender --background --python examples/temp-override-join/temp_override_join.py --

A runnable example that assembles a hurricane lantern from seven separate part objects with one object.join under bpy.context.temp_override, following the prefer-temp-override-over-context-copy rule and the operators skill: operators that need a fabricated active/selection context run under temp_override(**kwargs), not the deprecated bpy.context.copy() dict-pass form removed in Blender 5.x.

The parts are the way a prop artist blocks the lantern out: a red enamel fount, an amber glass globe, an iron wire guard, the two hurricane side air tubes, the bell cap, the wire bail with its wooden grip, and the brass wick knob and filler cap. Each is its own object with its own mesh, materials and transform. The join produces the single Lantern object an engine wants.

What it witnesses: object.join under temp_override actually consumes the sources and merges their material slots. The check asserts that exactly one mesh remains and it is the target, that all six sources are gone, and that no geometry was lost (verts and faces equal the sum over the parts). The joined mesh must carry exactly the five part materials, once each, and every material must still own exactly the faces its parts brought. So the glass is still glass and the grip is still wood. The local Z span must run from the fount's foot (0) to the top of the grip (closed form 2.303), which proves the part transforms were applied. A no-op override (the 5.x failure mode of the old dict-pass path) leaves seven objects.

Why the override passes the selection explicitly. Before the join, the check makes the target active, selects every part, and runs select_all(action='DESELECT'). The target is still context.active_object while selected_objects is empty and target.select_get() is False. Active and selected are independent, so an if obj is None guard passes on a selection that is gone (see the operators skill, Defensive context handling). The scene is then reset to no active object before the join. --clear-active-on-deselect is a self-check, not a falsifier: the script clears the active object itself, then shows exit 13 reports it. No Blender deselection clears the active object (that is the contract), so the default run is the witness: if a Blender release changed that, it would exit 13 (#489).

The render is the joined object. Everything in the still is one mesh object. It shows five materials because the slots merged and the per-face indices were remapped. If they had not been, the lantern would render in the target's red enamel from grip to foot. A shadowless warm point light inside the globe (render-only) stands in for the lit wick.

Run#

# Cheap correctness check (no render) — the CI check:
blender --background --python temp_override_join.py --

# Falsifier: join without temp_override. Must exit non-zero.
blender --background --python temp_override_join.py -- --no-override

# Self-check (not a falsifier): clear the active object by hand; exit 13 reports it.
blender --background --python temp_override_join.py -- --clear-active-on-deselect

# Also render a still (EEVEE on a GPU host; use --engine cycles on GPU-less hosts):
blender --background --python temp_override_join.py -- --output join.png
blender --background --python temp_override_join.py -- --output join.png --engine cycles

Exit codes#

Per-script sequential checks. 9 is a valid check code; there is no rule against it.

CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Mesh object count after join ≠ 1 (--no-override lands here)
4Joined target is not the sole remaining mesh
5Verts / faces ≠ the sum over the seven parts
6Source objects still present
7Local Z span ≠ [0, 2.303]: a part transform was not applied
8Material slots ≠ the five part materials, once each
9Faces per material ≠ what the parts brought (indices not remapped)
10--output framing gate (gallery_framing)
11--output asset-quality floors (gallery_asset_quality)
12--output produced no file
13After deselect-all the target is not still active-but-unselected, or something is still selected (the --clear-active-on-deselect self-check lands here)

The blender-smoke workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch). Smoke does not pass --output. Its catalog falsifier is --no-override (expects exit 3). --clear-active-on-deselect is not in the catalog: it reaches exit 13 only by writing the value the check reads, so it would pass whatever Blender did (#489).

Source

examples/temp-override-join/temp_override_join.py View the script on GitHub →
A red enamel hurricane lantern with a glowing amber glass globe, black wire guard, side air tubes, brass wick knob and a wire bail with a wooden grip, all one joined object on a dark studio floor.