Skip to content

Commit d848172

Browse files
committed
refactor, support error stacking
1 parent c4b999a commit d848172

11 files changed

Lines changed: 684 additions & 553 deletions

__init__.py

Lines changed: 10 additions & 551 deletions
Large diffs are not rendered by default.
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
ISO-10303-21;
2+
HEADER;
3+
FILE_DESCRIPTION(('ViewDefinition [CoordinationView]'),'2;1');
4+
FILE_NAME('','2022-05-04T08:08:30',(''),(''),'IfcOpenShell-0.7.0','IfcOpenShell-0.7.0','');
5+
FILE_SCHEMA(('IFC4'));
6+
ENDSEC;
7+
DATA;
8+
#1=IFCPERSON($,$,'',$,$,$,$,$);
9+
#2=IFCORGANIZATION($,'',$,$,$);
10+
#3=IFCPERSONANDORGANIZATION(#1,#2,$);
11+
#4=IFCAPPLICATION(#2,'0.7.0','IfcOpenShell-0.7.0','');
12+
#5=IFCOWNERHISTORY(#3,#4,$,.ADDED.,$,#3,#4,1651651710);
13+
#6=IFCDIRECTION((1.,0.,0.));
14+
#7=IFCDIRECTION((0.,0.,1.));
15+
#8=IFCCARTESIANPOINT((0.,0.,0.));
16+
#9=IFCAXIS2PLACEMENT3D(#8,#7,#6);
17+
#10=IFCDIRECTION((0.,1.,0.));
18+
#11=IFCGEOMETRICREPRESENTATIONCONTEXT($,'Model',3,1.E-05,#9,#10);
19+
#12=IFCDIMENSIONALEXPONENTS(0,0,0,0,0,0,0);
20+
#13=IFCSIUNIT(*,.LENGTHUNIT.,$,.METRE.);
21+
#14=IFCSIUNIT(*,.AREAUNIT.,$,.SQUARE_METRE.);
22+
#15=IFCSIUNIT(*,.VOLUMEUNIT.,$,.CUBIC_METRE.);
23+
#16=IFCSIUNIT(*,.PLANEANGLEUNIT.,$,.RADIAN.);
24+
#18=IFCMEASUREWITHUNIT(IFCPLANEANGLEMEASURE(0.017453292519943295),#16);
25+
#18=IFCCONVERSIONBASEDUNIT(#12,.PLANEANGLEUNIT.,'DEGREE',#17);
26+
#19=IFCUNITASSIGNMENT((#13,#14,#15,#18));
27+
#19=IFCPROJECT('2AyG2X0sb16Bjd4gQc07yZ',#5,'',$,$,$,$,(#11),#19);
28+
ENDSEC;
29+
END-ISO-10303-21;
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
ISO-10303-21;
2+
HEADER;
3+
FILE_DESCRIPTION(('ViewDefinition [ReferenceView_V1.2]', 'ExchangeRequirement [Any]'));
4+
FILE_NAME('Header.ifc','2025-02-13T15:58:45',('tricott'),('Trimble Inc.'),'TrimBimToIFC rel. 4.0.2','Example - Example - 2025.0','IFC4 model', '');
5+
FILE_SCHEMA(('IFC4'));
6+
ENDSEC;
7+
DATA;
8+
#1=IFCPERSON($,$,'',$,$,$,$,$);
9+
#2=IFCORGANIZATION($,'',$,$,$);
10+
#3=IFCPERSONANDORGANIZATION(#1,#2,$);
11+
#4=IFCAPPLICATION(#2,'v0.7.0-6c9e130ca','IfcOpenShell-v0.7.0-6c9e130ca','');
12+
#5=IFCOWNERHISTORY(#3,#4,$,.NOTDEFINED.,$,#3,#4,1700419055);
13+
#6=IFCDIRECTION((1.,0.,0.));
14+
#7=IFCDIRECTION((0.,0.,1.));
15+
#8=IFCCARTESIANPOINT((0.,0.,0.));
16+
#9=IFCAXIS2PLACEMENT3D(#8,#7,#6);
17+
#10=IFCDIRECTION((0.,1.));
18+
#11=IFCGEOMETRICREPRESENTATIONCONTEXT($,'Model',3,1.E-05,#9,#10);
19+
#12=IFCDIMENSIONALEXPONENTS(0,0,0,0,0,0,0);
20+
#13=IFCSIUNIT(*,.LENGTHUNIT.,$,.METRE.);
21+
#14=IFCSIUNIT(*,.AREAUNIT.,$,.SQUARE_METRE.);
22+
#15=IFCSIUNIT(*,.VOLUMEUNIT.,$,.CUBIC_METRE.);
23+
#16=IFCSIUNIT(*,.PLANEANGLEUNIT.,$,.RADIAN.);
24+
#17=IFCMEASUREWITHUNIT(IFCPLANEANGLEMEASURE(0.017453292519943295),#16);
25+
#18=IFCCONVERSIONBASEDUNIT(#12,.PLANEANGLEUNIT.,'DEGREE',#17);
26+
#19=IFCUNITASSIGNMENT((#13,#14,#15,#18));
27+
#20=IFCPROJECT('0iDmeiiLP3AOllitM2Favn',#5,'',$,$,$,$,(#11),#19);
28+
#21=IFCSITE('3rg2jGkIH10RFhrQsGZKRk',#5,$,$,$,$,$,$,$,$,$,$,$,$);
29+
ENDSEC;
30+
END-ISO-10303-21;

