First Commit

This commit is contained in:
2026-05-31 10:17:09 +07:00
commit 17a9c69379
4547 changed files with 1170384 additions and 0 deletions
@@ -0,0 +1,37 @@
# Step Physical File Validator
Pure-python Step Physical File Validator and Parser implemented using Lark (`pip install -r requirements.in`).
## Example command line usage:
~~~
$ python main.py fixtures\fail_double_comma.ifc
On line 8 column 21:
Unexpected comma (',')
Expecting one of DBLQUOTE DOT HASH INT LPAR NONE QUOTE REAL STAR UPPER
00008 | #1=IFCPERSON($,$,'',,$,$,$,$);
^
$ python main.py fixtures\fail_double_semi.ifc
On line 27 column 66:
Unexpected semicolon (';')
Expecting one of ENDSEC HASH
00027 | #20=IFCPROJECT('2AyG2X0sb16Bjd4gQc07yZ',#5,'',$,$,$,$,(#11),#19);;
^
$ python main.py fixtures\fail_duplicate_id.ifc
On line 27:
Duplicate instance name #19
00027 | #19=IFCPROJECT('2AyG2X0sb16Bjd4gQc07yZ',#5,'',$,$,$,$,(#11),#19);
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
$ python main.py fixtures\fail_no_header.ifc
On line 2 column 1:
Unexpected hex ('F')
Expecting HEADER
00002 | FILE_DESCRIPTION(('ViewDefinition [CoordinationView]'),'2;1');
^
$ python main.py fixtures\pass_1.ifc
Valid
~~~
@@ -0,0 +1,31 @@
try:
from parser.parse import parse
from parser.file import file, open
from parser.errors import (
_ValidationError,
CollectedValidationErrors,
DuplicateNameError,
HeaderFieldError,
InvalidNameError,
)
except:
from .parser.parse import parse
from .parser.file import file, open
from .parser.errors import (
_ValidationError,
CollectedValidationErrors,
DuplicateNameError,
HeaderFieldError,
InvalidNameError,
)
__all__ = [
"parse",
"open",
"file",
"_ValidationError",
"CollectedValidationErrors",
"DuplicateNameError",
"HeaderFieldError",
"InvalidNameError",
] # for testing
@@ -0,0 +1,41 @@
import sys
import json
import argparse
from . import parse, CollectedValidationErrors
def main():
parser = argparse.ArgumentParser(description="Parse and validate STEP file.")
parser.add_argument("filename", help="The STEP file to validate.")
parser.add_argument(
"--progress", action="store_true", help="Show progress during validation."
)
parser.add_argument(
"--json", action="store_true", help="Output errors in JSON format."
)
parser.add_argument(
"--only-header", action="store_true", help="Validate only the header section."
)
args = parser.parse_args()
try:
parse(
filename=args.filename,
with_progress=args.progress,
with_tree=False,
only_header=args.only_header,
)
if not args.json:
print("Valid", file=sys.stderr)
exit(0)
except CollectedValidationErrors as exc:
if not args.json:
print(exc, file=sys.stderr)
else:
json.dump([e.asdict() for e in exc.errors], sys.stdout, indent=2)
exit(1)
if __name__ == "__main__":
main()
@@ -0,0 +1,29 @@
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('','2022-05-04T08:08:30',(''),(''),'IfcOpenShell-0.7.0','IfcOpenShell-0.7.0','');
FILE_SCHEMA(('IFC4'));
ENDSEC;
DATA;
#1=IFCPERSON($,$,'',$,$,$,$);
#2=IFCORGANIZATION($,'',$,$,$);
#3=IFCPERSONANDORGANIZATION(#1,#2,$);
#4=IFCAPPLICATION(#2,'0.7.0','IfcOpenShell-0.7.0','');
#5=IFCOWNERHISTORY(#3,#4,$,.ADDED.,$,#3,#4,1651651710);
#6=IFCDIRECTION((1.,0.,0.));
#7=IFCDIRECTION((0.,0.,1.));
#8=IFCCARTESIANPOINT((0.,0.,0.));
#9=IFCAXIS2PLACEMENT3D(#8,#7,#6);
#10=IFCDIRECTION((0.,1.,0.));
#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('2AyG2X0sb16Bjd4gQc07yZ',#5,'',$,$,$,$,(#11),#19);
ENDSEC;
END-ISO-10303-21;
@@ -0,0 +1,29 @@
ISO-10303-21;
HEADER;
FILE_DESCRIPTION(('ViewDefinition [CoordinationView]'),'2;1');
FILE_NAME('','2022-05-04T08:08:30',(''),(''),'IfcOpenShell-0.7.0','IfcOpenShell-0.7.0','');
FILE_SCHEMA(('IFC4'));
ENDSEC;
DATA;
#1=IFCPERSON($,$,'',,$,$,$,$);
#2=IFCORGANIZATION($,'',$,$,$);
#3=IFCPERSONANDORGANIZATION(#1,#2,$);
#4=IFCAPPLICATION(#2,'0.7.0','IfcOpenShell-0.7.0','');
#5=IFCOWNERHISTORY(#3,#4,$,.ADDED.,$,#3,#4,1651651710);
#6=IFCDIRECTION((1.,0.,0.));
#7=IFCDIRECTION((0.,0.,1.));
#8=IFCCARTESIANPOINT((0.,0.,0.));
#9=IFCAXIS2PLACEMENT3D(#8,#7,#6);
#10=IFCDIRECTION((0.,1.,0.));
#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('2AyG2X0sb16Bjd4gQc07yZ',#5,'',$,$,$,$,(#11),#19);
ENDSEC;
END-ISO-10303-21;
@@ -0,0 +1,29 @@
ISO-10303-21;
HEADER;
FILE_DESCRIPTION(('ViewDefinition [CoordinationView]'),'2;1');
FILE_NAME('','2022-05-04T08:08:30',(''),(''),'IfcOpenShell-0.7.0','IfcOpenShell-0.7.0','');
FILE_SCHEMA(('IFC4'));
ENDSEC;
DATA;
#1=IFCPERSON($,$,'',$,$,$,$,$);
#2=IFCORGANIZATION($,'',$,$,$);
#3=IFCPERSONANDORGANIZATION(#1,#2,$);
#4=IFCAPPLICATION(#2,'0.7.0','IfcOpenShell-0.7.0','');
#5=IFCOWNERHISTORY(#3,#4,$,.ADDED.,$,#3,#4,1651651710);
#6=IFCDIRECTION((1.,0.,0.));
#7=IFCDIRECTION((0.,0.,1.));
#8=IFCCARTESIANPOINT((0.,0.,0.));
#9=IFCAXIS2PLACEMENT3D(#8,#7,#6);
#10=IFCDIRECTION((0.,1.,0.));
#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('2AyG2X0sb16Bjd4gQc07yZ',#5,'',$,$,$,$,(#11),#19);;
ENDSEC;
END-ISO-10303-21;
@@ -0,0 +1,29 @@
ISO-10303-21;
HEADER;
FILE_DESCRIPTION(('ViewDefinition [CoordinationView]'),'2;1');
FILE_NAME('','2022-05-04T08:08:30',(''),(''),'IfcOpenShell-0.7.0','IfcOpenShell-0.7.0','');
FILE_SCHEMA(('IFC4'));
ENDSEC;
DATA;
#1=IFCPERSON($,$,'',$,$,$,$,$);
#2=IFCORGANIZATION($,'',$,$,$);
#3=IFCPERSONANDORGANIZATION(#1,#2,$);
#4=IFCAPPLICATION(#2,'0.7.0','IfcOpenShell-0.7.0','');
#5=IFCOWNERHISTORY(#3,#4,$,.ADDED.,$,#3,#4,1651651710);
#6=IFCDIRECTION((1.,0.,0.));
#7=IFCDIRECTION((0.,0.,1.));
#8=IFCCARTESIANPOINT((0.,0.,0.));
#9=IFCAXIS2PLACEMENT3D(#8,#7,#6);
#10=IFCDIRECTION((0.,1.,0.));
#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));
#19=IFCPROJECT('2AyG2X0sb16Bjd4gQc07yZ',#5,'',$,$,$,$,(#11),#19);
ENDSEC;
END-ISO-10303-21;
@@ -0,0 +1,10 @@
ISO-10303-21;
HEADER;
FILE_DESCRIPTION(('ViewDefinition [CoordinationView]'),'2;1');
FILE_NAME('','2022-05-04T08:08:30',(''),(''),'IfcOpenShell-0.7.0','IfcOpenShell-0.7.0','');
FILE_SCHEMA(('IFC4'));
ENDSEC;
DATA;
#0=IFCPERSON($, $,'',$,$,$,$,$);
ENDSEC;
END-ISO-10303-21;
@@ -0,0 +1,46 @@
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'));
FILE_INFO(
'Report Date', '2025-04-21 18:16:37',
'IFC Schema', 'IFC2X3',
'MVD(s)', 'None',
'File Name in Header', 'generated (2).ifc',
'File Name', 'generated (2).ifc',
'File Size', '0.41 kB',
'File Date', '2025-04-21 18:16:37',
'Originating System', 'PHP Script',
'Preprocessor Version', '1.0.0',
'Company Name', 'Your Company Name',
'Application Name', 'IFC Generator PHP Script',
'Application Version', '1.0.0',
'Author', 'Your Name',
'Organization', 'Your Company Name'
);
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,29 @@
ISO-10303-21;
HEADER;
FILE_DESCRIPTION(('ViewDefinition [CoordinationView]'),'2;1');
FILE_NAME('','2022-05-04T08:08:30',(''),(''),'IfcOpenShell-0.7.0','IfcOpenShell-0.7.0','');
FILE_SCHEMA(('IFC4'));
ENDSEC;
DATA;
#1=IFCPERSON($,$,'',$,$,$,$,$);
#2=IFCORGANIZATION($,'',$,$,$);
#3=IFCPERSONANDORGANIZATION(#1,#2,$);
#4=IFCAPPLICATION(#2,'0.7.0','IfcOpenShell-0.7.0','');
#5=IFCOWNERHISTORY(#3,#4,$,.ADDED.,$,#3,#4,1651651710);
#6=IFCDIRECTION((1.,0.,0.));
#7=IFCDIRECTION((0.,0.,1.));
#8=IFCCARTESIANPOINT((0.,0.,0.));
#9=IFCAXIS2PLACEMENT3D(#8,#7,#6);
#10=IFCDIRECTION((0.,1.,0.));
#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.);
#18=IFCMEASUREWITHUNIT(IFCPLANEANGLEMEASURE(0.017453292519943295),#16);
#18=IFCCONVERSIONBASEDUNIT(#12,.PLANEANGLEUNIT.,'DEGREE',#17);
#19=IFCUNITASSIGNMENT((#13,#14,#15,#18));
#19=IFCPROJECT('2AyG2X0sb16Bjd4gQc07yZ',#5,'',$,$,$,$,(#11),#19);
ENDSEC;
END-ISO-10303-21;
@@ -0,0 +1,30 @@
ISO-10303-21;
HEADER;
FILE_DESCRIPTION(('ViewDefinition [ReferenceView_V1.2]', 'ExchangeRequirement [Any]'));
FILE_NAME('Header.ifc','2025-02-13T15:58:45',('tricott'),('Trimble Inc.'),'TrimBimToIFC rel. 4.0.2','Example - Example - 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,28 @@
ISO-10303-21;
FILE_DESCRIPTION(('ViewDefinition [CoordinationView]'),'2;1');
FILE_NAME('','2022-05-04T08:08:30',(''),(''),'IfcOpenShell-0.7.0','IfcOpenShell-0.7.0','');
FILE_SCHEMA(('IFC4'));
ENDSEC;
DATA;
#1=IFCPERSON($,$,'',$,$,$,$,$);
#2=IFCORGANIZATION($,'',$,$,$);
#3=IFCPERSONANDORGANIZATION(#1,#2,$);
#4=IFCAPPLICATION(#2,'0.7.0','IfcOpenShell-0.7.0','');
#5=IFCOWNERHISTORY(#3,#4,$,.ADDED.,$,#3,#4,1651651710);
#6=IFCDIRECTION((1.,0.,0.));
#7=IFCDIRECTION((0.,0.,1.));
#8=IFCCARTESIANPOINT((0.,0.,0.));
#9=IFCAXIS2PLACEMENT3D(#8,#7,#6);
#10=IFCDIRECTION((0.,1.,0.));
#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('2AyG2X0sb16Bjd4gQc07yZ',#5,'',$,$,$,$,(#11),#19);
ENDSEC;
END-ISO-10303-21;
@@ -0,0 +1,29 @@
ISO-10303-21;
HEADER;
FILE_DESCRIPTION(('ViewDefinition [CoordinationView]'),'2;1');
FILE_NAME('','2022-05-04T08:08:30',(''),(''),'IfcOpenShell-0.7.0','IfcOpenShell-0.7.0','');
FILE_SCHEMA(('IFC4'));
ENDSEC;
DATA;
#1=IFCPERSON($,$,'C:\path',$,$,$,$);
#2=IFCORGANIZATION($,'',$,$,$);
#3=IFCPERSONANDORGANIZATION(#1,#2,$);
#4=IFCAPPLICATION(#2,'0.7.0','IfcOpenShell-0.7.0','');
#5=IFCOWNERHISTORY(#3,#4,$,.ADDED.,$,#3,#4,1651651710);
#6=IFCDIRECTION((1.,0.,0.));
#7=IFCDIRECTION((0.,0.,1.));
#8=IFCCARTESIANPOINT((0.,0.,0.));
#9=IFCAXIS2PLACEMENT3D(#8,#7,#6);
#10=IFCDIRECTION((0.,1.,0.));
#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('2AyG2X0sb16Bjd4gQc07yZ',#5,'',$,$,$,$,(#11),#19);
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','Example - Example - 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,29 @@
ISO-10303-21;
HEADER;
FILE_DESCRIPTION(('ViewDefinition [CoordinationView]'),'2;1');
FILE_NAME('','2022-05-04T08:08:30',(''),(''),'IfcOpenShell-0.7.0','IfcOpenShell-0.7.0','');
FILE_SCHEMA(('IFC4'));
ENDSEC;
DATA;
#1=IFCPERSON($,$,'W\xc3\xa4nd',$,$,$,$);
#2=IFCORGANIZATION($,'',$,$,$);
#3=IFCPERSONANDORGANIZATION(#1,#2,$);
#4=IFCAPPLICATION(#2,'0.7.0','IfcOpenShell-0.7.0','');
#5=IFCOWNERHISTORY(#3,#4,$,.ADDED.,$,#3,#4,1651651710);
#6=IFCDIRECTION((1.,0.,0.));
#7=IFCDIRECTION((0.,0.,1.));
#8=IFCCARTESIANPOINT((0.,0.,0.));
#9=IFCAXIS2PLACEMENT3D(#8,#7,#6);
#10=IFCDIRECTION((0.,1.,0.));
#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('2AyG2X0sb16Bjd4gQc07yZ',#5,'',$,$,$,$,(#11),#19);
ENDSEC;
END-ISO-10303-21;
@@ -0,0 +1,30 @@
ISO-10303-21;
HEADER;
FILE_DESCRIPTION(('ViewDefinition [CoordinationView]'),'2;1');
FILE_NAME('','2022-05-04T08:08:30',(''),(''),'IfcOpenShell-0.7.0','IfcOpenShell-0.7.0','');
FILE_SCHEMA(('IFC4'));
ENDSEC;
DATA;
#1=IFCPERSON($, $,'',$,$,$,$,$);
#2=IFCORGANIZATION($,'',$,$,$);
#3=IFCPERSONANDORGANIZATION(#1,#2,$);
#4=IFCAPPLICATION(#2,'0.7.0','IfcOpenShell-0.7.0','');
#100=IFCAPPLICATION(#2,'0.7.0','Nested '' quotes','');
#5=IFCOWNERHISTORY(#3,#4,$,.ADDED.,$,#3,#4,1651651710);
#6=IFCDIRECTION((1.,0.,0.));
#7=IFCDIRECTION((0.,0.,1.));
#8=IFCCARTESIANPOINT((0.,0.,0.));
#9=IFCAXIS2PLACEMENT3D(#8,#7,#6);
#10=IFCDIRECTION((0.,1.,0.));
#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('2AyG2X0sb16Bjd4gQc07yZ',#5,'',$,$,$,$,(#11),#19);
ENDSEC;
END-ISO-10303-21;
@@ -0,0 +1,29 @@
ISO-10303-21;
HEADER;
FILE_DESCRIPTION(('ViewDefinition [CoordinationView]'),'2;1');
FILE_NAME('','2022-05-04T08:08:30',(''),(''),'IfcOpenShell-0.7.0','IfcOpenShell-0.7.0','');
FILE_SCHEMA(('IFC4'));
ENDSEC;
DATA;
#1=IFCPERSON($,$,'C:\\path',$,$,$,$);
#2=IFCORGANIZATION($,'',$,$,$);
#3=IFCPERSONANDORGANIZATION(#1,#2,$);
#4=IFCAPPLICATION(#2,'0.7.0','IfcOpenShell-0.7.0','');
#5=IFCOWNERHISTORY(#3,#4,$,.ADDED.,$,#3,#4,1651651710);
#6=IFCDIRECTION((1.,0.,0.));
#7=IFCDIRECTION((0.,0.,1.));
#8=IFCCARTESIANPOINT((0.,0.,0.));
#9=IFCAXIS2PLACEMENT3D(#8,#7,#6);
#10=IFCDIRECTION((0.,1.,0.));
#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('2AyG2X0sb16Bjd4gQc07yZ',#5,'',$,$,$,$,(#11),#19);
ENDSEC;
END-ISO-10303-21;
@@ -0,0 +1,29 @@
ISO-10303-21;
HEADER;
FILE_DESCRIPTION(('ViewDefinition [CoordinationView]'),'2;1');
FILE_NAME('','2022-05-04T08:08:30',(''),(''),'IfcOpenShell-0.7.0','IfcOpenShell-0.7.0','');
FILE_SCHEMA(('IFC4'));
ENDSEC;
DATA;
#1=IFCPERSON($,$,'abc\S\'def',$,$,$,$);
#2=IFCORGANIZATION($,'',$,$,$);
#3=IFCPERSONANDORGANIZATION(#1,#2,$);
#4=IFCAPPLICATION(#2,'0.7.0','IfcOpenShell-0.7.0','');
#5=IFCOWNERHISTORY(#3,#4,$,.ADDED.,$,#3,#4,1651651710);
#6=IFCDIRECTION((1.,0.,0.));
#7=IFCDIRECTION((0.,0.,1.));
#8=IFCCARTESIANPOINT((0.,0.,0.));
#9=IFCAXIS2PLACEMENT3D(#8,#7,#6);
#10=IFCDIRECTION((0.,1.,0.));
#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('2AyG2X0sb16Bjd4gQc07yZ',#5,'',$,$,$,$,(#11),#19);
ENDSEC;
END-ISO-10303-21;
@@ -0,0 +1,31 @@
ISO-10303-21;
HEADER;
FILE_DESCRIPTION(('ViewDefinition [CoordinationView]'),'2;1');
FILE_NAME('','2022-05-04T08:08:30',(''),(''),'IfcOpenShell-0.7.0','IfcOpenShell-0.7.0','');
FILE_SCHEMA(('IFC4'));
ENDSEC;
DATA;
/*C:\TEMP\path\...\ifc
*/
#1=IFCPERSON($,$,'',$,$,$,$);
#2=IFCORGANIZATION($,'',$,$,$);
#3=IFCPERSONANDORGANIZATION(#1,#2,$);
#4=IFCAPPLICATION(#2,'0.7.0','IfcOpenShell-0.7.0','');
#5=IFCOWNERHISTORY(#3,#4,$,.ADDED.,$,#3,#4,1651651710);
#6=IFCDIRECTION((1.,0.,0.));
#7=IFCDIRECTION((0.,0.,1.));
#8=IFCCARTESIANPOINT((0.,0.,0.));
#9=IFCAXIS2PLACEMENT3D(#8,#7,#6);
#10=IFCDIRECTION((0.,1.,0.));
#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('2AyG2X0sb16Bjd4gQc07yZ',#5,'',$,$,$,$,(#11),#19);
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,92 @@
import matplotlib.pyplot as plt
import networkx as nx
from itertools import count
import matplotlib.patches as mpatches
from matplotlib import pylab
class IfcEntity:
def __init__(self, data):
self.ifctype = data["ifc_type"]
self.id = data["id"]
def __str__(self):
return "#" + str(self.id) + "_" + self.ifctype
__repr__ = __str__
def get_cmap(n, name="hsv"):
"""Returns a function that maps each index in 0, 1, ..., n-1 to a distinct
RGB color; the keyword argument name must be a standard mpl colormap name."""
return plt.cm.get_cmap(name, n)
G = nx.Graph()
colors = []
color_map = []
ifctypes = set()
types_color = {}
for e in ents.values():
for r in e["attributes"][1]:
G.add_edge(e["id"], r)
color_map.append("red")
ifctypes.add(e["ifc_type"])
ifctypes.add(ents[r]["ifc_type"])
cmap = get_cmap(len(ifctypes), "rainbow")
colors = [cmap(i) for i in range(len(ifctypes))]
type_color_mapping = {}
cols = []
i = 0
for eid in G.nodes():
t = ents[eid]["ifc_type"]
# print(t)
if t in type_color_mapping.keys():
cols.append(type_color_mapping[t])
else:
type_color_mapping[t] = colors[i]
i = i + 1
cols.append(type_color_mapping[t])
# k controls the distance between the nodes and varies between 0 and 1
# iterations is the number of times simulated annealing is run
# default k=0.1 and iterations=50
pos = nx.spring_layout(G, k=0.8, iterations=60) # positions for all nodes
nodes = G.nodes()
print(get_cmap(len(nodes)))
cmap = get_cmap(len(nodes))
c_map = [cmap(i) for i in range(len(nodes))]
nc = nx.draw_networkx_nodes(G, pos, nodelist=nodes, node_color=cols, node_size=200)
# edges
elarge = [(u, v) for (u, v, d) in G.edges(data=True)]
nx.draw_networkx_edges(G, pos, edgelist=elarge, width=1)
nx.draw_networkx_labels(G, pos, font_size=8, font_family="sans-serif")
red_patch = mpatches.Patch(color="red", label="The red data")
blue_patch = mpatches.Patch(color="blue", label="The blue data")
patches = []
for k, v in type_color_mapping.items():
patches.append(mpatches.Patch(color=v, label=k))
plt.legend(handles=patches, fontsize=8)
plt.axis("off")
plt.show()
@@ -0,0 +1,140 @@
from lark.exceptions import UnexpectedToken
class _ValidationError(Exception):
def __init__(self, *args, **kwargs):
if self.__class__ is _ValidationError:
raise TypeError("Do not raise _ValidationError directly.")
super().__init__(*args, **kwargs)
class ErrorCollector:
def __init__(self):
self.errors = []
def add(self, error):
self.errors.append(error)
def raise_if_any(self):
if self.errors:
raise CollectedValidationErrors(self.errors)
class CollectedValidationErrors(_ValidationError):
def __init__(self, errors):
self.errors = errors
def asdict(self, with_message=True):
return [e.asdict(with_message=with_message) for e in self.errors]
def __str__(self):
return f"{len(self.errors)} validation error(s) collected:\n" + "\n\n".join(
str(e) for e in self.errors
)
class SyntaxError(_ValidationError):
def __init__(self, filecontent, exception):
self.filecontent = filecontent
self.exception = exception
def asdict(self, with_message=True):
return {
"type": (
"unexpected_token"
if isinstance(self.exception, UnexpectedToken)
else "unexpected_character"
),
"lineno": self.exception.line,
"column": self.exception.column,
"found_type": self.exception.token.type.lower(),
"found_value": self.exception.token.value,
"expected": sorted(x for x in self.exception.accepts if "__ANON" not in x),
"line": self.filecontent.split("\n")[self.exception.line - 1],
**({"message": str(self)} if with_message else {}),
}
def __str__(self):
d = self.asdict(with_message=False)
if len(d["expected"]) == 1:
exp = d["expected"][0]
else:
exp = f"one of {' '.join(d['expected'])}"
sth = "character" if d["type"] == "unexpected_character" else ""
return f"On line {d['lineno']} column {d['column']}:\nUnexpected {sth}{d['found_type']} ('{d['found_value']}')\nExpecting {exp}\n{d['lineno']:05d} | {d['line']}\n {' ' * (self.exception.column - 1)}^"
class DuplicateNameError(_ValidationError):
def __init__(self, filecontent, name, linenumbers):
self.name = name
self.filecontent = filecontent
self.linenumbers = linenumbers
def asdict(self, with_message=True):
return {
"type": "duplicate_name",
"name": self.name,
"lineno": self.linenumbers[0],
"line": self.filecontent.split("\n")[self.linenumbers[0] - 1],
**({"message": str(self)} if with_message else {}),
}
def __str__(self):
d = self.asdict(with_message=False)
def build():
yield f"On line {d['lineno']}:\nDuplicate instance name #{d['name']}"
yield f"{d['lineno']:05d} | {d['line']}"
yield " " * 8 + "^" * len(d["line"].rstrip())
return "\n".join(build())
class HeaderFieldError(_ValidationError):
def __init__(self, field, found_len, expected_len):
self.field = field
self.found_len = found_len
self.expected_len = expected_len
def asdict(self, with_message=True):
return {
"type": "invalid_header_field",
"field": self.field,
"expected_field_count": self.expected_len,
"actual_field_count": self.found_len,
**({"message": str(self)} if with_message else {}),
}
def __str__(self):
return (
f"Invalid number of parameters for HEADER field '{self.field}'. "
f"Expected {self.expected_len}, found {self.found_len}."
)
class InvalidNameError(_ValidationError):
def __init__(self, filecontent, name, linenumbers):
self.name = name
self.filecontent = filecontent
self.linenumbers = linenumbers
def asdict(self, with_message=True):
return {
"type": "invalid_name",
"name": self.name,
"lineno": self.linenumbers[0],
"line": self.filecontent.split("\n")[self.linenumbers[0] - 1],
**({"message": str(self)} if with_message else {}),
}
def __str__(self):
d = self.asdict(with_message=False)
def build():
yield f"On line {d['lineno']}:\nInvalid instance name #{d['name']}"
yield f"{d['lineno']:05d} | {d['line']}"
yield " " * 8 + "^" * len(d["line"].rstrip())
return "\n".join(build())
@@ -0,0 +1,108 @@
import types
import re
import numbers
import itertools
from .parse import parse, ParseResult
from .grammar import HEADER_FIELDS
from .transformer import entity_instance
try:
from .mvd_info import MvdInfo, LARK_AVAILABLE
except ImportError: # in case of running module locally (e.g. test_parser.py)
from mvd_info import MvdInfo, LARK_AVAILABLE
class file:
"""
A somewhat compatible interface (but very limited) to ifcopenshell.file
"""
def __init__(self, result: ParseResult):
self.header_ = result.header
self.data_ = result.entities
@property
def schema_identifier(self) -> str:
return self.header_["FILE_SCHEMA"][0][0]
@property
def schema(self) -> str:
"""General IFC schema version: IFC2X3, IFC4, IFC4X3."""
prefixes = ("IFC", "X", "_ADD", "_TC")
reg = "".join(f"(?P<{s}>{s}\\d+)?" for s in prefixes)
match = re.match(reg, self.schema_identifier)
version_tuple = tuple(
map(
lambda pp: int(pp[1][len(pp[0]) :]) if pp[1] else None,
((p, match.group(p)) for p in prefixes),
)
)
return "".join(
"".join(map(str, t)) if t[1] else ""
for t in zip(prefixes, version_tuple[0:2])
)
@property
def schema_version(self) -> tuple[int, int, int, int]:
"""Numeric representation of the full IFC schema version.
E.g. IFC4X3_ADD2 is represented as (4, 3, 2, 0).
"""
schema = self.schema
version = []
for prefix in ("IFC", "X", "_ADD", "_TC"):
number = re.search(prefix + r"(\d)", schema)
version.append(int(number.group(1)) if number else 0)
return tuple(version)
@property
def header(self):
header = {}
for field_name, namedtuple_class in HEADER_FIELDS.items():
field_data = self.header_.get(field_name.upper(), [])
header[field_name.lower()] = namedtuple_class(*field_data)
return types.SimpleNamespace(**header)
@property
def mvd(self):
if not LARK_AVAILABLE or MvdInfo is None:
return None
return MvdInfo(self.header)
def __getitem__(self, key: numbers.Integral) -> entity_instance:
return self.by_id(key)
def by_id(self, id: int) -> entity_instance:
"""Return an IFC entity instance filtered by IFC ID.
:param id: STEP numerical identifier
:type id: int
:raises RuntimeError: If `id` is not found or multiple definitions exist for `id`.
:rtype: entity_instance
"""
ns = self.data_.get(id, [])
if len(ns) == 0:
raise RuntimeError(f"Instance with id {id} not found")
elif len(ns) > 1:
raise RuntimeError(f"Duplicate definition for id {id}")
return ns[0]
def by_type(self, type: str) -> list[entity_instance]:
"""Return IFC objects filtered by IFC Type and wrapped with the entity_instance class.
:rtype: list[entity_instance]
"""
type_lc = type.lower()
return list(
filter(
lambda ent: ent.type.lower() == type_lc,
itertools.chain.from_iterable(self.data_.values()),
)
)
def open(fn, only_header=False) -> file:
return file(parse(filename=fn, only_header=only_header))
@@ -0,0 +1,125 @@
from collections import namedtuple
grammar = r"""
file: "ISO-10303-21;" header data_section "END-ISO-10303-21;"
header: "HEADER" ";" header_entity_list "ENDSEC" ";"
header_line: (SPECIAL|DIGIT|LOWER|UPPER)* "*"
data_section: "DATA" ";" (entity_instance)* "ENDSEC" ";"
entity_instance: simple_entity_instance|complex_entity_instance
simple_entity_instance: id "=" simple_record ";"
complex_entity_instance: id "=" subsuper_record ";"
subsuper_record : "(" simple_record_list ")"
simple_record_list:simple_record simple_record*
simple_record: keyword "("parameter_list?")"
header_entity_list: file_description file_name file_schema
file_description: "FILE_DESCRIPTION" "(" parameter_list ")" ";"
file_name: "FILE_NAME" "(" parameter_list ")" ";"
file_schema: "FILE_SCHEMA" "(" parameter_list ")" ";"
id: /#[0-9]+/
keyword: /[A-Z][0-9A-Z_]*/
parameter: untyped_parameter|typed_parameter|omitted_parameter
parameter_list: parameter ("," parameter)*
list: "(" parameter ("," parameter)* ")" |"("")"
typed_parameter: keyword "(" parameter ")"|"()"
untyped_parameter: string| NONE |INT |REAL |enumeration |id |binary |list
omitted_parameter:STAR
enumeration: "." keyword "."
binary: "\"" ("0"|"1"|"2"|"3") (HEX)* "\""
string: "'" (REVERSE_SOLIDUS REVERSE_SOLIDUS|SPECIAL|DIGIT|SPACE|LOWER|UPPER|CONTROL_DIRECTIVE|"\\*\\")* "'"
STAR: "*"
SLASH: "/"
NONE: "$"
SPECIAL : "!"
| "*"
| "$"
| "%"
| "&"
| "."
| "#"
| "+"
| ","
| "-"
| "("
| ")"
| "?"
| "/"
| ":"
| ";"
| "<"
| "="
| ">"
| "@"
| "["
| "]"
| "{"
| "|"
| "}"
| "^"
| "`"
| "~"
| "_"
| "\""
| "\"\""
| "''"
REAL: SIGN? DIGIT (DIGIT)* "." (DIGIT)* ("E" SIGN? DIGIT (DIGIT)* )?
INT: SIGN? DIGIT (DIGIT)*
CONTROL_DIRECTIVE: PAGE | ALPHABET | EXTENDED2 | EXTENDED4 | ARBITRARY
PAGE : REVERSE_SOLIDUS "S" REVERSE_SOLIDUS LATIN_CODEPOINT
LATIN_CODEPOINT : SPACE | DIGIT | LOWER | UPPER | SPECIAL | REVERSE_SOLIDUS | APOSTROPHE
ALPHABET : REVERSE_SOLIDUS "P" UPPER REVERSE_SOLIDUS
EXTENDED2: REVERSE_SOLIDUS "X2" REVERSE_SOLIDUS (HEX_TWO)* END_EXTENDED
EXTENDED4 :REVERSE_SOLIDUS "X4" REVERSE_SOLIDUS (HEX_FOUR)* END_EXTENDED
END_EXTENDED: REVERSE_SOLIDUS "X0" REVERSE_SOLIDUS
ARBITRARY: REVERSE_SOLIDUS "X" REVERSE_SOLIDUS HEX_ONE
HEX_FOUR: HEX_TWO HEX_TWO
HEX_TWO: HEX_ONE HEX_ONE
HEX_ONE: HEX HEX
HEX: "0"
| "1"
| "2"
| "3"
| "4"
| "5"
| "6"
| "7"
| "8"
| "9"
| "A"
| "B"
| "C"
| "D"
| "E"
| "F"
APOSTROPHE: "'"
REVERSE_SOLIDUS: "\\"
DIGIT: "0".."9"
SIGN: "+"|"-"
LOWER: "a".."z"
UPPER: "A".."Z"
ESCAPE : "\\" ( "$" | "\"" | CHAR )
CHAR : /[^$"\n]/
WORD : CHAR+
SPACE.10 : " "
%ignore /[ \t\f\r\n]/+
"""
HEADER_FIELDS = {
"file_description": namedtuple(
"file_description", ["description", "implementation_level"]
),
"file_name": namedtuple(
"file_name",
[
"name",
"time_stamp",
"author",
"organization",
"preprocessor_version",
"originating_system",
"authorization",
],
),
"file_schema": namedtuple("file_schema", ["schema_identifiers"]),
}
@@ -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()
@@ -0,0 +1,184 @@
from dataclasses import dataclass
from collections import defaultdict
import re
import sys
import builtins
from lark import Lark, UnexpectedCharacters, UnexpectedToken
# import transformer
from .transformer import (
Transformer,
entity_instance,
make_header_ent,
create_step_entity,
)
from .grammar import grammar, HEADER_FIELDS
from .errors import (
HeaderFieldError,
DuplicateNameError,
ErrorCollector,
SyntaxError,
InvalidNameError,
)
def validate_header_fields(header, error_collector, only_header=False):
for field in HEADER_FIELDS.keys():
observed = header.get(field.upper(), [])
expected = HEADER_FIELDS.get(field)._fields
if len(observed) != len(expected):
error_collector.add(
HeaderFieldError(field.upper(), len(observed), len(expected))
)
if only_header:
error_collector.raise_if_any()
@dataclass
class ParseResult:
header: dict
entities: dict[int, list[entity_instance]]
def process_tree(filecontent, file_tree, with_progress, error_collector):
ents = defaultdict(list)
header, data = file_tree.children
header = dict(map(make_header_ent, header.children[0].children))
validate_header_fields(header, error_collector)
n = len(data.children)
if n:
percentages = [i * 100.0 / n for i in range(n + 1)]
num_dots = [int(b) - int(a) for a, b in zip(percentages, percentages[1:])]
for idx, entity_tree in enumerate(data.children):
if with_progress:
sys.stdout.write(num_dots[idx] * ".")
sys.stdout.flush()
ent = create_step_entity(entity_tree)
id_ = int(ent["id"])
if id_ == 0:
error_collector.add(InvalidNameError(filecontent, ent["id"], ent["lines"]))
if ents[id_]:
error_collector.add(
DuplicateNameError(filecontent, ent["id"], ent["lines"])
)
else:
ents[id_].append(ent)
return header, ents
def parse(
*,
filename=None,
filecontent=None,
with_progress=False,
with_tree=True,
only_header=False,
) -> ParseResult:
error_collector = ErrorCollector()
if filename:
assert not filecontent
filecontent = builtins.open(filename, encoding=None).read()
# Match and remove the comments
p = r"/\*[\s\S]*?\*/"
def replace_fn(match):
return re.sub(r"[^\n]", " ", match.group(), flags=re.M)
filecontent_wo_comments = re.sub(p, replace_fn, filecontent)
if only_header:
# Extract just the HEADER section using regex
header_match = re.search(
r"ISO-10303-21;\s*HEADER;(.*?)ENDSEC;",
filecontent_wo_comments,
flags=re.DOTALL | re.IGNORECASE,
)
if not header_match:
error_collector.add(
HeaderFieldError("header", "", "No HEADER section found in file")
)
error_collector.raise_if_any()
header_text = f"HEADER;{header_match.group(1)}ENDSEC;"
full_header_text = f"ISO-10303-21;{header_text}DATA;ENDSEC;END-ISO-10303-21;"
parser = Lark(grammar, parser="lalr", start="file")
try:
ast = parser.parse(full_header_text)
except (UnexpectedToken, UnexpectedCharacters) as e:
error_collector.add(SyntaxError(filecontent, e))
error_collector.raise_if_any() # Immediately abort in case of critical error
header_tree = ast.children[0] # HEADER section
header = dict(map(make_header_ent, header_tree.children[0].children))
validate_header_fields(header, error_collector, only_header=True)
error_collector.raise_if_any()
return ParseResult(header=header, entities=defaultdict(list))
instance_identifiers = []
transformer = {}
if not with_tree:
# If we're not going to return the tree, we also don't need to
# keep in memory while parsing. So we build a transformer that
# just returns None for every rule. lark creates a dictionary
# of callbacks from the transformer type object, so we can't
# simply use __getattr__ we need an actual type objects with
# callback functions for the rules given in the
# Create a temporary parser just for analysing the grammar
temp = Lark(grammar, parser="lalr", start="file")
# Extract the rule names
rule_names = filter(
lambda s: not s.startswith("_"), set(r.origin.name for r in temp.rules)
)
null_function = lambda self, *args: None
# Create dictionary of methods for type() creation
methods = {r: null_function for r in rule_names}
# Even in this case we do want to report duplicate identifiers
# so these need to be captured
methods["id"] = lambda self, *args: args
methods["simple_entity_instance"] = (
lambda self, tree: instance_identifiers.append(
(int(tree[0][0][0][1:]), int(tree[0][0][0].line))
)
)
NT = type("NullTransformer", (Transformer,), methods)
transformer = {"transformer": NT()}
parser = Lark(grammar, parser="lalr", start="file", **transformer)
try:
ast = parser.parse(filecontent_wo_comments)
except (UnexpectedToken, UnexpectedCharacters) as e:
error_collector.add(SyntaxError(filecontent, e))
error_collector.raise_if_any() # Immediately abort in case of critical error
if with_tree:
header, data = process_tree(filecontent, ast, with_progress, error_collector)
error_collector.raise_if_any()
return ParseResult(header=header, entities=data)
else:
# process_tree() would take care of duplicate identifiers,
# but we need to do it ourselves now using our rudimentary
# transformer
seen = set()
for iden, lineno in instance_identifiers:
if iden == 0:
error_collector.add(
InvalidNameError(filecontent, iden, [lineno, lineno])
)
if iden in seen:
error_collector.add(
DuplicateNameError(filecontent, iden, [lineno, lineno])
)
else:
seen.add(iden)
error_collector.raise_if_any()
@@ -0,0 +1,107 @@
from lark import Transformer
from dataclasses import dataclass
import numbers
from lark import Lark, Transformer, Tree, Token
class IfcType:
def __init__(self, ifctype, value):
self.ifctype = ifctype
self.value = value
def __str__(self):
return self.ifctype + "(" + str(self.value) + ")"
__repr__ = __str__
@dataclass
class entity_instance:
id: int
type: str
attributes: tuple
lines: tuple
def __getitem__(self, k):
if isinstance(k, numbers.Integral):
return self.attributes[k]
else:
# compatibility with dict
return getattr(self, k)
def __repr__(self):
return f'#{self.id}={self.type}({",".join(map(str, self.attributes))})'
class T(Transformer):
def id(self, s):
return int(s[0][1:])
def string(self, s):
word = "".join(s).replace("''", "'")
return word
def keyword(self, s):
word = "".join(s)
return word
def untyped_parameter(self, s):
return s[0]
def parameter(self, s):
return s[0]
def typed_parameter(self, s):
if len(s):
return IfcType(s[0], s[1])
else:
return ()
def omitted_parameter(self, s):
return s[0]
def enumeration(self, s):
return s[0]
parameter_list = tuple
list = tuple
subsuper_record = list
INT = int
REAL = float
NONE = lambda *args: None
STAR = str
def create_step_entity(entity_tree):
t = T(visit_tokens=True).transform(entity_tree)
def get_line_number(t):
if isinstance(t, Token):
yield t.line
def traverse(fn, x):
yield from fn(x)
if isinstance(x, Tree):
for c in x.children:
yield from traverse(fn, c)
lines = list(traverse(get_line_number, entity_tree))
entity_id = t.children[0].children[0]
entity_type = t.children[0].children[1].children[0]
attributes_tree = t.children[0].children[1].children[1]
attributes = list(attributes_tree)
return entity_instance(
entity_id,
entity_type,
attributes,
(min(lines), max(lines)),
)
def make_header_ent(ast):
rule = ast.data
params = T(visit_tokens=True).transform(ast.children[0])
return rule.upper(), params
@@ -0,0 +1 @@
lark-parser
@@ -0,0 +1,206 @@
import glob
import pytest
try:
from .__init__ import (
parse,
open,
_ValidationError,
CollectedValidationErrors,
DuplicateNameError,
HeaderFieldError,
)
except:
from __init__ import (
parse,
open,
_ValidationError,
CollectedValidationErrors,
DuplicateNameError,
HeaderFieldError,
)
from contextlib import nullcontext
def create_context(fn):
if "fail_" in fn:
return pytest.raises(_ValidationError)
else:
return nullcontext()
@pytest.mark.parametrize("file", glob.glob("fixtures/*.ifc"))
def test_file_with_tree(file):
with create_context(file):
parse(filename=file, with_tree=True)
@pytest.mark.parametrize("file", glob.glob("fixtures/*.ifc"))
def test_file_without_tree(file):
if any(
sub in file
for sub in [
"fail_too_many_header_entity_fields.ifc",
"fail_multiple_wrong_header_fields",
]
):
pytest.skip(
"This file relies on header field validation using the parsed AST, "
"but with_tree=False uses a NullTransformer that discards the AST, "
"so validating the header field names is not possible in this mode."
)
with create_context(file):
parse(filename=file, with_tree=False)
def test_parse_features():
f = open("fixtures/pass_1.ifc")
assert f.by_id(1).id == 1
assert f.by_id(1).type == "IFCPERSON"
assert f.data_[1][0].type == "IFCPERSON"
assert f.by_type("ifcperson")[0].id == 1
assert f[1][0] is None
assert f.header.file_description[0][0] == "ViewDefinition [CoordinationView]"
assert f.header_.get("FILE_DESCRIPTION")[0][0]
assert f.by_type("ifcapplication")[1][2] == "Nested ' quotes"
def test_parse_valid_header():
f = open("fixtures/passing_header.ifc")
expected_description = {
"description": ("ViewDefinition [Alignment-basedView]",),
"implementation_level": "2;1",
}
expected_name = {
"name": "Header example2.ifc",
"time_stamp": "2022-09-16T10:35:07",
"author": ("Evandro Alfieri",),
"organization": ("buildingSMART Int.",),
"preprocessor_version": "IFC Motor 1.0",
"originating_system": "Company - Application - 26.0.0.0",
"authorization": "none",
}
expected_schema = {
"schema_identifiers": ("IFC4X3_ADD2",),
}
for key, val in expected_description.items():
assert getattr(f.header.file_description, key) == val, f"{key} mismatch"
for key, val in expected_name.items():
assert getattr(f.header.file_name, key) == val, f"{key} mismatch"
for key, val in expected_schema.items():
assert getattr(f.header.file_schema, key) == val, f"{key} mismatch"
def test_header_only_api():
f = open("fixtures/passing_header.ifc", only_header=True)
expected_description = {
"description": ("ViewDefinition [Alignment-basedView]",),
"implementation_level": "2;1",
}
expected_name = {
"name": "Header example2.ifc",
"time_stamp": "2022-09-16T10:35:07",
"author": ("Evandro Alfieri",),
"organization": ("buildingSMART Int.",),
"preprocessor_version": "IFC Motor 1.0",
"originating_system": "Company - Application - 26.0.0.0",
"authorization": "none",
}
expected_schema = {
"schema_identifiers": ("IFC4X3_ADD2",),
}
for key, val in expected_description.items():
assert getattr(f.header.file_description, key) == val, f"{key} mismatch"
for key, val in expected_name.items():
assert getattr(f.header.file_name, key) == val, f"{key} mismatch"
for key, val in expected_schema.items():
assert getattr(f.header.file_schema, key) == val, f"{key} mismatch"
def test_file_mvd_attr():
f = open("fixtures/extended_mvd.ifc", only_header=True)
assert "ReferenceView_V1.2" in f.mvd.view_definitions
assert all(
i in f.mvd.keywords
for i in ["exchange_requirements", "view_definitions", "remark", "comments"]
)
assert "Ramp" in f.mvd.options["ExcludedObjects"]
assert f.mvd.Remark["SomeKey"] == "SomeValue"
assert len(f.mvd.comments) == 2
assert all(
v in vars(f.header).keys()
for v in ["file_description", "file_name", "file_schema"]
)
assert len(f.header.file_name) == 7
@pytest.mark.parametrize(
"filename",
[
"fixtures/fail_invalid_header_entity.ifc",
"fixtures/fail_no_header.ifc",
],
)
def test_invalid_headers_(filename):
# error in header
with pytest.raises(_ValidationError):
parse(filename=filename, with_tree=False, only_header=True)
@pytest.mark.parametrize(
"filename",
[
"fixtures/fail_duplicate_id.ifc",
"fixtures/fail_double_comma.ifc",
"fixtures/fail_double_semi.ifc",
],
)
def test_valid_headers(filename):
# error in body
with nullcontext():
parse(filename=filename, with_tree=False, only_header=True)
def test_header_entity_fields():
with pytest.raises(_ValidationError):
parse(
filename="fixtures/fail_too_many_header_entity_fields.ifc", only_header=True
)
def test_header_entity_fields_whole_file():
with pytest.raises(_ValidationError):
parse(filename="fixtures/fail_too_many_header_entity_fields.ifc")
def test_header_entity_fields_whole_file():
with pytest.raises(CollectedValidationErrors) as exc_info:
parse(filename="fixtures/fail_multiple_duplicate_ids.ifc")
errors = exc_info.value.errors
assert len(errors) == 2
assert all(isinstance(e, DuplicateNameError) for e in errors)
def test_multiple_wrong_header_fields():
with pytest.raises(CollectedValidationErrors) as exc_info:
parse(filename="fixtures/fail_multiple_wrong_header_fields.ifc")
errors = exc_info.value.errors
assert len(errors) == 2
assert all(isinstance(e, HeaderFieldError) for e in errors)