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
9 changes: 6 additions & 3 deletions docs/gallery/cross-version-property-delete/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,11 @@ <h1>Cross Version Property Delete</h1>
<p>A pair of stage lamps that witnesses the custom <strong>ID-property</strong> delete contract from <a href="https://github.com/TMHSDigital/Blender-Developer-Tools/tree/main/snippets/cross-version-property-delete.py"><code>cross-version-property-delete.py</code></a> and <a href="https://github.com/TMHSDigital/Blender-Developer-Tools/tree/main/skills/custom-properties/SKILL.md"><code>custom-properties</code></a> — not the snippet&#x27;s <code>__main__</code>, which keys off <code>context.active_object</code> and prints nothing in <code>--background</code>.</p>
<p>The IDs are two stage-lamp housings built with <code>bpy.data.objects.new</code>. The Keep lamp still carries <code>[&quot;accession&quot;] = 42</code>; the Clear lamp had <code>del obj[&quot;accession&quot;]</code>. Same <code>del</code> on 4.5 LTS and 5.x; there is no version branch.</p>
<p><strong>What it witnesses:</strong> <code>property_unset</code> is a TypeError on a custom ID key and does <strong>not</strong> remove it. <code>del id_block[key]</code> does. After factory-empty, there is no <code>active_object</code>.</p>
<p>It also witnesses the other half of the split: a <strong>registered</strong> <code>bpy.props</code> value (<code>Scene.bdt_counter = IntProperty()</code>) is not an ID property on 5.0+. Measured on 4.5.11, 5.1.2 and 5.2.1:</p>
<div class="table-wrap"><table><thead><tr><th scope="col">On a registered prop</th><th scope="col">4.5 LTS</th><th scope="col">5.0+</th></tr></thead><tbody><tr><td><code>scene.bdt_counter</code></td><td>5</td><td>5</td></tr><tr><td><code>scene[&quot;bdt_counter&quot;]</code></td><td>5</td><td><code>KeyError</code></td></tr><tr><td><code>&quot;bdt_counter&quot; in scene.keys()</code></td><td>True</td><td>False</td></tr><tr><td><code>del scene[&quot;bdt_counter&quot;]</code></td><td>resets it</td><td><code>KeyError</code></td></tr><tr><td><code>path_resolve(&#x27;[&quot;bdt_counter&quot;]&#x27;)</code> (driver path)</td><td>5</td><td><code>ValueError</code></td></tr><tr><td><code>path_resolve(&quot;bdt_counter&quot;)</code>, <code>property_unset</code>, <code>is_property_set</code></td><td>work</td><td>work</td></tr></tbody></table></div>
<p>On 5.0+ the raw storage is <code>scene.bl_system_properties_get()</code>. A value saved by 4.5 still loads as the attribute on 5.2.</p>
<p><strong>What failure each check would catch:</strong></p>
<ul><li>exit 2 — someone set an active object; the snippet <code>__main__</code> would have been the only path</li><li>exit 3 — the ID property never landed</li><li>exit 4 — <code>property_unset</code> removed the key or failed to raise</li><li>exit 7 — <code>del</code> did not run (<code>--skip-delete</code> / <code>--unset-instead</code> falsify this: measured <code>clear_has=True</code>)</li></ul>
<ul><li>exit 2 — someone set an active object; the snippet <code>__main__</code> would have been the only path</li><li>exit 3 — the ID property never landed</li><li>exit 4 — <code>property_unset</code> removed the key or failed to raise</li><li>exit 7 — <code>del</code> did not run (<code>--skip-delete</code> / <code>--unset-instead</code> falsify this: measured <code>clear_has=True</code>)</li><li>exit 8 — a registered prop was read or reset the 4.x way, or sits on the wrong side of the ID-property split for this version (<code>--subscript-registered</code> falsifies this on 5.0+)</li></ul>
<h2 id="staging">Staging<a class="anchor" href="#staging" aria-label="Link to this section">#</a></h2>
<p>Both lamps hang in U-yokes from one T-stand. The render path reads each housing&#x27;s <code>keys()</code> after the check has run, so the still is driven by the checked IDs, not staged from the expected outcome. A lamp that still carries the property gets an emissive lens and a real SPOT light along its beam axis, which pools warm light on the placard below it. A lamp without the property keeps a dark lens and casts nothing. Each placard is set in the bundled DejaVu Sans Mono and shows the line of code that produced its lamp&#x27;s state. Only render-side geometry (stand, yokes, lenses, placards) is added in the render path; the check reads ID properties, never geometry.</p>
<h2 id="run">Run<a class="anchor" href="#run" aria-label="Link to this section">#</a></h2>
Expand All @@ -75,8 +78,8 @@ <h2 id="run">Run<a class="anchor" href="#run" aria-label="Link to this section">
<p>The <code>--output</code> render path measures framing against the Layer 1 band via <code>examples/gallery_framing.py</code> (exit 10 on violation) before writing the still.</p>
<h2 id="exit-codes">Exit codes<a class="anchor" href="#exit-codes" aria-label="Link to this section">#</a></h2>
<p>Per-script sequential checks. <code>9</code> is a valid check code; there is no rule against it. <code>10</code> is the shared framing helper.</p>
<div class="table-wrap"><table><thead><tr><th scope="col">Code</th><th scope="col">Meaning</th></tr></thead><tbody><tr><td>0</td><td>Success</td></tr><tr><td>1</td><td>Uncaught exception (FATAL wrapper)</td></tr><tr><td>2</td><td>argparse / usage; also an active object existed after the data-API build</td></tr><tr><td>3</td><td>Custom ID property did not land</td></tr><tr><td>4</td><td><code>property_unset</code> on a custom ID key did not TypeError, or it deleted the key</td></tr><tr><td>5</td><td><code>del</code> did not report removal</td></tr><tr><td>6</td><td>Keep lamp lost the ID property</td></tr><tr><td>7</td><td>Clear lamp still has the ID property (<code>--skip-delete</code> / <code>--unset-instead</code> land here)</td></tr><tr><td>10</td><td>Gallery framing violation</td></tr><tr><td>12</td><td><code>--output</code> produced no file</td></tr></tbody></table></div>
<p>The <code>blender-smoke</code> workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the <code>needs-5.1</code> PR label, or manual dispatch). Smoke does not pass <code>--output</code> or <code>--unset-instead</code>. Its catalog falsifier is <code>--skip-delete</code> (expects exit 7).</p>
<div class="table-wrap"><table><thead><tr><th scope="col">Code</th><th scope="col">Meaning</th></tr></thead><tbody><tr><td>0</td><td>Success</td></tr><tr><td>1</td><td>Uncaught exception (FATAL wrapper)</td></tr><tr><td>2</td><td>argparse / usage; also an active object existed after the data-API build</td></tr><tr><td>3</td><td>Custom ID property did not land</td></tr><tr><td>4</td><td><code>property_unset</code> on a custom ID key did not TypeError, or it deleted the key</td></tr><tr><td>5</td><td><code>del</code> did not report removal</td></tr><tr><td>6</td><td>Keep lamp lost the ID property</td></tr><tr><td>7</td><td>Clear lamp still has the ID property (<code>--skip-delete</code> / <code>--unset-instead</code> land here)</td></tr><tr><td>8</td><td>Registered-prop contract broke: attribute/<code>property_unset</code> failed, the prop sits on the wrong side of the 5.0 ID-property split, or <code>scene[&quot;name&quot;]</code> raised <code>KeyError</code> (<code>--subscript-registered</code> on 5.0+; exits 0 on 4.5, where subscript access still works)</td></tr><tr><td>10</td><td>Gallery framing violation</td></tr><tr><td>12</td><td><code>--output</code> produced no file</td></tr></tbody></table></div>
<p>The <code>blender-smoke</code> workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the <code>needs-5.1</code> PR label, or manual dispatch). Smoke does not pass <code>--output</code> or <code>--unset-instead</code>. Its catalog falsifiers are <code>--skip-delete</code> (expects exit 7); <code>--subscript-registered</code> (expects exit 8; 5.0+ only; SKIP on older Blender).</p>
</section>
<section class="detail-section src" id="source">
<h2>Source</h2>
Expand Down
22 changes: 21 additions & 1 deletion examples/cross-version-property-delete/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,22 @@ branch.
does **not** remove it. `del id_block[key]` does. After factory-empty, there is
no `active_object`.

