Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 21 additions & 18 deletions rules/prefer-temp-override-over-context-copy.mdc
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
description: Flag uses of `bpy.context.copy()` to override context for an operator call. The copy-and-pass pattern was deprecated in Blender 4.x and the override semantics were removed in 5.x. Use `bpy.context.temp_override(**overrides)` as a context manager instead.
description: Flag uses of `bpy.context.copy()` to override context for an operator call. Passing a context dict to an operator was deprecated in Blender 3.2 and removed in 4.0, so it already fails on the 4.5 LTS fallback. Use `bpy.context.temp_override(**overrides)` as a context manager instead.
alwaysApply: false
globs:
- "**/*.py"
Expand All @@ -9,15 +9,14 @@ standards-version: 1.10.0
# Prefer `temp_override` over `context.copy()`

`bpy.context.copy()` returns a dict snapshot of the current context. In
Blender 4.x and earlier it was common to mutate that dict and pass it as
Blender 3.x and earlier it was common to mutate that dict and pass it as
the first positional argument to an operator (`bpy.ops.x.y(ctx, ...)`)
to run the operator under a fabricated context. That mechanism was
deprecated in 4.x and the operator override semantics were removed in
5.x. The same code on 5.x either silently ignores the override or fails
with a confusing argument-type error.
deprecated in 3.2 and removed in 4.0. The same code on 4.0 and later,
including 4.5 LTS, fails with a confusing argument-type error.

The supported replacement is `bpy.context.temp_override(**overrides)`,
which works as a context manager and applies cleanly on both 4.5 LTS and
which works as a context manager and applies cleanly on 4.5 LTS and
5.x.

## What this rule flags
Expand Down Expand Up @@ -49,23 +48,28 @@ bpy.ops.object.delete({
})
```

Both forms run on 4.x with deprecation warnings. On 5.x they either no-op
the override silently or raise `TypeError: Calling operator "bpy.ops.object.delete"
Both forms ran with deprecation warnings on 3.2 to 3.6. On 4.0 and later
(4.5 LTS, 5.x) they raise `TypeError: Calling operator "bpy.ops.object.delete"
error, expected a string enum, not a dict`.

## Right

```python
import bpy

with bpy.context.temp_override(active_object=my_obj, selected_objects=[my_obj]):
with bpy.context.temp_override(active_object=my_obj, selected_editable_objects=[my_obj]):
bpy.ops.object.delete()
```

`temp_override` accepts the same keyword names as the keys you would
have put in the dict. It restores the previous context on exit, even
on exception. It is supported on 4.2+ and is the only override path
on 5.x.
on exception. It is supported on 3.2+ and is the only override path
on 4.0+.

Override the key the operator actually reads. `transform_apply` and
similar operators act on `selected_editable_objects`, so
`selected_objects=[obj]` alone does not narrow them to one object; use
`selected_editable_objects=[obj]`.

## When you also need a window override

Expand All @@ -84,19 +88,18 @@ with bpy.context.temp_override(window=window, area=area, region=region):

## Why it matters

The deprecated path silently drops the override on 5.x in many cases.
Code that "worked" on 4.x produces wrong results on 5.x without an
obvious failure. The user reports "my add-on broke after upgrade" and
the cause is a context override that is now ignored.
The dict-passing path was removed in 4.0, so code that "worked" on 3.x
raises on 4.5 LTS and 5.x. The user reports "my add-on broke after
upgrade" and the cause is a context override that no longer exists.

`temp_override` is the canonical, supported, and forward-compatible
replacement. There is no scenario in 5.x where the dict-passing form is
replacement. There is no scenario on 4.0+ where the dict-passing form is
preferable.

## Related

