BlinkenArea - GitList
Repositories
Blog
Wiki
partlib
Code
Commits
Branches
Tags
Search
Tree:
8a9acb1
Branches
Tags
master
stefan.experimental
partlib
pcb_footpr_conv
pcb_parser.py
PCB element Python types
Stefan Schuermans
commited
8a9acb1
at 2021-02-17 19:13:39
pcb_parser.py
Blame
History
Raw
""" Parser for GNU PCB files. """ import re import pcb_types class ParseError(Exception): def __init__(self, msg: str, pos: str): super().__init__(self, msg, pos) self.msg = msg self.pos = pos def __repr__(self) -> str: return f'ParseError({repr(self.msg):s}, {repr(self.pos):s})' def __str__(self) -> str: return f'ParseError: {self.msg:s} at {self.pos:s}' class StringBuffer: """ Store a string, an index and provide some operations. """ def __init__(self, string: str): self._string = string self._index = 0 self._stack = [] def __enter__(self): self.enter() return self def __exit__(self, exc_type, _exc_value, _traceback): if exc_type is None: self.complete() else: self.backtrack() def advance(self, length: int): """ Advance the index by length. """ self._index = min(len(self._string), self._index + length) def backtrack(self): """ Track back from parsing something. Pop index from stack and use it as the new current index. """ self._index = self._stack[-1] del self._stack[-1] def check(self, s: str) -> bool: """ Check if string s follows index. If so, advance by length of s and return True Otherwise, keep index and return False. """ if self.peek(len(s)) == s: self.advance(len(s)) return True return False def complete(self): """ Complete parsing something. Pop index from stack and throw it away. """ del self._stack[-1] def enter(self): """ Enter parsing something. Push current index onto stack. """ self._stack.append(self._index) def expect(self, s: str): """ Check that string s follows index. If so, advance. If not, throw ParseError. """ if not self.check(s): raise ParseError(f'expected {repr(s):s}', self.pos()) def get(self, length: int) -> str: """ Return the next length characters and advance the index. """ s = self.peek(length) self.advance(length) return s def getRe(self, ptrn: re.Pattern): """ Return the matching the regular expression pattern and advance index or throw ParseError. """ m = ptrn.match(self._string[self._index:]) if not m: raise ParseError(f'expected pattern {repr(ptrn.pattern):s}', self.pos()) self._index += len(m.group(0)) return m def peek(self, length: int) -> str: """ Return the next length characters without advancing the index. """ return self._string[self._index:self._index + length] def pos(self) -> str: """ Get string describing current position. """ before = self._string[:self._index].split('\n') after = self._string[self._index:].split('\n') line_no = len(before) column_no = len(before[-1]) + 1 rest = after[0] return f'line {line_no:d} column {column_no:d} {repr(rest):s}' class PcbBaseParser(StringBuffer): """ Parser for the basic element of GNU PCB files. """ re_float = re.compile(r'^[+-]?(?:[0-9]+(?:\.[0-9]*)?|\.[0-9]+)' r'(?:[]eE[+-]?[0-9]\+)?') re_int = re.compile(r'^[+-]?[0-9]+') re_quoted = re.compile(r'^"([^"]*)"') re_space = re.compile(r'^\s+', re.MULTILINE) re_space_opt = re.compile(r'^\s*', re.MULTILINE) known_string_flags = set([ 'edge2', 'hole', 'nopaste', 'octagon', 'onsolder', 'pin', 'showname', 'square', 'via' ]) def parseCoord(self) -> float: """ Parse a coordinate. """ with self: f = self.parseFloat() if self.check('mil'): f *= 100 # native unit == .01mil elif self.check('mm'): f *= 1e5 / 25.4 # 25.4mm == 1inch == 1e5 * .01mil return f def parseFloat(self) -> float: """ Parse a floating point value. """ with self: s = self.getRe(self.re_float).group(0) try: return float(s) except ValueError as exc: raise ParseError(f'invalid floating point value {repr(s):s}', self.pos()) def parseInt(self) -> int: """ Parse a decimal integer. """ with self: s = self.getRe(self.re_int).group(0) try: return int(s) except ValueError as exc: raise ParseError('invalid integer value {repr(s):s}', self.pos()) def parseQuoted(self) -> str: """ Parse a quoted string. """ with self: return self.getRe(self.re_quoted).group(1) def parseStringFlags(self) -> list: """ Parse string flags. """ with self: q = self.parseQuoted() fs = set(q.split(',')) fs -= {''} unk = fs - self.known_string_flags if unk: unknown = ','.join(sorted(unk)) raise ParseError(f'unknown string flags {unknown:s}', self.pos()) return sorted(fs) def parseSpace(self): """ Parse at least one whitespace character or more. """ with self: self.getRe(self.re_space) def parseSpaceOpt(self): """ Parse any number of whitespace characters. """ with self: self.getRe(self.re_space_opt) class PcbFootprintParser(PcbBaseParser): """ Parser for a GNU PCB footprint file. """ def parseBody(self, contents: list): """ Parse a body of a block. contents: list of lambdas with options for recursive descent """ with self: self.parseOpen() while True: excs = [] # collect parse errors # end of body try: self.parseClose() break except ParseError as exc: excs.append(exc) # not end of body, try options # try options for c in contents: try: c() # option succeded, go to next content break except ParseError as exc: # an option failed, try next option excs.append(exc) # all options failed else: raise ParseError( 'invalid syntax, all options failed: ' + ', '.join([exc.msg for exc in excs]), self.pos()) def parseClose(self): """ Parse a closing parenthesis. """ with self: self.parseSpaceOpt() self.expect(')') self.parseSpaceOpt() def parseElementArcClause(self) -> pcb_types.ElementArc: """ Parse a GNU PCB element arc clause. """ with self: self.parseSpaceOpt() self.expect('ElementArc') self.parseSpaceOpt() self.expect('[') self.parseSpaceOpt() element_arc = pcb_types.ElementArc() element_arc.r_x = self.parseCoord() self.parseSpace() element_arc.r_y = self.parseCoord() self.parseSpace() element_arc.width = self.parseCoord() self.parseSpace() element_arc.height = self.parseCoord() self.parseSpace() element_arc.start_angle = self.parseFloat() self.parseSpace() element_arc.end_angle = self.parseFloat() self.parseSpace() element_arc.thickness = self.parseCoord() self.parseSpaceOpt() self.expect(']') self.parseSpaceOpt() return element_arc def parseElementBlock(self) -> pcb_types.Element: """ Parse a GNU PCB element block. """ with self: element = self.parseElementClause() self.parseBody([ # parse options lambda: element.body.append(self.parseElementArcClause()), lambda: element.body.append(self.parseElementLineClause()), lambda: element.body.append(self.parsePadClause()), lambda: element.body.append(self.parsePinClause()) ]) return element def parseElementClause(self) -> pcb_types.Element: """ Parse a GNU PCB element clause. """ with self: self.parseSpaceOpt() self.expect('Element') self.parseSpaceOpt() self.expect('[') self.parseSpaceOpt() element = pcb_types.Element() element.s_flags = self.parseStringFlags() self.parseSpace() element.desc = self.parseQuoted() self.parseSpace() element.name = self.parseQuoted() self.parseSpace() element.value = self.parseQuoted() self.parseSpace() element.m_x = self.parseCoord() self.parseSpace() element.m_y = self.parseCoord() self.parseSpace() element.t_x = self.parseCoord() self.parseSpace() element.t_y = self.parseCoord() self.parseSpace() element.t_dir = self.parseInt() self.parseSpace() element.t_scale = self.parseInt() self.parseSpace() element.t_s_flags = self.parseStringFlags() self.parseSpaceOpt() self.expect(']') self.parseSpaceOpt() return element def parseElementLineClause(self) -> pcb_types.ElementLine: """ Parse a GNU PCB element line clause. """ with self: self.parseSpaceOpt() self.expect('ElementLine') self.parseSpaceOpt() self.expect('[') self.parseSpaceOpt() element_line = pcb_types.ElementLine() element_line.r_x1 = self.parseCoord() self.parseSpace() element_line.r_y1 = self.parseCoord() self.parseSpace() element_line.r_x2 = self.parseCoord() self.parseSpace() element_line.r_y2 = self.parseCoord() self.parseSpace() element_line.thickness = self.parseCoord() self.parseSpaceOpt() self.expect(']') self.parseSpaceOpt() return element_line def parseOpen(self): """ Parse an opening parenthesis. """ with self: self.parseSpaceOpt() self.expect('(') self.parseSpaceOpt() def parsePadClause(self) -> pcb_types.Pad: """ Parse a GNU PCB pad clause. """ with self: self.parseSpaceOpt() self.expect('Pad') self.parseSpaceOpt() self.expect('[') self.parseSpaceOpt() pad = pcb_types.Pad() pad.r_x1 = self.parseCoord() self.parseSpace() pad.r_y1 = self.parseCoord() self.parseSpace() pad.r_x2 = self.parseCoord() self.parseSpace() pad.r_y2 = self.parseCoord() self.parseSpace() pad.thickness = self.parseCoord() self.parseSpace() pad.clearance = self.parseCoord() self.parseSpace() pad.mask = self.parseCoord() self.parseSpace() pad.name = self.parseQuoted() self.parseSpace() pad.number = self.parseQuoted() self.parseSpace() pad.s_flags = self.parseStringFlags() self.parseSpaceOpt() self.expect(']') self.parseSpaceOpt() return pad def parsePinClause(self) -> pcb_types.Pin: """ Parse a GNU PCB pin clause. """ with self: self.parseSpaceOpt() self.expect('Pin') self.parseSpaceOpt() self.expect('[') self.parseSpaceOpt() pin = pcb_types.Pin() pin.r_x = self.parseCoord() self.parseSpace() pin.r_y = self.parseCoord() self.parseSpace() pin.thickness = self.parseCoord() self.parseSpace() pin.clearance = self.parseCoord() self.parseSpace() pin.mask = self.parseCoord() self.parseSpace() pin.drill = self.parseCoord() self.parseSpace() pin.name = self.parseQuoted() self.parseSpace() pin.number = self.parseQuoted() self.parseSpace() pin.s_flags = self.parseStringFlags() self.parseSpaceOpt() self.expect(']') self.parseSpaceOpt() return pin