Overhaul config parsing

This commit is contained in:
John Beard 2018-06-02 13:31:03 +01:00
parent fb7e579a64
commit 1519ff2e08
6 changed files with 160 additions and 70 deletions

View File

@ -2,9 +2,6 @@
kiplot: kiplot:
version: 1 version: 1
options:
basedir: /tmp/kiplot_out
outputs: outputs:
- name: 'gerbers' - name: 'gerbers'

View File

@ -19,6 +19,8 @@ def main():
help='The PCB .kicad-pcb board file') help='The PCB .kicad-pcb board file')
parser.add_argument('-c', '--plot-config', required=True, parser.add_argument('-c', '--plot-config', required=True,
help='The plotting config file to use') help='The plotting config file to use')
parser.add_argument('-d', '--out-dir', default='.',
help='The output directory (cwd if not given)')
args = parser.parse_args() args = parser.parse_args()
@ -37,6 +39,10 @@ def main():
with open(args.plot_config) as cf_file: with open(args.plot_config) as cf_file:
cfg = cr.read(cf_file) cfg = cr.read(cf_file)
# relative to CWD (absolute path overrides)
outdir = os.path.join(os.getcwd(), args.out_dir)
cfg.outdir = outdir
plotter = kiplot.Plotter(cfg) plotter = kiplot.Plotter(cfg)
plotter.plot(args.board_file) plotter.plot(args.board_file)

View File

