First Commit
This commit is contained in:
@@ -0,0 +1,344 @@
|
||||
try:
|
||||
from lark import Lark, Transformer
|
||||
from lark.exceptions import UnexpectedCharacters, UnexpectedEOF, UnexpectedToken
|
||||
|
||||
LARK_AVAILABLE = True
|
||||
except ImportError:
|
||||
LARK_AVAILABLE = False
|
||||
|
||||
import re
|
||||
|
||||
from typing import Union
|
||||
|
||||
if LARK_AVAILABLE:
|
||||
mvd_grammar = r"""
|
||||
start: entry+
|
||||
|
||||
entry: "ViewDefinition" "[" simple_value_list "]" -> view_definition
|
||||
| "Comment" "[" simple_value_list "]" -> comment
|
||||
| "ExchangeRequirement" "[" other_keyword "]" -> exchangerequirement
|
||||
| "Option" "[" other_keyword "]" -> option
|
||||
| GENERIC_KEYWORD "[" dynamic_option_word "]" -> dynamic_option
|
||||
|
||||
GENERIC_KEYWORD: /[A-Za-z0-9_]+/
|
||||
|
||||
simple_value_list: value ("," value)*
|
||||
|
||||
value_list_set: value_set (";" value_set)*
|
||||
|
||||
value_set: set_name ":" simple_value_list
|
||||
|
||||
set_name: /[A-Za-z0-9_]+/
|
||||
|
||||
value: /[A-Za-z0-9 _\.-]+/
|
||||
|
||||
other_keyword: /[^\[\]]+/
|
||||
|
||||
dynamic_option_word: /[^\[\]]+/
|
||||
|
||||
%import common.WS
|
||||
%ignore WS
|
||||
"""
|
||||
|
||||
parser = Lark(mvd_grammar, parser="lalr")
|
||||
|
||||
class DescriptionTransform(Transformer):
|
||||
def __init__(self):
|
||||
self.view_definitions = []
|
||||
self.keywords = set()
|
||||
self.comments = ""
|
||||
self.exchange_requirements = ""
|
||||
self.options = ""
|
||||
self._dynamic = {}
|
||||
|
||||
def view_definition(self, args):
|
||||
self.keywords.add("view_definitions")
|
||||
self.view_definitions.extend(args[0])
|
||||
|
||||
def store_text_attribute(self, args, keyword):
|
||||
self.keywords.add(keyword)
|
||||
setattr(
|
||||
self,
|
||||
keyword,
|
||||
" ".join(" ".join(str(child) for child in args[0].children).split()),
|
||||
)
|
||||
|
||||
def comment(self, args):
|
||||
self.keywords.add("comments")
|
||||
self.comments = args[0] if len(args[0]) > 1 else args[0][0]
|
||||
|
||||
def exchangerequirement(self, args):
|
||||
self.store_text_attribute(args, "exchange_requirements")
|
||||
|
||||
def option(self, args):
|
||||
if v := parse_semicolon_separated_kv(
|
||||
" ".join(" ".join(str(child) for child in args[0].children).split())
|
||||
):
|
||||
setattr(self, "options", v)
|
||||
else:
|
||||
self.store_text_attribute(args, "options")
|
||||
|
||||
def dynamic_option(self, args):
|
||||
try:
|
||||
original_keyword = str(args[0])
|
||||
key = original_keyword.lower()
|
||||
raw_text = args[1].children[0].value
|
||||
parsed_value = parse_semicolon_separated_kv(raw_text)
|
||||
self._dynamic[key] = (parsed_value, original_keyword)
|
||||
self.keywords.add(key)
|
||||
setattr(self, key, parsed_value)
|
||||
except Exception:
|
||||
setattr(self, key, None)
|
||||
|
||||
def simple_value_list(self, args):
|
||||
return [str(arg) for arg in args]
|
||||
|
||||
def value_list_set(self, args):
|
||||
return args
|
||||
|
||||
def value_set(self, args):
|
||||
return [str(args[0])] + args[1]
|
||||
|
||||
def value(self, args):
|
||||
return str(args[0])
|
||||
|
||||
def set_name(self, args):
|
||||
return str(args[0])
|
||||
|
||||
def parse_mvd(description):
|
||||
text = " ".join(description)
|
||||
parsed_description = DescriptionTransform()
|
||||
try:
|
||||
if not text:
|
||||
parsed_description.view_definitions = None
|
||||
return parsed_description
|
||||
parse_tree = parser.parse(text)
|
||||
parsed_description.transform(parse_tree)
|
||||
except (UnexpectedCharacters, UnexpectedEOF, UnexpectedToken):
|
||||
parsed_description.view_definitions = None
|
||||
return parsed_description
|
||||
|
||||
def parse_semicolon_separated_kv(
|
||||
text: str,
|
||||
) -> Union[dict[str, Union[str, list[str]]], None]:
|
||||
if not re.search(r"\w+\s*:\s*[^:]+", text):
|
||||
return None
|
||||
result = {}
|
||||
try:
|
||||
pairs = text.split(";")
|
||||
for pair in pairs:
|
||||
if ":" in pair:
|
||||
key, value = pair.split(":", 1)
|
||||
key = key.strip()
|
||||
values = [v.strip() for v in value.split(",")]
|
||||
result[key] = values[0] if len(values) == 1 else values
|
||||
return result
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
else:
|
||||
|
||||
def parse_mvd(description):
|
||||
return None
|
||||
|
||||
|
||||
class MvdInfo:
|
||||
def __init__(self, header):
|
||||
self._header = header
|
||||
self._parsed = None
|
||||
|
||||
def _ensure_parsed(self):
|
||||
if not LARK_AVAILABLE:
|
||||
return
|
||||
if self._parsed is None:
|
||||
description = self._header.file_description.description
|
||||
if not description:
|
||||
self._parsed = DescriptionTransform() # avoid AttributeError
|
||||
else:
|
||||
self._parsed = parse_mvd(description)
|
||||
|
||||
@property
|
||||
def description(self) -> list[str]:
|
||||
return self._header.file_description.description
|
||||
|
||||
@description.setter
|
||||
def description(self, new_description: list[str]):
|
||||
self._header.file_description.description = tuple(new_description)
|
||||
self._parsed = None
|
||||
|
||||
@property
|
||||
def view_definitions(self):
|
||||
self._ensure_parsed()
|
||||
if not self._parsed or self._parsed.view_definitions is None:
|
||||
return None #
|
||||
|
||||
vd = self._parsed.view_definitions
|
||||
vd_list = vd if isinstance(vd, list) else [vd] if vd else []
|
||||
return AutoCommitList(
|
||||
vd_list,
|
||||
callback=lambda val: (
|
||||
self._update_keyword("ViewDefinition", val),
|
||||
setattr(self, "_parsed", None),
|
||||
),
|
||||
formatter=lambda lst: ",".join(str(i) for i in lst),
|
||||
)
|
||||
|
||||
@view_definitions.setter
|
||||
def view_definitions(self, new_value: Union[str, list[str]]):
|
||||
if isinstance(new_value, list):
|
||||
value = ", ".join(new_value)
|
||||
else:
|
||||
value = str(new_value)
|
||||
self._update_keyword("ViewDefinition", value)
|
||||
|
||||
@property
|
||||
def comments(self):
|
||||
self._ensure_parsed()
|
||||
comments = self._parsed.comments
|
||||
comment_list = (
|
||||
comments if isinstance(comments, list) else [comments] if comments else []
|
||||
)
|
||||
return AutoCommitList(
|
||||
comment_list,
|
||||
callback=lambda val: self._update_keyword("Comment", val),
|
||||
formatter=lambda lst: ", ".join(str(i) for i in lst),
|
||||
)
|
||||
|
||||
@comments.setter
|
||||
def comments(self, new_value: Union[str, list[str]]):
|
||||
if isinstance(new_value, list):
|
||||
value = ", ".join(new_value)
|
||||
else:
|
||||
value = str(new_value)
|
||||
self._update_keyword("Comment", value)
|
||||
|
||||
@property
|
||||
def exchange_requirements(self):
|
||||
self._ensure_parsed()
|
||||
return self._parsed.exchange_requirements if self._parsed else None
|
||||
|
||||
@exchange_requirements.setter
|
||||
def exchange_requirements(self, new_value: str):
|
||||
self._update_keyword("ExchangeRequirement", new_value)
|
||||
|
||||
@property
|
||||
def options(self):
|
||||
self._ensure_parsed()
|
||||
if isinstance(self._parsed.options, dict):
|
||||
return DictionaryHandler(self._parsed.options, self, "Option")
|
||||
return self._parsed.options if self._parsed else None
|
||||
|
||||
@options.setter
|
||||
def options(self, new_value: str):
|
||||
self._update_keyword("Option", new_value)
|
||||
|
||||
@property
|
||||
def keywords(self):
|
||||
self._ensure_parsed()
|
||||
return self._parsed.keywords if self._parsed else set()
|
||||
|
||||
def _update_keyword(self, keyword: str, new_value: str):
|
||||
updated = False
|
||||
new_line = f"{keyword} [{new_value}]"
|
||||
lines = []
|
||||
for line in self.description:
|
||||
if line.strip().startswith(f"{keyword} ["):
|
||||
lines.append(new_line)
|
||||
updated = True
|
||||
else:
|
||||
lines.append(line)
|
||||
if not updated:
|
||||
lines.append(new_line)
|
||||
self.description = lines
|
||||
|
||||
def __getattr__(self, name):
|
||||
self._ensure_parsed()
|
||||
if hasattr(self._parsed, "_dynamic"):
|
||||
name_lc = name.lower()
|
||||
if name_lc in self._parsed._dynamic:
|
||||
value, original_keyword = self._parsed._dynamic[name_lc]
|
||||
return DictionaryHandler(value, self, original_keyword)
|
||||
raise AttributeError(f"'MvdInfo' object has no attribute '{name}'")
|
||||
|
||||
def __dir__(self):
|
||||
base = super().__dir__()
|
||||
if self._parsed and hasattr(self._parsed, "_dynamic"):
|
||||
return base + [kw for _, kw in self._parsed._dynamic.values()]
|
||||
return base
|
||||
|
||||
|
||||
class DictionaryHandler(dict):
|
||||
def __init__(self, initial_data, mvdinfo, keyword):
|
||||
super().__init__()
|
||||
self._mvdinfo = mvdinfo
|
||||
self._keyword = keyword
|
||||
for k, v in initial_data.items():
|
||||
if isinstance(v, list):
|
||||
super().__setitem__(k, AutoCommitList(v, self._commit))
|
||||
else:
|
||||
super().__setitem__(k, v)
|
||||
|
||||
def _commit(self):
|
||||
new_value = "; ".join(
|
||||
f"{k}: {', '.join(v) if isinstance(v, list) else v}"
|
||||
for k, v in self.items()
|
||||
)
|
||||
self._mvdinfo._update_keyword(self._keyword, new_value)
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
if isinstance(value, list):
|
||||
value = AutoCommitList(value, self._commit)
|
||||
super().__setitem__(key, value)
|
||||
self._commit()
|
||||
|
||||
def __delitem__(self, key):
|
||||
super().__delitem__(key)
|
||||
self._commit()
|
||||
|
||||
|
||||
class AutoCommitList(list):
|
||||
"ensures keyword attributes are written back to ifcopenshell.file.header"
|
||||
|
||||
def __init__(self, iterable, callback, formatter=None):
|
||||
super().__init__(iterable)
|
||||
self._callback = callback
|
||||
self._formatter = formatter
|
||||
|
||||
def _commit(self):
|
||||
if self._formatter:
|
||||
self._callback(self._formatter(self))
|
||||
else:
|
||||
self._callback()
|
||||
|
||||
def append(self, item):
|
||||
super().append(item)
|
||||
self._commit()
|
||||
|
||||
def extend(self, iterable):
|
||||
super().extend(iterable)
|
||||
self._commit()
|
||||
|
||||
def insert(self, index, item):
|
||||
super().insert(index, item)
|
||||
self._commit()
|
||||
|
||||
def remove(self, item):
|
||||
super().remove(item)
|
||||
self._commit()
|
||||
|
||||
def pop(self, index=-1):
|
||||
item = super().pop(index)
|
||||
self._commit()
|
||||
return item
|
||||
|
||||
def clear(self):
|
||||
super().clear()
|
||||
self._commit()
|
||||
|
||||
def __setitem__(self, index, value):
|
||||
super().__setitem__(index, value)
|
||||
self._commit()
|
||||
|
||||
def __delitem__(self, index):
|
||||
super().__delitem__(index)
|
||||
self._commit()
|
||||
Reference in New Issue
Block a user