From e764559133be176189fa33429e253e357337a200 Mon Sep 17 00:00:00 2001 From: Gorgious56 Date: Mon, 1 Jun 2026 10:47:57 +0200 Subject: [PATCH] Route _has_material_styles through tool.Root.has_material_styles MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pre-existing architectural smell on v0.8.0: core/root.py.copy_class called a module-level _has_material_styles helper that did ifcopenshell.util.element.get_materials() directly, bypassing the Prophecy mock seam that every other branch in copy_class flowed through. Symptom: test/core/test_root.py::TestCopyClass:: test_AAAAAAAAAAAA passed mock strings into copy_class, the helper called .is_a() on the string, AttributeError. Move the check to tool.Root.has_material_styles (paired with assign_body_styles — they're called in sequence as "is there a material style? if not, assign body style"). core/root.py now calls root.has_material_styles(new) like every other dependency, fixing the test failure and dropping the ifcopenshell.util.element import that was the only consumer of the ifcopenshell import at module load in core/root.py. * core/tool.py: add abstract has_material_styles to Root interface. * tool/root.py: add concrete classmethod near assign_body_styles. * core/root.py: replace _has_material_styles helper call site with root.has_material_styles; drop the local helper and its import. * test/core/test_root.py: add the new mock expectation root.has_material_styles("element").will_return(False) before the existing assign_body_styles expectation. Generated with the assistance of an AI coding tool. --- src/bonsai/bonsai/core/root.py | 23 +---------------------- src/bonsai/bonsai/core/tool.py | 1 + src/bonsai/bonsai/tool/root.py | 12 ++++++++++++ src/bonsai/test/core/test_root.py | 1 + 4 files changed, 15 insertions(+), 22 deletions(-) diff --git a/src/bonsai/bonsai/core/root.py b/src/bonsai/bonsai/core/root.py index 3a283a6a65..2276e0623f 100644 --- a/src/bonsai/bonsai/core/root.py +++ b/src/bonsai/bonsai/core/root.py @@ -20,8 +20,6 @@ from __future__ import annotations from typing import TYPE_CHECKING, Optional -import ifcopenshell.util.element - if TYPE_CHECKING: import bpy import ifcopenshell @@ -58,31 +56,12 @@ def copy_class( geometry.change_object_data(obj, data, is_global=True) geometry.rename_object(data, geometry.get_representation_name(ifc.get_entity(data))) # Only assign styles if element doesn't get them from material - if not _has_material_styles(ifc, new): + if not root.has_material_styles(new): root.assign_body_styles(new, obj) collector.assign(obj) return new -def _has_material_styles(ifc: type[tool.Ifc], element: ifcopenshell.entity_instance) -> bool: - """Check if element has styles defined through its material. - - Returns True if any constituent material has a style representation, - which means styles should NOT be applied directly to the geometry. - """ - materials = ifcopenshell.util.element.get_materials(element) - - if not materials: - return False - - # Check if any of the constituent materials have styles - for material in materials: - if hasattr(material, "HasRepresentation") and material.HasRepresentation: - return True - - return False - - def assign_class( ifc: type[tool.Ifc], collector: type[tool.Collector], diff --git a/src/bonsai/bonsai/core/tool.py b/src/bonsai/bonsai/core/tool.py index d3260fa278..8c12788acd 100644 --- a/src/bonsai/bonsai/core/tool.py +++ b/src/bonsai/bonsai/core/tool.py @@ -884,6 +884,7 @@ class Root: def get_object_name(cls, obj): pass def get_object_representation(cls, obj): pass def get_representation_context(cls, representation): pass + def has_material_styles(cls, element): pass def is_containable(cls, element): pass def is_drawing_annotation(cls, element): pass def is_element_a(cls, element, ifc_class): pass diff --git a/src/bonsai/bonsai/tool/root.py b/src/bonsai/bonsai/tool/root.py index 8880a168fe..524f590a81 100644 --- a/src/bonsai/bonsai/tool/root.py +++ b/src/bonsai/bonsai/tool/root.py @@ -71,6 +71,18 @@ class Root(bonsai.core.tool.Root): should_use_presentation_style_assignment=props.should_use_presentation_style_assignment, ) + @classmethod + def has_material_styles(cls, element: ifcopenshell.entity_instance) -> bool: + """``True`` if any constituent material on ``element`` carries a style + representation. Body styles should NOT be applied directly when this + is True — the material-inherited style is the authoritative source. + Paired with ``assign_body_styles``: callers check this first and only + call ``assign_body_styles`` when it returns False.""" + materials = ifcopenshell.util.element.get_materials(element) + if not materials: + return False + return any(getattr(m, "HasRepresentation", None) for m in materials) + @classmethod def copy_representation( cls, source: ifcopenshell.entity_instance, dest: ifcopenshell.entity_instance diff --git a/src/bonsai/test/core/test_root.py b/src/bonsai/test/core/test_root.py index 121236803d..96bd389d1d 100644 --- a/src/bonsai/test/core/test_root.py +++ b/src/bonsai/test/core/test_root.py @@ -56,6 +56,7 @@ class TestCopyClass: ifc.get_entity("data").should_be_called().will_return("new_representation") geometry.get_representation_name("new_representation").should_be_called().will_return("name") geometry.rename_object("data", "name").should_be_called() + root.has_material_styles("element").should_be_called().will_return(False) root.assign_body_styles("element", "obj").should_be_called() collector.assign("obj").should_be_called() subject.copy_class(ifc, collector, geometry, root, obj="obj")