parser/__init__.py

Whitespace-only changes.

parser/errors.py

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
from lark.exceptions import UnexpectedToken
2+
3+
class ValidationError(Exception):
4+
pass
5+
6+
class ErrorCollector:
7+
def __init__(self):
8+
self.errors = []
9+
10+
def add(self, error):
11+
self.errors.append(error)
12+
13+
def raise_if_any(self):
14+
if self.errors:
15+
raise CollectedValidationErrors(self.errors)
16+
17+
class CollectedValidationErrors(ValidationError):
18+
def __init__(self, errors):
19+
self.errors = errors
20+
21+
def asdict(self, with_message=True):
22+
return [e.asdict(with_message=with_message) for e in self.errors]
23+
24+
def __str__(self):
25+
return f"{len(self.errors)} validation error(s) collected:\n" + "\n\n".join(str(e) for e in self.errors)
26+
27+
class SyntaxError(ValidationError):
28+
def __init__(self, filecontent, exception):
29+
self.filecontent = filecontent
30+
self.exception = exception
31+
32+
def asdict(self, with_message=True):
33+
return {
34+
"type": (
35+
"unexpected_token"
36+
if isinstance(self.exception, UnexpectedToken)
37+
else "unexpected_character"
38+
),
39+
"lineno": self.exception.line,
40+
"column": self.exception.column,
41+
"found_type": self.exception.token.type.lower(),
42+
"found_value": self.exception.token.value,
43+
"expected": sorted(x for x in self.exception.accepts if "__ANON" not in x),
44+
"line": self.filecontent.split("\n")[self.exception.line - 1],
45+
**({"message": str(self)} if with_message else {}),
46+
}
47+
48+
def __str__(self):
49+
d = self.asdict(with_message=False)
50+
if len(d["expected"]) == 1:
51+
exp = d["expected"][0]
52+
else:
53+
exp = f"one of {' '.join(d['expected'])}"
54+
55+
sth = "character" if d["type"] == "unexpected_character" else ""
56+
57+
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)}^"
58+
59+
60+
class DuplicateNameError(ValidationError):
61+
def __init__(self, filecontent, name, linenumbers):
62+
self.name = name
63+
self.filecontent = filecontent
64+
self.linenumbers = linenumbers
65+
66+
def asdict(self, with_message=True):
67+
return {
68+
"type": "duplicate_name",
69+
"name": self.name,
70+
"lineno": self.linenumbers[0],
71+
"line": self.filecontent.split("\n")[self.linenumbers[0] - 1],
72+
**({"message": str(self)} if with_message else {}),
73+
}
74+
75+
def __str__(self):
76+
d = self.asdict(with_message=False)
77+
78+
def build():
79+
yield f"On line {d['lineno']}:\nDuplicate instance name #{d['name']}"
80+
yield f"{d['lineno']:05d} | {d['line']}"
81+
yield " " * 8 + "^" * len(d["line"].rstrip())
82+
83+
return "\n".join(build())
84+
85+
86+
class HeaderFieldError(ValidationError):
87+
def __init__(self, field, found_len, expected_len):
88+
self.field = field
89+
self.found_len = found_len
90+
self.expected_len = expected_len
91+
92+
def asdict(self, with_message=True):
93+
return {
94+
"type": "invalid_header_field",
95+
"field": self.field,
96+
"expected_field_count": self.expected_len,
97+
"actual_field_count": self.found_len,
98+
**({"message": str(self)} if with_message else {}),
99+
}
100+
101+
def __str__(self):
102+
return (
103+
f"Invalid number of parameters for HEADER field '{self.field}'. "
104+
f"Expected {self.expected_len}, found {self.found_len}."
105+
)

