From 81dcad6a146facf8b0579e68ebcd675902f7bb43 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Mon, 15 Apr 2024 16:00:40 +0500 Subject: [PATCH] library.assign_reference - support batching #4474 --- src/blenderbim/blenderbim/core/brick.py | 2 +- src/blenderbim/blenderbim/core/library.py | 2 +- src/blenderbim/test/core/test_brick.py | 8 +-- src/blenderbim/test/core/test_library.py | 2 +- .../ifcopenshell/api/__init__.py | 3 + .../api/library/assign_reference.py | 63 +++++++++++-------- .../api/library/unassign_reference.py | 2 +- .../test/api/library/test_assign_reference.py | 57 ++++++++++------- .../test/api/library/test_remove_reference.py | 2 +- .../api/library/test_unassign_reference.py | 2 +- src/ifcopenshell-python/test/api/test_api.py | 10 +++ .../test/util/test_element.py | 10 ++- 12 files changed, 103 insertions(+), 60 deletions(-) diff --git a/src/blenderbim/blenderbim/core/brick.py b/src/blenderbim/blenderbim/core/brick.py index c16b341b1c..58e7afed18 100644 --- a/src/blenderbim/blenderbim/core/brick.py +++ b/src/blenderbim/blenderbim/core/brick.py @@ -74,7 +74,7 @@ def assign_brick_reference(ifc, brick, element=None, library=None, brick_uri=Non if not reference: reference = ifc.run("library.add_reference", library=library) ifc.run("library.edit_reference", reference=reference, attributes=brick.export_brick_attributes(brick_uri)) - ifc.run("library.assign_reference", product=element, reference=reference) + ifc.run("library.assign_reference", products=[element], reference=reference) project = brick.get_brickifc_project() if not project: project = brick.add_brickifc_project(brick.get_namespace(brick_uri)) diff --git a/src/blenderbim/blenderbim/core/library.py b/src/blenderbim/blenderbim/core/library.py index eba4fc770a..c1103f5bf9 100644 --- a/src/blenderbim/blenderbim/core/library.py +++ b/src/blenderbim/blenderbim/core/library.py @@ -83,7 +83,7 @@ def edit_library_reference(ifc, library): def assign_library_reference(ifc, obj=None, reference=None): - ifc.run("library.assign_reference", product=ifc.get_entity(obj), reference=reference) + ifc.run("library.assign_reference", products=[ifc.get_entity(obj)], reference=reference) def unassign_library_reference(ifc, obj=None, reference=None): diff --git a/src/blenderbim/test/core/test_brick.py b/src/blenderbim/test/core/test_brick.py index 157a363682..e410fbe9b9 100644 --- a/src/blenderbim/test/core/test_brick.py +++ b/src/blenderbim/test/core/test_brick.py @@ -124,21 +124,21 @@ class TestAssignBrickReference: ifc.run("library.add_reference", library="library").should_be_called().will_return("reference") brick.export_brick_attributes("brick_uri").should_be_called().will_return("attributes") ifc.run("library.edit_reference", reference="reference", attributes="attributes").should_be_called() - ifc.run("library.assign_reference", product="element", reference="reference").should_be_called() + ifc.run("library.assign_reference", products=["element"], reference="reference").should_be_called() brick.get_brickifc_project().should_be_called().will_return("project") brick.add_brickifc_reference("brick_uri", "element", "project").should_be_called() subject.assign_brick_reference(ifc, brick, element="element", library="library", brick_uri="brick_uri") def test_assigning_to_an_existing_reference(self, ifc, brick): brick.get_library_brick_reference("library", "brick_uri").should_be_called().will_return("reference") - ifc.run("library.assign_reference", product="element", reference="reference").should_be_called() + ifc.run("library.assign_reference", products=["element"], reference="reference").should_be_called() brick.get_brickifc_project().should_be_called().will_return("project") brick.add_brickifc_reference("brick_uri", "element", "project").should_be_called() subject.assign_brick_reference(ifc, brick, element="element", library="library", brick_uri="brick_uri") def test_adding_a_brickifc_project_if_it_doesnt_exist(self, ifc, brick): brick.get_library_brick_reference("library", "brick_uri").should_be_called().will_return("reference") - ifc.run("library.assign_reference", product="element", reference="reference").should_be_called() + ifc.run("library.assign_reference", products=["element"], reference="reference").should_be_called() brick.get_brickifc_project().should_be_called().will_return(None) brick.get_namespace("brick_uri").should_be_called().will_return("namespace") brick.add_brickifc_project("namespace").should_be_called().will_return("project") @@ -277,4 +277,4 @@ class TestSetBrickListRoot: class TestRemoveBrickRelation: def test_run(self, brick): brick.remove_relation("brick_uri", "predicate", "object").should_be_called() - subject.remove_brick_relation(brick, brick_uri="brick_uri", predicate="predicate", object="object") \ No newline at end of file + subject.remove_brick_relation(brick, brick_uri="brick_uri", predicate="predicate", object="object") diff --git a/src/blenderbim/test/core/test_library.py b/src/blenderbim/test/core/test_library.py index 2809722e81..9e4f9ffee6 100644 --- a/src/blenderbim/test/core/test_library.py +++ b/src/blenderbim/test/core/test_library.py @@ -114,7 +114,7 @@ class TestEditLibraryReference: class TestAssignLibraryReference: def test_run(self, ifc): ifc.get_entity("obj").should_be_called().will_return("product") - ifc.run("library.assign_reference", product="product", reference="reference").should_be_called() + ifc.run("library.assign_reference", products=["product"], reference="reference").should_be_called() subject.assign_library_reference(ifc, obj="obj", reference="reference") diff --git a/src/ifcopenshell-python/ifcopenshell/api/__init__.py b/src/ifcopenshell-python/ifcopenshell/api/__init__.py index d2027e06e1..b9e60e65a6 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/__init__.py +++ b/src/ifcopenshell-python/ifcopenshell/api/__init__.py @@ -93,6 +93,9 @@ ARGUMENTS_DEPRECATION = { "classification.remove_reference": partial( batching_argument_deprecation, prev_argument="product", new_argument="products" ), + "library.assign_reference": partial( + batching_argument_deprecation, prev_argument="product", new_argument="products" + ), } diff --git a/src/ifcopenshell-python/ifcopenshell/api/library/assign_reference.py b/src/ifcopenshell-python/ifcopenshell/api/library/assign_reference.py index a1f3f83d17..8ee4a21eb5 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/library/assign_reference.py +++ b/src/ifcopenshell-python/ifcopenshell/api/library/assign_reference.py @@ -17,23 +17,30 @@ # along with IfcOpenShell. If not, see . import ifcopenshell +import ifcopenshell.api +import ifcopenshell.util.element +from typing import Union class Usecase: - def __init__(self, file, product=None, reference=None): - """Associates a product with a library reference + def __init__( + self, file: ifcopenshell.file, products: ifcopenshell.entity_instance, reference: ifcopenshell.entity_instance + ): + """Associates a list products with a library reference A product may be associated with zero, one, or many references across multiple libraries. See ifcopenshell.api.library.add_reference for more detail about how references work. - :param product: The IfcProduct you want to associate with the reference - :type product: ifcopenshell.entity_instance.entity_instance + :param products: The list of IfcProducts you want to associate with the reference + :type products: list[ifcopenshell.entity_instance.entity_instance] :param reference: The IfcLibraryReference you want the product to be associated with. :type reference: ifcopenshell.entity_instance.entity_instance :return: The IfcRelAssociatesLibrary relationship entity - :rtype: ifcopenshell.entity_instance.entity_instance + or `None` if `products` was an empty list or all products were + already assigned to the `reference`. + :rtype: Union[ifcopenshell.entity_instance.entity_instance, None] Example: @@ -51,40 +58,46 @@ class Usecase: ifc_class="IfcUnitaryEquipment", predefined_type="AIRHANDLER") # And now assign the IFC model's AHU with its Brickschema counterpart - ifcopenshell.api.run("library.assign_reference", model, reference=reference, product=ahu) + ifcopenshell.api.run("library.assign_reference", model, reference=reference, products=[ahu]) """ self.file = file self.settings = { - "product": product, + "products": products, "reference": reference, } - def execute(self): + def execute(self) -> Union[ifcopenshell.entity_instance, None]: + # TODO: do we need to support non-ifcroot elements like we do in classification.add_reference? + + referenced_elements = ifcopenshell.util.element.get_referenced_elements(self.settings["reference"]) + products: set[ifcopenshell.entity_instance] = set(self.settings["products"]) + products = products - referenced_elements + + if not products: + return + if self.file.schema == "IFC2X3": - rels = self.get_ifc2x3_rels() + rel = next( + ( + r + for r in self.file.by_type("IfcRelAssociatesLibrary") + if r.RelatingLibrary == self.settings["reference"] + ), + None, + ) else: - rels = self.settings["reference"].LibraryRefForObjects - if not rels: + rel = next(iter(self.settings["reference"].LibraryRefForObjects), None) + + if not rel: return self.file.create_entity( "IfcRelAssociatesLibrary", GlobalId=ifcopenshell.guid.new(), OwnerHistory=ifcopenshell.api.run("owner.create_owner_history", self.file), - RelatedObjects=[self.settings["product"]], + RelatedObjects=list(products), RelatingLibrary=self.settings["reference"], ) - for rel in rels: - if self.settings["product"] in rel.RelatedObjects: - return rel - - rel = rels[0] - related_objects = list(rel.RelatedObjects) - related_objects.append(self.settings["product"]) - rel.RelatedObjects = related_objects + related_objects = set(rel.RelatedObjects) | products + rel.RelatedObjects = list(related_objects) ifcopenshell.api.run("owner.update_owner_history", self.file, element=rel) return rel - - def get_ifc2x3_rels(self): - return [ - r for r in self.file.by_type("IfcRelAssociatesLibrary") if r.RelatingLibrary == self.settings["reference"] - ] diff --git a/src/ifcopenshell-python/ifcopenshell/api/library/unassign_reference.py b/src/ifcopenshell-python/ifcopenshell/api/library/unassign_reference.py index 639a3dd35b..5bd04bc581 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/library/unassign_reference.py +++ b/src/ifcopenshell-python/ifcopenshell/api/library/unassign_reference.py @@ -49,7 +49,7 @@ class Usecase: ifc_class="IfcUnitaryEquipment", predefined_type="AIRHANDLER") # And now assign the IFC model's AHU with its Brickschema counterpart - ifcopenshell.api.run("library.assign_reference", model, reference=reference, product=ahu) + ifcopenshell.api.run("library.assign_reference", model, reference=reference, products=[ahu]) # Let's change our mind and unassign it. ifcopenshell.api.run("library.unassign_reference", model, reference=reference, product=ahu) diff --git a/src/ifcopenshell-python/test/api/library/test_assign_reference.py b/src/ifcopenshell-python/test/api/library/test_assign_reference.py index 1133b592d6..550df3940f 100644 --- a/src/ifcopenshell-python/test/api/library/test_assign_reference.py +++ b/src/ifcopenshell-python/test/api/library/test_assign_reference.py @@ -20,39 +20,48 @@ import test.bootstrap import ifcopenshell.api +def validate_ifc_file(ifc_file: ifcopenshell.file, use_json=True): + import ifcopenshell + import ifcopenshell.validate + + if use_json: + logger = ifcopenshell.validate.json_logger() + else: + import logging + + logger = logging.getLogger("validate") + logger.setLevel(logging.DEBUG) + + ifcopenshell.validate.validate(ifc_file, logger, express_rules=True) + if use_json: + if logger.statements: + from pprint import pprint + + pprint(logger.statements) + else: + print("IFC is completely valid.") + + class TestAssignReference(test.bootstrap.IFC4): def test_assigning_a_reference(self): reference = self.file.createIfcLibraryReference() product = self.file.createIfcWall() product2 = self.file.createIfcWall() - ifcopenshell.api.run("library.assign_reference", self.file, product=product, reference=reference) - assert reference.LibraryRefForObjects[0].RelatedObjects == (product,) - ifcopenshell.api.run("library.assign_reference", self.file, product=product2, reference=reference) - assert reference.LibraryRefForObjects[0].RelatedObjects == (product, product2) + product3 = self.file.createIfcWall() + ifcopenshell.api.run("library.assign_reference", self.file, products=[product], reference=reference) + rel = self.file.by_type("IfcRelAssociatesLibrary")[0] + assert rel.RelatedObjects == (product,) + ifcopenshell.api.run("library.assign_reference", self.file, products=[product2, product3], reference=reference) + assert set(rel.RelatedObjects) == set((product, product2, product3)) def test_not_assigning_twice(self): reference = self.file.createIfcLibraryReference() product = self.file.createIfcWall() - ifcopenshell.api.run("library.assign_reference", self.file, product=product, reference=reference) - ifcopenshell.api.run("library.assign_reference", self.file, product=product, reference=reference) - assert reference.LibraryRefForObjects[0].RelatedObjects == (product,) - - -class TestAssignReferenceIFC2X3(test.bootstrap.IFC2X3): - def test_assigning_a_reference(self): - reference = self.file.createIfcLibraryReference() - product = self.file.createIfcWall() - product2 = self.file.createIfcWall() - ifcopenshell.api.run("library.assign_reference", self.file, product=product, reference=reference) + ifcopenshell.api.run("library.assign_reference", self.file, products=[product], reference=reference) + ifcopenshell.api.run("library.assign_reference", self.file, products=[product], reference=reference) rel = self.file.by_type("IfcRelAssociatesLibrary")[0] assert rel.RelatedObjects == (product,) - ifcopenshell.api.run("library.assign_reference", self.file, product=product2, reference=reference) - assert rel.RelatedObjects == (product, product2) - def test_not_assigning_twice(self): - reference = self.file.createIfcLibraryReference() - product = self.file.createIfcWall() - ifcopenshell.api.run("library.assign_reference", self.file, product=product, reference=reference) - ifcopenshell.api.run("library.assign_reference", self.file, product=product, reference=reference) - rel = self.file.by_type("IfcRelAssociatesLibrary")[0] - assert rel.RelatedObjects == (product,) + +class TestAssignReferenceIFC2X3(test.bootstrap.IFC2X3, TestAssignReference): + pass diff --git a/src/ifcopenshell-python/test/api/library/test_remove_reference.py b/src/ifcopenshell-python/test/api/library/test_remove_reference.py index 26a9106411..a98ed2f083 100644 --- a/src/ifcopenshell-python/test/api/library/test_remove_reference.py +++ b/src/ifcopenshell-python/test/api/library/test_remove_reference.py @@ -24,7 +24,7 @@ class TestRemoveReference(test.bootstrap.IFC4): def test_removing_a_reference(self): reference = self.file.createIfcLibraryReference() product = self.file.createIfcWall() - ifcopenshell.api.run("library.assign_reference", self.file, product=product, reference=reference) + ifcopenshell.api.run("library.assign_reference", self.file, products=[product], reference=reference) ifcopenshell.api.run("library.remove_reference", self.file, reference=reference) assert len(self.file.by_type("IfcLibraryReference")) == 0 assert len(self.file.by_type("IfcRelAssociatesLibrary")) == 0 diff --git a/src/ifcopenshell-python/test/api/library/test_unassign_reference.py b/src/ifcopenshell-python/test/api/library/test_unassign_reference.py index 900dcdcc12..598022cbab 100644 --- a/src/ifcopenshell-python/test/api/library/test_unassign_reference.py +++ b/src/ifcopenshell-python/test/api/library/test_unassign_reference.py @@ -24,6 +24,6 @@ class TestUnassignReference(test.bootstrap.IFC4): def test_unassigning_a_reference(self): reference = self.file.createIfcLibraryReference() product = self.file.createIfcWall() - ifcopenshell.api.run("library.assign_reference", self.file, product=product, reference=reference) + ifcopenshell.api.run("library.assign_reference", self.file, products=[product], reference=reference) ifcopenshell.api.run("library.unassign_reference", self.file, product=product, reference=reference) assert len(self.file.by_type("IfcRelAssociatesLibrary")) == 0 diff --git a/src/ifcopenshell-python/test/api/test_api.py b/src/ifcopenshell-python/test/api/test_api.py index 0156bfa6ad..ee20435b33 100644 --- a/src/ifcopenshell-python/test/api/test_api.py +++ b/src/ifcopenshell-python/test/api/test_api.py @@ -236,3 +236,13 @@ class TestTemporarySupportForDeprecatedAPIArguments(test.bootstrap.IFC4): ifcopenshell.api.run("classification.remove_reference", self.file, product=element, reference=reference) assert len(ifcopenshell.util.classification.get_references(element)) == 0 assert len(self.file.by_type("IfcClassificationReference")) == 0 + + @deprecation_check + def test_assigning_a_reference(self): + reference = self.file.createIfcLibraryReference() + product = self.file.createIfcWall() + product2 = self.file.createIfcWall() + ifcopenshell.api.run("library.assign_reference", self.file, product=product, reference=reference) + assert reference.LibraryRefForObjects[0].RelatedObjects == (product,) + ifcopenshell.api.run("library.assign_reference", self.file, product=product2, reference=reference) + assert set(reference.LibraryRefForObjects[0].RelatedObjects) == set((product, product2)) diff --git a/src/ifcopenshell-python/test/util/test_element.py b/src/ifcopenshell-python/test/util/test_element.py index a29c849e76..1a701f5ec9 100644 --- a/src/ifcopenshell-python/test/util/test_element.py +++ b/src/ifcopenshell-python/test/util/test_element.py @@ -788,7 +788,6 @@ class TestGetNestIFC2X3(test.bootstrap.IFC2X3, TestGetNestIFC4): class TestGetReferencedElements(test.bootstrap.IFC4): # TODO: test other references: # IfcDocumentReference - # IfcLibraryReference # IfcExternallyDefinedHatchStyle # IfcExternallyDefinedSurfaceStyle # IfcExternallyDefinedTextFont @@ -808,6 +807,15 @@ class TestGetReferencedElements(test.bootstrap.IFC4): ) assert subject.get_referenced_elements(reference) == set(elements) + def test_get_elements_referenced_by_library_reference(self): + reference = self.file.createIfcLibraryReference() + elements = [ + ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall"), + ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall"), + ] + ifcopenshell.api.run("library.assign_reference", self.file, reference=reference, products=elements) + assert subject.get_referenced_elements(reference) == set(elements) + class TestGetReferencedElementsIFC2X3(test.bootstrap.IFC2X3, TestGetReferencedElements): pass