It also witnesses the other half of the split: a **registered** `bpy.props`
value (`Scene.bdt_counter = IntProperty()`) is not an ID property on 5.0+.
Measured on 4.5.11, 5.1.2 and 5.2.1:

| On a registered prop | 4.5 LTS | 5.0+ |
| --- | --- | --- |
| `scene.bdt_counter` | 5 | 5 |
| `scene["bdt_counter"]` | 5 | `KeyError` |
| `"bdt_counter" in scene.keys()` | True | False |
| `del scene["bdt_counter"]` | resets it | `KeyError` |
| `path_resolve('["bdt_counter"]')` (driver path) | 5 | `ValueError` |
| `path_resolve("bdt_counter")`, `property_unset`, `is_property_set` | work | work |

On 5.0+ the raw storage is `scene.bl_system_properties_get()`. A value saved
by 4.5 still loads as the attribute on 5.2.

**What failure each check would catch:**

- exit 2 — someone set an active object; the snippet `__main__` would have
Expand All @@ -24,6 +40,9 @@ no `active_object`.
- exit 4 — `property_unset` removed the key or failed to raise
- exit 7 — `del` did not run (`--skip-delete` / `--unset-instead` falsify
this: measured `clear_has=True`)
- exit 8 — a registered prop was read or reset the 4.x way, or sits on the
wrong side of the ID-property split for this version
(`--subscript-registered` falsifies this on 5.0+)