parser/file.py

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import types
2+
import re
3+
import numbers
4+
import itertools
5+
6+
from .parse import parse, ParseResult
7+
from .grammar import HEADER_FIELDS
8+
from .transformer import entity_instance
9+
10+
try:
11+
from .mvd_info import MvdInfo, LARK_AVAILABLE
12+
except ImportError: # in case of running module locally (e.g. test_parser.py)
13+
from mvd_info import MvdInfo, LARK_AVAILABLE
14+
15+
class file:
16+
"""
17+
A somewhat compatible interface (but very limited) to ifcopenshell.file
18+
"""
19+
20+
def __init__(self, result:ParseResult):
21+
self.header_ = result.header
22+
self.data_ = result.entities
23+
24+
@property
25+
def schema_identifier(self) -> str:
26+
return self.header_["FILE_SCHEMA"][0][0]
27+
28+
@property
29+
def schema(self) -> str:
30+
"""General IFC schema version: IFC2X3, IFC4, IFC4X3."""
31+
prefixes = ("IFC", "X", "_ADD", "_TC")
32+
reg = "".join(f"(?P<{s}>{s}\\d+)?" for s in prefixes)
33+
match = re.match(reg, self.schema_identifier)
34+
version_tuple = tuple(
35+
map(
36+
lambda pp: int(pp[1][len(pp[0]) :]) if pp[1] else None,
37+
((p, match.group(p)) for p in prefixes),
38+
)
39+
)
40+
return "".join(
41+
"".join(map(str, t)) if t[1] else ""
42+
for t in zip(prefixes, version_tuple[0:2])
43+
)
44+
45+
@property
46+
def schema_version(self) -> tuple[int, int, int, int]:
47+
"""Numeric representation of the full IFC schema version.
48+
49+
E.g. IFC4X3_ADD2 is represented as (4, 3, 2, 0).
50+
"""
51+
schema = self.wrapped_data.schema
52+
version = []
53+
for prefix in ("IFC", "X", "_ADD", "_TC"):
54+
number = re.search(prefix + r"(\d)", schema)
55+
version.append(int(number.group(1)) if number else 0)
56+
return tuple(version)
57+
58+
59+
@property
60+
def header(self):
61+
header = {}
62+
for field_name, namedtuple_class in HEADER_FIELDS.items():
63+
field_data = self.header_.get(field_name.upper(), [])
64+
header[field_name.lower()] = namedtuple_class(*field_data)
65+
66+
return types.SimpleNamespace(**header)
67+
68+
69+
@property
70+
def mvd(self):
71+
if not LARK_AVAILABLE or MvdInfo is None:
72+
return None
73+
return MvdInfo(self.header)
74+
75+
def __getitem__(self, key: numbers.Integral) -> entity_instance:
76+
return self.by_id(key)
77+
78+
def by_id(self, id: int) -> entity_instance:
79+
"""Return an IFC entity instance filtered by IFC ID.
80+
81+
:param id: STEP numerical identifier
82+
:type id: int
83+
84+
:raises RuntimeError: If `id` is not found or multiple definitions exist for `id`.
85+
86+
:rtype: entity_instance
87+
"""
88+
ns = self.data_.get(id, [])
89+
if len(ns) == 0:
90+
raise RuntimeError(f"Instance with id {id} not found")
91+
elif len(ns) > 1:
92+
raise RuntimeError(f"Duplicate definition for id {id}")
93+
return ns[0]
94+
95+
def by_type(self, type: str) -> list[entity_instance]:
96+
"""Return IFC objects filtered by IFC Type and wrapped with the entity_instance class.
97+
:rtype: list[entity_instance]
98+
"""
99+
type_lc = type.lower()
100+
return list(
101+
filter(
102+
lambda ent: ent.type.lower() == type_lc,
103+
itertools.chain.from_iterable(self.data_.values()),
104+
)
105+
)
106+
107+
def open(fn, only_header= False) -> file:
108+
return file(parse(filename=fn, only_header=only_header))

