diff --git a/src/blenderbim/blenderbim/core/nest.py b/src/blenderbim/blenderbim/core/nest.py index 549a207022..2dec6b2280 100644 --- a/src/blenderbim/blenderbim/core/nest.py +++ b/src/blenderbim/blenderbim/core/nest.py @@ -47,7 +47,7 @@ def unassign_object(ifc, nest, collector, relating_obj=None, related_obj=None): if related_element: relating_obj = ifc.get_object(relating_element) if relating_obj: - ifc.run("nest.unassign_object", related_object=related_element) + ifc.run("nest.unassign_object", related_objects=[related_element]) if container: ifc.run("spatial.assign_container", products=[related_element], relating_structure=container) collector.assign(relating_obj) diff --git a/src/blenderbim/test/core/test_nest.py b/src/blenderbim/test/core/test_nest.py index 2de6e12b4e..2eb0ffbc33 100644 --- a/src/blenderbim/test/core/test_nest.py +++ b/src/blenderbim/test/core/test_nest.py @@ -53,7 +53,7 @@ class TestUnassignObject: ifc.get_entity("related_obj").should_be_called().will_return("element") nest.get_container("element").should_be_called().will_return("container") ifc.run("spatial.assign_container", products=["element"], relating_structure="container").should_be_called() - ifc.run("nest.unassign_object", related_object="element").should_be_called().will_return("rel") + ifc.run("nest.unassign_object", related_objects=["element"]).should_be_called().will_return("rel") collector.assign("relating_obj").should_be_called() collector.assign("related_obj").should_be_called() subject.unassign_object(ifc, nest, collector, relating_obj="relating_obj", related_obj="related_obj") diff --git a/src/ifcopenshell-python/ifcopenshell/api/__init__.py b/src/ifcopenshell-python/ifcopenshell/api/__init__.py index 6f2ad87abc..fea3e6a225 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/__init__.py +++ b/src/ifcopenshell-python/ifcopenshell/api/__init__.py @@ -70,6 +70,9 @@ ARGUMENTS_DEPRECATION = { "nest.assign_object": partial( batching_argument_deprecation, prev_argument="related_object", new_argument="related_objects" ), + "nest.unassign_object": partial( + batching_argument_deprecation, prev_argument="related_object", new_argument="related_objects" + ), } diff --git a/src/ifcopenshell-python/ifcopenshell/api/cost/copy_cost_item.py b/src/ifcopenshell-python/ifcopenshell/api/cost/copy_cost_item.py index 30f3bda555..2127bde2a6 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/cost/copy_cost_item.py +++ b/src/ifcopenshell-python/ifcopenshell/api/cost/copy_cost_item.py @@ -77,14 +77,13 @@ class Usecase: inverse = ifcopenshell.util.element.copy(self.file, inverse) inverse.RelatingObject = to_element inverse.RelatedObjects = new_cost_items - for cost_item in new_cost_items: - ifcopenshell.api.run("nest.unassign_object", self.file, related_object=cost_item) - rel = ifcopenshell.api.run( - "nest.assign_object", - self.file, - related_objects=[cost_item], - relating_object=to_element, - ) + ifcopenshell.api.run("nest.unassign_object", self.file, related_objects=new_cost_items) + ifcopenshell.api.run( + "nest.assign_object", + self.file, + related_objects=new_cost_items, + relating_object=to_element, + ) # elif inverse.is_a("IfcRelAssignsToProduct"): # continue # to_element.IsDefinedBy = inverse.RelatingOrder diff --git a/src/ifcopenshell-python/ifcopenshell/api/nest/unassign_object.py b/src/ifcopenshell-python/ifcopenshell/api/nest/unassign_object.py index e54fc918bc..1f7d0f0f54 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/nest/unassign_object.py +++ b/src/ifcopenshell-python/ifcopenshell/api/nest/unassign_object.py @@ -22,48 +22,48 @@ import ifcopenshell.util.element class Usecase: - def __init__(self, file, related_object=None): - """Unassigns a related_object from its nest. + def __init__(self, file: ifcopenshell.file, related_objects: list[ifcopenshell.entity_instance]): + """Unassigns related_objects from their nests. An object (the whole within a decomposition) is Nested by zero or one more smaller objects. This function will remove this nesting relationship. If the object is not part of a nesting relationship, nothing will happen. - :param related_object: The child of the nesting relationship, typically - an IfcElement. - :type related_object: ifcopenshell.entity_instance.entity_instance - :return: None if the nest has only one child, otherwise the IfcRelNests relationship instance + :param related_objects: The list of children of the nesting relationship, + typically IfcElements. + :type related_objects: list[ifcopenshell.entity_instance.entity_instance] + :return: None + :rtype: None Example: .. code:: python - task = ifcopenshell.api.run("root.create_entity", model, ifc_class="IfcSite") - subtask1 = ifcopenshell.api.run("root.create_entity", model, ifc_class="IfcBuilding") - subtask2 = ifcopenshell.api.run("root.create_entity", model, ifc_class="IfcBuilding") + task = ifcopenshell.api.run("root.create_entity", model, ifc_class="IfcTasks") + subtask1 = ifcopenshell.api.run("root.create_entity", model, ifc_class="IfcTask") + subtask2 = ifcopenshell.api.run("root.create_entity", model, ifc_class="IfcTask") ifcopenshell.api.run("nest.assign_object", model, related_objects=[subtask1], relating_object=task) ifcopenshell.api.run("nest.assign_object", model, related_objects=[subtask2], relating_object=task) - # The relationship is returned as task still has subtask2 - rel = ifcopenshell.api.run("nest.unassign_object", model, related_object=subtask1) - # Nothing is returned, as the relationship has no related objects - ifcopenshell.api.run("nest.unassign_object", model, related_object=subtask2) + # nothing is returned + rel = ifcopenshell.api.run("nest.unassign_object", model, related_objects=[subtask1]) + # nothing is returned, relationship is removed + ifcopenshell.api.run("nest.unassign_object", model, related_objects=[subtask2]) """ self.file = file - self.settings = {"related_object": related_object} + self.settings = {"related_objects": related_objects} - def execute(self): - for rel in self.settings["related_object"].Nests or []: - if not rel.is_a("IfcRelNests"): - continue - if len(rel.RelatedObjects) == 1: + def execute(self) -> None: + related_objects = set(self.settings["related_objects"]) + rels = set(rel for object in related_objects if (rel := next((rel for rel in object.Nests), None))) + + for rel in rels: + related_objects = set(rel.RelatedObjects) - related_objects + if related_objects: + rel.RelatedObjects = list(related_objects) + ifcopenshell.api.run("owner.update_owner_history", self.file, **{"element": rel}) + else: history = rel.OwnerHistory self.file.remove(rel) if history: ifcopenshell.util.element.remove_deep2(self.file, history) - return - related_objects = list(rel.RelatedObjects) - related_objects.remove(self.settings["related_object"]) - rel.RelatedObjects = related_objects - ifcopenshell.api.run("owner.update_owner_history", self.file, **{"element": rel}) - return rel diff --git a/src/ifcopenshell-python/ifcopenshell/api/sequence/duplicate_task.py b/src/ifcopenshell-python/ifcopenshell/api/sequence/duplicate_task.py index 5ca1a5d279..195c5c71cd 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/sequence/duplicate_task.py +++ b/src/ifcopenshell-python/ifcopenshell/api/sequence/duplicate_task.py @@ -52,9 +52,7 @@ class Usecase: def execute(self): self.tracker = {"current": [], "duplicate": []} self.duplicate_task(self.settings["task"]) - self.copy_sequence_relationship( - self.tracker["current"], self.tracker["duplicate"] - ) + self.copy_sequence_relationship(self.tracker["current"], self.tracker["duplicate"]) return self.tracker["current"], self.tracker["duplicate"] def duplicate_task(self, task): @@ -69,9 +67,7 @@ class Usecase: if inverse.is_a("IfcRelDefinesByProperties"): inverse = ifcopenshell.util.element.copy(self.file, inverse) inverse.RelatedObjects = [to_element] - pset = ifcopenshell.util.element.copy_deep( - self.file, inverse.RelatingPropertyDefinition - ) + pset = ifcopenshell.util.element.copy_deep(self.file, inverse.RelatingPropertyDefinition) inverse.RelatingPropertyDefinition = pset elif inverse.is_a("IfcRelNests") and inverse.RelatingObject == from_element: nested_tasks = [e for e in inverse.RelatedObjects] @@ -83,24 +79,19 @@ class Usecase: inverse = ifcopenshell.util.element.copy(self.file, inverse) inverse.RelatingObject = to_element inverse.RelatedObjects = new_tasks - for t in new_tasks: - ifcopenshell.api.run( - "nest.unassign_object", self.file, related_object=t - ) - rel = ifcopenshell.api.run( - "nest.assign_object", - self.file, - related_object=t, - relating_object=to_element, - ) + ifcopenshell.api.run("nest.unassign_object", self.file, related_objects=new_tasks) + ifcopenshell.api.run( + "nest.assign_object", + self.file, + related_objects=new_tasks, + relating_object=to_element, + ) + elif inverse.is_a("IfcRelSequence") and ( - inverse.RelatingProcess == from_element - or inverse.RelatedProcess == from_element + inverse.RelatingProcess == from_element or inverse.RelatedProcess == from_element ): continue - elif inverse.is_a( - "IfcRelAssignsToControl" - ) and inverse.RelatingControl.is_a("IfcWorkSchedule"): + elif inverse.is_a("IfcRelAssignsToControl") and inverse.RelatingControl.is_a("IfcWorkSchedule"): continue elif inverse.is_a("IfcRelDefinesByObject"): continue @@ -118,8 +109,7 @@ class Usecase: for i, original_task in enumerate(original_tasks): for inverse in self.file.get_inverse(original_task): if inverse.is_a("IfcRelSequence") and ( - inverse.RelatingProcess == original_task - or inverse.RelatedProcess == original_task + inverse.RelatingProcess == original_task or inverse.RelatedProcess == original_task ): original_task_index = original_tasks.index(original_task) duplicated_task = duplicated_tasks[original_task_index] @@ -129,16 +119,12 @@ class Usecase: else: related_process = duplicated_task if inverse.RelatedProcess in original_tasks: - related_process_index = original_tasks.index( - inverse.RelatedProcess - ) + related_process_index = original_tasks.index(inverse.RelatedProcess) related_process = duplicated_tasks[related_process_index] else: # thus the related process is not part of the duplicated tasks related_process = inverse.RelatedProcess if inverse.RelatingProcess in original_tasks: - relating_process_index = original_tasks.index( - inverse.RelatingProcess - ) + relating_process_index = original_tasks.index(inverse.RelatingProcess) relating_process = duplicated_tasks[relating_process_index] else: # thus the relating process is not part of the duplicated tasks relating_process = inverse.RelatingProcess @@ -154,11 +140,11 @@ class Usecase: "sequence.assign_lag_time", self.file, rel_sequence=rel, - lag_value=ifcopenshell.util.date.ifc2datetime( - inverse.TimeLag.LagValue.wrappedValue - ) - if inverse.TimeLag.LagValue - else None, + lag_value=( + ifcopenshell.util.date.ifc2datetime(inverse.TimeLag.LagValue.wrappedValue) + if inverse.TimeLag.LagValue + else None + ), duration_type=inverse.TimeLag.DurationType, ) @@ -170,17 +156,13 @@ class Usecase: related_objects = list(referenced_by.RelatedObjects) related_objects.append(related_object) referenced_by.RelatedObjects = related_objects - ifcopenshell.api.run( - "owner.update_owner_history", self.file, **{"element": referenced_by} - ) + ifcopenshell.api.run("owner.update_owner_history", self.file, **{"element": referenced_by}) else: referenced_by = self.file.create_entity( "IfcRelDefinesByObject", **{ "GlobalId": ifcopenshell.guid.new(), - "OwnerHistory": ifcopenshell.api.run( - "owner.create_owner_history", self.file - ), + "OwnerHistory": ifcopenshell.api.run("owner.create_owner_history", self.file), "RelatedObjects": [related_object], "RelatingObject": relating_object, } diff --git a/src/ifcopenshell-python/test/api/nest/test_unassign_object.py b/src/ifcopenshell-python/test/api/nest/test_unassign_object.py new file mode 100644 index 0000000000..d26074c026 --- /dev/null +++ b/src/ifcopenshell-python/test/api/nest/test_unassign_object.py @@ -0,0 +1,51 @@ +# IfcOpenShell - IFC toolkit and geometry engine +# Copyright (C) 2021 Dion Moult +# +# This file is part of IfcOpenShell. +# +# IfcOpenShell is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# IfcOpenShell 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 Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with IfcOpenShell. If not, see . + +import pytest +import test.bootstrap +import ifcopenshell.api +import ifcopenshell.util.element + + +class TestUnassignObject(test.bootstrap.IFC4): + def test_unassigning_an_object(self): + element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcTask") + subelement1 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcTask") + subelement2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcTask") + ifcopenshell.api.run( + "nest.assign_object", self.file, related_objects=[subelement1, subelement2], relating_object=element + ) + ifcopenshell.api.run("nest.unassign_object", self.file, related_objects=[subelement1, subelement2]) + assert ifcopenshell.util.element.get_nest(subelement1) is None + assert ifcopenshell.util.element.get_nest(subelement2) is None + + def test_the_rel_is_kept_if_there_are_more_nested_elements(self): + element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcTask") + subelement2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcTask") + subelement1 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcTask") + ifcopenshell.api.run("nest.assign_object", self.file, related_objects=[subelement1], relating_object=element) + ifcopenshell.api.run("nest.assign_object", self.file, related_objects=[subelement2], relating_object=element) + ifcopenshell.api.run("nest.unassign_object", self.file, related_objects=[subelement1]) + assert len(self.file.by_type("IfcRelNests")) == 1 + + def test_the_rel_is_purged_if_there_are_no_more_nested_elements(self): + element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcTask") + subelement = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcTask") + ifcopenshell.api.run("nest.assign_object", self.file, related_objects=[subelement], relating_object=element) + ifcopenshell.api.run("nest.unassign_object", self.file, related_objects=[subelement]) + assert len(self.file.by_type("IfcRelNests")) == 0 diff --git a/src/ifcopenshell-python/test/api/test_api.py b/src/ifcopenshell-python/test/api/test_api.py index c900471ade..15e81b03ba 100644 --- a/src/ifcopenshell-python/test/api/test_api.py +++ b/src/ifcopenshell-python/test/api/test_api.py @@ -115,3 +115,11 @@ class TestTemporarySupportForDeprecatedAPIArguments(test.bootstrap.IFC4): rel = ifcopenshell.api.run("nest.assign_object", self.file, related_object=subelement, relating_object=element) assert ifcopenshell.util.element.get_nest(subelement) == element assert rel.is_a("IfcRelNests") + + @deprecation_check + def test_unassigning_an_nesting(self): + element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcTask") + subelement1 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcTask") + ifcopenshell.api.run("nest.assign_object", self.file, related_objects=[subelement1], relating_object=element) + ifcopenshell.api.run("nest.unassign_object", self.file, related_object=subelement1) + assert ifcopenshell.util.element.get_nest(subelement1) is None