mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-09-23 14:06:26 +00:00
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:
@@ -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:
|
||||||
|
|||||||
Reference in New Issue
Block a user