Bonsai: copy-property-to-selection must not edit the inherited type pset (#7412)

"Copy property to selection" edited the value on the element's TYPE instead
of creating an override on the occurrence. core.copy_property_to_selection
resolved the target via tool.Pset.get_element_pset, which uses
ifcopenshell.util.element.get_pset with should_inherit=True (the default).
When the occurrence has no pset of its own but its type does, get_pset
returns the type's pset, so pset.edit_pset then changed the value for the
type and every occurrence sharing it.

Give get_element_pset a should_inherit parameter (default True, so the ~30
other callers are unchanged) and pass should_inherit=False from
copy_property_to_selection. Now only the element's own pset is considered;
if none exists the existing fallback creates an override pset on the
occurrence. The core interface stub and the four Prophecy mock
expectations are updated to match.

Verified live in headless Blender: with a type-level Pset_Custom.MyProp,
copying MyProp to two occurrences leaves the type value untouched and
creates an own-pset override on each occurrence. Core tests (test_pset.py)
pass.

Generated with the assistance of an AI coding tool.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Petru Conduraru
2026-07-12 08:37:44 +03:00
parent 0b7e25a3ef
commit 3a8facf019
4 changed files with 12 additions and 8 deletions
+5 -1
View File
@@ -40,7 +40,11 @@ def copy_property_to_selection(
element = ifc.get_entity(obj)
if not element:
return
ifc_pset = pset.get_element_pset(element, pset_name)
# Only consider the element's own property set, never one inherited from its
# type. Otherwise copying an occurrence override would edit the type's pset
# (and thus every occurrence) instead of creating/updating the override on
# this element. See https://github.com/IfcOpenShell/IfcOpenShell/issues/7412.
ifc_pset = pset.get_element_pset(element, pset_name, should_inherit=False)
if not ifc_pset:
ifc_pset = ifc.run("pset.add_pset" if is_pset else "pset.add_qto", product=element, name=pset_name)
if is_pset:
+1 -1
View File
@@ -814,7 +814,7 @@ class Pset:
def cast_string_to_primitive(cls, value: str): pass
def clear_blender_pset_properties(cls, props): pass
def enable_proposed_pset(cls, props, pset_name, pset_type, has_template): pass
def get_element_pset(cls, element, pset_name): pass
def get_element_pset(cls, element, pset_name, should_inherit=True): pass
def get_prop_template_primitive_type(cls, prop_template): pass
def get_pset_name(cls, obj, obj_type, pset_type): pass
def get_pset_template(cls, name): pass
+2 -2
View File
@@ -70,9 +70,9 @@ class Pset(bonsai.core.tool.Pset):
@classmethod
def get_element_pset(
cls, element: ifcopenshell.entity_instance, pset_name: str
cls, element: ifcopenshell.entity_instance, pset_name: str, should_inherit: bool = True
) -> Union[ifcopenshell.entity_instance, None]:
pset = ifcopenshell.util.element.get_pset(element, pset_name)
pset = ifcopenshell.util.element.get_pset(element, pset_name, should_inherit=should_inherit)
if pset:
return tool.Ifc.get().by_id(pset["id"])
+4 -4
View File
@@ -29,7 +29,7 @@ class TestCopyPropertyToSelection:
def test_copying_the_property_to_an_existing_pset(self, ifc, pset):
ifc.get_entity("obj").should_be_called().will_return("element")
pset.get_element_pset("element", "pset_name").should_be_called().will_return("pset")
pset.get_element_pset("element", "pset_name", should_inherit=False).should_be_called().will_return("pset")
ifc.run("pset.edit_pset", pset="pset", properties={"prop_name": "prop_value"}).should_be_called()
subject.copy_property_to_selection(
ifc, pset, is_pset=True, obj="obj", pset_name="pset_name", prop_name="prop_name", prop_value="prop_value"
@@ -37,7 +37,7 @@ class TestCopyPropertyToSelection:
def test_creating_a_new_pset_if_it_doesnt_exist(self, ifc, pset):
ifc.get_entity("obj").should_be_called().will_return("element")
pset.get_element_pset("element", "pset_name").should_be_called().will_return(None)
pset.get_element_pset("element", "pset_name", should_inherit=False).should_be_called().will_return(None)
ifc.run("pset.add_pset", product="element", name="pset_name").should_be_called().will_return("pset")
ifc.run("pset.edit_pset", pset="pset", properties={"prop_name": "prop_value"}).should_be_called()
subject.copy_property_to_selection(
@@ -46,7 +46,7 @@ class TestCopyPropertyToSelection:
def test_copying_the_quantity_to_an_existing_qto(self, ifc, pset):
ifc.get_entity("obj").should_be_called().will_return("element")
pset.get_element_pset("element", "qto_name").should_be_called().will_return("qto")
pset.get_element_pset("element", "qto_name", should_inherit=False).should_be_called().will_return("qto")
ifc.run("pset.edit_qto", qto="qto", properties={"prop_name": "prop_value"}).should_be_called()
subject.copy_property_to_selection(
ifc, pset, is_pset=False, obj="obj", pset_name="qto_name", prop_name="prop_name", prop_value="prop_value"
@@ -54,7 +54,7 @@ class TestCopyPropertyToSelection:
def test_creating_a_new_qto_if_it_doesnt_exist(self, ifc, pset):
ifc.get_entity("obj").should_be_called().will_return("element")
pset.get_element_pset("element", "qto_name").should_be_called().will_return(None)
pset.get_element_pset("element", "qto_name", should_inherit=False).should_be_called().will_return(None)
ifc.run("pset.add_qto", product="element", name="qto_name").should_be_called().will_return("qto")
ifc.run("pset.edit_qto", qto="qto", properties={"prop_name": "prop_value"}).should_be_called()
subject.copy_property_to_selection(