#!/usr/bin/env python # -*- coding: utf-8 -*- # copyright 2014 Hamilton Kibbe # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # http://www.apache.org/licenses/LICENSE-2.0 # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """ Excellon Statements ==================== **Excellon file statement classes** """ import re import uuid from .utils import (parse_gerber_value, write_gerber_value, decimal_string, inch, metric) __all__ = ['ExcellonTool', 'ToolSelectionStmt', 'CoordinateStmt', 'CommentStmt', 'HeaderBeginStmt', 'HeaderEndStmt', 'RewindStopStmt', 'EndOfProgramStmt', 'UnitStmt', 'IncrementalModeStmt', 'VersionStmt', 'FormatStmt', 'LinkToolStmt', 'MeasuringModeStmt', 'RouteModeStmt', 'LinearModeStmt', 'DrillModeStmt', 'AbsoluteModeStmt', 'RepeatHoleStmt', 'UnknownStmt', 'ExcellonStatement', 'ZAxisRoutPositionStmt', 'RetractWithClampingStmt', 'RetractWithoutClampingStmt', 'CutterCompensationOffStmt', 'CutterCompensationLeftStmt', 'CutterCompensationRightStmt', 'ZAxisInfeedRateStmt', 'NextToolSelectionStmt', 'SlotStmt'] class ExcellonStatement(object): """ Excellon Statement abstract base class """ @classmethod def from_excellon(cls, line): raise NotImplementedError('from_excellon must be implemented in a ' 'subclass') def __init__(self, unit='inch', id=None): self.units = unit self.id = uuid.uuid4().int if id is None else id def to_excellon(self, settings=None): raise NotImplementedError('to_excellon must be implemented in a ' 'subclass') def to_inch(self): self.units = 'inch' def to_metric(self): self.units = 'metric' def offset(self, x_offset=0, y_offset=0): pass def __eq__(self, other): return self.__dict__ == other.__dict__ class ExcellonTool(ExcellonStatement): """ Excellon Tool class Parameters ---------- settings : FileSettings (dict-like) File-wide settings. kwargs : dict-like Tool settings from the excellon statement. Valid keys are: - `diameter` : Tool diameter [expressed in file units] - `rpm` : Tool RPM - `feed_rate` : Z-axis tool feed rate - `retract_rate` : Z-axis tool retraction rate - `max_hit_count` : Number of hits allowed before a tool change - `depth_offset` : Offset of tool depth from tip of tool. Attributes ---------- number : integer Tool number from the excellon file diameter : float Tool diameter in file units rpm : float Tool RPM feed_rate : float Tool Z-axis feed rate. retract_rate : float Tool Z-axis retract rate depth_offset : float Offset of depth measurement from tip of tool max_hit_count : integer Maximum number of tool hits allowed before a tool change hit_count : integer Number of tool hits in excellon file. """ PLATED_UNKNOWN = None PLATED_YES = 'plated' PLATED_NO = 'nonplated' PLATED_OPTIONAL = 'optional' @classmethod def from_tool(cls, tool): args = {} args['depth_offset'] = tool.depth_offset args['diameter'] = tool.diameter args['feed_rate'] = tool.feed_rate args['max_hit_count'] = tool.max_hit_count args['number'] = tool.number args['plated'] = tool.plated args['retract_rate'] = tool.retract_rate args['rpm'] = tool.rpm return cls(None, **args) @classmethod def from_excellon(cls, line, settings, id=None, plated=None): """ Create a Tool from an excellon file tool definition line. Parameters ---------- line : string Tool definition line from an excellon file. settings : FileSettings (dict-like) Excellon file-wide settings Returns ------- tool : Tool An ExcellonTool representing the tool defined in `line` """ commands = re.split('([BCFHSTZ])', line)[1:] commands = [(command, value) for command, value in pairwise(commands)] args = {} args['id'] = id nformat = settings.format zero_suppression = settings.zero_suppression for cmd, val in commands: if cmd == 'B': args['retract_rate'] = parse_gerber_value(val, nformat, zero_suppression) elif cmd == 'C': args['diameter'] = parse_gerber_value(val, nformat, zero_suppression) elif cmd == 'F': args['feed_rate'] = parse_gerber_value(val, nformat, zero_suppression) elif cmd == 'H': args['max_hit_count'] = parse_gerber_value(val, nformat, zero_suppression) elif cmd == 'S': args['rpm'] = 1000 * parse_gerber_value(val, nformat, zero_suppression) elif cmd == 'T': args['number'] = int(val) elif cmd == 'Z': args['depth_offset'] = parse_gerber_value(val, nformat, zero_suppression) if plated != ExcellonTool.PLATED_UNKNOWN: # Sometimees we can can parse the args['plated'] = plated return cls(settings, **args) @classmethod def from_dict(cls, settings, tool_dict): """ Create an ExcellonTool from a dict. Parameters ---------- settings : FileSettings (dict-like) Excellon File-wide settings tool_dict : dict Excellon tool parameters as a dict Returns ------- tool : ExcellonTool An ExcellonTool initialized with the parameters in tool_dict. """ return cls(settings, **tool_dict) def __init__(self, settings, **kwargs): if kwargs.get('id') is not None: super(ExcellonTool, self).__init__(id=kwargs.get('id')) self.settings = settings self.number = kwargs.get('number') self.feed_rate = kwargs.get('feed_rate') self.retract_rate = kwargs.get('retract_rate') self.rpm = kwargs.get('rpm') self.diameter = kwargs.get('diameter') self.max_hit_count = kwargs.get('max_hit_count') self.depth_offset = kwargs.get('depth_offset') self.plated = kwargs.get('plated') self.hit_count = 0 def to_excellon(self, settings=None): if self.settings and not settings: settings = self.settings fmt = settings.format zs = settings.zero_suppression stmt = 'T%02d' % self.number if self.retract_rate is not None: stmt += 'B%s' % write_gerber_value(self.retract_rate, fmt, zs) if self.feed_rate is not None: stmt += 'F%s' % write_gerber_value(self.feed_rate, fmt, zs) if self.max_hit_count is not None: stmt += 'H%s' % write_gerber_value(self.max_hit_count, fmt, zs) if self.rpm is not None: if self.rpm < 100000.: stmt += 'S%s' % write_gerber_value(self.rpm / 1000., fmt, zs) else: stmt += 'S%g' % (self.rpm / 1000.) if self.diameter is not None: stmt += 'C%s' % decimal_string(self.diameter, fmt[1], True) if self.depth_offset is not None: stmt += 'Z%s' % write_gerber_value(self.depth_offset, fmt, zs) return stmt def to_inch(self): if self.settings.units != 'inch': self.settings.units = 'inch' if self.diameter is not None: self.diameter = inch(self.diameter) def to_metric(self): if self.settings.units != 'metric': self.settings.units = 'metric' if self.diameter is not None: self.diameter = metric(self.diameter) def _hit(self): self.hit_count += 1 def equivalent(self, other): """ Is the other tool equal to this, ignoring the tool number, and other file specified properties """ if type(self) != type(other): return False return (self.diameter == other.diameter and self.feed_rate == other.feed_rate and self.retract_rate == other.retract_rate and self.rpm == other.rpm and self.depth_offset == other.depth_offset and self.max_hit_count == other.max_hit_count and self.plated == other.plated and self.settings.units == other.settings.units) def __repr__(self): unit = 'in.' if self.settings.units == 'inch' else 'mm' fmtstr = '' % self.settings.format return fmtstr % (self.number, self.diameter, unit) class ToolSelectionStmt(ExcellonStatement): @classmethod def from_excellon(cls, line, **kwargs): """ Create a ToolSelectionStmt from an excellon file line. Parameters ---------- line : string Line from an Excellon file Returns ------- tool_statement : ToolSelectionStmt ToolSelectionStmt representation of `line.` """ line = line[1:] compensation_index = None # up to 3 characters for tool number (Frizting uses that) if len(line) <= 3: tool = int(line) else: tool = int(line[:2]) compensation_index = int(line[2:]) return cls(tool, compensation_index, **kwargs) def __init__(self, tool, compensation_index=None, **kwargs): super(ToolSelectionStmt, self).__init__(**kwargs) tool = int(tool) compensation_index = (int(compensation_index) if compensation_index is not None else None) self.tool = tool self.compensation_index = compensation_index def to_excellon(self, settings=None): stmt = 'T%02d' % self.tool if self.compensation_index is not None: stmt += '%02d' % self.compensation_index return stmt class NextToolSelectionStmt(ExcellonStatement): # TODO the statement exists outside of the context of the file, # so it is imposible to know that it is really the next tool def __init__(self, cur_tool, next_tool, **kwargs): """ Select the next tool in the wheel. Parameters ---------- cur_tool : the tool that is currently selected next_tool : the that that is now selected """ super(NextToolSelectionStmt, self).__init__(**kwargs) self.cur_tool = cur_tool self.next_tool = next_tool def to_excellon(self, settings=None): stmt = 'M00' return stmt class ZAxisInfeedRateStmt(ExcellonStatement): @classmethod def from_excellon(cls, line, **kwargs): """ Create a ZAxisInfeedRate from an excellon file line. Parameters ---------- line : string Line from an Excellon file Returns ------- z_axis_infeed_rate : ToolSelectionStmt ToolSelectionStmt representation of `line.` """ rate = int(line[1:]) return cls(rate, **kwargs) def __init__(self, rate, **kwargs): super(ZAxisInfeedRateStmt, self).__init__(**kwargs) self.rate = rate def to_excellon(self, settings=None): return 'F%02d' % self.rate class CoordinateStmt(ExcellonStatement): @classmethod def from_point(cls, point, mode=None): stmt = cls(point[0], point[1]) if mode: stmt.mode = mode return stmt @classmethod def from_excellon(cls, line, settings, **kwargs): x_coord = None y_coord = None if line[0] == 'X': splitline = line.strip('X').split('Y') x_coord = parse_gerber_value(splitline[0], settings.format, settings.zero_suppression) if len(splitline) == 2: y_coord = parse_gerber_value(splitline[1], settings.format, settings.zero_suppression) else: y_coord = parse_gerber_value(line.strip(' Y'), settings.format, settings.zero_suppression) c = cls(x_coord, y_coord, **kwargs) c.units = settings.units return c def __init__(self, x=None, y=None, **kwargs): super(CoordinateStmt, self).__init__(**kwargs) self.x = x self.y = y self.mode = None def to_excellon(self, settings): stmt = '' if self.mode == "ROUT": stmt += "G00" if self.mode == "LINEAR": stmt += "G01" if self.x is not None: stmt += 'X%s' % write_gerber_value(self.x, settings.format, settings.zero_suppression) if self.y is not None: stmt += 'Y%s' % write_gerber_value(self.y, settings.format, settings.zero_suppression) return stmt def to_inch(self): if self.units == 'metric': self.units = 'inch' if self.x is not None: self.x = inch(self.x) if self.y is not None: self.y = inch(self.y) def to_metric(self): if self.units == 'inch': self.units = 'metric' if self.x is not None: self.x = metric(self.x) if self.y is not None: self.y = metric(self.y) def offset(self, x_offset=0, y_offset=0): if self.x is not None: self.x += x_offset if self.y is not None: self.y += y_offset def __str__(self): coord_str = '' if self.x is not None: coord_str += 'X: %g ' % self.x if self.y is not None: coord_str += 'Y: %g ' % self.y return '' % coord_str class RepeatHoleStmt(ExcellonStatement): @classmethod def from_excellon(cls, line, settings, **kwargs): match = re.compile(r'R(?P[0-9]*)X?(?P[+\-]?\d*\.?\d*)?Y?' '(?P[+\-]?\d*\.?\d*)?').match(line) stmt = match.groupdict() count = int(stmt['rcount']) xdelta = (parse_gerber_value(stmt['xdelta'], settings.format, settings.zero_suppression) if stmt['xdelta'] is not '' else None) ydelta = (parse_gerber_value(stmt['ydelta'], settings.format, settings.zero_suppression) if stmt['ydelta'] is not '' else None) c = cls(count, xdelta, ydelta, **kwargs) c.units = settings.units return c def __init__(self, count, xdelta=0.0, ydelta=0.0, **kwargs): super(RepeatHoleStmt, self).__init__(**kwargs) self.count = count self.xdelta = xdelta self.ydelta = ydelta def to_excellon(self, settings): stmt = 'R%d' % self.count if self.xdelta is not None and self.xdelta != 0.0: stmt += 'X%s' % write_gerber_value(self.xdelta, settings.format, settings.zero_suppression) if self.ydelta is not None and self.ydelta != 0.0: stmt += 'Y%s' % write_gerber_value(self.ydelta, settings.format, settings.zero_suppression) return stmt def to_inch(self): if self.units == 'metric': self.units = 'inch' if self.xdelta is not None: self.xdelta = inch(self.xdelta) if self.ydelta is not None: self.ydelta = inch(self.ydelta) def to_metric(self): if self.units == 'inch': self.units = 'metric' if self.xdelta is not None: self.xdelta = metric(self.xdelta) if self.ydelta is not None: self.ydelta = metric(self.ydelta) def __str__(self): return '' % ( self.count, self.xdelta if self.xdelta is not None else 0, self.ydelta if self.ydelta is not None else 0) class CommentStmt(ExcellonStatement): @classmethod def from_excellon(cls, line, **kwargs): return cls(line.lstrip(';')) def __init__(self, comment, **kwargs): super(CommentStmt, self).__init__(**kwargs) self.comment = comment def to_excellon(self, settings=None): return ';%s' % self.comment class HeaderBeginStmt(ExcellonStatement): def __init__(self, **kwargs): super(HeaderBeginStmt, self).__init__(**kwargs) def to_excellon(self, settings=None): return 'M48' class HeaderEndStmt(ExcellonStatement): def __init__(self, **kwargs): super(HeaderEndStmt, self).__init__(**kwargs) def to_excellon(self, settings=None): return 'M95' class RewindStopStmt(ExcellonStatement): def __init__(self, **kwargs): super(RewindStopStmt, self).__init__(**kwargs) def to_excellon(self, settings=None): return '%' class ZAxisRoutPositionStmt(ExcellonStatement): def __init__(self, **kwargs): super(ZAxisRoutPositionStmt, self).__init__(**kwargs) def to_excellon(self, settings=None): return 'M15' class RetractWithClampingStmt(ExcellonStatement): def __init__(self, **kwargs): super(RetractWithClampingStmt, self).__init__(**kwargs) def to_excellon(self, settings=None): return 'M16' class RetractWithoutClampingStmt(ExcellonStatement): def __init__(self, **kwargs): super(RetractWithoutClampingStmt, self).__init__(**kwargs) def to_excellon(self, settings=None): return 'M17' class CutterCompensationOffStmt(ExcellonStatement): def __init__(self, **kwargs): super(CutterCompensationOffStmt, self).__init__(**kwargs) def to_excellon(self, settings=None): return 'G40' class CutterCompensationLeftStmt(ExcellonStatement): def __init__(self, **kwargs): super(CutterCompensationLeftStmt, self).__init__(**kwargs) def to_excellon(self, settings=None): return 'G41' class CutterCompensationRightStmt(ExcellonStatement): def __init__(self, **kwargs): super(CutterCompensationRightStmt, self).__init__(**kwargs) def to_excellon(self, settings=None): return 'G42' class EndOfProgramStmt(ExcellonStatement): @classmethod def from_excellon(cls, line, settings, **kwargs): match = re.compile(r'M30X?(?P\d*\.?\d*)?Y?' '(?P\d*\.?\d*)?').match(line) stmt = match.groupdict() x = (parse_gerber_value(stmt['x'], settings.format, settings.zero_suppression) if stmt['x'] is not '' else None) y = (parse_gerber_value(stmt['y'], settings.format, settings.zero_suppression) if stmt['y'] is not '' else None) c = cls(x, y, **kwargs) c.units = settings.units return c def __init__(self, x=None, y=None, **kwargs): super(EndOfProgramStmt, self).__init__(**kwargs) self.x = x self.y = y def to_excellon(self, settings=None): stmt = 'M30' if self.x is not None: stmt += 'X%s' % write_gerber_value(self.x) if self.y is not None: stmt += 'Y%s' % write_gerber_value(self.y) return stmt def to_inch(self): if self.units == 'metric': self.units = 'inch' if self.x is not None: self.x = inch(self.x) if self.y is not None: self.y = inch(self.y) def to_metric(self): if self.units == 'inch': self.units = 'metric' if self.x is not None: self.x = metric(self.x) if self.y is not None: self.y = metric(self.y) def offset(self, x_offset=0, y_offset=0): if self.x is not None: self.x += x_offset if self.y is not None: self.y += y_offset class UnitStmt(ExcellonStatement): @classmethod def from_settings(cls, settings): """Create the unit statement from the FileSettings""" return cls(settings.units, settings.zeros) @classmethod def from_excellon(cls, line, **kwargs): units = 'inch' if 'INCH' in line else 'metric' zeros = 'leading' if 'LZ' in line else 'trailing' if '0000.00' in line: format = (4, 2) elif '000.000' in line: format = (3, 3) elif '00.0000' in line: format = (2, 4) else: format = None return cls(units, zeros, format, **kwargs) def __init__(self, units='inch', zeros='leading', format=None, **kwargs): super(UnitStmt, self).__init__(**kwargs) self.units = units.lower() self.zeros = zeros self.format = format def to_excellon(self, settings=None): # TODO This won't export the invalid format statement if it exists stmt = '%s,%s' % ('INCH' if self.units == 'inch' else 'METRIC', 'LZ' if self.zeros == 'leading' else 'TZ') return stmt def to_inch(self): self.units = 'inch' def to_metric(self): self.units = 'metric' class IncrementalModeStmt(ExcellonStatement): @classmethod def from_excellon(cls, line, **kwargs): return cls('off', **kwargs) if 'OFF' in line else cls('on', **kwargs) def __init__(self, mode='off', **kwargs): super(IncrementalModeStmt, self).__init__(**kwargs) if mode.lower() not in ['on', 'off']: raise ValueError('Mode may be "on" or "off"') self.mode = mode def to_excellon(self, settings=None): return 'ICI,%s' % ('OFF' if self.mode == 'off' else 'ON') class VersionStmt(ExcellonStatement): @classmethod def from_excellon(cls, line, **kwargs): version = int(line.split(',')[1]) return cls(version, **kwargs) def __init__(self, version=1, **kwargs): super(VersionStmt, self).__init__(**kwargs) version = int(version) if version not in [1, 2]: raise ValueError('Valid versions are 1 or 2') self.version = version def to_excellon(self, settings=None): return 'VER,%d' % self.version class FormatStmt(ExcellonStatement): @classmethod def from_excellon(cls, line, **kwargs): fmt = int(line.split(',')[1]) return cls(fmt, **kwargs) def __init__(self, format=1, **kwargs): super(FormatStmt, self).__init__(**kwargs) format = int(format) if format not in [1, 2]: raise ValueError('Valid formats are 1 or 2') self.format = format def to_excellon(self, settings=None): return 'FMAT,%d' % self.format @property def format_tuple(self): return (self.format, 6 - self.format) class LinkToolStmt(ExcellonStatement): @classmethod def from_excellon(cls, line, **kwargs): linked = [int(tool) for tool in line.split('/')] return cls(linked, **kwargs) def __init__(self, linked_tools, **kwargs): super(LinkToolStmt, self).__init__(**kwargs) self.linked_tools = [int(x) for x in linked_tools] def to_excellon(self, settings=None): return '/'.join([str(x) for x in self.linked_tools]) class MeasuringModeStmt(ExcellonStatement): @classmethod def from_excellon(cls, line, **kwargs): if not ('M71' in line or 'M72' in line): raise ValueError('Not a measuring mode statement') return cls('inch', **kwargs) if 'M72' in line else cls('metric', **kwargs) def __init__(self, units='inch', **kwargs): super(MeasuringModeStmt, self).__init__(**kwargs) units = units.lower() if units not in ['inch', 'metric']: raise ValueError('units must be "inch" or "metric"') self.units = units def to_excellon(self, settings=None): return 'M72' if self.units == 'inch' else 'M71' def to_inch(self): self.units = 'inch' def to_metric(self): self.units = 'metric' class RouteModeStmt(ExcellonStatement): def __init__(self, **kwargs): super(RouteModeStmt, self).__init__(**kwargs) def to_excellon(self, settings=None): return 'G00' class LinearModeStmt(ExcellonStatement): def __init__(self, **kwargs): super(LinearModeStmt, self).__init__(**kwargs) def to_excellon(self, settings=None): return 'G01' class DrillModeStmt(ExcellonStatement): def __init__(self, **kwargs): super(DrillModeStmt, self).__init__(**kwargs) def to_excellon(self, settings=None): return 'G05' class AbsoluteModeStmt(ExcellonStatement): def __init__(self, **kwargs): super(AbsoluteModeStmt, self).__init__(**kwargs) def to_excellon(self, settings=None): return 'G90' class UnknownStmt(ExcellonStatement): @classmethod def from_excellon(cls, line, **kwargs): return cls(line, **kwargs) def __init__(self, stmt, **kwargs): super(UnknownStmt, self).__init__(**kwargs) self.stmt = stmt def to_excellon(self, settings=None): return self.stmt def __str__(self): return "" % self.stmt class SlotStmt(ExcellonStatement): """ G85 statement. Defines a slot created by multiple drills between two specified points. Format is two coordinates, split by G85in the middle, for example, XnY0nG85XnYn """ @classmethod def from_points(cls, start, end): return cls(start[0], start[1], end[0], end[1]) @classmethod def from_excellon(cls, line, settings, **kwargs): # Split the line based on the G85 separator sub_coords = line.split('G85') (x_start_coord, y_start_coord) = SlotStmt.parse_sub_coords(sub_coords[0], settings) (x_end_coord, y_end_coord) = SlotStmt.parse_sub_coords(sub_coords[1], settings) # Some files seem to specify only one of the coordinates if x_end_coord == None: x_end_coord = x_start_coord if y_end_coord == None: y_end_coord = y_start_coord c = cls(x_start_coord, y_start_coord, x_end_coord, y_end_coord, **kwargs) c.units = settings.units return c @staticmethod def parse_sub_coords(line, settings): x_coord = None y_coord = None if line[0] == 'X': splitline = line.strip('X').split('Y') x_coord = parse_gerber_value(splitline[0], settings.format, settings.zero_suppression) if len(splitline) == 2: y_coord = parse_gerber_value(splitline[1], settings.format, settings.zero_suppression) else: y_coord = parse_gerber_value(line.strip(' Y'), settings.format, settings.zero_suppression) return (x_coord, y_coord) def __init__(self, x_start=None, y_start=None, x_end=None, y_end=None, **kwargs): super(SlotStmt, self).__init__(**kwargs) self.x_start = x_start self.y_start = y_start self.x_end = x_end self.y_end = y_end self.mode = None def to_excellon(self, settings): stmt = '' if self.x_start is not None: stmt += 'X%s' % write_gerber_value(self.x_start, settings.format, settings.zero_suppression) if self.y_start is not None: stmt += 'Y%s' % write_gerber_value(self.y_start, settings.format, settings.zero_suppression) stmt += 'G85' if self.x_end is not None: stmt += 'X%s' % write_gerber_value(self.x_end, settings.format, settings.zero_suppression) if self.y_end is not None: stmt += 'Y%s' % write_gerber_value(self.y_end, settings.format, settings.zero_suppression) return stmt def to_inch(self): if self.units == 'metric': self.units = 'inch' if self.x_start is not None: self.x_start = inch(self.x_start) if self.y_start is not None: self.y_start = inch(self.y_start) if self.x_end is not None: self.x_end = inch(self.x_end) if self.y_end is not None: self.y_end = inch(self.y_end) def to_metric(self): if self.units == 'inch': self.units = 'metric' if self.x_start is not None: self.x_start = metric(self.x_start) if self.y_start is not None: self.y_start = metric(self.y_start) if self.x_end is not None: self.x_end = metric(self.x_end) if self.y_end is not None: self.y_end = metric(self.y_end) def offset(self, x_offset=0, y_offset=0): if self.x_start is not None: self.x_start += x_offset if self.y_start is not None: self.y_start += y_offset if self.x_end is not None: self.x_end += x_offset if self.y_end is not None: self.y_end += y_offset def __str__(self): start_str = '' if self.x_start is not None: start_str += 'X: %g ' % self.x_start if self.y_start is not None: start_str += 'Y: %g ' % self.y_start end_str = '' if self.x_end is not None: end_str += 'X: %g ' % self.x_end if self.y_end is not None: end_str += 'Y: %g ' % self.y_end return '' % (start_str, end_str) def pairwise(iterator): """ Iterate over list taking two elements at a time. e.g. [1, 2, 3, 4, 5, 6] ==> [(1, 2), (3, 4), (5, 6)] """ itr = iter(iterator) while True: yield tuple([next(itr) for i in range(2)])