Selector element key queries now allow for regex matching, and single regex matches return that only item, not a list with one item in it.

This commit is contained in:
Dion Moult
2023-08-26 16:42:18 +10:00
parent a30b2dff31
commit 2298201e50
@@ -52,8 +52,8 @@ filter_elements_grammar = lark.Lark(
value: special | quoted_string | regex_string | unquoted_string value: special | quoted_string | regex_string | unquoted_string
unquoted_string: /[^.=\\s]+/ unquoted_string: /[^.=\\s]+/
quoted_string: ESCAPED_STRING
regex_string: "/" /[^\\/]+/ "/" regex_string: "/" /[^\\/]+/ "/"
quoted_string: ESCAPED_STRING
special: null | true | false special: null | true | false
@@ -94,19 +94,18 @@ filter_elements_grammar = lark.Lark(
) )
get_element_grammar = lark.Lark( get_element_grammar = lark.Lark(
"""start: WORD | ESCAPED_STRING | keys_regex | keys_quoted | keys_simple """start: keys
keys_regex: "r" ESCAPED_STRING ("." ESCAPED_STRING)*
keys_quoted: ESCAPED_STRING ("." ESCAPED_STRING)* keys: key ("." key)*
keys_simple: /[^\\W][^.=<>!%*\\]]*/ ("." /[^\\W][^.=<>!%*\\]]*/)* key: quoted_string | regex_string | unquoted_string
unquoted_string: /[^.=\\/\\s]+/
regex_string: "/" /[^\\/]+/ "/"
quoted_string: ESCAPED_STRING
// Embed common.lark for packaging // Embed common.lark for packaging
_STRING_INNER: /.*?/ _STRING_INNER: /.*?/
_STRING_ESC_INNER: _STRING_INNER /(?<!\\\\)(\\\\\\\\)*?/ _STRING_ESC_INNER: _STRING_INNER /(?<!\\\\)(\\\\\\\\)*?/
ESCAPED_STRING : "\\"" _STRING_ESC_INNER "\\"" ESCAPED_STRING : "\\"" _STRING_ESC_INNER "\\""
LCASE_LETTER: "a".."z"
UCASE_LETTER: "A".."Z"
LETTER: UCASE_LETTER | LCASE_LETTER
WORD: LETTER+
WS: /[ \\t\\f\\r\\n]/+ WS: /[ \\t\\f\\r\\n]/+
%ignore WS // Disregard spaces in text %ignore WS // Disregard spaces in text
@@ -210,14 +209,36 @@ class FormatTransformer(lark.Transformer):
) )
class GetElementTransformer(lark.Transformer):
def start(self, args):
return args[0]
def keys(self, args):
return args
def key(self, args):
return args[0]
def quoted_string(self, args):
return str(args[0])
def regex_string(self, args):
return re.compile(args[0])
def unquoted_string(self, args):
return str(args[0])
def ESCAPED_STRING(self, args):
return args[1:-1].replace("\\", "")
def format(query): def format(query):
return FormatTransformer().transform(format_grammar.parse(query)) return FormatTransformer().transform(format_grammar.parse(query))
def get_element_value(element, query): def get_element_value(element, query):
start = get_element_grammar.parse(query) keys = GetElementTransformer().transform(get_element_grammar.parse(query))
filter_query = Selector.parse_filter_query(start.children[0]) return Selector.get_element_value(element, keys)
return Selector.get_element_value(element, filter_query["keys"], filter_query["is_regex"])
def filter_elements(ifc_file, query, elements=None, edit_in_place=False): def filter_elements(ifc_file, query, elements=None, edit_in_place=False):
@@ -230,13 +251,14 @@ def filter_elements(ifc_file, query, elements=None, edit_in_place=False):
def set_element_value(ifc_file, element, query, value): def set_element_value(ifc_file, element, query, value):
start = get_element_grammar.parse(query) if isinstance(query, (list, tuple)):
filter_query = Selector.parse_filter_query(start.children[0]) keys = query
keys = filter_query["keys"] else:
is_regex = filter_query["is_regex"] keys = GetElementTransformer().transform(get_element_grammar.parse(query))
for i, key in enumerate(keys): for i, key in enumerate(keys):
key = key.strip() if isinstance(key, str):
key = key.strip()
if element is None: if element is None:
return return
if key == "type": if key == "type":
@@ -265,7 +287,7 @@ def set_element_value(ifc_file, element, query, value):
elif key == "site": elif key == "site":
element = ifcopenshell.util.element.get_container(element, ifc_class="IfcSite") element = ifcopenshell.util.element.get_container(element, ifc_class="IfcSite")
elif key == "class": elif key == "class":
if element.is_a() != value: if element.is_a().lower() != value.lower():
return ifcopenshell.util.schema.reassign_class(ifc_file, element, value) return ifcopenshell.util.schema.reassign_class(ifc_file, element, value)
elif key == "id": elif key == "id":
return return
@@ -273,42 +295,49 @@ def set_element_value(ifc_file, element, query, value):
if key == "Name" and element.is_a("IfcMaterialLayerSet"): if key == "Name" and element.is_a("IfcMaterialLayerSet"):
key = "LayerSetName" # This oddity in the IFC spec is annoying so we account for it. key = "LayerSetName" # This oddity in the IFC spec is annoying so we account for it.
if hasattr(element, key): if isinstance(key, str) and hasattr(element, key):
if getattr(element, key) != value: if getattr(element, key) != value:
return setattr(element, key, value) return setattr(element, key, value)
else: else:
# Try to extract pset # Try to extract pset
if is_regex: if isinstance(key, re.Pattern):
psets = ifcopenshell.util.element.get_psets(element) psets = ifcopenshell.util.element.get_psets(element)
matching_psets = [] matching_psets = []
for pset_name, pset in psets.items(): for pset_name, pset in psets.items():
if re.match(key, pset_name): if key.match(pset_name):
matching_psets.append(pset) matching_psets.append(pset)
result = matching_psets or None result = matching_psets or None
if result and len(result) == 1:
result = result[0]
else: else:
result = ifcopenshell.util.element.get_pset(element, key) result = ifcopenshell.util.element.get_pset(element, key)
if value and not result and len(keys) == i + 2: # The next key is the prop name if value and not result and len(keys) == i + 2: # The next key is the prop name
if "qto" in key.lower() or "quantity" in key.lower() or "quantities" in key.lower(): if "qto" in key.lower() or "quantity" in key.lower() or "quantities" in key.lower():
pset = ifcopenshell.api.run("pset.add_qto", ifc_file, product=element, name=key) pset = ifcopenshell.api.run("pset.add_qto", ifc_file, product=element, name=key)
else: else:
pset = ifcopenshell.api.run("pset.add_pset", ifc_file, product=element, name=key) pset = ifcopenshell.api.run("pset.add_pset", ifc_file, product=element, name=key)
result = {"id": pset.id()} result = {"id": pset.id()}
element = result element = result
elif isinstance(element, dict): # Such as from the result of a prior get_pset elif isinstance(element, dict): # Such as from the result of a prior get_pset
pset = ifc_file.by_id(element["id"]) pset = ifc_file.by_id(element["id"])
if is_regex: if isinstance(key, re.Pattern):
for prop, prop_value in element.items(): for prop, prop_value in element.items():
if re.match(key, prop): if key.match(prop):
if pset.is_a("IfcPropertySet") and prop_value != value: if pset.is_a("IfcPropertySet") and prop_value != value:
ifcopenshell.api.run("pset.edit_pset", ifc_file, pset=pset, properties={prop: value}) ifcopenshell.api.run("pset.edit_pset", ifc_file, pset=pset, properties={prop: value})
elif pset.is_a("IfcElementQuantity") and prop_value != float(value): elif pset.is_a("IfcElementQuantity") and prop_value != float(value):
ifcopenshell.api.run("pset.edit_qto", ifc_file, qto=pset, properties={prop: float(value)}) ifcopenshell.api.run("pset.edit_qto", ifc_file, qto=pset, properties={prop: float(value)})
elif pset.is_a("IfcPropertySet") and element[key] != value: elif pset.is_a("IfcPropertySet") and element.get(key, None) != value:
ifcopenshell.api.run("pset.edit_pset", ifc_file, pset=pset, properties={key: value}) ifcopenshell.api.run("pset.edit_pset", ifc_file, pset=pset, properties={key: value})
elif pset.is_a("IfcElementQuantity") and element[key] != float(value): elif pset.is_a("IfcElementQuantity"):
ifcopenshell.api.run("pset.edit_qto", ifc_file, qto=pset, properties={key: float(value)}) try:
value = float(value)
if element.get(key, None) != value:
ifcopenshell.api.run("pset.edit_qto", ifc_file, qto=pset, properties={key: value})
except:
pass
return return
elif isinstance(element, (list, tuple)): # If we use regex elif isinstance(element, (list, tuple)): # If we use regex
if key.isnumeric(): if key.isnumeric():
@@ -319,7 +348,7 @@ def set_element_value(ifc_file, element, query, value):
else: else:
results = [] results = []
for v in element: for v in element:
cls.set_element_value(v, keys[i + 1 :], is_regex=is_regex) cls.set_element_value(ifc_file, v, keys[i + 1 :], value)
return return
@@ -713,7 +742,9 @@ class Selector:
elif token_type == "NULL": elif token_type == "NULL":
value = None value = None
for element in elements: for element in elements:
element_value = cls.get_element_value(element, filter_query["keys"], is_regex=filter_query["is_regex"]) if filter_query["is_regex"]:
filter_query["keys"] = [re.compile(k) for k in filter_query["keys"]]
element_value = cls.get_element_value(element, filter_query["keys"])
if element_value is None and value is not None and "not" not in comparison: if element_value is None and value is not None and "not" not in comparison:
continue continue
if comparison and cls.filter_element(element, element_value, comparison, value, is_regex=is_regex): if comparison and cls.filter_element(element, element_value, comparison, value, is_regex=is_regex):
@@ -738,10 +769,11 @@ class Selector:
return {"keys": keys, "is_regex": is_regex} return {"keys": keys, "is_regex": is_regex}
@classmethod @classmethod
def get_element_value(cls, element, keys, is_regex=False): def get_element_value(cls, element, keys):
value = element value = element
for key in keys: for key in keys:
key = key.strip() if isinstance(key, str):
key = key.strip()
if value is None: if value is None:
return return
if key == "type": if key == "type":
@@ -785,37 +817,47 @@ class Selector:
if key == "Name" and value.is_a("IfcMaterialLayerSet"): if key == "Name" and value.is_a("IfcMaterialLayerSet"):
key = "LayerSetName" # This oddity in the IFC spec is annoying so we account for it. key = "LayerSetName" # This oddity in the IFC spec is annoying so we account for it.
attribute = getattr(value, key, None) if isinstance(key, re.Pattern):
attribute = None # Should we support regex attributes? Probably not for now.
else:
attribute = getattr(value, key, None)
if attribute is not None: if attribute is not None:
value = attribute value = attribute
else: else:
# Try to extract pset # Try to extract pset
if is_regex: if isinstance(key, re.Pattern):
psets = ifcopenshell.util.element.get_psets(value) psets = ifcopenshell.util.element.get_psets(value)
matching_psets = [] matching_psets = []
for pset_name, pset in psets.items(): for pset_name, pset in psets.items():
if re.match(key, pset_name): if key.match(pset_name):
del pset["id"]
matching_psets.append(pset) matching_psets.append(pset)
result = matching_psets or None result = matching_psets or None
if result and len(result) == 1:
result = result[0]
else: else:
result = ifcopenshell.util.element.get_pset(value, key) result = ifcopenshell.util.element.get_pset(value, key)
if result:
del result["id"]
value = result value = result
elif isinstance(value, dict): # Such as from the result of a prior get_pset elif isinstance(value, dict): # Such as from the result of a prior get_pset
if is_regex: if isinstance(key, re.Pattern):
results = [] results = []
for prop_name, prop_value in value.items(): for prop_name, prop_value in value.items():
if re.match(key, prop_name): if key.match(prop_name):
if isinstance(prop_value, (list, tuple)): if isinstance(prop_value, (list, tuple)):
results.extend(prop_value) results.extend(prop_value)
else: else:
results.append(prop_value) results.append(prop_value)
value = results value = results or None
if value and len(value) == 1:
value = value[0]
else: else:
value = value.get(key, None) value = value.get(key, None)
elif isinstance(value, (list, tuple)): # If we use regex elif isinstance(value, (list, tuple)): # If we use regex
if key.isnumeric(): if isinstance(key, str) and key.isnumeric():
try: try:
value = value[int(key)] value = value[int(key)]
except IndexError: except IndexError:
@@ -823,7 +865,7 @@ class Selector:
else: else:
results = [] results = []
for v in value: for v in value:
subvalue = cls.get_element_value(v, [key], is_regex=is_regex) subvalue = cls.get_element_value(v, [key])
if isinstance(subvalue, list): if isinstance(subvalue, list):
results.extend(subvalue) results.extend(subvalue)
else: else: