364 lines
13 KiB
Python
364 lines
13 KiB
Python
"""
|
|
Class to read KiPlot config files
|
|
"""
|
|
|
|
import os
|
|
from sys import (exit, maxsize)
|
|
from collections import OrderedDict
|
|
|
|
from .error import (KiPlotConfigurationError)
|
|
from .kiplot import (Layer, load_board)
|
|
from .misc import (NO_YAML_MODULE, EXIT_BAD_CONFIG, EXIT_BAD_ARGS, EXAMPLE_CFG, WONT_OVERWRITE)
|
|
from mcpy import activate # noqa: F401
|
|
# Output classes
|
|
from .out_base import BaseOutput
|
|
from . import out_gerber # noqa: F401
|
|
from . import out_ps # noqa: F401
|
|
from . import out_hpgl # noqa: F401
|
|
from . import out_dxf # noqa: F401
|
|
from . import out_pdf # noqa: F401
|
|
from . import out_svg # noqa: F401
|
|
from . import out_gerb_drill # noqa: F401
|
|
from . import out_excellon # noqa: F401
|
|
from . import out_position # noqa: F401
|
|
from . import out_step # noqa: F401
|
|
from . import out_kibom # noqa: F401
|
|
from . import out_ibom # noqa: F401
|
|
from . import out_pdf_sch_print # noqa: F401
|
|
from . import out_pdf_pcb_print # noqa: F401
|
|
# PreFlight classes
|
|
from .pre_base import BasePreFlight
|
|
from . import pre_drc # noqa: F401
|
|
from . import pre_erc # noqa: F401
|
|
from . import pre_update_xml # noqa: F401
|
|
from . import pre_check_zone_fills # noqa: F401
|
|
from . import pre_ignore_unconnected # noqa: F401
|
|
from . import pre_filters # noqa: F401
|
|
# Logger
|
|
from . import log
|
|
|
|
logger = log.get_logger(__name__)
|
|
|
|
try:
|
|
import yaml
|
|
except ImportError: # pragma: no cover
|
|
log.init(False, False)
|
|
logger.error('No yaml module for Python, install python3-yaml')
|
|
exit(NO_YAML_MODULE)
|
|
|
|
|
|
def config_error(msg):
|
|
logger.error(msg)
|
|
exit(EXIT_BAD_CONFIG)
|
|
|
|
|
|
class CfgYamlReader(object):
|
|
def __init__(self):
|
|
super(CfgYamlReader, self).__init__()
|
|
|
|
def _check_version(self, v):
|
|
if not isinstance(v, dict):
|
|
config_error("Incorrect `kiplot` section")
|
|
if 'version' not in v:
|
|
config_error("YAML config needs `kiplot.version`.")
|
|
version = v['version']
|
|
# Only version 1 is known
|
|
if version != 1:
|
|
config_error("Unknown KiPlot config version: "+str(version))
|
|
return version
|
|
|
|
def _parse_layers(self, layers_to_parse):
|
|
# Check we have a list of layers
|
|
if not isinstance(layers_to_parse, list):
|
|
raise KiPlotConfigurationError("`layers` must be a list")
|
|
# Parse the elements
|
|
layers = []
|
|
for l in layers_to_parse:
|
|
# Extract the attributes
|
|
layer = None
|
|
description = 'no desc'
|
|
suffix = ''
|
|
for k, v in l.items():
|
|
if k == 'layer':
|
|
layer = str(v)
|
|
elif k == 'description':
|
|
description = str(v)
|
|
elif k == 'suffix':
|
|
suffix = str(v)
|
|
else:
|
|
raise KiPlotConfigurationError("Unknown `{}` attribute for `layer`".format(k))
|
|
# Check we got the layer name
|
|
if layer is None:
|
|
raise KiPlotConfigurationError("Missing `layer` attribute for layer entry ({})".format(l))
|
|
# Create an object for it
|
|
layers.append(Layer(layer, suffix, description))
|
|
return layers
|
|
|
|
def _parse_output(self, o_obj):
|
|
# Default values
|
|
name = None
|
|
desc = None
|
|
otype = None
|
|
options = None
|
|
outdir = '.'
|
|
layers = []
|
|
# Parse all of them
|
|
for k, v in o_obj.items():
|
|
if k == 'name':
|
|
name = v
|
|
elif k == 'comment':
|
|
desc = v
|
|
elif k == 'type':
|
|
otype = v
|
|
elif k == 'options':
|
|
options = v
|
|
elif k == 'dir':
|
|
outdir = v
|
|
elif k == 'layers':
|
|
layers = v
|
|
else:
|
|
config_error("Unknown key `{}` in `{}` ({})".format(k, name, otype))
|
|
# Validate them
|
|
if not name:
|
|
config_error("Output needs a name in: "+str(o_obj))
|
|
if not otype:
|
|
config_error("Output `"+name+"` needs a type")
|
|
name_type = "`"+name+"` ("+otype+")"
|
|
|
|
# Is a valid type?
|
|
if not BaseOutput.is_registered(otype):
|
|
config_error("Unknown output type: `{}`".format(otype))
|
|
# Load it
|
|
logger.debug("Parsing output options for "+name_type)
|
|
o_out = BaseOutput.get_class_for(otype)(name, otype, desc)
|
|
# Apply the options
|
|
try:
|
|
# If we have layers parse them
|
|
if layers:
|
|
layers = self._parse_layers(layers)
|
|
o_out.config(outdir, options, layers)
|
|
except KiPlotConfigurationError as e:
|
|
config_error("In section '"+name+"' ("+otype+"): "+str(e))
|
|
|
|
return o_out
|
|
|
|
def _parse_filters(self, filters):
|
|
if not isinstance(filters, list):
|
|
config_error("'filters' must be a list")
|
|
parsed = None
|
|
for filter in filters:
|
|
if 'filter' in filter:
|
|
comment = filter['filter']
|
|
if 'number' in filter:
|
|
number = filter['number']
|
|
if number is None:
|
|
config_error("empty 'number' in 'filter' definition ("+str(filter)+")")
|
|
else:
|
|
config_error("missing 'number' for 'filter' definition ("+str(filter)+")")
|
|
if 'regex' in filter:
|
|
regex = filter['regex']
|
|
if regex is None:
|
|
config_error("empty 'regex' in 'filter' definition ("+str(filter)+")")
|
|
else:
|
|
config_error("missing 'regex' for 'filter' definition ("+str(filter)+")")
|
|
logger.debug("Adding DRC/ERC filter '{}','{}','{}'".format(comment, number, regex))
|
|
if parsed is None:
|
|
parsed = ''
|
|
if comment:
|
|
parsed += '# '+comment+'\n'
|
|
parsed += '{},{}\n'.format(number, regex)
|
|
else:
|
|
config_error("'filters' section of 'preflight' must contain 'filter' definitions (not "+str(filter)+")")
|
|
return parsed
|
|
|
|
def _parse_preflight(self, pf):
|
|
logger.debug("Parsing preflight options: {}".format(pf))
|
|
if not isinstance(pf, dict):
|
|
config_error("Incorrect `preflight` section")
|
|
|
|
for k, v in pf.items():
|
|
if not BasePreFlight.is_registered(k):
|
|
config_error("Unknown preflight: `{}`".format(k))
|
|
try:
|
|
logger.debug("Parsing preflight "+k)
|
|
if k == 'filters':
|
|
v = self._parse_filters(v)
|
|
o_pre = BasePreFlight.get_class_for(k)(k, v)
|
|
except KiPlotConfigurationError as e:
|
|
config_error("In preflight '"+k+"': "+str(e))
|
|
BasePreFlight.add_preflight(o_pre)
|
|
|
|
def read(self, fstream):
|
|
"""
|
|
Read a file object into a config object
|
|
|
|
:param fstream: file stream of a config YAML file
|
|
"""
|
|
try:
|
|
data = yaml.safe_load(fstream)
|
|
except yaml.YAMLError as e:
|
|
config_error("Error loading YAML "+str(e))
|
|
# List of outputs
|
|
outputs = []
|
|
version = None
|
|
# Analize each section
|
|
for k, v in data.items():
|
|
# logger.debug('{} {}'.format(k, v))
|
|
if k == 'kiplot':
|
|
version = self._check_version(v)
|
|
elif k == 'preflight':
|
|
self._parse_preflight(v)
|
|
elif k == 'outputs':
|
|
if isinstance(v, list):
|
|
for o in v:
|
|
outputs.append(self._parse_output(o))
|
|
else:
|
|
config_error("`outputs` must be a list")
|
|
else:
|
|
config_error('Unknown section `{}` in config.'.format(k))
|
|
if version is None:
|
|
config_error("YAML config needs `kiplot.version`.")
|
|
return outputs
|
|
|
|
|
|
def trim(docstring):
|
|
""" PEP 257 recommended trim for __doc__ """
|
|
if not docstring:
|
|
return ''
|
|
# Convert tabs to spaces (following the normal Python rules)
|
|
# and split into a list of lines:
|
|
lines = docstring.expandtabs().splitlines()
|
|
# Determine minimum indentation (first line doesn't count):
|
|
indent = maxsize
|
|
for line in lines[1:]:
|
|
stripped = line.lstrip()
|
|
if stripped:
|
|
indent = min(indent, len(line) - len(stripped))
|
|
# Remove indentation (first line is special):
|
|
trimmed = [lines[0].strip()]
|
|
if indent < maxsize:
|
|
for line in lines[1:]:
|
|
trimmed.append(line[indent:].rstrip())
|
|
# Strip off trailing and leading blank lines:
|
|
while trimmed and not trimmed[-1]:
|
|
trimmed.pop()
|
|
while trimmed and not trimmed[0]:
|
|
trimmed.pop(0)
|
|
# Return a single string:
|
|
return trimmed
|
|
|
|
|
|
def print_output_options(name, cl):
|
|
obj = cl('', name, '')
|
|
print(' * Options:')
|
|
num_opts = 0
|
|
for k, v in BaseOutput.get_attrs_gen(obj):
|
|
help = getattr(obj, '_help_'+k)
|
|
print(' - `{}`: {}.'.format(k, help.rstrip() if help else 'Undocumented'))
|
|
num_opts = num_opts+1
|
|
if num_opts == 0:
|
|
print(' - No available options')
|
|
|
|
|
|
def print_one_out_help(details, n, o):
|
|
lines = trim(o.__doc__)
|
|
if len(lines) == 0:
|
|
lines = ['Undocumented', 'No description']
|
|
if details:
|
|
print('* '+lines[0])
|
|
print(' * Type: `{}`'.format(n))
|
|
print(' * Description: '+lines[1])
|
|
for ln in range(2, len(lines)):
|
|
print(' '+lines[ln])
|
|
print_output_options(n, o)
|
|
else:
|
|
print('* {} [{}]'.format(lines[0], n))
|
|
|
|
|
|
def print_outputs_help(details=False):
|
|
outs = BaseOutput.get_registered()
|
|
logger.debug('{} supported outputs'.format(len(outs)))
|
|
print('Supported outputs:')
|
|
for n, o in OrderedDict(sorted(outs.items())).items():
|
|
if details:
|
|
print()
|
|
print_one_out_help(details, n, o)
|
|
|
|
|
|
def print_output_help(name):
|
|
if not BaseOutput.is_registered(name):
|
|
logger.error('Unknown output type `{}`, try --help-list-outputs'.format(name))
|
|
exit(EXIT_BAD_ARGS)
|
|
print_one_out_help(True, name, BaseOutput.get_class_for(name))
|
|
|
|
|
|
def print_preflights_help():
|
|
pres = BasePreFlight.get_registered()
|
|
logger.debug('{} supported preflights'.format(len(pres)))
|
|
print('Supported preflight options:\n')
|
|
for n, o in pres.items():
|
|
help = o.__doc__
|
|
if help is None:
|
|
help = 'Undocumented'
|
|
print('- {}: {}.'.format(n, help.rstrip()))
|
|
|
|
|
|
def create_example(pcb_file):
|
|
if os.path.isfile(EXAMPLE_CFG):
|
|
logger.error(EXAMPLE_CFG+" already exists, won't overwrite")
|
|
exit(WONT_OVERWRITE)
|
|
with open(EXAMPLE_CFG, 'w') as f:
|
|
logger.info('Creating {} example configuration'.format(EXAMPLE_CFG))
|
|
f.write('kiplot:\n version: 1\n')
|
|
# Preflights
|
|
f.write('\npreflight:\n')
|
|
pres = BasePreFlight.get_registered()
|
|
for n, o in pres.items():
|
|
if o.__doc__:
|
|
f.write(' #'+o.__doc__.rstrip()+'\n')
|
|
f.write(' {}: {}\n'.format(n, o.get_example()))
|
|
# Outputs
|
|
outs = BaseOutput.get_registered()
|
|
f.write('\noutputs:\n')
|
|
# List of layers
|
|
if pcb_file:
|
|
# We have a PCB to take as reference
|
|
load_board(pcb_file)
|
|
layers = Layer.get_pcb_layers()
|
|
else:
|
|
# Use the default list of layers
|
|
layers = Layer.get_default_layers()
|
|
for n, o in OrderedDict(sorted(outs.items())).items():
|
|
lines = trim(o.__doc__)
|
|
if len(lines) == 0:
|
|
lines = ['Undocumented', 'No description']
|
|
f.write(' # '+lines[0].rstrip()+':\n')
|
|
for ln in range(2, len(lines)):
|
|
f.write(' # '+lines[ln].rstrip()+'\n')
|
|
f.write(" - name: '{}_example'\n".format(n))
|
|
f.write(" comment: '{}'\n".format(lines[1]))
|
|
f.write(" type: '{}'\n".format(n))
|
|
f.write(" dir: 'Example/{}_dir'\n".format(n))
|
|
f.write(" options:\n")
|
|
obj = o('', n, '')
|
|
for k, v in BaseOutput.get_attrs_gen(obj):
|
|
help = getattr(obj, '_help_'+k)
|
|
if help:
|
|
help_lines = help.split('\n')
|
|
for hl in help_lines:
|
|
f.write(' # '+hl.strip()+'\n')
|
|
val = getattr(obj, k)
|
|
if isinstance(val, str):
|
|
val = "'{}'".format(val)
|
|
elif isinstance(val, bool):
|
|
val = str(val).lower()
|
|
f.write(' {}: {}\n'.format(k, val))
|
|
if '_layers' in obj.__dict__:
|
|
f.write(' layers:\n')
|
|
for layer in layers:
|
|
f.write(" - layer: '{}'\n".format(layer.name))
|
|
f.write(" suffix: '{}'\n".format(layer.suffix))
|
|
if layer.desc:
|
|
f.write(" description: '{}'\n".format(layer.desc))
|
|
f.write('\n')
|