From a1c1d6d971257f03f1926db9dc44e659d2773d2d Mon Sep 17 00:00:00 2001 From: jaseg Date: Mon, 24 Jan 2022 14:19:01 +0100 Subject: Add more tests --- gerbonara/gerber/excellon.py | 73 +++++++++++++++++++++++++------------------- 1 file changed, 42 insertions(+), 31 deletions(-) (limited to 'gerbonara/gerber/excellon.py') diff --git a/gerbonara/gerber/excellon.py b/gerbonara/gerber/excellon.py index 02e63d7..3931660 100755 --- a/gerbonara/gerber/excellon.py +++ b/gerbonara/gerber/excellon.py @@ -52,10 +52,14 @@ class ExcellonContext: def route_mode(self, unit, x, y): x, y = self.settings.unit(x, unit), self.settings.unit(y, unit) - if self.mode == ProgramState.ROUTING and (self.x, self.y) == (x, y): - return # nothing to do + if self.mode == ProgramState.ROUTING: + if (self.x, self.y) == (x, y): + return # nothing to do + else: + yield 'M16' # drill up yield 'G00' + 'X' + self.settings.write_excellon_value(x) + 'Y' + self.settings.write_excellon_value(y) + yield 'M15' # drill down def set_current_point(self, unit, x, y): self.current_point = self.settings.unit(x, unit), self.settings.unit(y, unit) @@ -179,13 +183,13 @@ class ExcellonFile(CamFile): return kls(objects=parser.objects, comments=parser.comments, import_settings=settings, generator_hints=parser.generator_hints, filename=filename) - def _generate_statements(self, settings): + def _generate_statements(self, settings, drop_comments=True): yield '; XNC file generated by gerbonara' - if self.comments: + if self.comments and not drop_comments: yield '; Comments found in original file:' - for comment in self.comments: - yield ';' + comment + for comment in self.comments: + yield ';' + comment yield 'M48' yield 'METRIC' if settings.unit == MM else 'INCH' @@ -219,7 +223,7 @@ class ExcellonFile(CamFile): yield 'M30' - def to_excellon(self, settings=None): + def to_excellon(self, settings=None, drop_comments=True): ''' Export to Excellon format. This function always generates XNC, which is a well-defined subset of Excellon. ''' if settings is None: @@ -229,11 +233,11 @@ class ExcellonFile(CamFile): settings = FileSettings() settings.zeros = None settings.number_format = (3,5) - return '\n'.join(self._generate_statements(settings)) + return '\n'.join(self._generate_statements(settings, drop_comments=drop_comments)) - def save(self, filename, settings=None): + def save(self, filename, settings=None, drop_comments=True): with open(filename, 'w') as f: - f.write(self.to_excellon(settings)) + f.write(self.to_excellon(settings, drop_comments=drop_comments)) def offset(self, x=0, y=0, unit=MM): self.objects = [ obj.with_offset(x, y, unit) for obj in self.objects ] @@ -346,14 +350,20 @@ class ExcellonParser(object): self.is_plated = None self.comments = [] self.generator_hints = [] + self.lineno = None + self.filename = None + + def warn(self, msg): + warnings.warn(f'{self.filename}:{self.lineno} "{self.line}": {msg}', SyntaxWarning) def do_parse(self, data, filename=None): # filename arg is for error messages - filename = filename or '' + self.filename = filename = filename or '' leftover = None for lineno, line in enumerate(data.splitlines(), start=1): line = line.strip() + self.lineno, self.line = lineno, line # for warnings if not line: continue @@ -361,7 +371,7 @@ class ExcellonParser(object): # Coordinates of G00 and G01 may be on the next line if line == 'G00' or line == 'G01': if leftover: - warnings.warn('Two consecutive G00/G01 commands without coordinates. Ignoring first.', SyntaxWarning) + self.warn('Two consecutive G00/G01 commands without coordinates. Ignoring first.') leftover = line continue @@ -370,10 +380,11 @@ class ExcellonParser(object): leftover = None if line and self.program_state == ProgramState.FINISHED: - warnings.warn('Commands found following end of program statement.', SyntaxWarning) + self.warn('Commands found following end of program statement.') # TODO check first command in file is "start of header" command. try: + #print(f'{lineno} "{line}"', end=' ') if not self.exprs.handle(self, line): raise ValueError('Unknown excellon statement:', line) except Exception as e: @@ -392,7 +403,7 @@ class ExcellonParser(object): raise SyntaxError('BUG: Allegro excellon tool def has mismatching tool indices. Please file a bug report on our issue tracker and provide this file!') if index in self.tools: - warnings.warn('Re-definition of tool index {index}, overwriting old definition.', SyntaxWarning) + self.warn('Re-definition of tool index {index}, overwriting old definition.') # NOTE: We map "optionally" plated holes to plated holes for API simplicity. If you hit a case where that's a # problem, please raise an issue on our issue tracker, explain why you need this and provide an example file. @@ -407,9 +418,9 @@ class ExcellonParser(object): unit = MM if unit != self.settings.unit: - warnings.warn('Allegro Excellon drill file tool definitions in {unit.name}, but file parameters say the ' + self.warn('Allegro Excellon drill file tool definitions in {unit.name}, but file parameters say the ' 'file should be in {settings.unit.name}. Please double-check that this is correct, and if it is, ' - 'please raise an issue on our issue tracker.', SyntaxWarning) + 'please raise an issue on our issue tracker.') self.tools[index] = ExcellonTool(diameter=diameter, plated=is_plated, unit=unit) @@ -420,7 +431,7 @@ class ExcellonParser(object): tool = ExcellonTool(diameter=float(match['diameter']), unit=unit, plated=self.is_plated) if (index := int(match['index'])) in self.tools: - warnings.warn('Re-definition of tool index {index}, overwriting old definition.', SyntaxWarning) + self.warn('Re-definition of tool index {index}, overwriting old definition.') self.tools[index] = tool self.generator_hints.append('easyeda') @@ -431,7 +442,7 @@ class ExcellonParser(object): # not a parser for the type of Excellon files a CAM program sends to the machine. if (index := int(match[1])) in self.tools: - warnings.warn('Re-definition of tool index {index}, overwriting old definition.', SyntaxWarning) + self.warn('Re-definition of tool index {index}, overwriting old definition.') params = { m[0]: self.settings.parse_gerber_value(m[1:]) for m in re.findall('[BCFHSTZ][.0-9]+', match[2]) } @@ -481,9 +492,9 @@ class ExcellonParser(object): def wrapper(self, *args, **kwargs): nonlocal name if self.program_state is None: - warnings.warn(f'{name} header statement found before start of header', SyntaxWarning) + self.warn(f'{name} header statement found before start of header') elif self.program_state != ProgramState.HEADER: - warnings.warn(f'{name} header statement found after end of header', SyntaxWarning) + self.warn(f'{name} header statement found after end of header') fun(self, *args, **kwargs) return wrapper return wrap @@ -495,7 +506,7 @@ class ExcellonParser(object): # of the file. self.generator_hints.append('fritzing') elif self.program_state is not None: - warnings.warn(f'M48 "header start" statement found in the middle of the file, currently in {self.program_state}', SyntaxWarning) + self.warn(f'M48 "header start" statement found in the middle of the file, currently in {self.program_state}') self.program_state = ProgramState.HEADER @exprs.match('M95') @@ -510,7 +521,7 @@ class ExcellonParser(object): self.active_tool = self.tools[self.tools.index(self.active_tool) + 1] else: - warnings.warn('M00 statement found before first tool selection statement.', SyntaxWarning) + self.warn('M00 statement found before first tool selection statement.') @exprs.match('M15') def handle_drill_down(self, match): @@ -524,7 +535,7 @@ class ExcellonParser(object): @exprs.match('M30') def handle_end_of_program(self, match): if self.program_state in (None, ProgramState.HEADER): - warnings.warn('M30 statement found before end of header.', SyntaxWarning) + self.warn('M30 statement found before end of header.') self.program_state = ProgramState.FINISHED # ignore. # TODO: maybe add warning if this is followed by other commands. @@ -551,7 +562,7 @@ class ExcellonParser(object): @exprs.match('G00' + xy_coord) def handle_start_routing(self, match): if self.program_state is None: - warnings.warn('Routing mode command found before header.', SyntaxWarning) + self.warn('Routing mode command found before header.') self.program_state = ProgramState.ROUTING self.do_move(match) @@ -572,7 +583,7 @@ class ExcellonParser(object): if self.active_tool: return self.active_tool - warnings.warn('Routing command found before first tool definition.', SyntaxWarning) + self.warn('Routing command found before first tool definition.') return None @exprs.match('(?PG01|G02|G03)' + xy_coord + coord('A') + coord('I') + coord('J')) @@ -598,19 +609,19 @@ class ExcellonParser(object): if self.interpolation_mode == InterpMode.LINEAR: if a or i or j: - warnings.warn('A/I/J arc coordinates found in linear mode.', SyntaxWarning) + self.warn('A/I/J arc coordinates found in linear mode.') self.objects.append(Line(*start, *end, self.active_tool, unit=self.settings.unit)) else: if (x or y) and not (a or i or j): - warnings.warn('Arc without radius found.', SyntaxWarning) + self.warn('Arc without radius found.') clockwise = (self.interpolation_mode == InterpMode.CIRCULAR_CW) if a: # radius given if i or j: - warnings.warn('Arc without both radius and center specified.', SyntaxWarning) + self.warn('Arc without both radius and center specified.') # Convert endpoint-radius-endpoint notation to endpoint-center-endpoint notation. We always use the # smaller arc here. @@ -651,7 +662,7 @@ class ExcellonParser(object): self.settings.number_format = len(integer), len(fractional) elif self.settings.number_format == (None, None) and not metric: - warnings.warn('Using implicit number format from naked "INCH" statement. This is normal for Fritzing, Diptrace, Geda and pcb-rnd.', SyntaxWarning) + self.warn('Using implicit number format from naked "INCH" statement. This is normal for Fritzing, Diptrace, Geda and pcb-rnd.') self.settings.number_format = (2,4) @exprs.match('G90') @@ -680,11 +691,11 @@ class ExcellonParser(object): @exprs.match('G40|G41|G42|{coord("F")}') def handle_unhandled(self, match): - warnings.warn(f'{match[0]} excellon command intended for CAM tools found in EDA file.', SyntaxWarning) + self.warn(f'{match[0]} excellon command intended for CAM tools found in EDA file.') @exprs.match(coord('X', 'x1') + coord('Y', 'y1') + 'G85' + coord('X', 'x2') + coord('Y', 'y2')) def handle_slot_dotted(self, match): - warnings.warn('Weird G85 excellon slot command used. Please raise an issue on our issue tracker and provide this file for testing.', SyntaxWarning) + self.warn('Weird G85 excellon slot command used. Please raise an issue on our issue tracker and provide this file for testing.') self.do_move(match, 'X1', 'Y1') start, end = self.do_move(match, 'X2', 'Y2') -- cgit