- Skill `operators`
- Snippet `temp-override-context.py`
- `bpy.types.Context.temp_override`: https://docs.blender.org/api/current/bpy.types.Context.html#bpy.types.Context.temp_override
- Blender 4.0 release notes (deprecation): https://developer.blender.org/docs/release_notes/4.0/python_api/
- Blender 5.0 release notes (removal): https://developer.blender.org/docs/release_notes/5.0/python_api/
- Blender 3.2 release notes (deprecation): https://developer.blender.org/docs/release_notes/3.2/python_api/
- Blender 4.0 release notes (removal): https://developer.blender.org/docs/release_notes/4.0/python_api/
2 changes: 1 addition & 1 deletion rules/validate-imported-mesh-scale.mdc
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ for obj in bpy.context.selected_objects:
sx, sy, sz = obj.scale
if abs(sx - 1.0) > 1e-6 or abs(sy - 1.0) > 1e-6 or abs(sz - 1.0) > 1e-6:
with bpy.context.temp_override(
object=obj, active_object=obj, selected_objects=[obj]
object=obj, active_object=obj, selected_editable_objects=[obj]
):
bpy.ops.object.transform_apply(
location=False, rotation=True, scale=True
Expand Down
8 changes: 4 additions & 4 deletions skills/addon-scaffolding/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ Use this skill when the user:
## Required inputs

- **Add-on name and short id** (snake_case for the manifest `id`, human-readable `name`)
- **Target Blender versions** (defaults to `blender_version_min = "4.5.0"` so the add-on works on 4.5 LTS and 5.1)
- **Target Blender versions** (defaults to `blender_version_min = "4.5.0"` so the add-on works on 4.5 LTS, 5.1 and 5.2 LTS)
- **Maintainer string** (`Name <email@example.com>` form)
- **License SPDX identifier** (`SPDX:MIT`, `SPDX:GPL-2.0-or-later`, etc.)

Expand All @@ -35,12 +35,12 @@ id = "my_addon"
version = "0.1.0"
name = "My Add-on"
tagline = "Short description, max 64 chars, no trailing period"
maintainer = "TMHSDigital <contact@example.com>"
maintainer = "Your Name <you@your-domain.example>"
type = "add-on"

blender_version_min = "4.5.0"

license = ["SPDX:MIT"]
license = ["SPDX:GPL-3.0-or-later"]
copyright = ["2026 TMHSDigital"]
```

Expand Down Expand Up @@ -199,7 +199,7 @@ def unregister():

## Compatibility paths

For libraries you want to be installable on both 4.5 LTS and 5.1, also keep `bl_info` for 4.5 fallback even though new submissions to the extensions platform require the manifest. Blender prefers the manifest when both are present.
For libraries you want to be installable on both 4.5 LTS and 5.2 LTS, also keep `bl_info` for 4.5 fallback even though new submissions to the extensions platform require the manifest. Blender prefers the manifest when both are present.

```python
bl_info = {
Expand Down
6 changes: 3 additions & 3 deletions skills/bl-info-migration/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ Use this skill when the user:

Yes, with caveats.

- **In 5.1 stable**: `bl_info` is supported as a fallback for legacy add-ons. The Edit > Preferences > Add-ons panel still has "Install legacy Add-on" and recognizes `bl_info` dicts.
- **In 5.2 LTS (current stable; 5.1 is prior stable)**: `bl_info` is supported as a fallback for legacy add-ons. The Edit > Preferences > Add-ons panel still has "Install legacy Add-on" and recognizes `bl_info` dicts.
- **For the Extensions Platform** (extensions.blender.org and the new add-ons UI): you need a `blender_manifest.toml`. `bl_info` is ignored.
- **Dual format** is officially recognized: ship a `blender_manifest.toml` and keep the `bl_info` dict. The platform reads the manifest; legacy installers read `bl_info`. Both code paths work without conditional logic.

Expand Down Expand Up @@ -220,9 +220,9 @@ This produces a `.zip` in `dist\` that the user installs via Edit > Preferences

## Version correctness

| Topic | 4.5 LTS | 5.1 stable |
| Topic | 4.5 LTS | 5.1 / 5.2 LTS |
| --- | --- | --- |
| `bl_info` recognized | Yes (primary path) | Yes (legacy fallback only) |
| `bl_info` recognized | Yes (legacy; Extensions is the primary path since 4.2) | Yes (legacy fallback only) |
| `blender_manifest.toml` recognized | Yes (Extensions Platform was added in 4.2) | Yes |
| Synthetic package name | `bl_ext.<repo>.<id>` | Same |
| `extension build` command | Available | Available |
Expand Down
2 changes: 1 addition & 1 deletion skills/drivers-and-app-handlers/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,7 @@ The `exit_pre` handler list is new in Blender 5.1. On 4.5 LTS, fall back to OS-l

## Version correctness

| Topic | 4.5 LTS | 5.1 stable |
| Topic | 4.5 LTS | 5.1 / 5.2 LTS |
| --- | --- | --- |
| `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) |
Expand Down
19 changes: 10 additions & 9 deletions skills/engine-export-presets/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,15 +32,16 @@ Before any preset: meters in the scene (`scale_length == 1.0`), identity object

```python
def apply_selected_mesh_transforms():
for obj in list(bpy.context.selected_objects):
if obj.type != "MESH":
continue
with bpy.context.temp_override(
object=obj, active_object=obj, selected_objects=[obj]
):
bpy.ops.object.transform_apply(
location=False, rotation=True, scale=True
)
# One operator call for the whole selection. transform_apply reads
# selected_editable_objects, so that is the key to override; overriding
# selected_objects alone does not narrow it.
meshes = [o for o in bpy.context.selected_objects if o.type == "MESH"]
if not meshes:
return
with bpy.context.temp_override(
object=meshes[0], active_object=meshes[0], selected_editable_objects=meshes
):
bpy.ops.object.transform_apply(location=False, rotation=True, scale=True)
```

`use_selection=True` on every preset. Draco is opt-in on glTF; do not copy `gltf_draco_export.py` wholesale.
Expand Down
6 changes: 3 additions & 3 deletions skills/procedural-materials-and-shaders/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,10 +158,10 @@ When the same subgraph appears across multiple materials, factor it into a `Shad

## Version correctness

| Topic | 4.5 LTS | 5.1 stable | Notes |
| Topic | 4.5 LTS | 5.1 / 5.2 LTS | Notes |
| --- | --- | --- | --- |
| Principled BSDF | `ShaderNodeBsdfPrincipled` | Same | Some inputs renamed in 5.0; `Specular` -> `Specular IOR Level`. Use string lookup with the 5.x name. |
| Node group socket interface | `group.inputs.new` / `group.outputs.new` | `group.interface.new_socket` | Different APIs, see snippet `shader-node-group.py`. |
| Principled BSDF | `ShaderNodeBsdfPrincipled` | Same | Inputs were renamed in 4.0 with the Principled v2 rewrite: `Specular` -> `Specular IOR Level` (see the 4.0 release notes, https://developer.blender.org/docs/release_notes/4.0/). Use string lookup with the 4.0+ name. |
| Node group socket interface | `group.interface.new_socket` | `group.interface.new_socket` | Same API on every supported version. `group.inputs.new` / `outputs.new` were removed in 4.0; see snippet `shader-node-group.py`. |
| EEVEE engine string | `'BLENDER_EEVEE_NEXT'` | `'BLENDER_EEVEE'` | Legacy EEVEE was removed in 4.2. EEVEE Next used the id `'BLENDER_EEVEE_NEXT'` on 4.2-4.5, then reclaimed the plain `'BLENDER_EEVEE'` id in 5.0. |
| Layered Textures | Not present | Not present in 5.1 | Roadmap pushed to 2027. Do not generate code referencing it. |

Expand Down
5 changes: 3 additions & 2 deletions skills/vse-python/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ else:
strip.color = (0.85, 0.10, 0.22)
```

Branch on `bpy.app.version`, never on `bpy.app.version_string`. An empty `bpy_prop_collection` is falsy — `se.strips or se.sequences` silently falls through to the legacy accessor on an empty timeline. Always branch on `hasattr`.
Branch on the `bpy.app.version` tuple for known API boundaries (such as the 5.2 COLOR strip `width`/`height` bake), never on `bpy.app.version_string`. Pick the `.strips` vs `.sequences` accessor with `hasattr`, not version: an empty `bpy_prop_collection` is falsy, so `se.strips or se.sequences` silently falls through to the legacy accessor on an empty timeline.

## Accessor: `.strips` vs `.sequences`

Expand Down Expand Up @@ -152,6 +152,7 @@ A lone COLOR strip **does** honor `transform.scale_*` on 5.2. The break is media
## References

- `bpy.types.SequenceEditor`: https://docs.blender.org/api/current/bpy.types.SequenceEditor.html
- `bpy.types.Sequence`: https://docs.blender.org/api/current/bpy.types.Sequence.html
- `bpy.types.Strip` (5.x name of `Sequence`): https://docs.blender.org/api/current/bpy.types.Strip.html
- 4.5 LTS `Sequence`: https://docs.blender.org/api/4.5/bpy.types.Sequence.html
- 4.5 LTS `SequenceEditor`: https://docs.blender.org/api/4.5/bpy.types.SequenceEditor.html
- 5.1 `SequenceEditor`: https://docs.blender.org/api/5.1/bpy.types.SequenceEditor.html
1 change: 1 addition & 0 deletions snippets/action-ensure-channelbag-for-slot.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
# action.fcurves API is still present, so use it directly.
#
# Verified on Blender 4.5.10 LTS and 5.1.1. Import path (5.0+):
# from bpy_extras import anim_utils
#
# Reference:
# https://docs.blender.org/api/current/bpy_extras.anim_utils.html
Expand Down
19 changes: 10 additions & 9 deletions snippets/export_preset_godot.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,16 @@


def apply_selected_mesh_transforms():
for obj in list(bpy.context.selected_objects):
if obj.type != "MESH":
continue
with bpy.context.temp_override(
object=obj, active_object=obj, selected_objects=[obj]
):
bpy.ops.object.transform_apply(
location=False, rotation=True, scale=True
)
# One operator call for the whole selection. transform_apply reads
# selected_editable_objects, so that is the key to override; overriding
# selected_objects alone does not narrow it.
meshes = [o for o in bpy.context.selected_objects if o.type == "MESH"]
if not meshes:
return
with bpy.context.temp_override(
object=meshes[0], active_object=meshes[0], selected_editable_objects=meshes
):
bpy.ops.object.transform_apply(location=False, rotation=True, scale=True)


def export_preset_godot(filepath, selected_only=True, draco=False):
Expand Down
19 changes: 10 additions & 9 deletions snippets/export_preset_unity.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,16 @@


def apply_selected_mesh_transforms():
for obj in list(bpy.context.selected_objects):
if obj.type != "MESH":
continue
with bpy.context.temp_override(
object=obj, active_object=obj, selected_objects=[obj]
):
bpy.ops.object.transform_apply(
location=False, rotation=True, scale=True
)
# One operator call for the whole selection. transform_apply reads
# selected_editable_objects, so that is the key to override; overriding
# selected_objects alone does not narrow it.
meshes = [o for o in bpy.context.selected_objects if o.type == "MESH"]
if not meshes:
return
with bpy.context.temp_override(
object=meshes[0], active_object=meshes[0], selected_editable_objects=meshes
):
bpy.ops.object.transform_apply(location=False, rotation=True, scale=True)


def export_preset_unity(filepath, selected_only=True, draco=False):
Expand Down
27 changes: 16 additions & 11 deletions snippets/export_preset_unreal.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
# Unreal presets: centimeter scale. glTF has no global_scale and no
# axis_forward / axis_up; bake 100x then export_yup=True. FBX uses
# global_scale=100.0 plus axis_forward='-Z' and axis_up='Y'. That RNA
# split is the contract. glTF bake mutates selected mesh objects.
# split is the contract. glTF bake mutates selected mesh objects
# (scale and location both multiplied by 100).
# Draco is glTF-only and opt-in; see snippets/gltf_draco_export.py.
#
# Assumption: scene units are meters before the 100x bake / FBX scale.
Expand All @@ -16,22 +17,26 @@


def apply_selected_mesh_transforms():
for obj in list(bpy.context.selected_objects):
if obj.type != "MESH":
continue
with bpy.context.temp_override(
object=obj, active_object=obj, selected_objects=[obj]
):
bpy.ops.object.transform_apply(
location=False, rotation=True, scale=True
)
# One operator call for the whole selection. transform_apply reads
# selected_editable_objects, so that is the key to override; overriding
# selected_objects alone does not narrow it.
meshes = [o for o in bpy.context.selected_objects if o.type == "MESH"]
if not meshes:
return
with bpy.context.temp_override(
object=meshes[0], active_object=meshes[0], selected_editable_objects=meshes
):
bpy.ops.object.transform_apply(location=False, rotation=True, scale=True)


def export_preset_unreal_gltf(filepath, selected_only=True, draco=False):
apply_selected_mesh_transforms()
# Scale location with scale so inter-object spacing grows with the
# geometry. Assumes unparented objects; parented ones need the parent
# chain baked first.
for obj in list(bpy.context.selected_objects):
if obj.type != "MESH":
continue
obj.location = (obj.location[0] * 100.0, obj.location[1] * 100.0, obj.location[2] * 100.0)
obj.scale = (obj.scale[0] * 100.0, obj.scale[1] * 100.0, obj.scale[2] * 100.0)
apply_selected_mesh_transforms()
bpy.ops.export_scene.gltf(
Expand Down
15 changes: 5 additions & 10 deletions snippets/shader-node-group.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Reusable shader node group with the cross-version interface API.
# 4.5 LTS uses group.inputs/outputs; 5.x uses group.interface.new_socket.
# Reusable shader node group via NodeTree.interface (4.0+: 4.5 LTS, 5.1, 5.2).
# group.inputs/outputs.new were removed in 4.0; there is no version branch.
# See skill: procedural-materials-and-shaders.
# Refs: docs.blender.org/api/current/bpy.types.ShaderNodeTree.html

Expand All @@ -9,14 +9,9 @@
def make_tint_group(name="Tint"):
group = bpy.data.node_groups.new(name=name, type='ShaderNodeTree')

if hasattr(group, 'interface'):
group.interface.new_socket(name='Color', in_out='INPUT', socket_type='NodeSocketColor')
group.interface.new_socket(name='Strength', in_out='INPUT', socket_type='NodeSocketFloat')
group.interface.new_socket(name='Result', in_out='OUTPUT', socket_type='NodeSocketColor')
else:
group.inputs.new('NodeSocketColor', 'Color')
group.inputs.new('NodeSocketFloat', 'Strength')
group.outputs.new('NodeSocketColor', 'Result')
group.interface.new_socket(name='Color', in_out='INPUT', socket_type='NodeSocketColor')
group.interface.new_socket(name='Strength', in_out='INPUT', socket_type='NodeSocketFloat')
group.interface.new_socket(name='Result', in_out='OUTPUT', socket_type='NodeSocketColor')

nodes = group.nodes
links = group.links
Expand Down
2 changes: 1 addition & 1 deletion templates/ai-asset-pipeline-template/pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ def scale_is_identity(obj, tol=1e-6):

def apply_object_transform(obj):
with bpy.context.temp_override(
object=obj, active_object=obj, selected_objects=[obj]
object=obj, active_object=obj, selected_editable_objects=[obj]
):
bpy.ops.object.transform_apply(location=False, rotation=True, scale=True)

Expand Down
6 changes: 4 additions & 2 deletions templates/extension-addon-template/blender_manifest.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@ id = "example_addon"
version = "0.1.0"
name = "Example Addon"
tagline = "Minimal Extensions Platform starting point"
maintainer = "TMHSDigital <contact@example.com>"
# Replace the maintainer line with your own name and a real contact address.
maintainer = "Your Name <you@your-domain.example>"
type = "add-on"

blender_version_min = "4.5.0"

license = ["SPDX:MIT"]
# extensions.blender.org requires GPL-3.0-or-later for add-ons that ship code.
license = ["SPDX:GPL-3.0-or-later"]
copyright = ["2026 TMHSDigital"]

website = "https://github.com/TMHSDigital/Blender-Developer-Tools"
Expand Down
Loading