From 44afa0df5d05a6f15a9ae6906efc199cf703b21f Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Mon, 21 Mar 2022 17:28:16 +1100 Subject: [PATCH] Selector now uses classmethods to make it easier for scriptwriters to use --- .../ifcopenshell/util/selector.py | 62 +++++++++++-------- .../test/util/test_selector.py | 47 ++++++++++++++ 2 files changed, 83 insertions(+), 26 deletions(-) create mode 100644 src/ifcopenshell-python/test/util/test_selector.py diff --git a/src/ifcopenshell-python/ifcopenshell/util/selector.py b/src/ifcopenshell-python/ifcopenshell/util/selector.py index 326a36de56..b423cf4fbe 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/selector.py +++ b/src/ifcopenshell-python/ifcopenshell/util/selector.py @@ -23,8 +23,9 @@ import lark class Selector: - def parse(self, ifc_file, query): - self.file = ifc_file + @classmethod + def parse(cls, ifc_file, query): + cls.file = ifc_file l = lark.Lark( """start: query (lfunction query)* @@ -82,13 +83,14 @@ class Selector: ) start = l.parse(query) - return self.get_group(start) + return cls.get_group(start) - def get_group(self, group): + @classmethod + def get_group(cls, group): lfunction = None for child in group.children: if child.data == "query": - new_results = self.get_query(child) + new_results = cls.get_query(child) if not lfunction: results = new_results elif lfunction == "or": @@ -100,14 +102,16 @@ class Selector: lfunction = child.children[0].data return results - def get_query(self, query): + @classmethod + def get_query(cls, query): for child in query.children: if child.data == "selector": - return self.get_selector(child) + return cls.get_selector(child) elif child.data == "group": - return self.get_group(child) + return cls.get_group(child) - def get_selector(self, selector): + @classmethod + def get_selector(cls, selector): if len(selector.children) == 1: inverse_relationship = None class_or_guid_selector = selector.children[0] @@ -116,15 +120,16 @@ class Selector: class_or_guid_selector = selector.children[1] if class_or_guid_selector.data == "class_selector": - results = self.get_class_selector(class_or_guid_selector) + results = cls.get_class_selector(class_or_guid_selector) elif class_or_guid_selector.data == "guid_selector": - results = self.get_guid_selector(class_or_guid_selector) + results = cls.get_guid_selector(class_or_guid_selector) if not inverse_relationship: return results - return self.parse_inverse_relationship(results, inverse_relationship.children[0].data) + return cls.parse_inverse_relationship(results, inverse_relationship.children[0].data) - def parse_inverse_relationship(self, elements, inverse_relationship): + @classmethod + def parse_inverse_relationship(cls, elements, inverse_relationship): results = [] for element in elements: if inverse_relationship == "types": @@ -140,20 +145,22 @@ class Selector: results.append(relationship.RelatedBuildingElement) return results - def get_class_selector(self, class_selector): + @classmethod + def get_class_selector(cls, class_selector): if class_selector.children[0] == "COBie": - elements = ifcopenshell.util.fm.get_cobie_components(self.file) + elements = ifcopenshell.util.fm.get_cobie_components(cls.file) elif class_selector.children[0] == "COBieType": - elements = ifcopenshell.util.fm.get_cobie_types(self.file) + elements = ifcopenshell.util.fm.get_cobie_types(cls.file) elif class_selector.children[0] == "FMHEM": - elements = ifcopenshell.util.fm.get_fmhem_types(self.file) + elements = ifcopenshell.util.fm.get_fmhem_types(cls.file) else: - elements = self.file.by_type(class_selector.children[0]) + elements = cls.file.by_type(class_selector.children[0]) if len(class_selector.children) > 1 and class_selector.children[1].data == "filter": - return self.filter_elements(elements, class_selector.children[1]) + return cls.filter_elements(elements, class_selector.children[1]) return elements - def filter_elements(self, elements, filter_rule): + @classmethod + def filter_elements(cls, elements, filter_rule): results = [] key = filter_rule.children[0].children[0] if not isinstance(key, str): @@ -163,14 +170,15 @@ class Selector: comparison = filter_rule.children[1].children[0].data value = filter_rule.children[2].children[0][1:-1] for element in elements: - element_value = self.get_element_value(element, key) + element_value = cls.get_element_value(element, key) if element_value is None: continue - if not comparison or self.filter_element(element, element_value, comparison, value): + if not comparison or cls.filter_element(element, element_value, comparison, value): results.append(element) return results - def get_element_value(self, element, key): + @classmethod + def get_element_value(cls, element, key): if "." in key and key.split(".")[0] == "type": try: element = ifcopenshell.util.element.get_type(element) @@ -206,7 +214,8 @@ class Selector: if pset_name in psets and prop in psets[pset_name]: return psets[pset_name][prop] - def filter_element(self, element, element_value, comparison, value): + @classmethod + def filter_element(cls, element, element_value, comparison, value): if comparison == "equal": return str(element_value) == value elif comparison == "contains": @@ -221,5 +230,6 @@ class Selector: return element_value <= float(value) return False - def get_guid_selector(self, guid_selector): - return [self.file.by_id(guid_selector.children[0])] + @classmethod + def get_guid_selector(cls, guid_selector): + return [cls.file.by_id(guid_selector.children[0])] diff --git a/src/ifcopenshell-python/test/util/test_selector.py b/src/ifcopenshell-python/test/util/test_selector.py new file mode 100644 index 0000000000..683c3151f3 --- /dev/null +++ b/src/ifcopenshell-python/test/util/test_selector.py @@ -0,0 +1,47 @@ +# IfcOpenShell - IFC toolkit and geometry engine +# Copyright (C) 2022 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.selector as subject + + +class TestSelector(test.bootstrap.IFC4): + def test_selecting_by_class(self): + element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcSlab") + assert subject.Selector.parse(self.file, ".IfcWall") == [element] + + def test_selecting_by_globalid(self): + element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcSlab") + assert subject.Selector.parse(self.file, f"#{element.GlobalId}") == [element] + + def test_selecting_by_attribute(self): + element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + element.Name = "Foobar" + ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcSlab") + assert subject.Selector.parse(self.file, '.IfcElement[Name="Foobar"]') == [element] + assert subject.Selector.parse(self.file, '.IfcElement[Name="Foobaz"]') == [] + + def test_selecting_by_string_property(self): + element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + pset = ifcopenshell.api.run("pset.add_pset", self.file, product=element, name="Foo_Bar") + ifcopenshell.api.run("pset.edit_pset", self.file, pset=pset, properties={"Foo": "Bar"}) + assert subject.Selector.parse(self.file, '.IfcElement[Foo_Bar.Foo="Bar"]') == [element]