Route _has_material_styles through tool.Root.has_material_styles

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.
This commit is contained in:
Gorgious56
2026-06-01 10:47:57 +02:00
parent a3f92eb427
commit e764559133
4 changed files with 15 additions and 22 deletions
+1 -22
View File
@@ -20,8 +20,6 @@ from __future__ import annotations
from typing import TYPE_CHECKING, Optional from typing import TYPE_CHECKING, Optional
import ifcopenshell.util.element
if TYPE_CHECKING: if TYPE_CHECKING:
import bpy import bpy
import ifcopenshell import ifcopenshell
@@ -58,31 +56,12 @@ def copy_class(
geometry.change_object_data(obj, data, is_global=True) geometry.change_object_data(obj, data, is_global=True)
geometry.rename_object(data, geometry.get_representation_name(ifc.get_entity(data))) geometry.rename_object(data, geometry.get_representation_name(ifc.get_entity(data)))
# Only assign styles if element doesn't get them from material # 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) root.assign_body_styles(new, obj)
collector.assign(obj) collector.assign(obj)
return new 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( def assign_class(
ifc: type[tool.Ifc], ifc: type[tool.Ifc],
collector: type[tool.Collector], collector: type[tool.Collector],
+1
View File
@@ -884,6 +884,7 @@ class Root:
def get_object_name(cls, obj): pass def get_object_name(cls, obj): pass
def get_object_representation(cls, obj): pass def get_object_representation(cls, obj): pass
def get_representation_context(cls, representation): pass def get_representation_context(cls, representation): pass
def has_material_styles(cls, element): pass
def is_containable(cls, element): pass def is_containable(cls, element): pass
def is_drawing_annotation(cls, element): pass def is_drawing_annotation(cls, element): pass
def is_element_a(cls, element, ifc_class): pass def is_element_a(cls, element, ifc_class): pass
+12
View File
@@ -71,6 +71,18 @@ class Root(bonsai.core.tool.Root):
should_use_presentation_style_assignment=props.should_use_presentation_style_assignment, 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 @classmethod
def copy_representation( def copy_representation(
cls, source: ifcopenshell.entity_instance, dest: ifcopenshell.entity_instance cls, source: ifcopenshell.entity_instance, dest: ifcopenshell.entity_instance
+1
View File
@@ -56,6 +56,7 @@ class TestCopyClass:
ifc.get_entity("data").should_be_called().will_return("new_representation") ifc.get_entity("data").should_be_called().will_return("new_representation")
geometry.get_representation_name("new_representation").should_be_called().will_return("name") geometry.get_representation_name("new_representation").should_be_called().will_return("name")
geometry.rename_object("data", "name").should_be_called() 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() root.assign_body_styles("element", "obj").should_be_called()
collector.assign("obj").should_be_called() collector.assign("obj").should_be_called()
subject.copy_class(ifc, collector, geometry, root, obj="obj") subject.copy_class(ifc, collector, geometry, root, obj="obj")