From 964fb045f73d3b82f84ebc44b5a155ac8fc6d164 Mon Sep 17 00:00:00 2001 From: Gorgious56 Date: Thu, 11 Jun 2026 09:17:35 +0200 Subject: [PATCH] Guard HasShapeAspects access on IFC2X3 representation iteration IFC2X3 representations have no HasShapeAspects inverse; opening the Geometry & Materials subpanel on an IFC2X3 object raised AttributeError and left the items list empty. Wrap the access with a getattr default so pre-IFC4 schemas return an empty iterable, and pin the contract with an AST forward-compat guard that scans bim/, tool/, and core/ for any future direct .HasShapeAspects access. Closes #8157 Generated with the assistance of an AI coding tool. --- .../bonsai/bim/module/geometry/operator.py | 2 +- .../test/bim/module/geometry/__init__.py | 17 ++++++ .../test_shape_aspects_forward_compat.py | 61 +++++++++++++++++++ 3 files changed, 79 insertions(+), 1 deletion(-) create mode 100644 src/bonsai/test/bim/module/geometry/__init__.py create mode 100644 src/bonsai/test/bim/module/geometry/test_shape_aspects_forward_compat.py diff --git a/src/bonsai/bonsai/bim/module/geometry/operator.py b/src/bonsai/bonsai/bim/module/geometry/operator.py index 72e974815a..47c6b75dfa 100644 --- a/src/bonsai/bonsai/bim/module/geometry/operator.py +++ b/src/bonsai/bonsai/bim/module/geometry/operator.py @@ -3170,7 +3170,7 @@ class EnableEditingRepresentationItems(bpy.types.Operator, tool.Ifc.Operator): product_reps = element.RepresentationMaps item_aspect = {} for product_rep in product_reps: - for aspect in product_rep.HasShapeAspects: + for aspect in getattr(product_rep, "HasShapeAspects", ()): for aspect_rep in aspect.ShapeRepresentations: if aspect_rep.ContextOfItems != representation.ContextOfItems: continue diff --git a/src/bonsai/test/bim/module/geometry/__init__.py b/src/bonsai/test/bim/module/geometry/__init__.py new file mode 100644 index 0000000000..fa692422fc --- /dev/null +++ b/src/bonsai/test/bim/module/geometry/__init__.py @@ -0,0 +1,17 @@ +# Bonsai - OpenBIM Blender Add-on +# Copyright (C) 2021 Dion Moult +# +# 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 . diff --git a/src/bonsai/test/bim/module/geometry/test_shape_aspects_forward_compat.py b/src/bonsai/test/bim/module/geometry/test_shape_aspects_forward_compat.py new file mode 100644 index 0000000000..ecfd0c9621 --- /dev/null +++ b/src/bonsai/test/bim/module/geometry/test_shape_aspects_forward_compat.py @@ -0,0 +1,61 @@ +# 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 . +# +# This file was generated with the assistance of an AI coding tool. + +"""Forward-compat AST contract: ``HasShapeAspects`` is an IFC4+ inverse; +direct attribute access raises ``AttributeError`` on pre-IFC4 entity +instances. Production code must read it through ``getattr`` so the +absence in earlier schemas degrades to an empty iterable.""" + +import ast +from pathlib import Path + +import pytest + +pytestmark = pytest.mark.geometry + + +BONSAI_ROOT = Path(__file__).parent.parent.parent.parent.parent / "bonsai" +PRODUCTION_DIRS = (BONSAI_ROOT / "bim", BONSAI_ROOT / "tool", BONSAI_ROOT / "core") + +ATTR_NAME = "HasShapeAspects" + + +def _iter_production_sources(): + for root in PRODUCTION_DIRS: + yield from root.rglob("*.py") + + +def test_has_shape_aspects_access_uses_getattr_guard(): + """Every read of ``HasShapeAspects`` in production code must go through + ``getattr(, "HasShapeAspects", )`` so files using + schemas that omit the inverse return the default instead of raising.""" + offenders = [] + for source in _iter_production_sources(): + tree = ast.parse(source.read_text(encoding="utf-8")) + for node in ast.walk(tree): + if isinstance(node, ast.Attribute) and node.attr == ATTR_NAME: + offenders.append(f"{source.relative_to(BONSAI_ROOT.parent)}:{node.lineno}") + if offenders: + joined = "\n ".join(sorted(offenders)) + pytest.fail( + f"Direct .{ATTR_NAME} attribute access in production code:\n {joined}\n" + f"Wrap with getattr(, '{ATTR_NAME}', ()) so pre-IFC4 schemas " + f"do not raise AttributeError." + )