PCB element Python types
Stefan Schuermans

Stefan Schuermans commited on 2021-02-17 19:13:39
Showing 4 changed files, with 602 additions and 448 deletions.

... ...
@@ -0,0 +1 @@
1
+/__pycache__/
... ...
@@ -1,457 +1,18 @@
1 1
 #! /usr/bin/env python3
2 2
 
3
+import pcb_parser
4
+import pcb_types
5
+
3 6
 import ezdxf
4
-import re
5 7
 import sys
6 8
 
7 9
 
8
-class ParseError(Exception):
9
-    def __init__(self, msg: str, pos: str):
10
-        super().__init__(self, msg, pos)
11
-        self.msg = msg
12
-        self.pos = pos
13
-
14
-    def __repr__(self) -> str:
15
-        return f'ParseError({repr(self.msg):s}, {repr(self.pos):s})'
16
-
17
-    def __str__(self) -> str:
18
-        return f'ParseError: {self.msg:s} at {self.pos:s}'
19
-
20
-
21
-class StringBuffer:
22
-    """
23
-    Store a string, an index and provide some operations.
24
-    """
25
-
26
-    def __init__(self, string: str):
27
-        self._string = string
28
-        self._index = 0
29
-        self._stack = []
30
-
31
-    def __enter__(self):
32
-        self.enter()
33
-        return self
34
-
35
-    def __exit__(self, exc_type, _exc_value, _traceback):
36
-        if exc_type is None:
37
-            self.complete()
38
-        else:
39
-            self.backtrack()
40
-
41
-    def advance(self, length: int):
42
-        """
43
-        Advance the index by length.
44
-        """
45
-        self._index = min(len(self._string), self._index + length)
46
-
47
-    def backtrack(self):
48
-        """
49
-        Track back from parsing something.
50
-        Pop index from stack and use it as the new current index.
51
-        """
52
-        self._index = self._stack[-1]
53
-        del self._stack[-1]
54
-
55
-    def check(self, s: str) -> bool:
56
-        """
57
-        Check if string s follows index.
58
-        If so, advance by length of s and return True
59
-        Otherwise, keep index and return False.
60
-        """
61
-        if self.peek(len(s)) == s:
62
-            self.advance(len(s))
63
-            return True
64
-        return False
65
-
66
-    def complete(self):
67
-        """
68
-        Complete parsing something.
69
-        Pop index from stack and throw it away.
70
-        """
71
-        del self._stack[-1]
72
-
73
-    def enter(self):
74
-        """
75
-        Enter parsing something.
76
-        Push current index onto stack.
77
-        """
78
-        self._stack.append(self._index)
79
-
80
-    def expect(self, s: str):
81
-        """
82
-        Check that string s follows index.
83
-        If so, advance.
84
-        If not, throw ParseError.
85
-        """
86
-        if not self.check(s):
87
-            raise ParseError(f'expected {repr(s):s}', self.pos())
88
-
89
-    def get(self, length: int) -> str:
90
-        """
91
-        Return the next length characters and advance the index.
92
-        """
93
-        s = self.peek(length)
94
-        self.advance(length)
95
-        return s
96
-
97
-    def getRe(self, ptrn: re.Pattern):
98
-        """
99
-        Return the matching the regular expression pattern and advance
100
-        index or throw ParseError.
101
-        """
102
-        m = ptrn.match(self._string[self._index:])
103
-        if not m:
104
-            raise ParseError(f'expected pattern {repr(ptrn.pattern):s}',
105
-                             self.pos())
106
-        self._index += len(m.group(0))
107
-        return m
108
-
109
-    def peek(self, length: int) -> str:
110
-        """
111
-        Return the next length characters without advancing the index.
112
-        """
113
-        return self._string[self._index:self._index + length]
114
-
115
-    def pos(self) -> str:
116
-        """
117
-        Get string describing current position.
118
-        """
119
-        before = self._string[:self._index].split('\n')
120
-        after = self._string[self._index:].split('\n')
121
-        line_no = len(before)
122
-        column_no = len(before[-1]) + 1
123
-        rest = after[0]
124
-        return f'line {line_no:d} column {column_no:d} {repr(rest):s}'
125
-
126
-
127
-class PcbBaseParser(StringBuffer):
128
-    """
129
-    Parser for the basic element of GNU PCB files.
130
-    """
131
-
132
-    re_float = re.compile(r'^[+-]?(?:[0-9]+(?:\.[0-9]*)?|\.[0-9]+)'
133
-                          r'(?:[]eE[+-]?[0-9]\+)?')
134
-    re_int = re.compile(r'^[+-]?[0-9]+')
135
-    re_quoted = re.compile(r'^"([^"]*)"')
136
-    re_space = re.compile(r'^\s+', re.MULTILINE)
137
-    re_space_opt = re.compile(r'^\s*', re.MULTILINE)
138
-
139
-    known_string_flags = set([
140
-        'edge2', 'hole', 'nopaste', 'octagon', 'onsolder', 'pin', 'showname',
141
-        'square', 'via'
142
-    ])
143
-
144
-    def parseCoord(self) -> float:
145
-        """
146
-        Parse a coordinate.
147
-        """
148
-        with self:
149
-            f = self.parseFloat()
150
-            if self.check('mil'):
151
-                f *= 100  # native unit == .01mil
152
-            elif self.check('mm'):
153
-                f *= 1e5 / 25.4  # 25.4mm == 1inch == 1e5 * .01mil
154
-            return f
155
-
156
-    def parseFloat(self) -> float:
157
-        """
158
-        Parse a floating point value.
159
-        """
160
-        with self:
161
-            s = self.getRe(self.re_float).group(0)
162
-            try:
163
-                return float(s)
164
-            except ValueError as exc:
165
-                raise ParseError(f'invalid floating point value {repr(s):s}',
166
-                                 self.pos())
167
-
168
-    def parseInt(self) -> int:
169
-        """
170
-        Parse a decimal integer.
171
-        """
172
-        with self:
173
-            s = self.getRe(self.re_int).group(0)
174
-            try:
175
-                return int(s)
176
-            except ValueError as exc:
177
-                raise ParseError('invalid integer value {repr(s):s}',
178
-                                 self.pos())
179
-
180
-    def parseQuoted(self) -> str:
181
-        """
182
-        Parse a quoted string.
183
-        """
184
-        with self:
185
-            return self.getRe(self.re_quoted).group(1)
186
-
187
-    def parseStringFlags(self) -> list:
188
-        """
189
-        Parse string flags.
190
-        """
191
-        with self:
192
-            q = self.parseQuoted()
193
-            fs = set(q.split(','))
194
-            fs -= {''}
195
-            unk = fs - self.known_string_flags
196
-            if unk:
197
-                unknown = ','.join(sorted(unk))
198
-                raise ParseError(f'unknown string flags {unknown:s}',
199
-                                 self.pos())
200
-            return sorted(fs)
201
-
202
-    def parseSpace(self):
203
-        """
204
-        Parse at least one whitespace character or more.
205
-        """
206
-        with self:
207
-            self.getRe(self.re_space)
208
-
209
-    def parseSpaceOpt(self):
210
-        """
211
-        Parse any number of whitespace characters.
212
-        """
213
-        with self:
214
-            self.getRe(self.re_space_opt)
215
-
216
-
217
-class PcbFootprintParser(PcbBaseParser):
218
-    """
219
-    Parser for a GNU PCB footprint file.
220
-    """
221
-
222
-    def parseBody(self, contents: list):
223
-        """
224
-        Parse a body of a block.
225
-        contents: list of lambdas with options for recursive descent
226
-        """
227
-        with self:
228
-            self.parseOpen()
229
-            while True:
230
-                excs = []  # collect parse errors
231
-                # end of body
232
-                try:
233
-                    self.parseClose()
234
-                    break
235
-                except ParseError as exc:
236
-                    excs.append(exc)
237
-                    # not end of body, try options
238
-                # try options
239
-                for c in contents:
240
-                    try:
241
-                        c()
242
-                        # option succeded, go to next content
243
-                        break
244
-                    except ParseError as exc:
245
-                        # an option failed, try next option
246
-                        excs.append(exc)
247
-                # all options failed
248
-                else:
249
-                    raise ParseError(
250
-                        'invalid syntax, all options failed: ' +
251
-                        ', '.join([exc.msg for exc in excs]), self.pos())
252
-
253
-    def parseClose(self):
254
-        """
255
-        Parse a closing parenthesis.
256
-        """
257
-        with self:
258
-            self.parseSpaceOpt()
259
-            self.expect(')')
260
-            self.parseSpaceOpt()
261
-
262
-    def parseElementArcClause(self):
263
-        """
264
-        Parse a GNU PCB element arc clause.
265
-        """
266
-        with self:
267
-            self.parseSpaceOpt()
268
-            self.expect('ElementArc')
269
-            self.parseSpaceOpt()
270
-            self.expect('[')
271
-            self.parseSpaceOpt()
272
-            r_x = self.parseCoord()
273
-            self.parseSpace()
274
-            r_y = self.parseCoord()
275
-            self.parseSpace()
276
-            width = self.parseCoord()
277
-            self.parseSpace()
278
-            height = self.parseCoord()
279
-            self.parseSpace()
280
-            start_angle = self.parseFloat()
281
-            self.parseSpace()
282
-            end_angle = self.parseFloat()
283
-            self.parseSpace()
284
-            thickness = self.parseCoord()
285
-            self.parseSpaceOpt()
286
-            self.expect(']')
287
-            self.parseSpaceOpt()
288
-
289
-    def parseElementBlock(self):
290
-        """
291
-        Parse a GNU PCB element block.
292
-        """
293
-        with self:
294
-            self.parseElementClause()
295
-            self.parseBody([
296
-                # parse options
297
-                lambda: self.parseElementArcClause(),
298
-                lambda: self.parseElementLineClause(),
299
-                lambda: self.parsePadClause(),
300
-                lambda: self.parsePinClause()
301
-            ])
302
-
303
-    def parseElementClause(self):
304
-        """
305
-        Parse a GNU PCB element clause.
306
-        """
307
-        with self:
308
-            self.parseSpaceOpt()
309
-            self.expect('Element')
310
-            self.parseSpaceOpt()
311
-            self.expect('[')
312
-            self.parseSpaceOpt()
313
-            s_flags = self.parseStringFlags()
314
-            self.parseSpace()
315
-            desc = self.parseQuoted()
316
-            self.parseSpace()
317
-            name = self.parseQuoted()
318
-            self.parseSpace()
319
-            value = self.parseQuoted()
320
-            self.parseSpace()
321
-            m_x = self.parseCoord()
322
-            self.parseSpace()
323
-            m_y = self.parseCoord()
324
-            self.parseSpace()
325
-            t_x = self.parseCoord()
326
-            self.parseSpace()
327
-            t_y = self.parseCoord()
328
-            self.parseSpace()
329
-            t_dir = self.parseInt()
330
-            self.parseSpace()
331
-            t_scale = self.parseInt()
332
-            self.parseSpace()
333
-            t_s_flags = self.parseStringFlags()
334
-            self.parseSpaceOpt()
335
-            self.expect(']')
336
-            self.parseSpaceOpt()
337
-
338
-    def parseElementLineClause(self):
339
-        """
340
-        Parse a GNU PCB element line clause.
341
-        """
342
-        with self:
343
-            self.parseSpaceOpt()
344
-            self.expect('ElementLine')
345
-            self.parseSpaceOpt()
346
-            self.expect('[')
347
-            self.parseSpaceOpt()
348
-            r_x1 = self.parseCoord()
349
-            self.parseSpace()
350
-            r_y1 = self.parseCoord()
351
-            self.parseSpace()
352
-            r_x2 = self.parseCoord()
353
-            self.parseSpace()
354
-            r_y2 = self.parseCoord()
355
-            self.parseSpace()
356
-            thickness = self.parseCoord()
357
-            self.parseSpaceOpt()
358
-            self.expect(']')
359
-            self.parseSpaceOpt()
360
-
361
-    def parseOpen(self):
362
-        """
363
-        Parse an opening parenthesis.
364
-        """
365
-        with self:
366
-            self.parseSpaceOpt()
367
-            self.expect('(')
368
-            self.parseSpaceOpt()
369
-
370
-    def parsePadClause(self):
371
-        """
372
-        Parse a GNU PCB pad clause.
373
-        """
374
-        with self:
375
-            self.parseSpaceOpt()
376
-            self.expect('Pad')
377
-            self.parseSpaceOpt()
378
-            self.expect('[')
379
-            self.parseSpaceOpt()
380
-            r_x1 = self.parseCoord()
381
-            self.parseSpace()
382
-            r_y1 = self.parseCoord()
383
-            self.parseSpace()
384
-            r_x2 = self.parseCoord()
385
-            self.parseSpace()
386
-            r_y2 = self.parseCoord()
387
-            self.parseSpace()
388
-            thickness = self.parseCoord()
389
-            self.parseSpace()
390
-            clearance = self.parseCoord()
391
-            self.parseSpace()
392
-            mask = self.parseCoord()
393
-            self.parseSpace()
394
-            name = self.parseQuoted()
395
-            self.parseSpace()
396
-            number = self.parseQuoted()
397
-            self.parseSpace()
398
-            s_flags = self.parseStringFlags()
399
-            self.parseSpaceOpt()
400
-            self.expect(']')
401
-            self.parseSpaceOpt()
402
-
403
-    def parsePinClause(self):
404
-        """
405
-        Parse a GNU PCB pin clause.
406
-        """
407
-        with self:
408
-            self.parseSpaceOpt()
409
-            self.expect('Pin')
410
-            self.parseSpaceOpt()
411
-            self.expect('[')
412
-            self.parseSpaceOpt()
413
-            r_x = self.parseCoord()
414
-            self.parseSpace()
415
-            r_y = self.parseCoord()
416
-            self.parseSpace()
417
-            thickness = self.parseCoord()
418
-            self.parseSpace()
419
-            clearance = self.parseCoord()
420
-            self.parseSpace()
421
-            mask = self.parseCoord()
422
-            self.parseSpace()
423
-            drill = self.parseCoord()
424
-            self.parseSpace()
425
-            name = self.parseQuoted()
426
-            self.parseSpace()
427
-            number = self.parseQuoted()
428
-            self.parseSpace()
429
-            s_flags = self.parseStringFlags()
430
-            self.parseSpaceOpt()
431
-            self.expect(']')
432
-            self.parseSpaceOpt()
433
-
434
-
435
-class PcbFootprint:
436
-    """
437
-    A footprint of GNU PCB.
438
-    """
439
-
440
-    def __init__(self):
441
-        self.reset()
442
-
443
-    def readPcb(self, file_name: str):
444
-        """
445
-        Read a PCB footprint file.
446
-        """
447
-        self.reset()
10
+def read_footprint(file_name: str) -> pcb_types.Element:
448 11
     with open(file_name, 'r') as f:
