""" 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')