From 4ebcf8133c5ba9d8983e5aafd303a3664a7aec92 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Thu, 9 Mar 2023 11:07:13 +1100 Subject: [PATCH] Fix #2435. The selector now supports regex filtering. --- .../ifcopenshell/util/selector.py | 71 +++++++++++++------ .../test/util/test_selector.py | 23 ++++++ 2 files changed, 72 insertions(+), 22 deletions(-) diff --git a/src/ifcopenshell-python/ifcopenshell/util/selector.py b/src/ifcopenshell-python/ifcopenshell/util/selector.py index a0fe77e002..56c5df4899 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/selector.py +++ b/src/ifcopenshell-python/ifcopenshell/util/selector.py @@ -16,10 +16,11 @@ # You should have received a copy of the GNU Lesser General Public License # along with IfcOpenShell. If not, see . +import re +import lark import ifcopenshell.util import ifcopenshell.util.fm import ifcopenshell.util.element -import lark class Selector: @@ -35,8 +36,9 @@ class Selector: guid_selector: "#" /[0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_$]{22}/ class_selector: "." WORD filter ? filter: "[" filter_key (comparison filter_value)? "]" - filter_key: WORD | ESCAPED_STRING | keys_quoted | keys_simple + filter_key: WORD | ESCAPED_STRING | keys_regex | keys_quoted | keys_simple filter_value: ESCAPED_STRING | SIGNED_FLOAT | SIGNED_INT | BOOLEAN | NULL + keys_regex: "r" ESCAPED_STRING "." ESCAPED_STRING keys_quoted: ESCAPED_STRING "." ESCAPED_STRING keys_simple: /[^\\W][^.=<>]*[^\\W]/ "." /[^\\W][^.=<>]*[^\\W]/ lfunction: and | or @@ -179,15 +181,17 @@ class Selector: @classmethod def filter_elements(cls, elements, filter_rule): results = [] - key = filter_rule.children[0].children[0] - if not isinstance(key, str): - k0 = key.children[0] - if k0.type == "ESCAPED_STRING": - k0 = k0[1:-1].replace("\\\"", '"') - k1 = key.children[1] - if k1.type == "ESCAPED_STRING": - k1 = k1[1:-1].replace("\\\"", '"') - key = [k0, k1] + keys = filter_rule.children[0].children[0] + is_regex = False + if isinstance(keys, str): + keys = [keys] + elif keys.data == "keys_regex": + is_regex = True + keys = [keys.children[0][1:-1].replace("\\\"", '"'), keys.children[1][1:-1].replace("\\\"", '"')] + elif keys.data == "keys_quoted": + keys = [keys.children[0][1:-1].replace("\\\"", '"'), keys.children[1][1:-1].replace("\\\"", '"')] + elif keys.data == "keys_simple": + keys = [keys.children[0], keys.children[1]] comparison = value = None if len(filter_rule.children) > 1: comparison = filter_rule.children[1].children[0].data @@ -205,7 +209,7 @@ class Selector: elif token_type == "NULL": value = None for element in elements: - element_value = cls.get_element_value(element, key, value) + element_value = cls.get_element_value(element, keys, is_regex=is_regex) if element_value is None and value is not None and "not" not in comparison: continue if comparison and cls.filter_element( @@ -217,9 +221,7 @@ class Selector: return results @classmethod - def get_element_value(cls, element, keys, value=None): - if not isinstance(keys, (tuple, list)): - keys = [keys] + def get_element_value(cls, element, keys, is_regex=False): value = element for key in keys: if key == "type": @@ -229,16 +231,41 @@ class Selector: elif key == "container": value = ifcopenshell.util.element.get_container(element) elif isinstance(value, ifcopenshell.entity_instance): - result = value.get_info().get(key, None) - if result is not None: - return result - result = ifcopenshell.util.element.get_pset(value, key) - if result is None: - value = None + attribute = value.get_info().get(key, None) + + if attribute is not None: + value = attribute else: + # Try to extract pset + if is_regex: + psets = ifcopenshell.util.element.get_psets(value) + matching_psets = [] + for pset_name, pset in psets.items(): + if re.match(key, pset_name): + matching_psets.append(pset) + result = matching_psets or None + else: + result = ifcopenshell.util.element.get_pset(value, key) + value = result elif isinstance(value, dict): # Such as from the result of a prior get_pset - return value.get(key, None) + if is_regex: + results = [] + for prop_name, prop_value in value.items(): + if re.match(key, prop_name): + results.append(prop_value) + value = results + else: + value = value.get(key, None) + elif isinstance(value, list): # If we use regex + results = [] + for v in value: + subvalue = cls.get_element_value(v, [key], is_regex=is_regex) + if isinstance(subvalue, list): + results.extend(subvalue) + else: + results.append(subvalue) + value = results return value @classmethod diff --git a/src/ifcopenshell-python/test/util/test_selector.py b/src/ifcopenshell-python/test/util/test_selector.py index 54cf9622d8..0068845567 100644 --- a/src/ifcopenshell-python/test/util/test_selector.py +++ b/src/ifcopenshell-python/test/util/test_selector.py @@ -188,3 +188,26 @@ class TestSelector(test.bootstrap.IFC4): element2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcSlab") assert subject.Selector.parse(self.file, ".IfcWall", elements=[element]) assert not subject.Selector.parse(self.file, ".IfcWall", elements=[element2]) + + def test_selecting_a_property_via_a_wildcard_pset_name(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="Foobar") + ifcopenshell.api.run("pset.edit_pset", self.file, pset=pset, properties={"Foo": "Bar"}) + pset = ifcopenshell.api.run("pset.add_pset", self.file, product=element, name="Foobaz") + ifcopenshell.api.run("pset.edit_pset", self.file, pset=pset, properties={"Foo": "Baz"}) + assert subject.Selector.parse(self.file, '.IfcElement[r"Foo.*"."Foo"="Bar"]') == [element] + + def test_selecting_a_property_via_a_wildcard_property_name(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="Foobar") + ifcopenshell.api.run("pset.edit_pset", self.file, pset=pset, properties={"Foo": "Bar"}) + pset = ifcopenshell.api.run("pset.add_pset", self.file, product=element, name="Foobaz") + ifcopenshell.api.run("pset.edit_pset", self.file, pset=pset, properties={"Foo": "Baz"}) + assert subject.Selector.parse(self.file, '.IfcElement[r"Foo.*"."F.*"="Bar"]') == [element] + + def test_selecting_a_property_via_a_type_attribute(self): + element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + element_type = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWallType") + element_type.Name = "Foo" + ifcopenshell.api.run("type.assign_type", self.file, related_object=element, relating_type=element_type) + assert set(subject.Selector.parse(self.file, '.IfcWall[type.Name="Foo"]')) == {element}