parser/grammar.py

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
from collections import namedtuple
2+
3+
grammar = r"""
4+
file: "ISO-10303-21;" header data_section "END-ISO-10303-21;"
5+
header: "HEADER" ";" header_entity_list "ENDSEC" ";"
6+
header_line: (SPECIAL|DIGIT|LOWER|UPPER)* "*"
7+
data_section: "DATA" ";" (entity_instance)* "ENDSEC" ";"
8+
entity_instance: simple_entity_instance|complex_entity_instance
9+
simple_entity_instance: id "=" simple_record ";"
10+
complex_entity_instance: id "=" subsuper_record ";"
11+
subsuper_record : "(" simple_record_list ")"
12+
simple_record_list:simple_record simple_record*
13+
simple_record: keyword "("parameter_list?")"
14+
header_entity_list: file_description file_name file_schema
15+
file_description: "FILE_DESCRIPTION" "(" parameter_list ")" ";"
16+
file_name: "FILE_NAME" "(" parameter_list ")" ";"
17+
file_schema: "FILE_SCHEMA" "(" parameter_list ")" ";"
18+
id: /#[0-9]+/
19+
keyword: /[A-Z][0-9A-Z_]*/
20+
parameter: untyped_parameter|typed_parameter|omitted_parameter
21+
parameter_list: parameter ("," parameter)*
22+
list: "(" parameter ("," parameter)* ")" |"("")"
23+
typed_parameter: keyword "(" parameter ")"|"()"
24+
untyped_parameter: string| NONE |INT |REAL |enumeration |id |binary |list
25+
omitted_parameter:STAR
26+
enumeration: "." keyword "."
27+
binary: "\"" ("0"|"1"|"2"|"3") (HEX)* "\""
28+
string: "'" (REVERSE_SOLIDUS REVERSE_SOLIDUS|SPECIAL|DIGIT|SPACE|LOWER|UPPER|CONTROL_DIRECTIVE|"\\*\\")* "'"
29+
30+
STAR: "*"
31+
SLASH: "/"
32+
NONE: "$"
33+
SPECIAL : "!"
34+
| "*"
35+
| "$"
36+
| "%"
37+
| "&"
38+
| "."
39+
| "#"
40+
| "+"
41+
| ","
42+
| "-"
43+
| "("
44+
| ")"
45+
| "?"
46+
| "/"
47+
| ":"
48+
| ";"
49+
| "<"
50+
| "="
51+
| ">"
52+
| "@"
53+
| "["
54+
| "]"
55+
| "{"
56+
| "|"
57+
| "}"
58+
| "^"
59+
| "`"
60+
| "~"
61+
| "_"
62+
| "\""
63+
| "\"\""
64+
| "''"
65+
REAL: SIGN? DIGIT (DIGIT)* "." (DIGIT)* ("E" SIGN? DIGIT (DIGIT)* )?
66+
INT: SIGN? DIGIT (DIGIT)*
67+
CONTROL_DIRECTIVE: PAGE | ALPHABET | EXTENDED2 | EXTENDED4 | ARBITRARY
68+
PAGE : REVERSE_SOLIDUS "S" REVERSE_SOLIDUS LATIN_CODEPOINT
69+
LATIN_CODEPOINT : SPACE | DIGIT | LOWER | UPPER | SPECIAL | REVERSE_SOLIDUS | APOSTROPHE
70+
ALPHABET : REVERSE_SOLIDUS "P" UPPER REVERSE_SOLIDUS
71+
EXTENDED2: REVERSE_SOLIDUS "X2" REVERSE_SOLIDUS (HEX_TWO)* END_EXTENDED
72+
EXTENDED4 :REVERSE_SOLIDUS "X4" REVERSE_SOLIDUS (HEX_FOUR)* END_EXTENDED
73+
END_EXTENDED: REVERSE_SOLIDUS "X0" REVERSE_SOLIDUS
74+
ARBITRARY: REVERSE_SOLIDUS "X" REVERSE_SOLIDUS HEX_ONE
75+
HEX_FOUR: HEX_TWO HEX_TWO
76+
HEX_TWO: HEX_ONE HEX_ONE
77+
HEX_ONE: HEX HEX
78+
HEX: "0"
79+
| "1"
80+
| "2"
81+
| "3"
82+
| "4"
83+
| "5"
84+
| "6"
85+
| "7"
86+
| "8"
87+
| "9"
88+
| "A"
89+
| "B"
90+
| "C"
91+
| "D"
92+
| "E"
93+
| "F"
94+
APOSTROPHE: "'"
95+
REVERSE_SOLIDUS: "\\"
96+
DIGIT: "0".."9"
97+
SIGN: "+"|"-"
98+
LOWER: "a".."z"
99+
UPPER: "A".."Z"
100+
ESCAPE : "\\" ( "$" | "\"" | CHAR )
101+
CHAR : /[^$"\n]/
102+
WORD : CHAR+
103+
SPACE.10 : " "
104+
105+
%ignore /[ \t\f\r\n]/+
106+
"""
107+
108+
HEADER_FIELDS = {
109+
"file_description": namedtuple('file_description', ['description', 'implementation_level']),
110+
"file_name": namedtuple('file_name', ['name', 'time_stamp', 'author', 'organization', 'preprocessor_version', 'originating_system', 'authorization']),
111+
"file_schema": namedtuple('file_schema', ['schema_identifiers']),
112+
}
File renamed without changes.

0 commit comments

Comments
 (0)