From f8449ad2b60b8a715d0867325e257a8297193b49 Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Tue, 30 Sep 2014 23:42:02 -0400 Subject: tests update --- gerber/__init__.py | 11 +++ gerber/cnc.py | 9 ++- gerber/render/__init__.py | 2 +- gerber/render/svg.py | 165 -------------------------------------- gerber/render/svgwrite_backend.py | 165 ++++++++++++++++++++++++++++++++++++++ gerber/statements.py | 7 +- gerber/tests/test_cnc.py | 50 ++++++++++++ gerber/tests/test_statements.py | 87 ++++++++++++++++++++ gerber/tests/tests.py | 18 +++++ requirements.txt | 3 + 10 files changed, 341 insertions(+), 176 deletions(-) delete mode 100644 gerber/render/svg.py create mode 100644 gerber/render/svgwrite_backend.py create mode 100644 gerber/tests/test_cnc.py create mode 100644 gerber/tests/test_statements.py create mode 100644 gerber/tests/tests.py diff --git a/gerber/__init__.py b/gerber/__init__.py index 089d7b6..e31bd6f 100644 --- a/gerber/__init__.py +++ b/gerber/__init__.py @@ -18,6 +18,17 @@ def read(filename): """ Read a gerber or excellon file and return a representative object. + + Parameters + ---------- + filename : string + Filename of the file to read. + + Returns + ------- + file : CncFile subclass + CncFile object representing the file, either GerberFile or + ExcellonFile. Returns None if file is not an Excellon or Gerber file. """ import gerber import excellon diff --git a/gerber/cnc.py b/gerber/cnc.py index a7f3b85..aaa1a42 100644 --- a/gerber/cnc.py +++ b/gerber/cnc.py @@ -29,7 +29,7 @@ class FileSettings(object): Provides a common representation of gerber/excellon file settings """ def __init__(self, notation='absolute', units='inch', - zero_suppression='trailing', format=(2,5)): + zero_suppression='trailing', format=(2, 5)): if notation not in ['absolute', 'incremental']: raise ValueError('Notation must be either absolute or incremental') self.notation = notation @@ -43,7 +43,7 @@ class FileSettings(object): trailling') self.zero_suppression = zero_suppression - if len(format) != 2: + if len(format) != 2: raise ValueError('Format must be a tuple(n=2) of integers') self.format = format @@ -52,13 +52,14 @@ class FileSettings(object): return self.notation elif key == 'units': return self.units - elif key =='zero_suppression': + elif key == 'zero_suppression': return self.zero_suppression elif key == 'format': return self.format else: raise KeyError() + class CncFile(object): """ Base class for Gerber/Excellon files. @@ -101,7 +102,7 @@ class CncFile(object): self.notation = 'absolute' self.units = 'inch' self.zero_suppression = 'trailing' - self.format = (2,5) + self.format = (2, 5) self.filename = filename @property diff --git a/gerber/render/__init__.py b/gerber/render/__init__.py index cc87ee0..0d3527b 100644 --- a/gerber/render/__init__.py +++ b/gerber/render/__init__.py @@ -24,5 +24,5 @@ SVG is the only supported format. """ -from svg import GerberSvgContext +from svgwrite_backend import GerberSvgContext diff --git a/gerber/render/svg.py b/gerber/render/svg.py deleted file mode 100644 index 7d5c8fd..0000000 --- a/gerber/render/svg.py +++ /dev/null @@ -1,165 +0,0 @@ -#! /usr/bin/env python -# -*- coding: utf-8 -*- - -# Copyright 2014 Hamilton Kibbe -# Based on render_svg.py by Paulo Henrique Silva - -# 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. - -from .render import GerberContext -from .apertures import Circle, Rect, Obround, Polygon -import svgwrite - -SCALE = 300 - - -class SvgCircle(Circle): - def draw(self, ctx, x, y): - return ctx.dwg.line(start=(ctx.x * SCALE, -ctx.y * SCALE), - end=(x * SCALE, -y * SCALE), - stroke="rgb(184, 115, 51)", - stroke_width=SCALE * self.diameter, - stroke_linecap="round") - - def flash(self, ctx, x, y): - return [ctx.dwg.circle(center=(x * SCALE, -y * SCALE), - r = SCALE * (self.diameter / 2.0), - fill='rgb(184, 115, 51)'), ] - - -class SvgRect(Rect): - def draw(self, ctx, x, y): - return ctx.dwg.line(start=(ctx.x * SCALE, -ctx.y * SCALE), - end=(x * SCALE, -y * SCALE), - stroke="rgb(184, 115, 51)", stroke_width=2, - stroke_linecap="butt") - - def flash(self, ctx, x, y): - xsize, ysize = self.size - return [ctx.dwg.rect(insert=(SCALE * (x - (xsize / 2)), - -SCALE * (y + (ysize / 2))), - size=(SCALE * xsize, SCALE * ysize), - fill="rgb(184, 115, 51)"), ] - - -class SvgObround(Obround): - def draw(self, ctx, x, y): - pass - - def flash(self, ctx, x, y): - xsize, ysize = self.size - - # horizontal obround - if xsize == ysize: - return [ctx.dwg.circle(center=(x * SCALE, -y * SCALE), - r = SCALE * (x / 2.0), - fill='rgb(184, 115, 51)'), ] - if xsize > ysize: - rectx = xsize - ysize - recty = ysize - lcircle = ctx.dwg.circle(center=((x - (rectx / 2.0)) * SCALE, - -y * SCALE), - r = SCALE * (ysize / 2.0), - fill='rgb(184, 115, 51)') - - rcircle = ctx.dwg.circle(center=((x + (rectx / 2.0)) * SCALE, - -y * SCALE), - r = SCALE * (ysize / 2.0), - fill='rgb(184, 115, 51)') - - rect = ctx.dwg.rect(insert=(SCALE * (x - (xsize / 2.)), - -SCALE * (y + (ysize / 2.))), - size=(SCALE * xsize, SCALE * ysize), - fill='rgb(184, 115, 51)') - return [lcircle, rcircle, rect, ] - - # Vertical obround - else: - rectx = xsize - recty = ysize - xsize - lcircle = ctx.dwg.circle(center=(x * SCALE, - (y - (recty / 2.)) * -SCALE), - r = SCALE * (xsize / 2.), - fill='rgb(184, 115, 51)') - - ucircle = ctx.dwg.circle(center=(x * SCALE, - (y + (recty / 2.)) * -SCALE), - r = SCALE * (xsize / 2.), - fill='rgb(184, 115, 51)') - - rect = ctx.dwg.rect(insert=(SCALE * (x - (xsize / 2.)), - -SCALE * (y + (ysize / 2.))), - size=(SCALE * xsize, SCALE * ysize), - fill='rgb(184, 115, 51)') - return [lcircle, ucircle, rect, ] - - -class GerberSvgContext(GerberContext): - def __init__(self): - GerberContext.__init__(self) - - self.apertures = {} - self.dwg = svgwrite.Drawing() - #self.dwg.add(self.dwg.rect(insert=(0, 0), size=(2000, 2000), fill="black")) - - def set_bounds(self, bounds): - xbounds, ybounds = bounds - size = (SCALE * (xbounds[1] - xbounds[0]), SCALE * (ybounds[1] - ybounds[0])) - self.dwg.add(self.dwg.rect(insert=(SCALE * xbounds[0], -SCALE * ybounds[1]), size=size, fill="black")) - - def define_aperture(self, d, shape, modifiers): - aperture = None - if shape == 'C': - aperture = SvgCircle(diameter=float(modifiers[0][0])) - elif shape == 'R': - aperture = SvgRect(size=modifiers[0][0:2]) - elif shape == 'O': - aperture = SvgObround(size=modifiers[0][0:2]) - self.apertures[d] = aperture - - def stroke(self, x, y): - super(GerberSvgContext, self).stroke(x, y) - - if self.interpolation == 'linear': - self.line(x, y) - elif self.interpolation == 'arc': - self.arc(x, y) - - def line(self, x, y): - super(GerberSvgContext, self).line(x, y) - x, y = self.resolve(x, y) - ap = self.apertures.get(self.aperture, None) - if ap is None: - return - self.dwg.add(ap.draw(self, x, y)) - self.move(x, y, resolve=False) - - def arc(self, x, y): - super(GerberSvgContext, self).arc(x, y) - - def flash(self, x, y): - super(GerberSvgContext, self).flash(x, y) - x, y = self.resolve(x, y) - ap = self.apertures.get(self.aperture, None) - if ap is None: - return - for shape in ap.flash(self, x, y): - self.dwg.add(shape) - self.move(x, y, resolve=False) - - def drill(self, x, y, diameter): - hit = self.dwg.circle(center=(x*SCALE, -y*SCALE), r=SCALE*(diameter/2.0), fill='gray') - self.dwg.add(hit) - - def dump(self, filename): - self.dwg.saveas(filename) diff --git a/gerber/render/svgwrite_backend.py b/gerber/render/svgwrite_backend.py new file mode 100644 index 0000000..7d5c8fd --- /dev/null +++ b/gerber/render/svgwrite_backend.py @@ -0,0 +1,165 @@ +#! /usr/bin/env python +# -*- coding: utf-8 -*- + +# Copyright 2014 Hamilton Kibbe +# Based on render_svg.py by Paulo Henrique Silva + +# 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. + +from .render import GerberContext +from .apertures import Circle, Rect, Obround, Polygon +import svgwrite + +SCALE = 300 + + +class SvgCircle(Circle): + def draw(self, ctx, x, y): + return ctx.dwg.line(start=(ctx.x * SCALE, -ctx.y * SCALE), + end=(x * SCALE, -y * SCALE), + stroke="rgb(184, 115, 51)", + stroke_width=SCALE * self.diameter, + stroke_linecap="round") + + def flash(self, ctx, x, y): + return [ctx.dwg.circle(center=(x * SCALE, -y * SCALE), + r = SCALE * (self.diameter / 2.0), + fill='rgb(184, 115, 51)'), ] + + +class SvgRect(Rect): + def draw(self, ctx, x, y): + return ctx.dwg.line(start=(ctx.x * SCALE, -ctx.y * SCALE), + end=(x * SCALE, -y * SCALE), + stroke="rgb(184, 115, 51)", stroke_width=2, + stroke_linecap="butt") + + def flash(self, ctx, x, y): + xsize, ysize = self.size + return [ctx.dwg.rect(insert=(SCALE * (x - (xsize / 2)), + -SCALE * (y + (ysize / 2))), + size=(SCALE * xsize, SCALE * ysize), + fill="rgb(184, 115, 51)"), ] + + +class SvgObround(Obround): + def draw(self, ctx, x, y): + pass + + def flash(self, ctx, x, y): + xsize, ysize = self.size + + # horizontal obround + if xsize == ysize: + return [ctx.dwg.circle(center=(x * SCALE, -y * SCALE), + r = SCALE * (x / 2.0), + fill='rgb(184, 115, 51)'), ] + if xsize > ysize: + rectx = xsize - ysize + recty = ysize + lcircle = ctx.dwg.circle(center=((x - (rectx / 2.0)) * SCALE, + -y * SCALE), + r = SCALE * (ysize / 2.0), + fill='rgb(184, 115, 51)') + + rcircle = ctx.dwg.circle(center=((x + (rectx / 2.0)) * SCALE, + -y * SCALE), + r = SCALE * (ysize / 2.0), + fill='rgb(184, 115, 51)') + + rect = ctx.dwg.rect(insert=(SCALE * (x - (xsize / 2.)), + -SCALE * (y + (ysize / 2.))), + size=(SCALE * xsize, SCALE * ysize), + fill='rgb(184, 115, 51)') + return [lcircle, rcircle, rect, ] + + # Vertical obround + else: + rectx = xsize + recty = ysize - xsize + lcircle = ctx.dwg.circle(center=(x * SCALE, + (y - (recty / 2.)) * -SCALE), + r = SCALE * (xsize / 2.), + fill='rgb(184, 115, 51)') + + ucircle = ctx.dwg.circle(center=(x * SCALE, + (y + (recty / 2.)) * -SCALE), + r = SCALE * (xsize / 2.), + fill='rgb(184, 115, 51)') + + rect = ctx.dwg.rect(insert=(SCALE * (x - (xsize / 2.)), + -SCALE * (y + (ysize / 2.))), + size=(SCALE * xsize, SCALE * ysize), + fill='rgb(184, 115, 51)') + return [lcircle, ucircle, rect, ] + + +class GerberSvgContext(GerberContext): + def __init__(self): + GerberContext.__init__(self) + + self.apertures = {} + self.dwg = svgwrite.Drawing() + #self.dwg.add(self.dwg.rect(insert=(0, 0), size=(2000, 2000), fill="black")) + + def set_bounds(self, bounds): + xbounds, ybounds = bounds + size = (SCALE * (xbounds[1] - xbounds[0]), SCALE * (ybounds[1] - ybounds[0])) + self.dwg.add(self.dwg.rect(insert=(SCALE * xbounds[0], -SCALE * ybounds[1]), size=size, fill="black")) + + def define_aperture(self, d, shape, modifiers): + aperture = None + if shape == 'C': + aperture = SvgCircle(diameter=float(modifiers[0][0])) + elif shape == 'R': + aperture = SvgRect(size=modifiers[0][0:2]) + elif shape == 'O': + aperture = SvgObround(size=modifiers[0][0:2]) + self.apertures[d] = aperture + + def stroke(self, x, y): + super(GerberSvgContext, self).stroke(x, y) + + if self.interpolation == 'linear': + self.line(x, y) + elif self.interpolation == 'arc': + self.arc(x, y) + + def line(self, x, y): + super(GerberSvgContext, self).line(x, y) + x, y = self.resolve(x, y) + ap = self.apertures.get(self.aperture, None) + if ap is None: + return + self.dwg.add(ap.draw(self, x, y)) + self.move(x, y, resolve=False) + + def arc(self, x, y): + super(GerberSvgContext, self).arc(x, y) + + def flash(self, x, y): + super(GerberSvgContext, self).flash(x, y) + x, y = self.resolve(x, y) + ap = self.apertures.get(self.aperture, None) + if ap is None: + return + for shape in ap.flash(self, x, y): + self.dwg.add(shape) + self.move(x, y, resolve=False) + + def drill(self, x, y, diameter): + hit = self.dwg.circle(center=(x*SCALE, -y*SCALE), r=SCALE*(diameter/2.0), fill='gray') + self.dwg.add(hit) + + def dump(self, filename): + self.dwg.saveas(filename) diff --git a/gerber/statements.py b/gerber/statements.py index 418a852..47dbd69 100644 --- a/gerber/statements.py +++ b/gerber/statements.py @@ -48,11 +48,6 @@ class FSParamStmt(ParamStmt): notation = 'absolute' if stmt_dict.get('notation') == 'A' else 'incremental' x = map(int, stmt_dict.get('x').strip()) format = (x[0], x[1]) - if notation == 'incremental': - print('This file uses incremental notation. To quote the gerber \ - file specification:\nIncremental notation is a source of \ - endless confusion. Always use absolute notation.\n\nYou \ - have been warned') return cls(param, zeros, notation, format) def __init__(self, param, zero_suppression='leading', @@ -172,7 +167,7 @@ class IPParamStmt(ParamStmt): self.ip = ip def to_gerber(self): - ip = 'POS' if self.ip == 'positive' else 'negative' + ip = 'POS' if self.ip == 'positive' else 'NEG' return '%IP{0}*%'.format(ip) def __str__(self): diff --git a/gerber/tests/test_cnc.py b/gerber/tests/test_cnc.py new file mode 100644 index 0000000..ace047e --- /dev/null +++ b/gerber/tests/test_cnc.py @@ -0,0 +1,50 @@ +#! /usr/bin/env python +# -*- coding: utf-8 -*- + +# Author: Hamilton Kibbe + +from ..cnc import CncFile, FileSettings +from tests import * + + +def test_smoke_filesettings(): + """ Smoke test FileSettings class + """ + fs = FileSettings() + + +def test_filesettings_defaults(): + """ Test FileSettings default values + """ + fs = FileSettings() + assert_equal(fs.format, (2, 5)) + assert_equal(fs.notation, 'absolute') + assert_equal(fs.zero_suppression, 'trailing') + assert_equal(fs.units, 'inch') + + +def test_filesettings_dict(): + """ Test FileSettings Dict + """ + fs = FileSettings() + assert_equal(fs['format'], (2, 5)) + assert_equal(fs['notation'], 'absolute') + assert_equal(fs['zero_suppression'], 'trailing') + assert_equal(fs['units'], 'inch') + + +def test_filesettings_assign(): + """ Test FileSettings attribute assignment + """ + fs = FileSettings() + fs.units = 'test' + fs.notation = 'test' + fs.zero_suppression = 'test' + fs.format = 'test' + assert_equal(fs.units, 'test') + assert_equal(fs.notation, 'test') + assert_equal(fs.zero_suppression, 'test') + assert_equal(fs.format, 'test') + + def test_smoke_cncfile(): + pass diff --git a/gerber/tests/test_statements.py b/gerber/tests/test_statements.py new file mode 100644 index 0000000..47fbb48 --- /dev/null +++ b/gerber/tests/test_statements.py @@ -0,0 +1,87 @@ +#! /usr/bin/env python +# -*- coding: utf-8 -*- + +# Author: Hamilton Kibbe + +from .tests import * +from ..statements import * + + +def test_FSParamStmt_factory(): + """ Test FSParamStruct factory correctly handles parameters + """ + stmt = {'param': 'FS', 'zero': 'L', 'notation': 'A', 'x': '27'} + fs = FSParamStmt.from_dict(stmt) + assert_equal(fs.param, 'FS') + assert_equal(fs.zero_suppression, 'leading') + assert_equal(fs.notation, 'absolute') + assert_equal(fs.format, (2, 7)) + + stmt = {'param': 'FS', 'zero': 'T', 'notation': 'I', 'x': '27'} + fs = FSParamStmt.from_dict(stmt) + assert_equal(fs.param, 'FS') + assert_equal(fs.zero_suppression, 'trailing') + assert_equal(fs.notation, 'incremental') + assert_equal(fs.format, (2, 7)) + + +def test_FSParamStmt_dump(): + """ Test FSParamStmt to_gerber() + """ + stmt = {'param': 'FS', 'zero': 'L', 'notation': 'A', 'x': '27'} + fs = FSParamStmt.from_dict(stmt) + assert_equal(fs.to_gerber(), '%FSLAX27Y27*%') + + stmt = {'param': 'FS', 'zero': 'T', 'notation': 'I', 'x': '25'} + fs = FSParamStmt.from_dict(stmt) + assert_equal(fs.to_gerber(), '%FSTIX25Y25*%') + + +def test_MOParamStmt_factory(): + """ Test MOParamStruct factory correctly handles parameters + """ + stmt = {'param': 'MO', 'mo': 'IN'} + mo = MOParamStmt.from_dict(stmt) + assert_equal(mo.param, 'MO') + assert_equal(mo.mode, 'inch') + + stmt = {'param': 'MO', 'mo': 'MM'} + mo = MOParamStmt.from_dict(stmt) + assert_equal(mo.param, 'MO') + assert_equal(mo.mode, 'metric') + + +def test_MOParamStmt_dump(): + """ Test MOParamStmt to_gerber() + """ + stmt = {'param': 'MO', 'mo': 'IN'} + mo = MOParamStmt.from_dict(stmt) + assert_equal(mo.to_gerber(), '%MOIN*%') + + stmt = {'param': 'MO', 'mo': 'MM'} + mo = MOParamStmt.from_dict(stmt) + assert_equal(mo.to_gerber(), '%MOMM*%') + + +def test_IPParamStmt_factory(): + """ Test IPParamStruct factory correctly handles parameters + """ + stmt = {'param': 'IP', 'ip': 'POS'} + ip = IPParamStmt.from_dict(stmt) + assert_equal(ip.ip, 'positive') + + stmt = {'param': 'IP', 'ip': 'NEG'} + ip = IPParamStmt.from_dict(stmt) + assert_equal(ip.ip, 'negative') + + +def test_IPParamStmt_dump(): + """ Test IPParamStmt to_gerber() + """ + stmt = {'param': 'IP', 'ip': 'POS'} + ip = IPParamStmt.from_dict(stmt) + assert_equal(ip.to_gerber(), '%IPPOS*%') + + stmt = {'param': 'IP', 'ip': 'NEG'} + ip = IPParamStmt.from_dict(stmt) + assert_equal(ip.to_gerber(), '%IPNEG*%') diff --git a/gerber/tests/tests.py b/gerber/tests/tests.py new file mode 100644 index 0000000..29b7899 --- /dev/null +++ b/gerber/tests/tests.py @@ -0,0 +1,18 @@ +#! /usr/bin/env python +# -*- coding: utf-8 -*- + +# Author: Hamilton Kibbe + +from nose.tools import assert_in +from nose.tools import assert_not_in +from nose.tools import assert_equal +from nose.tools import assert_not_equal +from nose.tools import assert_true +from nose.tools import assert_false +from nose.tools import assert_raises +from nose.tools import raises +from nose import with_setup + +__all__ = ['assert_in', 'assert_not_in', 'assert_equal', 'assert_not_equal', + 'assert_true', 'assert_false', 'assert_raises', 'raises', + 'with_setup' ] diff --git a/requirements.txt b/requirements.txt index 2bb614a..338cb65 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,8 +1,11 @@ +## The following requirements were added by pip --freeze: Jinja2==2.7.3 MarkupSafe==0.23 Pygments==1.6 Sphinx==1.2.3 +coverage==3.7.1 docutils==0.12 +nose==1.3.4 pyparsing==2.0.2 svgwrite==1.1.6 wsgiref==0.1.2 -- cgit