@ -10,6 +10,7 @@ import re
import pcbnew import pcbnew
from . import plot_config as PC from . import plot_config as PC
from . import error
class CfgReader(object): class CfgReader(object):
@ -18,11 +19,12 @@ class CfgReader(object):
pass pass
class CfgYamlReader(CfgReader): class YamlError(error.KiPlotError):
class YamlError(Exception):
pass pass
class CfgYamlReader(CfgReader):
def __init__(self): def __init__(self):
super(CfgYamlReader, self).__init__() super(CfgYamlReader, self).__init__()
@ -31,11 +33,11 @@ class CfgYamlReader(CfgReader):
try: try:
version = data['kiplot']['version'] version = data['kiplot']['version']
except KeyError: except KeyError:
raise self.YamlError("YAML config needs kiplot.version.") raise YamlError("YAML config needs kiplot.version.")
return None return None
if version != 1: if version != 1:
raise self.YamlError("Unknown KiPlot config version: {}" raise YamlError("Unknown KiPlot config version: {}"
.format(version)) .format(version))
return None return None
@ -46,7 +48,7 @@ class CfgYamlReader(CfgReader):
try: try:
val = data[key] val = data[key]
except KeyError: except KeyError:
raise self.YamlError("Value is needed for {}".format(key)) raise YamlError("Value is needed for {}".format(key))
return val return val
@ -68,7 +70,7 @@ class CfgYamlReader(CfgReader):
try: try:
mo.type = TYPES[type_s] mo.type = TYPES[type_s]
except KeyError: except KeyError:
raise self.YamlError("Unknown drill map type: {}".format(type_s)) raise YamlError("Unknown drill map type: {}".format(type_s))
return mo return mo
@ -80,55 +82,128 @@ class CfgYamlReader(CfgReader):
return opts return opts
def _perform_config_mapping(self, otype, cfg_options, mapping_list,
target):
"""
Map a config dict onto a target object given a mapping list
"""
for map_type in mapping_list:
# if this output type matches the mapping specification:
if otype in map_type['types']:
# for each mapping:
for key, mapping in map_type['options'].items():
# set the internal option as needed
if mapping['required'](cfg_options):
cfg_val = self._get_required(cfg_options, key)
elif key in cfg_options:
# not required but given anyway
cfg_val = cfg_options[key]
else:
continue
# transform the value if needed
if 'transform' in mapping:
cfg_val = mapping['transform'](cfg_val)
setattr(target, mapping['to'], cfg_val)
def _parse_out_opts(self, otype, options): def _parse_out_opts(self, otype, options):
# mappings from YAML keys to type_option keys
MAPPINGS = [
{
# Options for a general layer type
'types': ['gerber'],
'options': {
'exclude_edge_layer': {
'to': 'exclude_edge_layer',
'required': lambda opts: True,
},
'exclude_pads_from_silkscreen': {
'to': 'exclude_pads_from_silkscreen',
'required': lambda opts: True,
},
'use_aux_axis_as_origin': {
'to': 'use_aux_axis_as_origin',
'required': lambda opts: True,
},
'line_width': {
'to': 'line_width',
'required': lambda opts: True,
},
},
},
{
# Gerber only
'types': ['gerber'],
'options': {
'subtract_mask_from_silk': {
'to': 'subtract_mask_from_silk',
'required': lambda opts: True,
},
'use_protel_extensions': {
'to': 'use_protel_extensions',
'required': lambda opts: True,
},
},
},
{
# Drill files
'types': ['excellon'],
'options': {
'use_aux_axis_as_origin': {
'to': 'use_aux_axis_as_origin',
'required': lambda opts: True,
},
'map': {
'to': 'map_options',
'required': lambda opts: False,
'transform': self._parse_drill_map
},
'report': {
'to': 'report_options',
'required': lambda opts: False,
'transform': self._parse_drill_report
},
},
},
{
# Excellon drill files
'types': ['excellon'],
'options': {
'metric_units': {
'to': 'metric_units',
'required': lambda opts: True,
},
'pth_and_npth_single_file': {
'to': 'pth_and_npth_single_file',
'required': lambda opts: True,
},
'minimal_header': {
'to': 'minimal_header',
'required': lambda opts: True,
},
'mirror_y_axis': {
'to': 'mirror_y_axis',
'required': lambda opts: True,
},
},
},
]
po = PC.OutputOptions(otype) po = PC.OutputOptions(otype)
# options that apply to the specific output type # options that apply to the specific output type
to = po.type_options to = po.type_options
# common options - layer outputs self._perform_config_mapping(otype, options, MAPPINGS, to)
if otype in ['gerber']:
to.line_width = self._get_required(options, 'line_width')
to.exclude_edge_layer = self._get_required(
options, 'exclude_edge_layer')
to.exclude_pads_from_silkscreen = self._get_required(
options, 'exclude_pads_from_silkscreen')
to.use_aux_axis_as_origin = self._get_required(
options, 'use_aux_axis_as_origin')
# common options - drill outputs
if otype in ['excellon']:
to.use_aux_axis_as_origin = self._get_required(
options, 'use_aux_axis_as_origin')
to.generate_map = 'map' in options
to.generate_report = 'report' in options
if to.generate_map:
to.map_options = self._parse_drill_map(options['map'])
if to.generate_map:
to.report_options = self._parse_drill_report(options['report'])
# set type-specific options
if otype == 'gerber':
to.subtract_mask_from_silk = self._get_required(
options, 'subtract_mask_from_silk')
to.use_protel_extensions = self._get_required(
options, 'use_protel_extensions')
if otype == 'excellon':
to.metric_units = self._get_required(
options, 'metric_units')
to.mirror_y_axis = self._get_required(
options, 'mirror_y_axis')
to.minimal_header = self._get_required(
options, 'minimal_header')
to.pth_and_npth_single_file = self._get_required(
options, 'pth_and_npth_single_file')
print to, to.__dict__
return po return po
def _get_layer_from_str(self, s): def _get_layer_from_str(self, s):
@ -167,12 +242,12 @@ class CfgYamlReader(CfgReader):
m = re.match(r"^Inner\.([0-9]+)$", s) m = re.match(r"^Inner\.([0-9]+)$", s)
if not m: if not m:
raise self.YamlError("Malformed inner layer name: {}" raise YamlError("Malformed inner layer name: {}"
.format(s)) .format(s))
layer = PC.LayerInfo(int(m.group(1)), True) layer = PC.LayerInfo(int(m.group(1)), True)
else: else:
raise self.YamlError("Unknown layer name: {}".format(s)) raise YamlError("Unknown layer name: {}".format(s))
return layer return layer
@ -202,15 +277,15 @@ class CfgYamlReader(CfgReader):
try: try:
otype = o_obj['type'] otype = o_obj['type']
except KeyError: except KeyError:
raise self.YamlError("Output needs a type") raise YamlError("Output needs a type")
if otype not in ['gerber', 'excellon']: if otype not in ['gerber', 'excellon']:
raise self.YamlError("Unknown output type: {}".format(otype)) raise YamlError("Unknown output type: {}".format(otype))
try: try:
options = o_obj['options'] options = o_obj['options']
except KeyError: except KeyError:
raise self.YamlError("Output need to have options specified") raise YamlError("Output need to have options specified")
outdir = self._get_required(o_obj, 'dir') outdir = self._get_required(o_obj, 'dir')
@ -239,7 +314,7 @@ class CfgYamlReader(CfgReader):
try: try:
data = yaml.load(fstream) data = yaml.load(fstream)
except yaml.YAMLError as e: except yaml.YAMLError as e:
raise self.YamlError("Error loading YAML") raise YamlError("Error loading YAML")
return None return None
self._check_version(data) self._check_version(data)
@ -249,13 +324,8 @@ class CfgYamlReader(CfgReader):
except KeyError: except KeyError:
outdir = "" outdir = ""
# relative to CWD (absolute path overrides)
outdir = os.path.join(os.getcwd(), outdir)
cfg = PC.PlotConfig() cfg = PC.PlotConfig()
cfg.outdir = outdir
for o in data['outputs']: for o in data['outputs']:
op_cfg = self._parse_output(o) op_cfg = self._parse_output(o)

