mvd info to module, add testing, setattr & getattr

This commit is contained in:
Geert Hesselink
2025-04-20 17:51:31 +01:00
committed by Thomas Krijnen
parent 9f11568aff
commit cbe8fc1ebd
9 changed files with 633 additions and 164 deletions
+9 -164
View File
@@ -37,9 +37,7 @@ from typing import Any
from . import ifcopenshell_wrapper
from .entity_instance import entity_instance
from lark import Lark, Transformer
from lark.exceptions import UnexpectedCharacters, UnexpectedEOF, UnexpectedToken
from ifcopenshell.util.mvd_info import MvdInfo, LARK_AVAILABLE
if TYPE_CHECKING:
import ifcopenshell.util.schema
@@ -211,106 +209,6 @@ NO_HEADER = ifcopenshell_wrapper.file_open_status.NO_HEADER
UNSUPPORTED_SCHEMA = ifcopenshell_wrapper.file_open_status.UNSUPPORTED_SCHEMA
INVALID_SYNTAX = ifcopenshell_wrapper.file_open_status.INVALID_SYNTAX
mvd_grammar = r'''
start: entry+
entry: "ViewDefinition" "[" simple_value_list "]" -> view_definition
| "Comment" "[" comment_text "]" -> comment
| GENERIC_KEYWORD "[" value_list_set "]" -> dynamic_option
%declare COMMENT_TEXT // Ensure Lark treats it with higher priority
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 _\.-]+/
comment_text: /[^\[\]]+/ // Free text inside COMMENT brackets
%import common.WS
%ignore WS
'''
parser = Lark(mvd_grammar, parser='lalr')
class DescriptionTransform(Transformer):
def __init__(self):
self.mvd = []
self.keywords = set()
self.comments = ""
self.exchangerequirement = ""
self.option = ''
def view_definition(self, args):
self.keywords.add('mvd')
self.mvd.extend(args[0])
def dynamic_option(self, args):
"""
e.g. in case of 'Remark' as optional keyword in the description
The value can be retrieved through DescriptionTransform.remark
"""
key = str(args[0]).lower()
attr_name = f"{key}"
if attr_name not in self.keywords:
setattr(self, attr_name, {})
self.keywords.add(attr_name)
dynamic_dict = getattr(self, attr_name)
for value_set in args[1]:
set_name, *values = value_set
dynamic_dict[set_name] = values if len(values) > 1 else values[0]
def comment(self, args):
self.keywords.add('comment')
self.comments = " ".join(str(child) for child in args[0].children).strip()
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])
@property
def other_keywords(self):
""""
The predefined keywords are 'ViewDefinition', 'Option', 'Comment', 'ExchangeRequirement' and 'Option'
Keywords in the description not from this lists are returned
"""
return {k for k in self.keywords if k not in {'mvd', 'comment', 'exchangerequirement', 'option'}}
def parse_mvd(description):
text = ' '.join(description)
parser = Lark(mvd_grammar, parser='lalr')
parsed_description = DescriptionTransform()
try:
if not text:
parsed_description.mvd = None
return parsed_description
parse_tree = parser.parse(text)
parsed_description.transform(parse_tree)
except (UnexpectedCharacters, UnexpectedEOF, UnexpectedToken) as e:
parsed_description.mvd = None
return parsed_description
class file:
"""Base class for containing IFC files.
@@ -409,7 +307,6 @@ class file:
import weakref
file_dict[self.file_pointer()] = weakref.ref(self)
self.parsed_description = parse_mvd(self.wrapped_data.header.file_description.description)
def __del__(self) -> None:
# Avoid infinite recursion if file is failed to initialize
@@ -569,68 +466,16 @@ class file:
number = re.search(prefix + r"(\d)", schema)
version.append(int(number.group(1)) if number else 0)
return tuple(version)
@property
def mvd(self) -> str:
"""
View Definition supported by the exporting application that is reflected in the IFC file
For example:
“CoordinationView”
| “PresentationView”
| “StructuralAnalysisView”
| “FMHandOverView”
| “QuantityTakeOffAddOnView”
| “SpaceBoundary1stLevelAddOnView”
| “SpaceBoundary2ndLevelAddOnView”
"""
return ','.join(self.parsed_description.mvd)
@property
def mvd_comments(self) -> str:
"""
For example; 'Comment [This
export contains Boolean Operation geometry that may not be fully
supported by other application]'
f.mvd_comments
'This export contains Boolean Operation geometry that may not be fully supported by other application'
"""
return str(self.parsed_description.comments)
@property
def mvd_exchange_requirements(self) -> str:
return str(self.parsed_description.exchangerequirement)
@property
def mvd_options(self) -> str:
"""
Option being used by the exporting application for e.g. quality control and debugging
a. this is an optional field providing informal information, the values for the Option keyword
reflect the export settings of the exporting software application, those settings may be
specific to the exporting software
For example:
> "{'ExcludedObjects': [' Stair', ' Ramp', ' Space'], 'SplitLevel': ' On'}"
"""
return str(self.parsed_description.option)
@property
def mvd_keywords(self) -> str:
"""
Returns all keywords from description, e.g.
f.mvd_keywords
"{'exchangerequirement', 'mvd', 'option', 'comment', 'remark'}"_
"""
return str(self.parsed_description.keywords)
@property
def mvd_optional_keyword_fields(self) -> str:
""""
Returns fields from keywords other than 'ViewDefinition', 'Option', 'Comment' and 'ExchangeRequirement'.
For example, in case of
'REMARK [SomeKey: SomeValue; AnotherKey: AnotherValue]'
file.mvd_optional_keywords_fields == "{'remark': {'SomeKey': ' SomeValue', 'AnotherKey': ' AnotherValue'}}"
"""
return str({kw: getattr(self.parsed_description, kw) for kw in self.parsed_description.other_keywords})
def mvd(self):
if not LARK_AVAILABLE:
return None
file_description = self.wrapped_data.header.file_description
return MvdInfo(
get_description=lambda: file_description.description,
set_description=lambda d: setattr(file_description, "description", tuple(d))
)
def __getattr__(self, attr) -> Union[Any, Callable[..., ifcopenshell.entity_instance]]:
if attr[0:6] == "create":
@@ -0,0 +1,310 @@
try:
from lark import Lark, Transformer
from lark.exceptions import UnexpectedCharacters, UnexpectedEOF, UnexpectedToken
LARK_AVAILABLE = True
except ImportError:
LARK_AVAILABLE = False
from typing import Callable
import re
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) -> dict[str, 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, get_description: Callable[[], list[str]], set_description: Callable[[list[str]], None]):
self._get_description = get_description
self._set_description = set_description
self._parsed = None
def _ensure_parsed(self):
if not LARK_AVAILABLE:
return
if self._parsed is None:
self._parsed = parse_mvd(self.description)
@property
def description(self) -> list[str]:
return self._get_description()
@description.setter
def description(self, new_description: list[str]):
self._set_description(new_description)
self._parsed = None
@property
def view_definitions(self):
self._ensure_parsed()
return ', '.join(' '.join(item.split()) for item in self._parsed.view_definitions) if self._parsed else None
@view_definitions.setter
def view_definitions(self, new_value: str):
self._update_keyword("ViewDefinition", new_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: 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()
@@ -0,0 +1,30 @@
ISO-10303-21;
HEADER;
FILE_DESCRIPTION(('ViewDefinition [ReferenceView_V1.2]', 'Comment [Any]'),'2;1');
FILE_NAME('Header.ifc','2025-02-13T15:58:45',('tricott'),('Trimble Inc.'),'TrimBimToIFC rel. 4.0.2','Trimble Inc. - SketchUp - 2025.0','IFC4 model');
FILE_SCHEMA(('IFC4'));
ENDSEC;
DATA;
#1=IFCPERSON($,$,'',$,$,$,$,$);
#2=IFCORGANIZATION($,'',$,$,$);
#3=IFCPERSONANDORGANIZATION(#1,#2,$);
#4=IFCAPPLICATION(#2,'v0.7.0-6c9e130ca','IfcOpenShell-v0.7.0-6c9e130ca','');
#5=IFCOWNERHISTORY(#3,#4,$,.NOTDEFINED.,$,#3,#4,1700419055);
#6=IFCDIRECTION((1.,0.,0.));
#7=IFCDIRECTION((0.,0.,1.));
#8=IFCCARTESIANPOINT((0.,0.,0.));
#9=IFCAXIS2PLACEMENT3D(#8,#7,#6);
#10=IFCDIRECTION((0.,1.));
#11=IFCGEOMETRICREPRESENTATIONCONTEXT($,'Model',3,1.E-05,#9,#10);
#12=IFCDIMENSIONALEXPONENTS(0,0,0,0,0,0,0);
#13=IFCSIUNIT(*,.LENGTHUNIT.,$,.METRE.);
#14=IFCSIUNIT(*,.AREAUNIT.,$,.SQUARE_METRE.);
#15=IFCSIUNIT(*,.VOLUMEUNIT.,$,.CUBIC_METRE.);
#16=IFCSIUNIT(*,.PLANEANGLEUNIT.,$,.RADIAN.);
#17=IFCMEASUREWITHUNIT(IFCPLANEANGLEMEASURE(0.017453292519943295),#16);
#18=IFCCONVERSIONBASEDUNIT(#12,.PLANEANGLEUNIT.,'DEGREE',#17);
#19=IFCUNITASSIGNMENT((#13,#14,#15,#18));
#20=IFCPROJECT('0iDmeiiLP3AOllitM2Favn',#5,'',$,$,$,$,(#11),#19);
#21=IFCSITE('3rg2jGkIH10RFhrQsGZKRk',#5,$,$,$,$,$,$,$,$,$,$,$,$);
ENDSEC;
END-ISO-10303-21;
@@ -0,0 +1,30 @@
ISO-10303-21;
HEADER;
FILE_DESCRIPTION(('ViewDefinition [ReferenceView_V1.2]', 'ExchangeRequirement [Any]'),'2;1');
FILE_NAME('Header.ifc','2025-02-13T15:58:45',('tricott'),('Trimble Inc.'),'TrimBimToIFC rel. 4.0.2','Trimble Inc. - SketchUp - 2025.0','IFC4 model');
FILE_SCHEMA(('IFC4'));
ENDSEC;
DATA;
#1=IFCPERSON($,$,'',$,$,$,$,$);
#2=IFCORGANIZATION($,'',$,$,$);
#3=IFCPERSONANDORGANIZATION(#1,#2,$);
#4=IFCAPPLICATION(#2,'v0.7.0-6c9e130ca','IfcOpenShell-v0.7.0-6c9e130ca','');
#5=IFCOWNERHISTORY(#3,#4,$,.NOTDEFINED.,$,#3,#4,1700419055);
#6=IFCDIRECTION((1.,0.,0.));
#7=IFCDIRECTION((0.,0.,1.));
#8=IFCCARTESIANPOINT((0.,0.,0.));
#9=IFCAXIS2PLACEMENT3D(#8,#7,#6);
#10=IFCDIRECTION((0.,1.));
#11=IFCGEOMETRICREPRESENTATIONCONTEXT($,'Model',3,1.E-05,#9,#10);
#12=IFCDIMENSIONALEXPONENTS(0,0,0,0,0,0,0);
#13=IFCSIUNIT(*,.LENGTHUNIT.,$,.METRE.);
#14=IFCSIUNIT(*,.AREAUNIT.,$,.SQUARE_METRE.);
#15=IFCSIUNIT(*,.VOLUMEUNIT.,$,.CUBIC_METRE.);
#16=IFCSIUNIT(*,.PLANEANGLEUNIT.,$,.RADIAN.);
#17=IFCMEASUREWITHUNIT(IFCPLANEANGLEMEASURE(0.017453292519943295),#16);
#18=IFCCONVERSIONBASEDUNIT(#12,.PLANEANGLEUNIT.,'DEGREE',#17);
#19=IFCUNITASSIGNMENT((#13,#14,#15,#18));
#20=IFCPROJECT('0iDmeiiLP3AOllitM2Favn',#5,'',$,$,$,$,(#11),#19);
#21=IFCSITE('3rg2jGkIH10RFhrQsGZKRk',#5,$,$,$,$,$,$,$,$,$,$,$,$);
ENDSEC;
END-ISO-10303-21;
@@ -0,0 +1,30 @@
ISO-10303-21;
HEADER;
FILE_DESCRIPTION(('ViewDefinition [ReferenceView_V1.2]', 'Option [Any]'),'2;1');
FILE_NAME('Header.ifc','2025-02-13T15:58:45',('tricott'),('Trimble Inc.'),'TrimBimToIFC rel. 4.0.2','Trimble Inc. - SketchUp - 2025.0','IFC4 model');
FILE_SCHEMA(('IFC4'));
ENDSEC;
DATA;
#1=IFCPERSON($,$,'',$,$,$,$,$);
#2=IFCORGANIZATION($,'',$,$,$);
#3=IFCPERSONANDORGANIZATION(#1,#2,$);
#4=IFCAPPLICATION(#2,'v0.7.0-6c9e130ca','IfcOpenShell-v0.7.0-6c9e130ca','');
#5=IFCOWNERHISTORY(#3,#4,$,.NOTDEFINED.,$,#3,#4,1700419055);
#6=IFCDIRECTION((1.,0.,0.));
#7=IFCDIRECTION((0.,0.,1.));
#8=IFCCARTESIANPOINT((0.,0.,0.));
#9=IFCAXIS2PLACEMENT3D(#8,#7,#6);
#10=IFCDIRECTION((0.,1.));
#11=IFCGEOMETRICREPRESENTATIONCONTEXT($,'Model',3,1.E-05,#9,#10);
#12=IFCDIMENSIONALEXPONENTS(0,0,0,0,0,0,0);
#13=IFCSIUNIT(*,.LENGTHUNIT.,$,.METRE.);
#14=IFCSIUNIT(*,.AREAUNIT.,$,.SQUARE_METRE.);
#15=IFCSIUNIT(*,.VOLUMEUNIT.,$,.CUBIC_METRE.);
#16=IFCSIUNIT(*,.PLANEANGLEUNIT.,$,.RADIAN.);
#17=IFCMEASUREWITHUNIT(IFCPLANEANGLEMEASURE(0.017453292519943295),#16);
#18=IFCCONVERSIONBASEDUNIT(#12,.PLANEANGLEUNIT.,'DEGREE',#17);
#19=IFCUNITASSIGNMENT((#13,#14,#15,#18));
#20=IFCPROJECT('0iDmeiiLP3AOllitM2Favn',#5,'',$,$,$,$,(#11),#19);
#21=IFCSITE('3rg2jGkIH10RFhrQsGZKRk',#5,$,$,$,$,$,$,$,$,$,$,$,$);
ENDSEC;
END-ISO-10303-21;
@@ -0,0 +1,30 @@
ISO-10303-21;
HEADER;
FILE_DESCRIPTION(('ViewDefinition [ReferenceView_V1.2]', 'ViewDefinition [QuantityTakeOffAddOnView]', 'Option [ExcludedObjects: Stair, Ramp, Space; SplitLevel: On]', 'ExchangeRequirement [CustomRequirement: Value1, Value2]', 'Remark [SomeKey: SomeValue; AnotherKey: AnotherValue]', 'Comment [This is a free text comment, or a comma-separated list of items]'),'2;1');
FILE_NAME('Header example2.ifc', '2022-09-16T10:35:07', ('Evandro Alfieri'), ('buildingSMART Int.'), 'IFC Motor 1.0', 'Company - Application - 26.0.0.0', 'none');
FILE_SCHEMA(('IFC4X3_ADD2'));
ENDSEC;
DATA;
#1=IFCPERSON($,$,'',$,$,$,$,$);
#2=IFCORGANIZATION($,'',$,$,$);
#3=IFCPERSONANDORGANIZATION(#1,#2,$);
#4=IFCAPPLICATION(#2,'v0.7.0-6c9e130ca','IfcOpenShell-v0.7.0-6c9e130ca','');
#5=IFCOWNERHISTORY(#3,#4,$,.NOTDEFINED.,$,#3,#4,1700419055);
#6=IFCDIRECTION((1.,0.,0.));
#7=IFCDIRECTION((0.,0.,1.));
#8=IFCCARTESIANPOINT((0.,0.,0.));
#9=IFCAXIS2PLACEMENT3D(#8,#7,#6);
#10=IFCDIRECTION((0.,1.));
#11=IFCGEOMETRICREPRESENTATIONCONTEXT($,'Model',3,1.E-05,#9,#10);
#12=IFCDIMENSIONALEXPONENTS(0,0,0,0,0,0,0);
#13=IFCSIUNIT(*,.LENGTHUNIT.,$,.METRE.);
#14=IFCSIUNIT(*,.AREAUNIT.,$,.SQUARE_METRE.);
#15=IFCSIUNIT(*,.VOLUMEUNIT.,$,.CUBIC_METRE.);
#16=IFCSIUNIT(*,.PLANEANGLEUNIT.,$,.RADIAN.);
#17=IFCMEASUREWITHUNIT(IFCPLANEANGLEMEASURE(0.017453292519943295),#16);
#18=IFCCONVERSIONBASEDUNIT(#12,.PLANEANGLEUNIT.,'DEGREE',#17);
#19=IFCUNITASSIGNMENT((#13,#14,#15,#18));
#20=IFCPROJECT('0iDmeiiLP3AOllitM2Favn',#5,'',$,$,$,$,(#11),#19);
#21=IFCSITE('3rg2jGkIH10RFhrQsGZKRk',#5,$,$,$,$,$,$,$,$,$,$,$,$);
ENDSEC;
END-ISO-10303-21;
@@ -0,0 +1,30 @@
ISO-10303-21;
HEADER;
FILE_DESCRIPTION(('ViewDefinition [Alignment-basedView]'),'2;1');
FILE_NAME('Header example2.ifc', '2022-09-16T10:35:07', ('Evandro Alfieri'), ('buildingSMART Int.'), 'IFC Motor 1.0', 'Company - Application - 26.0.0.0', 'none');
FILE_SCHEMA(('IFC4X3_ADD2'));
ENDSEC;
DATA;
#1=IFCPERSON($,$,'',$,$,$,$,$);
#2=IFCORGANIZATION($,'',$,$,$);
#3=IFCPERSONANDORGANIZATION(#1,#2,$);
#4=IFCAPPLICATION(#2,'v0.7.0-6c9e130ca','IfcOpenShell-v0.7.0-6c9e130ca','');
#5=IFCOWNERHISTORY(#3,#4,$,.NOTDEFINED.,$,#3,#4,1700419055);
#6=IFCDIRECTION((1.,0.,0.));
#7=IFCDIRECTION((0.,0.,1.));
#8=IFCCARTESIANPOINT((0.,0.,0.));
#9=IFCAXIS2PLACEMENT3D(#8,#7,#6);
#10=IFCDIRECTION((0.,1.));
#11=IFCGEOMETRICREPRESENTATIONCONTEXT($,'Model',3,1.E-05,#9,#10);
#12=IFCDIMENSIONALEXPONENTS(0,0,0,0,0,0,0);
#13=IFCSIUNIT(*,.LENGTHUNIT.,$,.METRE.);
#14=IFCSIUNIT(*,.AREAUNIT.,$,.SQUARE_METRE.);
#15=IFCSIUNIT(*,.VOLUMEUNIT.,$,.CUBIC_METRE.);
#16=IFCSIUNIT(*,.PLANEANGLEUNIT.,$,.RADIAN.);
#17=IFCMEASUREWITHUNIT(IFCPLANEANGLEMEASURE(0.017453292519943295),#16);
#18=IFCCONVERSIONBASEDUNIT(#12,.PLANEANGLEUNIT.,'DEGREE',#17);
#19=IFCUNITASSIGNMENT((#13,#14,#15,#18));
#20=IFCPROJECT('0iDmeiiLP3AOllitM2Favn',#5,'',$,$,$,$,(#11),#19);
#21=IFCSITE('3rg2jGkIH10RFhrQsGZKRk',#5,$,$,$,$,$,$,$,$,$,$,$,$);
ENDSEC;
END-ISO-10303-21;
@@ -0,0 +1,30 @@
ISO-10303-21;
HEADER;
FILE_DESCRIPTION(('ViewDefinition [CoordinationView_V2.0]', 'ViewDefinition[SpaceBoundaryAddonView]'),'2;1');
FILE_NAME('Header example2.ifc', '2022-09-16T10:35:07', ('Evandro Alfieri'), ('buildingSMART Int.'), 'IFC Motor 1.0', 'Company - Application - 26.0.0.0', 'none');
FILE_SCHEMA(('IFC2X3'));
ENDSEC;
DATA;
#1=IFCPERSON($,$,'',$,$,$,$,$);
#2=IFCORGANIZATION($,'',$,$,$);
#3=IFCPERSONANDORGANIZATION(#1,#2,$);
#4=IFCAPPLICATION(#2,'v0.7.0-6c9e130ca','IfcOpenShell-v0.7.0-6c9e130ca','');
#5=IFCOWNERHISTORY(#3,#4,$,.NOTDEFINED.,$,#3,#4,1700419055);
#6=IFCDIRECTION((1.,0.,0.));
#7=IFCDIRECTION((0.,0.,1.));
#8=IFCCARTESIANPOINT((0.,0.,0.));
#9=IFCAXIS2PLACEMENT3D(#8,#7,#6);
#10=IFCDIRECTION((0.,1.));
#11=IFCGEOMETRICREPRESENTATIONCONTEXT($,'Model',3,1.E-05,#9,#10);
#12=IFCDIMENSIONALEXPONENTS(0,0,0,0,0,0,0);
#13=IFCSIUNIT(*,.LENGTHUNIT.,$,.METRE.);
#14=IFCSIUNIT(*,.AREAUNIT.,$,.SQUARE_METRE.);
#15=IFCSIUNIT(*,.VOLUMEUNIT.,$,.CUBIC_METRE.);
#16=IFCSIUNIT(*,.PLANEANGLEUNIT.,$,.RADIAN.);
#17=IFCMEASUREWITHUNIT(IFCPLANEANGLEMEASURE(0.017453292519943295),#16);
#18=IFCCONVERSIONBASEDUNIT(#12,.PLANEANGLEUNIT.,'DEGREE',#17);
#19=IFCUNITASSIGNMENT((#13,#14,#15,#18));
#20=IFCPROJECT('0iDmeiiLP3AOllitM2Favn',#5,'',$,$,$,$,(#11),#19);
#21=IFCSITE('3rg2jGkIH10RFhrQsGZKRk',#5,$,$,$,$,$,$,$,$,$,$,$,$);
ENDSEC;
END-ISO-10303-21;
@@ -0,0 +1,134 @@
import os
import pytest
import ifcopenshell
from ifcopenshell.util import mvd_info
from ifcopenshell.util.mvd_info import parse_mvd
@pytest.fixture
def load_fixture():
base_dir = os.path.join(os.path.dirname(__file__), "fixtures", "mvd_parsing")
def _load(filename):
return ifcopenshell.open(os.path.join(base_dir, filename))
return _load
class TestViewDefinition:
def test_single_view(self, load_fixture):
f = load_fixture("passing_header.ifc")
assert f.mvd.view_definitions == "Alignment-basedView"
def test_multiple_views(self, load_fixture):
f = load_fixture("two_views.ifc")
assert f.mvd.view_definitions == 'CoordinationView_V2.0, SpaceBoundaryAddonView'
class TestExchangeRequirements:
def test_parsing(self, load_fixture):
f = load_fixture("contains_exchange_requirement.ifc")
parsed = parse_mvd(f.mvd.description)
assert parsed.exchange_requirements == 'Any'
def test_access_and_modification(self, load_fixture):
f = load_fixture("contains_exchange_requirement.ifc")
f.header.file_description.description = (
'ViewDefinition [Alignment-basedView]',
'ExchangeRequirement [SomethingElse]'
)
assert f.mvd.exchange_requirements == 'SomethingElse'
f.mvd.view_definitions = 'CoordinationView_V2.0'
assert f.mvd.view_definitions == 'CoordinationView_V2.0'
class TestComments:
def test_read_and_append(self, load_fixture):
f = load_fixture("contains_comment.ifc")
assert f.mvd.comments == ['Any']
f.mvd.comments = ['SomethingElse']
assert f.mvd.comments == ['SomethingElse']
f.mvd.comments.append('AnotherComment')
assert f.mvd.comments == ['SomethingElse', ' AnotherComment']
assert f.mvd.description[1] == 'Comment [SomethingElse, AnotherComment]'
def test_comment_list_modifications(self, load_fixture):
f = load_fixture("contains_comment.ifc")
f.mvd.comments = ''
f.mvd.comments.append('OnlyOne')
assert 'OnlyOne' in f.mvd.comments
f.mvd.comments.insert(0, 'FirstOne')
f.mvd.comments[0] == 'FirstOne'
f.mvd.comments.pop()
assert f.mvd.comments[0] == 'FirstOne'
del f.mvd.comments[0]
assert not f.mvd.comments
class TestOptions:
def test_string_options(self, load_fixture):
f = load_fixture("contains_options.ifc")
assert f.mvd.options == 'Any'
assert 'options' in f.mvd.keywords
class TestDynamicFields:
def test_options_modifications(self, load_fixture):
f = load_fixture("dynamic_fields.ifc")
f.mvd.options['ExcludedObjects'].append('Chair')
f.mvd.options['SplitLevel'] = 'Off'
f.mvd.options['OtherAttr'] = 'SomeValue'
assert f.mvd.description[2].startswith('Option [')
assert 'OtherAttr: SomeValue' in f.mvd.description[2]
assert f.mvd.description == f.header.file_description.description
def test_remark_editing(self, load_fixture):
f = load_fixture("dynamic_fields.ifc")
assert f.mvd.remark == {'SomeKey': 'SomeValue', 'AnotherKey': 'AnotherValue'}
f.mvd.remark['AnotherKey'] = 'SometingElse'
f.mvd.remark['IncludedObjects'] = ['Floor', 'Roof']
assert f.mvd.remark['AnotherKey'] == 'SometingElse'
assert f.mvd.remark['IncludedObjects'] == ['Floor', 'Roof']
assert 'remark' in f.mvd.keywords
def test_custom_dict_behavior(self, load_fixture):
f = load_fixture("dynamic_fields.ifc")
# delete
del f.mvd.options['SplitLevel']
assert not f.mvd.options.get('SplitLevel')
# keys, values, items
assert set(f.mvd.remark.keys()) == {'SomeKey', 'AnotherKey'}
assert 'SomeValue' in f.mvd.remark.values()
assert ('SomeKey', 'SomeValue') in f.mvd.remark.items()
# containment
assert 'SomeKey' in f.mvd.remark
assert 'MissingKey' not in f.mvd.remark
class TestKeywords:
@pytest.mark.parametrize("filename, expected_keywords", [
("contains_comment.ifc", {"view_definitions", "comments"}),
("contains_exchange_requirement.ifc", {"view_definitions", "exchange_requirements"}),
("contains_options.ifc", {"view_definitions", "options"}),
("dynamic_fields.ifc", {"view_definitions", "exchange_requirements", "comments", "remark"})
])
def test_keywords_present(self, load_fixture, filename, expected_keywords):
f = load_fixture(filename)
assert f.mvd.keywords == expected_keywords
class TestFallbackBehavior:
def test_parse_mvd_fallback(self, monkeypatch):
monkeypatch.setattr(mvd_info, "LARK_AVAILABLE", False)
mvd = mvd_info.MvdInfo(
get_description=lambda: ["ViewDefinition [ShouldNotParse]"],
set_description=lambda d: None
)
assert mvd.view_definitions is None
assert mvd.keywords == set()