Files
IfcOpenShell/src/bonsai/test/bim/test_parametric_registry.py
T
Gorgious56 d71856d884 Migrate Modifier shim callers + drop the shim block
Completes the PR4/PR5 cleanup the FIXME at tool/blender.py
flagged: every is_<type> / Array.<helper> shim on
tool.Blender.Modifier delegated one-for-one to tool.Parametric /
tool.Array. Callers now reach the canonical home directly, and the
shim block — seven is_<type> classmethods plus the inner class Array
— comes out.

Renames (no semantic change):

* tool.Blender.Modifier.is_<door|railing|roof|stair|wall|window>
  → tool.Parametric.is_<x>
  13 sites across tool/loader.py, bim/import_ifc.py,
  bim/module/geometry/{data,operator}.py, bim/module/model/{door,
  railing,roof,stair,ui,wall,window}.py.

* tool.Blender.Modifier.Array.<helper> → tool.Array.<helper>
  4 sites across tool/root.py, bim/import_ifc.py,
  bim/module/geometry/operator.py.

* test_parametric_registry.py: the two getattr probes that hunt
  predicates by name now look on tool.Parametric. Docstring + the
  test function name (test_every_entry_has_modifier_predicate →
  test_every_entry_has_parametric_predicate) follow the move.

Kept on tool.Blender.Modifier (non-shim, no equivalent on
tool.Parametric): try_applying_edit_mode,
try_canceling_editing_modifier_parameters_or_path,
is_eligible_for_<x>_modifier (×5), is_array_child, is_slab.

Verified: 109 model-lane tests + 8 parametric-registry tests pass
(the one pre-existing failure in test_wall_header_refresh.py is
unrelated — it patches handler.update_bim_tool_props which has been
renamed). git grep for tool\.Blender\.Modifier\.(is_<type>|Array\.)
returns empty. black + ruff clean on every touched file.

Generated with the assistance of an AI coding tool.
2026-06-11 18:34:42 +02:00

147 lines
6.0 KiB
Python

# Bonsai - OpenBIM Blender Add-on
# Copyright (C) 2026
#
# This file is part of Bonsai.
#
# Bonsai is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Bonsai is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
#
# This file was generated with the assistance of an AI coding tool.
"""Registration smoke test for `tool.Parametric.EDIT_TYPES`.
The registry is the single source of truth for which parametric element types
exist. Every consumer (auto-commit on save, finish/cancel chains, the
``PointerProperty`` attachment, the ``GizmoPreferences`` per-feature toggle)
derives identifiers from each entry's short ``name`` token. Forget any
downstream registration and the silent-desync the framework exists to prevent
will ship.
These tests pin the registry-to-runtime contract: for every entry the operator
``bl_idname``s resolve to registered ``bpy.ops.bim.*`` callables, the
``PropertyGroup`` class is attached to ``bpy.types.Object``, and the per-type
predicate exists on `tool.Parametric`."""
import types
import bpy
import pytest
pytestmark = pytest.mark.model
@pytest.fixture(autouse=True)
def _require_real_bpy():
if not isinstance(bpy, types.ModuleType) or hasattr(bpy, "_mock_name"):
pytest.skip("requires real Blender (bpy is mocked or absent)")
@pytest.fixture
def registry():
from bonsai import tool
return tool.Parametric.EDIT_TYPES
def test_registry_is_non_empty(registry):
assert len(registry) >= 1
def test_every_entry_has_enable_op_registered(registry):
missing = [e.enable_op for e in registry if not hasattr(bpy.ops.bim, e.enable_op.removeprefix("bim."))]
assert not missing, f"Missing enable operators: {missing}"
def test_every_entry_has_finish_op_registered(registry):
missing = [e.finish_op for e in registry if not hasattr(bpy.ops.bim, e.finish_op.removeprefix("bim."))]
assert not missing, f"Missing finish operators: {missing}"
def test_every_entry_has_cancel_op_registered(registry):
missing = [e.cancel_op for e in registry if not hasattr(bpy.ops.bim, e.cancel_op.removeprefix("bim."))]
assert not missing, f"Missing cancel operators: {missing}"
def test_every_entry_has_property_group_attached(registry):
# ``register_object_properties`` runs at addon enable; if any entry's
# PropertyGroup class is missing on prop module the attribute is skipped.
missing = [e.props_attr for e in registry if not hasattr(bpy.types.Object, e.props_attr)]
assert not missing, (
f"bpy.types.Object missing attributes: {missing}"
f"verify the matching PropertyGroup classes exist in bim.module.model.prop"
)
def test_every_entry_has_parametric_predicate(registry):
from bonsai import tool
missing = [e.name for e in registry if getattr(tool.Parametric, f"is_{e.name}", None) is None]
assert not missing, f"tool.Parametric missing is_<name> predicates: {missing}"
def test_every_predicate_does_not_raise_on_non_matching_element(registry):
"""Each ``is_<name>`` predicate must be **total**: accept any IFC entity
and return a truthy/falsy value, never raise.
The registry iterates every predicate against the active IFC element on
save; a raising predicate (e.g. ``AttributeError`` from a missing pset
accessor when handed a non-matching element type) propagates upward and
breaks the save path for *all* parametric types, not just its own.
This test probes each predicate with an ``IfcAnnotation`` (an element
that carries none of the BBIM_<Type> psets the predicates look up) and
asserts the call does not raise. Falsy returns are acceptable — the
registry treats them as 'no match'. What's forbidden is raising."""
import ifcopenshell
from bonsai import tool
probe = ifcopenshell.file(schema="IFC4").create_entity("IfcAnnotation")
raised = []
for feature in registry:
predicate = getattr(tool.Parametric, f"is_{feature.name}", None)
if predicate is None:
continue
try:
predicate(probe)
except Exception as e:
raised.append((feature.name, type(e).__name__, str(e)))
assert not raised, (
f"is_<name> predicates raised on a non-matching IfcAnnotation: {raised}. "
f"Predicates must be total — return bool, never raise. Add an "
f"`if not element.is_a('IfcXxx'): return False` short-circuit or guard the pset lookup."
)
def test_gizmo_preferences_field_per_registry_entry(registry):
"""Every registry entry must have a matching ``<name>: BoolProperty`` field
on ``ui.GizmoPreferences`` so the addon-preferences UI auto-renders a
toggle for it and ``BaseParametricGizmoGroup.poll`` can gate the whole
gizmo group on ``prefs.gizmos.<name>``.
Checks ``__annotations__`` rather than ``hasattr`` because Blender's
PropertyGroup syntax (``field: bpy.props.BoolProperty(...)``) is an
annotation-only assignment — the attribute only materialises on the
class after Blender's metaclass installs the bpy_struct descriptor,
which depends on registration timing. Reading ``__annotations__``
pins the source-level contract independently of when register() ran."""
from bonsai.bim import ui
annotations = getattr(ui.GizmoPreferences, "__annotations__", {})
missing = [feature.name for feature in registry if feature.name not in annotations]
assert not missing, (
f"ui.GizmoPreferences missing BoolProperty field(s) for: {missing}"
f"each registry entry must have a matching ``<name>: BoolProperty(...)`` "
f"field on ``ui.GizmoPreferences`` so the preferences UI surfaces a toggle"
)