""" Class to read KiPlot config files """ import os from sys import (exit, maxsize) from collections import OrderedDict from .error import (KiPlotConfigurationError, config_error) from .kiplot import (load_board) from .misc import (NO_YAML_MODULE, EXIT_BAD_ARGS, EXAMPLE_CFG, WONT_OVERWRITE) from .gs import GS from .reg_out import RegOutput from .pre_base import BasePreFlight # 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) class CfgYamlReader(object): def __init__(self): super().__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_output(self, o_tree): try: name = o_tree['name'] except KeyError: config_error("Output needs a name in: "+str(o_tree)) try: otype = o_tree['type'] except KeyError: config_error("Output `"+name+"` needs a type") name_type = "`"+name+"` ("+otype+")" # Is a valid type? if not RegOutput.is_registered(otype): config_error("Unknown output type: `{}`".format(otype)) # Load it logger.debug("Parsing output options for "+name_type) o_out = RegOutput.get_class_for(otype)() o_out.set_tree(o_tree) # Set the data we already know, so we can skip the configurations that aren't requested o_out.name = name o_out.type = otype return o_out 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) 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 _parse_global(self, gb): """ Get global options """ logger.debug("Parsing global options: {}".format(gb)) if not isinstance(gb, dict): config_error("Incorrect `global` section") for k, v in gb.items(): if k == 'output': if not isinstance(v, str): config_error("Global `output` must be a string") GS.global_output = v else: logger.warning("Unknown global option `{}`".format(k)) 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 == 'global': self._parse_global(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__ """ # 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, indent): ind_str = indent*' ' obj = cl() num_opts = 0 for k, v in obj.get_attrs_gen(): if k == 'type' and indent == 2: # Type is fixed for an output continue if not num_opts: # We found one, put the title print(ind_str+'* Valid keys:') help, alias, is_alias = obj.get_doc(k) if is_alias: help = 'Alias for '+alias entry = ' - *{}*: ' else: entry = ' - `{}`: ' if help is None: help = 'Undocumented' # pragma: no cover lines = help.split('\n') preface = ind_str+entry.format(k) clines = len(lines) print('{}{}{}'.format(preface, lines[0].strip(), '.' if clines == 1 else '')) ind_help = len(preface)*' ' for ln in range(1, clines): text = lines[ln].strip() # Dots at the beggining are replaced by spaces. # Used to keep indentation. if text[0] == '.': for i in range(1, len(text)): if text[i] != '.': break text = ' '*i+text[i:] print('{}{}'.format(ind_help+text, '.' if ln+1 == clines else '')) num_opts = num_opts+1 if isinstance(v, type): print_output_options('', v, indent+4) # if num_opts == 0: # print(ind_str+' - No available options') def print_one_out_help(details, n, o): lines = trim(o.__doc__) if len(lines) == 0: lines = ['Undocumented', 'No description'] # pragma: no cover 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, 2) else: print('* {} [{}]'.format(lines[0], n)) def print_outputs_help(details=False): outs = RegOutput.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 RegOutput.is_registered(name): logger.error('Unknown output type `{}`, try --help-list-outputs'.format(name)) exit(EXIT_BAD_ARGS) print_one_out_help(True, name, RegOutput.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 OrderedDict(sorted(pres.items())).items(): help, options = o.get_doc() if help is None: help = 'Undocumented' # pragma: no cover print('- {}: {}.'.format(n, help.strip())) if options: print_output_options(n, options, 2) def print_example_options(f, cls, name, indent, po): ind_str = indent*' ' obj = cls() if po: obj.read_vals_from_po(po) for k, v in obj.get_attrs_gen(): help, alias, is_alias = obj.get_doc(k) if is_alias: f.write(ind_str+'# `{}` is an alias for `{}`\n'.format(k, alias)) continue if help: help_lines = help.split('\n') for hl in help_lines: f.write(ind_str+'# '+hl.strip()+'\n') val = getattr(obj, k) if isinstance(val, str): val = "'{}'".format(val) elif isinstance(val, bool): val = str(val).lower() if isinstance(val, type): f.write(ind_str+'{}:\n'.format(k)) print_example_options(f, val, '', indent+2, None) else: f.write(ind_str+'{}: {}\n'.format(k, val)) return obj def create_example(pcb_file, out_dir, copy_options, copy_expand): if not os.path.exists(out_dir): os.makedirs(out_dir) fname = os.path.join(out_dir, EXAMPLE_CFG) if os.path.isfile(fname): logger.error(fname+" already exists, won't overwrite") exit(WONT_OVERWRITE) with open(fname, 'w') as f: logger.info('Creating {} example configuration'.format(fname)) f.write('kiplot:\n version: 1\n') # Preflights f.write('\npreflight:\n') pres = BasePreFlight.get_registered() for n, o in OrderedDict(sorted(pres.items())).items(): if o.__doc__: f.write(' #'+o.__doc__.rstrip()+'\n') f.write(' {}: {}\n'.format(n, o.get_example())) # Outputs outs = RegOutput.get_registered() f.write('\noutputs:\n') # List of layers po = None layers = 'all' if pcb_file: # We have a PCB to take as reference board = load_board(pcb_file) if copy_options or copy_expand: # Layers and plot options from the PCB layers = 'selected' po = board.GetPlotOptions() for n, cls in OrderedDict(sorted(outs.items())).items(): lines = trim(cls.__doc__) if len(lines) == 0: lines = ['Undocumented', 'No description'] # pragma: no cover 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 = cls() print_example_options(f, obj.options, n, 6, po) if 'layers' in obj.__dict__: if copy_expand: f.write(' layers:\n') layers = obj.layers.solve(layers) for layer in layers: f.write(" - layer: '{}'\n".format(layer.layer)) f.write(" suffix: '{}'\n".format(layer.suffix)) if layer.description: f.write(" description: '{}'\n".format(layer.description)) else: f.write(' layers: {}\n'.format(layers)) f.write('\n')