7
kiplot/error.py Normal file
View File

@ -0,0 +1,7 @@
"""
KiPlot errors
"""
class KiPlotError(Exception):
pass

View File

@ -6,6 +6,7 @@ import logging
import os import os
from . import plot_config as PCfg from . import plot_config as PCfg
from . import error
try: try:
import pcbnew import pcbnew
@ -15,6 +16,10 @@ except ImportError:
raise raise
class PlotError(error.KiPlotError):
pass
class Plotter(object): class Plotter(object):
""" """
Main Plotter class - this is what will perform the plotting Main Plotter class - this is what will perform the plotting
@ -44,7 +49,7 @@ class Plotter(object):
elif self._output_is_drill(op): elif self._output_is_drill(op):
self._do_drill_plot(board, pc, op) self._do_drill_plot(board, pc, op)
else: else:
raise ValueError("Don't know how to plot type {}" raise PlotError("Don't know how to plot type {}"
.format(op.options.type)) .format(op.options.type))
pc.ClosePlot() pc.ClosePlot()
@ -85,7 +90,7 @@ class Plotter(object):
if layer.is_inner: if layer.is_inner:
if layer.layer < 1 or layer.layer >= layer_cnt - 1: if layer.layer < 1 or layer.layer >= layer_cnt - 1:
raise ValueError( raise PlotError(
"Inner layer {} is not valid for this board" "Inner layer {} is not valid for this board"
.format(layer.layer)) .format(layer.layer))

View File

@ -36,12 +36,17 @@ class GerberOptions(LayerOptions):
class DrillOptions(object): class DrillOptions(object):
def __init__(self): def __init__(self):
self.generate_map = False
self.generate_report = False
self.map_options = None self.map_options = None
self.report_options = None self.report_options = None
@property
def generate_map(self):
return self.map_options is not None
@property
def generate_report(self):
return self.report_options is not None
class ExcellonOptions(DrillOptions): class ExcellonOptions(DrillOptions):