## Staging

Expand Down Expand Up @@ -65,9 +84,10 @@ against it. `10` is the shared framing helper.
| 5 | `del` did not report removal |
| 6 | Keep lamp lost the ID property |
| 7 | Clear lamp still has the ID property (`--skip-delete` / `--unset-instead` land here) |
| 8 | Registered-prop contract broke: attribute/`property_unset` failed, the prop sits on the wrong side of the 5.0 ID-property split, or `scene["name"]` raised `KeyError` (`--subscript-registered` on 5.0+; exits 0 on 4.5, where subscript access still works) |
| 10 | Gallery framing violation |
| 12 | `--output` produced no file |

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` or `--unset-instead`. Its catalog falsifier is `--skip-delete` (expects exit 7).
Smoke does not pass `--output` or `--unset-instead`. Its catalog falsifiers are `--skip-delete` (expects exit 7); `--subscript-registered` (expects exit 8; 5.0+ only; SKIP on older Blender).
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,13 @@
the ID property — that RNA call resets a registered property to default.
3. ``del obj["accession"]`` removes it. Same on 4.5 LTS and 5.x; no version
branch.
4. A *registered* ``bpy.props`` value is a different thing on 5.0+: it is no
longer an ID property, so ``scene["name"]``, ``"name" in scene.keys()``,
``del scene["name"]`` and a driver path of ``'["name"]'`` all stop seeing
it. Attribute access, ``property_unset()``, ``is_property_set()`` and the
plain ``"name"`` path work on every version. ``--subscript-registered``
reads it the 4.x way and exits 8 on 5.0+ (exit 0 on 4.5, where it still
works).

By default it runs only the correctness check (no render) — the CI smoke
check. Pass --output to also render a still:
Expand All @@ -25,6 +32,7 @@

KEY = "accession"
VALUE = 42
REG = "bdt_counter" # registered bpy.props IntProperty on Scene


def beveled_box(name, size, bevel, segments=2):
Expand Down Expand Up @@ -413,6 +421,52 @@ def light(name, loc, energy, size, col, rot):
return 0


def check_registered(subscript=False):
"""Registered bpy.props vs ID properties: the 5.0 split (#354)."""
split = bpy.app.version >= (5, 0, 0)
bpy.types.Scene.bdt_counter = bpy.props.IntProperty(default=1)
try:
scene = bpy.context.scene
scene.bdt_counter = 5
if subscript:
# The 4.x idiom: worked when registered values were ID properties.
value = scene[REG]
else:
value = scene.bdt_counter
in_keys = REG in scene.keys()
sub_path = True
try:
scene.path_resolve(f'["{REG}"]')
except ValueError:
sub_path = False
print(f"registered value={value} in_keys={in_keys} "
f"subscript_path={sub_path} split={split}")
if value != 5 or not scene.is_property_set(REG):
print("ERROR: registered property did not read back by attribute",
file=sys.stderr)
return 8
if in_keys == split or sub_path == split:
print(f"ERROR: expected registered props {'outside' if split else 'inside'} "
f"the ID-property group on {bpy.app.version_string}", file=sys.stderr)
return 8
if scene.path_resolve(REG) != 5:
print("ERROR: plain data path did not resolve the registered prop",
file=sys.stderr)
return 8
scene.property_unset(REG)
if scene.bdt_counter != 1 or scene.is_property_set(REG):
print("ERROR: property_unset did not reset the registered prop",
file=sys.stderr)
return 8
except KeyError as exc:
print(f"ERROR: KeyError {exc}: on 5.0+ a registered bpy.props value is "
f"not an ID property; read it as scene.{REG}", file=sys.stderr)
return 8
finally:
del bpy.types.Scene.bdt_counter
return 0


def main():
argv = sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else []
p = argparse.ArgumentParser()
Expand All @@ -426,13 +480,20 @@ def main():
"--unset-instead", action="store_true",
help="falsification: call property_unset instead of del",
)
p.add_argument(
"--subscript-registered", action="store_true",
help="falsification: read a registered prop as scene['name'] (exit 8 on 5.0+)",
)
args = p.parse_args(argv)

keep, clear = build()
code = check(
keep, clear,
skip_delete=args.skip_delete, unset_instead=args.unset_instead,
)
if code:
return code
code = check_registered(subscript=args.subscript_registered)
if code:
return code

Expand Down
23 changes: 22 additions & 1 deletion skills/custom-properties/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -231,14 +231,35 @@ This is a frequent AI mistake when generating quick scripts. Always wrap state i

6. **Confusing `subtype='FILE_PATH'` and `'DIR_PATH'`**. `'FILE_PATH'` lets the user pick a file, `'DIR_PATH'` a directory.

7. **Reading a registered property as an ID property on 5.x**: `scene["my_prop"]`, `"my_prop" in scene.keys()`, `del scene["my_prop"]`, or a driver path of `'["my_prop"]'`. These worked on 4.x and raise `KeyError` / `ValueError` (or silently skip the prop) on 5.0+. Use `scene.my_prop`, `scene.property_unset("my_prop")`, `scene.is_property_set("my_prop")` and the plain `"my_prop"` path. See the compatibility section below.

## Compatibility paths (4.5 LTS vs 5.0+)

### Registered properties are not ID properties on 5.0+

On 4.x a value assigned to a registered `bpy.props` property was stored as an ID property, so dict-style access reached it. On 5.0+ it lives in a separate system-property group. Measured with `bpy.types.Scene.my_prop = IntProperty(default=1)` and `scene.my_prop = 5`:

| Access | 4.5 LTS | 5.0+ (5.1.2, 5.2.1) |
| --- | --- | --- |
| `scene.my_prop` | 5 | 5 |
| `scene["my_prop"]` | 5 | `KeyError` |
| `scene.get("my_prop")` | 5 | `None` |
| `"my_prop" in scene.keys()` | True | False |
| `del scene["my_prop"]` | resets to default | `KeyError` |
| `scene.path_resolve('["my_prop"]')` (driver / keyframe path) | 5 | `ValueError` |
| `scene.path_resolve("my_prop")` | 5 | 5 |
| `scene.property_unset("my_prop")`, `scene.is_property_set("my_prop")` | work | work |

So the cross-version code needs no branch: use attribute access to read and write, `property_unset()` to reset, `is_property_set()` to test, and the plain `"my_prop"` path for drivers and keyframes. `keys()` lists only user custom properties on 5.0+; the raw storage is `id.bl_system_properties_get()` (5.0+ only), which you should rarely need. A value saved by 4.5 still loads as the attribute on 5.2. Witness: [`examples/cross-version-property-delete/`](https://github.com/TMHSDigital/Blender-Developer-Tools/tree/main/examples/cross-version-property-delete) (exit 8, falsifier `--subscript-registered`).

### Deleting

Two distinct operations, both version-stable via `del`:

- **Unbind a type-level property** (e.g. a `PointerProperty` bound to `bpy.types.Scene`): `del bpy.types.Scene.my_addon`. Same on 4.5 LTS and 5.0+.
- **Remove a custom ID property** (the dict-style `obj["key"]`): `del obj["key"]`. Same on 4.5 LTS and 5.0+. The snippet `cross-version-property-delete.py` shows this.

Do not reach for `property_unset()` here. It resets a registered RNA property to its default, which is neither unbinding a type property nor removing an ID property.
Do not reach for `property_unset()` here. It resets a registered RNA property to its default, which is neither unbinding a type property nor removing an ID property. Conversely, `del id["name"]` is not a way to reset a *registered* property: it raises `KeyError` on 5.0+.

## Related

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 @@ -45,7 +45,7 @@ def add_simple_driver(obj, data_path, index, expression):

`driver_add(data_path, index)`:

- `data_path` is the RNA path of the property, like `"location"` or `"scale"` or `'["my_custom_prop"]'`.
- `data_path` is the RNA path of the property, like `"location"` or `"scale"`. A user custom (ID) property set with `obj["my_custom_prop"] = 1.0` is `'["my_custom_prop"]'`; a registered `bpy.props` property is its plain name, `"my_prop"`. On 5.0+ `'["my_prop"]'` no longer resolves a registered property (`ValueError`), though it did on 4.x.
- `index` is the array index for vector or color properties (0 = X, 1 = Y, 2 = Z), or `-1` for scalar properties.
- Returns the `FCurve` whose `.driver` is now active.

Expand Down
4 changes: 4 additions & 0 deletions snippets/cross-version-property-delete.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@
# RNA property to its default value; it does not remove a custom ID property.
# That is a different operation.
#
# The reverse also holds: a REGISTERED bpy.props value is not an ID property
# on 5.0+, so `del id_block["my_prop"]` raises KeyError there (it reset the
# value on 4.x). Reset those with id_block.property_unset("my_prop").
#
# Reference:
# https://docs.blender.org/api/current/bpy.types.bpy_struct.html

Expand Down
2 changes: 1 addition & 1 deletion tests/smoke/catalog.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@
{"name": "lightmap-uv-channel", "script": "examples/lightmap-uv-channel/lightmap_uv_channel.py", "falsifiers": [{"args": ["--overlap-islands"], "expect_exit": 7}]},
{"name": "socket-attach-points", "script": "examples/socket-attach-points/socket_attach_points.py", "falsifiers": [{"args": ["--skip-mpi"], "expect_exit": 3}]},
{"name": "vertex-color-ao", "script": "examples/vertex-color-ao/vertex_color_ao.py", "falsifiers": [{"args": ["--uniform-hemisphere"], "expect_exit": 3}]},
{"name": "cross-version-property-delete", "script": "examples/cross-version-property-delete/cross_version_property_delete.py", "falsifiers": [{"args": ["--skip-delete"], "expect_exit": 7}]},
{"name": "cross-version-property-delete", "script": "examples/cross-version-property-delete/cross_version_property_delete.py", "falsifiers": [{"args": ["--skip-delete"], "expect_exit": 7}, {"args": ["--subscript-registered"], "expect_exit": 8, "min_version": "5.0"}]},
{"name": "usd-export-evaluation-mode", "script": "examples/usd-export-evaluation-mode/usd_export_evaluation_mode.py", "falsifiers": [{"args": ["--subdivision", "BEST_MATCH"], "expect_exit": 4}]},
{"name": "gn-zone-iterate", "script": "examples/gn-zone-iterate/gn_zone_iterate.py", "falsifiers": [{"args": ["--unpair-repeat"], "expect_exit": 3}]},
{"name": "gn-sim-fountain", "script": "examples/gn-sim-fountain/gn_sim_fountain.py", "falsifiers": [{"args": ["--unpair"], "expect_exit": 3}]},
Expand Down
Loading