""" Class to read KiPlot config files """ import re import sys import pcbnew from .error import (KiPlotConfigurationError) from .kiplot import (Layer, get_layer_id_from_pcb) from .misc import (NO_YAML_MODULE, EXIT_BAD_CONFIG, EXIT_BAD_ARGS) 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') sys.exit(NO_YAML_MODULE) def config_error(msg): logger.error(msg) sys.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 _get_layer_from_str(self, s): """ Get the pcbnew layer from a string in the config """ D = { 'F.Cu': pcbnew.F_Cu, 'B.Cu': pcbnew.B_Cu, 'F.Adhes': pcbnew.F_Adhes, 'B.Adhes': pcbnew.B_Adhes, 'F.Paste': pcbnew.F_Paste, 'B.Paste': pcbnew.B_Paste, 'F.SilkS': pcbnew.F_SilkS, 'B.SilkS': pcbnew.B_SilkS, 'F.Mask': pcbnew.F_Mask, 'B.Mask': pcbnew.B_Mask, 'Dwgs.User': pcbnew.Dwgs_User, 'Cmts.User': pcbnew.Cmts_User, 'Eco1.User': pcbnew.Eco1_User, 'Eco2.User': pcbnew.Eco2_User, 'Edge.Cuts': pcbnew.Edge_Cuts, 'Margin': pcbnew.Margin, 'F.CrtYd': pcbnew.F_CrtYd, 'B.CrtYd': pcbnew.B_CrtYd, 'F.Fab': pcbnew.F_Fab, 'B.Fab': pcbnew.B_Fab, } layer = None # Priority # 1) Internal list if s in D: layer = Layer(D[s], False, s) else: id = get_layer_id_from_pcb(s) if id is not None: # 2) List from the PCB layer = Layer(id, id < pcbnew.B_Cu, s) elif s.startswith("Inner"): # 3) Inner.N names m = re.match(r"^Inner\.([0-9]+)$", s) if not m: raise KiPlotConfigurationError("Malformed inner layer name: {}, use Inner.N".format(s)) layer = Layer(int(m.group(1)), True, s) else: raise KiPlotConfigurationError('Unknown layer name: '+s) return layer 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 layer = self._get_layer_from_str(layer) layer.set_extra(suffix, description) layers.append(layer) 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 = sys.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 < sys.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 attrs = BaseOutput.get_attrs_for(obj) for k, v in attrs.items(): if k[0] != '_': help_attr = '_help_'+k help = attrs.get(help_attr) 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 outs.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)) sys.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()))