449 12
         s = f.read()
450
-        p = PcbFootprintParser(s)
451
-        p.parseElementBlock()
452
-
453
-    def reset(self):
454
-        pass
13
+    parser = pcb_parser.PcbFootprintParser(s)
14
+    element = parser.parseElementBlock()
15
+    return element
455 16
 
456 17
 
457 18
 def write_dxf(file_name: str):
... ...
@@ -462,8 +23,8 @@ def write_dxf(file_name: str):
462 23
 
463 24
 
464 25
 def main():
465
-    fp = PcbFootprint()
466
-    fp.readPcb(sys.argv[1])
26
+    fp = read_footprint(sys.argv[1])
27
+    print(fp)
467 28
 
468 29
 
469 30
 if __name__ == '__main__':
... ...
@@ -0,0 +1,445 @@
1
+"""
2
+Parser for GNU PCB files.
3
+"""
4
+
5
+import re
6
+
7
+import pcb_types
8
+
9
+
10
+class ParseError(Exception):
11
+    def __init__(self, msg: str, pos: str):
12
+        super().__init__(self, msg, pos)
13
+        self.msg = msg
14
+        self.pos = pos
15
+
16
+    def __repr__(self) -> str:
17
+        return f'ParseError({repr(self.msg):s}, {repr(self.pos):s})'
18
+
19
+    def __str__(self) -> str:
20
+        return f'ParseError: {self.msg:s} at {self.pos:s}'
21
+
22
+
23
+class StringBuffer:
24
+    """
25
+    Store a string, an index and provide some operations.
26
+    """
27
+
28
+    def __init__(self, string: str):
29
+        self._string = string
30
+        self._index = 0
31
+        self._stack = []
32
+
33
+    def __enter__(self):
34
+        self.enter()
35
+        return self
36
+
37
+    def __exit__(self, exc_type, _exc_value, _traceback):
38
+        if exc_type is None:
39
+            self.complete()
40
+        else:
41
+            self.backtrack()
42
+
43
+    def advance(self, length: int):
44
+        """
45
+        Advance the index by length.
46
+        """
47
+        self._index = min(len(self._string), self._index + length)
48
+
49
+    def backtrack(self):
50
+        """
51
+        Track back from parsing something.
52
+        Pop index from stack and use it as the new current index.
53
+        """
54
+        self._index = self._stack[-1]
55
+        del self._stack[-1]
56
+
57
+    def check(self, s: str) -> bool:
58
+        """
59
+        Check if string s follows index.
60
+        If so, advance by length of s and return True
61
+        Otherwise, keep index and return False.
62
+        """
63
+        if self.peek(len(s)) == s:
64
+            self.advance(len(s))
65
+            return True
66
+        return False
67
+
68
+    def complete(self):
69
+        """
70
+        Complete parsing something.
71
+        Pop index from stack and throw it away.
72
+        """
73
+        del self._stack[-1]
74
+
75
+    def enter(self):
76
+        """
77
+        Enter parsing something.
78
+        Push current index onto stack.
79
+        """
80
+        self._stack.append(self._index)
81
+
82
+    def expect(self, s: str):
83
+        """
84
+        Check that string s follows index.
85
+        If so, advance.
86
+        If not, throw ParseError.
87
+        """
88
+        if not self.check(s):
89
+            raise ParseError(f'expected {repr(s):s}', self.pos())
90
+
91
+    def get(self, length: int) -> str:
92
+        """
93
+        Return the next length characters and advance the index.
94
+        """
95
+        s = self.peek(length)
96
+        self.advance(length)
97
+        return s
98
+
99
+    def getRe(self, ptrn: re.Pattern):
100
+        """
101
+        Return the matching the regular expression pattern and advance
102
+        index or throw ParseError.
103
+        """
104
+        m = ptrn.match(self._string[self._index:])
105
+        if not m:
106
+            raise ParseError(f'expected pattern {repr(ptrn.pattern):s}',
107
+                             self.pos())
108
+        self._index += len(m.group(0))
109
+        return m
110
+
111
+    def peek(self, length: int) -> str:
112
+        """
113
+        Return the next length characters without advancing the index.
114
+        """
115
+        return self._string[self._index:self._index + length]
116
+
117
+    def pos(self) -> str:
118
+        """
119
+        Get string describing current position.
120
+        """
121
+        before = self._string[:self._index].split('\n')
122
+        after = self._string[self._index:].split('\n')
123
+        line_no = len(before)
124
+        column_no = len(before[-1]) + 1
125
+        rest = after[0]
126
+        return f'line {line_no:d} column {column_no:d} {repr(rest):s}'
127
+
128
+
129
+class PcbBaseParser(StringBuffer):
130
+    """
131
+    Parser for the basic element of GNU PCB files.
132
+    """
133
+
134
+    re_float = re.compile(r'^[+-]?(?:[0-9]+(?:\.[0-9]*)?|\.[0-9]+)'
135
+                          r'(?:[]eE[+-]?[0-9]\+)?')
136
+    re_int = re.compile(r'^[+-]?[0-9]+')
137
+    re_quoted = re.compile(r'^"([^"]*)"')
138
+    re_space = re.compile(r'^\s+', re.MULTILINE)
139
+    re_space_opt = re.compile(r'^\s*', re.MULTILINE)
140
+
141
+    known_string_flags = set([
142
+        'edge2', 'hole', 'nopaste', 'octagon', 'onsolder', 'pin', 'showname',
143
+        'square', 'via'
144
+    ])
145
+
146
+    def parseCoord(self) -> float:
147
+        """
148
+        Parse a coordinate.
149
+        """
150
+        with self:
151
+            f = self.parseFloat()
152
+            if self.check('mil'):
153
+                f *= 100  # native unit == .01mil
154
+            elif self.check('mm'):
155
+                f *= 1e5 / 25.4  # 25.4mm == 1inch == 1e5 * .01mil
156
+            return f
157
+
158
+    def parseFloat(self) -> float:
159
+        """
160
+        Parse a floating point value.
161
+        """
162
+        with self:
163
+            s = self.getRe(self.re_float).group(0)
164
+            try:
165
+                return float(s)
166
+            except ValueError as exc:
167
+                raise ParseError(f'invalid floating point value {repr(s):s}',
168
+                                 self.pos())
169
+
170
+    def parseInt(self) -> int:
171
+        """
172
+        Parse a decimal integer.
173
+        """
174
+        with self:
175
+            s = self.getRe(self.re_int).group(0)
176
+            try:
177
+                return int(s)
178
+            except ValueError as exc:
179
+                raise ParseError('invalid integer value {repr(s):s}',
180
+                                 self.pos())
181
+
182
+    def parseQuoted(self) -> str:
183
+        """
184
+        Parse a quoted string.
185
+        """
186
+        with self:
187
+            return self.getRe(self.re_quoted).group(1)
188
+
189
+    def parseStringFlags(self) -> list:
190
+        """
191
+        Parse string flags.
192
+        """
193
+        with self:
194
+            q = self.parseQuoted()
195
+            fs = set(q.split(','))
196
+            fs -= {''}
197
+            unk = fs - self.known_string_flags
198
+            if unk:
199
+                unknown = ','.join(sorted(unk))
200
+                raise ParseError(f'unknown string flags {unknown:s}',
201
+                                 self.pos())
202
+            return sorted(fs)
203
+
204
+    def parseSpace(self):
205
+        """
206
+        Parse at least one whitespace character or more.
207
+        """
208
+        with self:
209
+            self.getRe(self.re_space)
210
+
211
+    def parseSpaceOpt(self):
212
+        """
213
+        Parse any number of whitespace characters.
214
+        """
215
+        with self:
216
+            self.getRe(self.re_space_opt)
217
+
218
+
219
+class PcbFootprintParser(PcbBaseParser):
220
+    """
221
+    Parser for a GNU PCB footprint file.
222
+    """
223
+
224
+    def parseBody(self, contents: list):
225
+        """
226
+        Parse a body of a block.
227
+        contents: list of lambdas with options for recursive descent
228
+        """
229
+        with self:
230
+            self.parseOpen()
231
+            while True:
232
+                excs = []  # collect parse errors
233
+                # end of body
234
+                try:
235
+                    self.parseClose()
236
+                    break
237
+                except ParseError as exc:
238
+                    excs.append(exc)
239
+                    # not end of body, try options
240
+                # try options
241
+                for c in contents:
242
+                    try:
243
+                        c()
244
+                        # option succeded, go to next content
245
+                        break
246
+                    except ParseError as exc:
247
+                        # an option failed, try next option
248
+                        excs.append(exc)
249
+                # all options failed
250
+                else:
251
+                    raise ParseError(
252
+                        'invalid syntax, all options failed: ' +
253
+                        ', '.join([exc.msg for exc in excs]), self.pos())
254
+
255
+    def parseClose(self):
256
+        """
257
+        Parse a closing parenthesis.
258
+        """
259
+        with self:
260
+            self.parseSpaceOpt()
261
+            self.expect(')')
262
+            self.parseSpaceOpt()
263
+
264
+    def parseElementArcClause(self) -> pcb_types.ElementArc:
265
+        """
266
+        Parse a GNU PCB element arc clause.
267
+        """
268
+        with self:
269
+            self.parseSpaceOpt()
270
+            self.expect('ElementArc')
271
+            self.parseSpaceOpt()
272
+            self.expect('[')
273
+            self.parseSpaceOpt()
274
+            element_arc = pcb_types.ElementArc()
275
+            element_arc.r_x = self.parseCoord()
276
+            self.parseSpace()
277
+            element_arc.r_y = self.parseCoord()
278
+            self.parseSpace()
279
+            element_arc.width = self.parseCoord()
280
+            self.parseSpace()
281
+            element_arc.height = self.parseCoord()
282
+            self.parseSpace()
283
+            element_arc.start_angle = self.parseFloat()
284
+            self.parseSpace()
285
+            element_arc.end_angle = self.parseFloat()
286
+            self.parseSpace()
287
+            element_arc.thickness = self.parseCoord()
288
+            self.parseSpaceOpt()
289
+            self.expect(']')
290
+            self.parseSpaceOpt()
291
+            return element_arc
292
+
293
+    def parseElementBlock(self) -> pcb_types.Element:
294
+        """
295
+        Parse a GNU PCB element block.
296
+        """
297
+        with self:
298
+            element = self.parseElementClause()
299
+            self.parseBody([
300
+                # parse options
301
+                lambda: element.body.append(self.parseElementArcClause()),
302
+                lambda: element.body.append(self.parseElementLineClause()),
303
+                lambda: element.body.append(self.parsePadClause()),
304
+                lambda: element.body.append(self.parsePinClause())
305
+            ])
306
+            return element
307
+
308
+    def parseElementClause(self) -> pcb_types.Element:
309
+        """
310
+        Parse a GNU PCB element clause.
311
+        """
312
+        with self:
313
+            self.parseSpaceOpt()
314
+            self.expect('Element')
315
+            self.parseSpaceOpt()
316
+            self.expect('[')
317
+            self.parseSpaceOpt()
318
+            element = pcb_types.Element()
319
+            element.s_flags = self.parseStringFlags()
320
+            self.parseSpace()
321
+            element.desc = self.parseQuoted()
322
+            self.parseSpace()
323
+            element.name = self.parseQuoted()
324
+            self.parseSpace()
325
+            element.value = self.parseQuoted()
326
+            self.parseSpace()
327
+            element.m_x = self.parseCoord()
328
+            self.parseSpace()
329
+            element.m_y = self.parseCoord()
330
+            self.parseSpace()
331
+            element.t_x = self.parseCoord()
332
+            self.parseSpace()
333
+            element.t_y = self.parseCoord()
334
+            self.parseSpace()
335
+            element.t_dir = self.parseInt()
336
+            self.parseSpace()
337
+            element.t_scale = self.parseInt()
338
+            self.parseSpace()
339
+            element.t_s_flags = self.parseStringFlags()
340
+            self.parseSpaceOpt()
341
+            self.expect(']')
342
+            self.parseSpaceOpt()
343
+            return element
344
+
345
+    def parseElementLineClause(self) -> pcb_types.ElementLine:
346
+        """
347
+        Parse a GNU PCB element line clause.
348
+        """
349
+        with self:
350
+            self.parseSpaceOpt()
351
+            self.expect('ElementLine')
352
+            self.parseSpaceOpt()
353
+            self.expect('[')
354
+            self.parseSpaceOpt()
355
+            element_line = pcb_types.ElementLine()
356
+            element_line.r_x1 = self.parseCoord()
357
+            self.parseSpace()
358
+            element_line.r_y1 = self.parseCoord()
359
+            self.parseSpace()
360
+            element_line.r_x2 = self.parseCoord()
361
+            self.parseSpace()
362
+            element_line.r_y2 = self.parseCoord()
363
+            self.parseSpace()
364
+            element_line.thickness = self.parseCoord()
365
+            self.parseSpaceOpt()
366
+            self.expect(']')
367
+            self.parseSpaceOpt()
368
+            return element_line
369
+
370
+    def parseOpen(self):
371
+        """
372
+        Parse an opening parenthesis.
373
+        """
374
+        with self:
375
+            self.parseSpaceOpt()
376
+            self.expect('(')
377
+            self.parseSpaceOpt()
378
+
379
+    def parsePadClause(self) -> pcb_types.Pad:
380
+        """
381
+        Parse a GNU PCB pad clause.
382
+        """
383
+        with self:
384
+            self.parseSpaceOpt()
385
+            self.expect('Pad')
386
+            self.parseSpaceOpt()
387
+            self.expect('[')
388
+            self.parseSpaceOpt()
389
+            pad = pcb_types.Pad()
390
+            pad.r_x1 = self.parseCoord()
391
+            self.parseSpace()
392
+            pad.r_y1 = self.parseCoord()
393
+            self.parseSpace()
394
+            pad.r_x2 = self.parseCoord()
395
+            self.parseSpace()
396
+            pad.r_y2 = self.parseCoord()
397
+            self.parseSpace()
398
+            pad.thickness = self.parseCoord()
399
+            self.parseSpace()
400
+            pad.clearance = self.parseCoord()
401
+            self.parseSpace()
402
+            pad.mask = self.parseCoord()
403
+            self.parseSpace()
404
+            pad.name = self.parseQuoted()
405
+            self.parseSpace()
406
+            pad.number = self.parseQuoted()
407
+            self.parseSpace()
408
+            pad.s_flags = self.parseStringFlags()
409
+            self.parseSpaceOpt()
410
+            self.expect(']')
411
+            self.parseSpaceOpt()
412
+            return pad
413
+
414
+    def parsePinClause(self) -> pcb_types.Pin:
415
+        """
416
+        Parse a GNU PCB pin clause.
417
+        """
418
+        with self:
419
+            self.parseSpaceOpt()
420
+            self.expect('Pin')
421
+            self.parseSpaceOpt()
422
+            self.expect('[')
423
+            self.parseSpaceOpt()
424
+            pin = pcb_types.Pin()
425
+            pin.r_x = self.parseCoord()
426
+            self.parseSpace()
427
+            pin.r_y = self.parseCoord()
428
+            self.parseSpace()
429
+            pin.thickness = self.parseCoord()
430
+            self.parseSpace()
431
+            pin.clearance = self.parseCoord()
432
+            self.parseSpace()
433
+            pin.mask = self.parseCoord()
434
+            self.parseSpace()
435
+            pin.drill = self.parseCoord()
436
+            self.parseSpace()
437
+            pin.name = self.parseQuoted()
438
+            self.parseSpace()
439
+            pin.number = self.parseQuoted()
440
+            self.parseSpace()
441
+            pin.s_flags = self.parseStringFlags()
442
+            self.parseSpaceOpt()
443
+            self.expect(']')
444
+            self.parseSpaceOpt()
445
+            return pin
... ...
@@ -0,0 +1,147 @@
1
+"""
2
+Types representing entities of GNU PCB files.
3
+"""
4
+
5
+import collections
6
+
7
+
8
+class Struct:
9
+    """
10
+    Base class for struct-like Python objects
11
+    """
12
+
13
+    _attrs = collections.OrderedDict()  # attribute name: str -> default value
14
+
15
+    def __init__(self, **kwargs):
16
+        for name, def_val in self._attrs.items():
17
+            setattr(self, name, def_val)
18
+        for name, val in kwargs.items():
19
+            if name not in self._attrs:
20
+                raise ValueError(
21
+                    f'{type(self).__name__:s} does not have attribute {name:s}'
22
+                )
23
+            setattr(self, name, val)
24
+
25
+    def __repr__(self) -> str:
26
+        vals = ', '.join([
27
+            f'{name:s}={repr(getattr(self, name)):s}'
28
+            for name in self._attrs.keys()
29
+        ])
30
+        return f'{type(self).__name__:s}({vals:s})'
31
+
32
+    def _compare(self, other) -> int:
33
+        if self._attrs != other._attrs:
34
+            raise ValueError(f'{type(self).__name__:s} cannot be'
35
+                             f' compared to {type(other).__name__:s}')
36
+        for name in self._attrs:
37
+            s = getattr(self, name)
38
+            o = getattr(other, name)
39
+            if s < o:
40
+                return -1
41
+            if s > o:
42
+                return 1
43
+        return 0
44
+
45
+    def __eq__(self, other) -> bool:
46
+        return self._compare(other) == 0
47
+
48
+    def __ne__(self, other) -> bool:
49
+        return self._compare(other) != 0
50
+
51
+    def __lt__(self, other) -> bool:
52
+        return self._compare(other) < 0
53
+
54
+    def __le__(self, other) -> bool:
55
+        return self._compare(other) <= 0
56
+
57
+    def __gt__(self, other) -> bool:
58
+        return self._compare(other) > 0
59
+
60
+    def __ge__(self, other) -> bool:
61
+        return self._compare(other) >= 0
62
+
63
+
64
+class Element(Struct):
65
+    """
66
+    Element entity in a GNU PCB file.
67
+    """
68
+
69
+    _attrs = collections.OrderedDict([('s_flags', []), ('desc', ''),
70
+                                      ('name', ''), ('value', ''),
71
+                                      ('m_x', 0.0), ('m_y', 0.0), ('t_x', 0.0),
72
+                                      ('t_y', 0.0), ('t_dir', 0),
73
+                                      ('t_scale', 0.0), ('t_s_flags', []),
74
+                                      ('body', [])])
75
+
76
+    def __init__(self, **kwargs):
77
+        super().__init__(**kwargs)
78
+
79
+
80
+class ElementArc(Struct):
81
+    """
82
+    ElementArc entity in a GNU PCB file.
83
+    """
84
+
85
+    _attrs = collections.OrderedDict([('r_x', 0.0), ('r_y', 0.0),
86
+                                      ('width', 0.0), ('height', 0.0),
87
+                                      ('start_angle', 0.0), ('end_angle', 0.0),
88
+                                      ('thickness', 0.0)])
89
+
90
+    def __init__(self, **kwargs):
91
+        super().__init__(**kwargs)
92
+
93
+
94
+class ElementLine(Struct):
95
+    """
96
+    ElementLine entity in a GNU PCB file.
97
+    """
98
+
99
+    _attrs = collections.OrderedDict([('r_x1', 0.0), ('r_y1', 0.0),
100
+                                      ('r_x2', 0.0), ('r_y2', 0.0),
101
+                                      ('thickness', 0.0)])
102
+
103
+    def __init__(self, **kwargs):
104
+        super().__init__(**kwargs)
105
+
106
+
107
+class Pad(Struct):
108
+    """
109
+    Pad entity in a GNU PCB file.
110
+    """
111
+
112
+    _attrs = collections.OrderedDict([
113
+        ('r_x1', 0.0),
114
+        ('r_y1', 0.0),
115
+        ('r_x2', 0.0),
116
+        ('r_y2', 0.0),
117
+        ('thickness', 0.0),
118
+        ('clearance', 0.0),
119
+        ('mask', 0.0),
120
+        ('name', ''),
121
+        ('number', ''),
122
+        ('s_flags', []),
123
+    ])
124
+
125
+    def __init__(self, **kwargs):
126
+        super().__init__(**kwargs)
127
+
128
+
129
+class Pin(Struct):
130
+    """
131
+    Pin entity in a GNU PCB file.
132
+    """
133
+
134
+    _attrs = collections.OrderedDict([
135
+        ('r_x', 0.0),
136
+        ('r_y', 0.0),
137
+        ('thickness', 0.0),
138
+        ('clearance', 0.0),
139
+        ('mask', 0.0),
140
+        ('drill', 0.0),
141
+        ('name', ''),
142
+        ('number', ''),
143
+        ('s_flags', []),
144
+    ])
145
+
146
+    def __init__(self, **kwargs):
147
+        super().__init__(**kwargs)
0 148