# -*- coding: utf-8 -*- # Copyright (c) 2020-2022 Salvador E. Tropea # Copyright (c) 2020-2022 Instituto Nacional de TecnologĂ­a Industrial # Copyright (c) 2018 John Beard # License: GPL-3.0 # Project: KiBot (formerly KiPlot) # Adapted from: https://github.com/johnbeard/kiplot """ Class to read KiBot config files """ import os import json from sys import (exit, maxsize) from collections import OrderedDict from .error import KiPlotConfigurationError from .misc import (NO_YAML_MODULE, EXIT_BAD_ARGS, EXAMPLE_CFG, WONT_OVERWRITE, W_NOOUTPUTS, W_UNKOUT, W_NOFILTERS, W_NOVARIANTS, W_NOGLOBALS, TRY_INSTALL_CHECK, W_NOPREFLIGHTS) from .gs import GS from .registrable import RegOutput, RegVariant, RegFilter, RegDependency from .pre_base import BasePreFlight from . import __pypi_deps__ # Logger from . import log logger = log.get_logger() LOCAL_OPTIONAL = 1 GLOBAL_OPTIONAL = LOCAL_OPTIONAL*100 LOCAL_MANDATORY = GLOBAL_OPTIONAL*100 GLOBAL_MANDATORY = LOCAL_MANDATORY*100 GITHUB_RAW = 'https://raw.githubusercontent.com/INTI-CMNB/KiBot/master/docs/images/' DEB_LOGO = '![Debian]('+GITHUB_RAW+'debian-openlogo-22x22.png)' PYPI_LOGO = ('![PyPi dependency]('+GITHUB_RAW+'PyPI_logo_simplified-22x22.png)') PY_LOGO = ('![Python module]('+GITHUB_RAW+'Python-logo-notext-22x22.png)') TOOL_LOGO = '![Tool]('+GITHUB_RAW+'llave-inglesa-22x22.png)' AUTO_DOWN = '![Auto-download]('+GITHUB_RAW+'auto_download-22x22.png)' VALID_SECTIONS = {'kiplot', 'kibot', 'import', 'global', 'filters', 'variants', 'preflight', 'outputs'} try: import yaml except ImportError: log.init() logger.error('No yaml module for Python, install python3-yaml') logger.error(TRY_INSTALL_CHECK) exit(NO_YAML_MODULE) class CollectedImports(object): def __init__(self): super().__init__() self.outputs = [] self.filters = {} self.variants = {} self.globals = {} self.preflights = [] class CfgYamlReader(object): def __init__(self): super().__init__() self.imported_globals = {} self.no_run_by_default = [] def _check_version(self, v): if not isinstance(v, dict): raise KiPlotConfigurationError("Incorrect `kibot` section") if 'version' not in v: raise KiPlotConfigurationError("YAML config needs `kibot.version`.") version = v['version'] # Only version 1 is known if version != 1: raise KiPlotConfigurationError("Unknown KiBot config version: "+str(version)) return version def _parse_output(self, o_tree): try: name = str(o_tree['name']) if not name: raise KeyError except KeyError: raise KiPlotConfigurationError("Output needs a name in: "+str(o_tree)) try: otype = o_tree['type'] if not otype: raise KeyError except KeyError: raise KiPlotConfigurationError("Output `"+name+"` needs a type") try: comment = o_tree['comment'] except KeyError: comment = '' if comment is None: comment = '' name_type = "`"+name+"` ("+otype+")" # Is a valid type? if not RegOutput.is_registered(otype): raise KiPlotConfigurationError("Unknown output type: `{}`".format(otype)) # Load it logger.debug("Pre-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 o_out.comment = comment o_out.extends = o_tree.get('extends', '') # Pre-parse the run_by_default option o_out.run_by_default = o_tree.get('run_by_default', True) if not isinstance(o_out.run_by_default, bool): o_out.run_by_default = True o_out.disable_run_by_default = o_tree.get('disable_run_by_default', '') # Pre-parse the disable_run_by_default option if isinstance(o_out.disable_run_by_default, str): if o_out.disable_run_by_default: self.no_run_by_default.append(o_out.disable_run_by_default) elif isinstance(o_out.disable_run_by_default, bool): # True means to disable the one we extend if o_out.disable_run_by_default and o_out.extends: self.no_run_by_default.append(o_out.extends) else: o_out.disable_run_by_default = '' return o_out def _parse_outputs(self, v): outputs = [] if isinstance(v, list): for o in v: outputs.append(self._parse_output(o)) else: raise KiPlotConfigurationError("`outputs` must be a list") return outputs def _parse_variant_or_filter(self, o_tree, kind, reg_class): kind_f = kind[0].upper()+kind[1:] try: name = str(o_tree['name']) if not name: raise KeyError except KeyError: raise KiPlotConfigurationError(kind_f+" needs a name in: "+str(o_tree)) try: otype = o_tree['type'] if not otype: raise KeyError except KeyError: raise KiPlotConfigurationError(kind_f+" `"+name+"` needs a type") # Is a valid type? if not reg_class.is_registered(otype): raise KiPlotConfigurationError("Unknown {} type: `{}`".format(kind, otype)) # Load it name_type = "`"+name+"` ("+otype+")" logger.debug("Parsing "+kind+" "+name_type) o_var = reg_class.get_class_for(otype)() o_var.set_tree(o_tree) o_var.name = name o_var._name_type = name_type # Don't configure it yet, wait until we finish loading (could be an import) return o_var def _parse_variants(self, v): variants = {} if isinstance(v, list): for o in v: o_var = self._parse_variant_or_filter(o, 'variant', RegVariant) variants[o_var.name] = o_var else: raise KiPlotConfigurationError("`variants` must be a list") return variants def _parse_filters(self, v): filters = {} if isinstance(v, list): for o in v: o_fil = self._parse_variant_or_filter(o, 'filter', RegFilter) self.configure_variant_or_filter(o_fil) filters[o_fil.name] = o_fil else: raise KiPlotConfigurationError("`filters` must be a list") return filters def _parse_preflights(self, pf): logger.debug("Parsing preflight options: {}".format(pf)) if not isinstance(pf, dict): raise KiPlotConfigurationError("Incorrect `preflight` section") preflights = [] for k, v in pf.items(): if not BasePreFlight.is_registered(k): raise KiPlotConfigurationError("Unknown preflight: `{}`".format(k)) try: logger.debug("Parsing preflight "+k) o_pre = BasePreFlight.get_class_for(k)(k, v) except KiPlotConfigurationError as e: raise KiPlotConfigurationError("In preflight '"+k+"': "+str(e)) preflights.append(o_pre) return preflights def _parse_global(self, gb): """ Get global options """ logger.debug("Parsing global options: {}".format(gb)) if not isinstance(gb, dict): raise KiPlotConfigurationError("Incorrect `global` section (must be a dict)") if self.imported_globals: gb.update(self.imported_globals) logger.debug("Global options + imported: {}".format(gb)) # Parse all keys inside it glb = GS.class_for_global_opts() glb.set_tree(gb) try: glb.config(None) except KiPlotConfigurationError as e: raise KiPlotConfigurationError("In `global` section: "+str(e)) @staticmethod def _config_error_import(fname, error): if fname is None: fname = '*unnamed*' raise KiPlotConfigurationError('{} in {} import'.format(error, fname)) @staticmethod def _parse_import_items(kind, fname, value): if isinstance(value, str): if value == 'all': return None elif value == 'none': return [] return [value] if isinstance(value, list): values = [] for v in value: if isinstance(v, str): values.append(v) else: CfgYamlReader._config_error_import(fname, '`{}` items must be strings ({})'.format(kind, str(v))) return values CfgYamlReader._config_error_import(fname, '`{}` must be a string or a list ({})'.format(kind, str(v))) def _parse_import_outputs(self, outs, explicit_outs, fn_rel, data, imported): sel_outs = [] if (outs is None or len(outs) > 0) and 'outputs' in data: i_outs = imported.outputs+self._parse_outputs(data['outputs']) if outs is not None: for o in i_outs: if o.name in outs: sel_outs.append(o) outs.remove(o.name) for o in outs: logger.warning(W_UNKOUT+"can't import `{}` output from `{}` (missing)".format(o, fn_rel)) else: sel_outs = i_outs if len(sel_outs) == 0: logger.warning(W_NOOUTPUTS+"No outputs found in `{}`".format(fn_rel)) else: logger.debug('Outputs loaded from `{}`: {}'.format(fn_rel, list(map(lambda c: c.name, sel_outs)))) if outs is None and explicit_outs and 'outputs' not in data: logger.warning(W_NOOUTPUTS+"No outputs found in `{}`".format(fn_rel)) return sel_outs def _parse_import_preflights(self, pre, explicit_pres, fn_rel, data, imported): sel_pres = [] if (pre is None or len(pre) > 0) and 'preflight' in data: i_pres = imported.preflights+self._parse_preflights(data['preflight']) if pre is not None: for p in i_pres: if p._name in pre: sel_pres.append(p) pre.remove(p._name) for p in pre: logger.warning(W_UNKOUT+"can't import `{}` preflight from `{}` (missing)".format(p, fn_rel)) else: sel_pres = i_pres if len(sel_pres) == 0: logger.warning(W_NOPREFLIGHTS+"No preflights found in `{}`".format(fn_rel)) else: logger.debug('Preflights loaded from `{}`: {}'.format(fn_rel, list(map(lambda c: c._name, sel_pres)))) if pre is None and explicit_pres and 'preflight' not in data: logger.warning(W_NOPREFLIGHTS+"No preflights found in `{}`".format(fn_rel)) return sel_pres def _parse_import_filters(self, filters, explicit_fils, fn_rel, data, imported): sel_fils = {} if (filters is None or len(filters) > 0) and 'filters' in data: imported.filters.update(self._parse_filters(data['filters'])) i_fils = imported.filters if filters is not None: for f in filters: if f in i_fils: sel_fils[f] = i_fils[f] else: logger.warning(W_UNKOUT+"can't import `{}` filter from `{}` (missing)".format(f, fn_rel)) else: sel_fils = i_fils if len(sel_fils) == 0: logger.warning(W_NOFILTERS+"No filters found in `{}`".format(fn_rel)) else: logger.debug('Filters loaded from `{}`: {}'.format(fn_rel, sel_fils.keys())) if filters is None and explicit_fils and 'filters' not in data: logger.warning(W_NOFILTERS+"No filters found in `{}`".format(fn_rel)) return sel_fils def _parse_import_variants(self, vars, explicit_vars, fn_rel, data, imported): sel_vars = {} if (vars is None or len(vars) > 0) and 'variants' in data: imported.variants.update(self._parse_variants(data['variants'])) i_vars = imported.variants if vars is not None: for f in vars: if f in i_vars: sel_vars[f] = i_vars[f] else: logger.warning(W_UNKOUT+"can't import `{}` variant from `{}` (missing)".format(f, fn_rel)) else: sel_vars = i_vars if len(sel_vars) == 0: logger.warning(W_NOVARIANTS+"No variants found in `{}`".format(fn_rel)) else: logger.debug('Variants loaded from `{}`: {}'.format(fn_rel, sel_vars.keys())) if vars is None and explicit_vars and 'variants' not in data: logger.warning(W_NOVARIANTS+"No variants found in `{}`".format(fn_rel)) return sel_vars def _parse_import_globals(self, globals, explicit_globals, fn_rel, data, imported): sel_globals = {} if (globals is None or len(globals) > 0) and 'global' in data: i_globals = data['global'] if not isinstance(i_globals, dict): raise KiPlotConfigurationError("Incorrect `global` section (must be a dict), while importing from {}". format(fn_rel)) imported.globals.update(i_globals) i_globals = imported.globals if globals is not None: for f in globals: if f in i_globals: sel_globals[f] = i_globals[f] else: logger.warning(W_UNKOUT+"can't import `{}` global from `{}` (missing)".format(f, fn_rel)) else: sel_globals = i_globals if len(sel_globals) == 0: logger.warning(W_NOGLOBALS+"No globals found in `{}`".format(fn_rel)) else: logger.debug('Globals loaded from `{}`: {}'.format(fn_rel, sel_globals.keys())) if globals is None and explicit_globals and 'global' not in data: logger.warning(W_NOGLOBALS+"No globals found in `{}`".format(fn_rel)) return sel_globals def configure_variant_or_filter(self, o_var): o_var.config(None) def configure_variants(self, variants): logger.debug('Configuring variants') for o_var in variants.values(): self.configure_variant_or_filter(o_var) def _parse_import(self, imp, name, apply=True, depth=0): """ Get imports """ logger.debug("Parsing imports: {}".format(imp)) depth += 1 if depth > 20: raise KiPlotConfigurationError("Import depth greater than 20, make sure this isn't an infinite loop") if not isinstance(imp, list): raise KiPlotConfigurationError("Incorrect `import` section (must be a list)") # Import the files dir = os.path.dirname(os.path.abspath(name)) all_collected = CollectedImports() for entry in imp: if isinstance(entry, str): fn = entry outs = None filters = [] vars = [] globals = [] pre = [] explicit_outs = True explicit_fils = False explicit_vars = False explicit_globals = False explicit_pres = False elif isinstance(entry, dict): fn = outs = filters = vars = globals = pre = None explicit_outs = explicit_fils = explicit_vars = explicit_globals = explicit_pres = False for k, v in entry.items(): if k == 'file': if not isinstance(v, str): raise KiPlotConfigurationError("`import.file` must be a string ({})".format(str(v))) fn = v elif k == 'outputs': outs = self._parse_import_items(k, fn, v) explicit_outs = True elif k == 'preflights': pre = self._parse_import_items(k, fn, v) explicit_pres = True elif k == 'filters': filters = self._parse_import_items(k, fn, v) explicit_fils = True elif k == 'variants': vars = self._parse_import_items(k, fn, v) explicit_vars = True elif k in ['global', 'globals']: globals = self._parse_import_items(k, fn, v) explicit_globals = True else: self._config_error_import(fn, "unknown import entry `{}`".format(str(v))) if fn is None: raise KiPlotConfigurationError("`import` entry without `file` ({})".format(str(entry))) else: raise KiPlotConfigurationError("`import` items must be strings or dicts ({})".format(str(entry))) fn = os.path.expandvars(os.path.expanduser(fn)) if not os.path.isabs(fn): fn = os.path.join(dir, fn) if not os.path.isfile(fn): raise KiPlotConfigurationError("missing import file `{}`".format(fn)) fn_rel = os.path.relpath(fn) data = self.load_yaml(open(fn)) if 'import' in data: # Do a recursive import imported = self._parse_import(data['import'], fn, apply=False, depth=depth) else: # Nothing to import, start fresh imported = CollectedImports() # Parse and filter all stuff, add them to all_collected # Outputs all_collected.outputs.extend(self._parse_import_outputs(outs, explicit_outs, fn_rel, data, imported)) # Preflights all_collected.preflights.extend(self._parse_import_preflights(pre, explicit_pres, fn_rel, data, imported)) # Filters all_collected.filters.update(self._parse_import_filters(filters, explicit_fils, fn_rel, data, imported)) # Variants all_collected.variants.update(self._parse_import_variants(vars, explicit_vars, fn_rel, data, imported)) # Globals all_collected.globals.update(self._parse_import_globals(globals, explicit_globals, fn_rel, data, imported)) if apply: # This is the main import (not a recursive one) apply the results RegOutput.add_filters(all_collected.filters) self.configure_variants(all_collected.variants) RegOutput.add_variants(all_collected.variants) self.imported_globals = all_collected.globals BasePreFlight.add_preflights(all_collected.preflights) RegOutput.add_outputs(all_collected.outputs, fn_rel) return all_collected def load_yaml(self, fstream): try: data = yaml.safe_load(fstream) except yaml.YAMLError as e: raise KiPlotConfigurationError("Error loading YAML "+str(e)) # Accept `globals` for `global` if 'globals' in data and 'global' not in data: data['global'] = data['globals'] del data['globals'] return data def read(self, fstream): """ Read a file object into a config object :param fstream: file stream of a config YAML file """ data = self.load_yaml(fstream) # Analyze the version # Currently just checks for v1 v1 = data.get('kiplot', None) v2 = data.get('kibot', None) if v1 and v2: raise KiPlotConfigurationError("Use `kibot` or `kiplot` but not both.") if not v1 and not v2: raise KiPlotConfigurationError("YAML config needs `kibot.version`.") if v1 or v2: self._check_version(v1 or v2) # Look for imports v1 = data.get('import', None) if v1: self._parse_import(v1, fstream.name) # Look for globals # If no globals defined initialize them with default values self._parse_global(data.get('global', {})) # Look for filters v1 = data.get('filters', None) if v1: RegOutput.add_filters(self._parse_filters(v1)) # Look for variants v1 = data.get('variants', None) if v1: variants = self._parse_variants(v1) self.configure_variants(variants) RegOutput.add_variants(variants) # Solve the global variant if GS.global_variant: try: GS.solved_global_variant = RegOutput.check_variant(GS.global_variant) except KiPlotConfigurationError as e: raise KiPlotConfigurationError("In global section: "+str(e)) # Look for preflights v1 = data.get('preflight', None) if v1: BasePreFlight.add_preflights(self._parse_preflights(v1)) # Look for outputs v1 = data.get('outputs', None) if v1: RegOutput.add_outputs(self._parse_outputs(v1)) # Report invalid sections (the first we find) defined_sections = set(data.keys()) invalid_sections = defined_sections-VALID_SECTIONS for k in invalid_sections: raise KiPlotConfigurationError('Unknown section `{}` in config.'.format(k)) # Ok, now we have all the outputs loaded, so we can apply the disable_run_by_default for name in self.no_run_by_default: o = RegOutput.get_output(name) if o: o.run_by_default = False logger.debug("Disabling the default run for `{}`".format(o)) return RegOutput.get_outputs() def trim(docstring): """ PEP 257 recommended trim for __doc__ """ if docstring is None: 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, indent): ind_str = indent*' ' obj = cl() num_opts = 0 for k, v in sorted(obj.get_attrs_gen(), key=lambda x: not obj.is_basic_option(x[0])): 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) is_basic = False if help and help[0] == '*': help = help[1:] is_basic = True if is_alias: help = 'Alias for '+alias entry = ' - *{}*: ' elif is_basic: entry = ' - **`{}`**: ' else: entry = ' - `{}`: ' if help is None: help = 'Undocumented' logger.error('Undocumented option: `{}`'.format(k)) 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 beginning 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'] 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:') if details: print('\nNotes:') print('1. Most relevant options are listed first and in **bold**. ' 'Which ones are more relevant is quite arbitrary, comments are welcome.') print('2. Aliases are listed in *italics*.') 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(): prefs = BasePreFlight.get_registered() logger.debug('{} supported preflights'.format(len(prefs))) print('Supported preflight options:\n') for n, o in OrderedDict(sorted(prefs.items())).items(): help, options = o.get_doc() if help is None: help = 'Undocumented' print('- `{}`: {}.'.format(n, help.strip())) if options: print_output_options(n, options, 2) def print_filters_help(): filters = RegFilter.get_registered() logger.debug('{} supported filters'.format(len(filters))) print('Supported filters:\n') for n, o in OrderedDict(sorted(filters.items())).items(): help = o.__doc__ if help is None: help = 'Undocumented' print('- {}: {}.'.format(n, help.strip())) print_output_options(n, o, 2) def print_global_options_help(): print_output_options('Global options', GS.class_for_global_opts, 2) def quoted(val): if "'" in val: return '"{}"'.format(val) return "'{}'".format(val) def print_example_options(f, cls, name, indent, po, is_list=False): ind_str = indent*' ' obj = cls() first = True if po: obj.read_vals_from_po(po) for k, _ in obj.get_attrs_gen(): help, alias, is_alias = obj.get_doc(k, no_basic=True) 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: # Dots at the beginning are replaced by spaces. # Used to keep indentation. hl = hl.strip() if hl[0] == '.': for i in range(1, len(hl)): if hl[i] != '.': break hl = ' '*i+hl[i:] f.write(ind_str+'# '+hl+'\n') example_attr = '_'+k+'_example' if hasattr(obj, example_attr): val = getattr(obj, example_attr) else: val = getattr(obj, k) if isinstance(val, str): val = quoted(val) elif isinstance(val, bool): val = str(val).lower() if isinstance(val, type): if val.__name__ == 'Optionable' and help and '=' in help_lines[0]: # Get the text after = txt = help_lines[0].split('=')[1] # Get the text before the space, without the ] txt = txt.split()[0][:-1] f.write(ind_str+'{}: {}\n'.format(k, txt)) elif val.get_default(): f.write(ind_str+'{}: {}\n'.format(k, val.get_default())) else: if is_list and first: k = '- '+k f.write(ind_str+'{}:\n'.format(k)) extra_indent = 2 if not is_list else 4 print_example_options(f, val, '', indent+extra_indent, None, help and 'list(dict' in help_lines[0]) if is_list and first: ind_str += ' ' first = False else: if is_list and first: k = '- '+k if val is None: val = 'null' f.write(ind_str+'{}: {}\n'.format(k, val)) if is_list and first: ind_str += ' ' first = False 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("# ATTENTION! THIS ISN'T A FULLY FUNCTIONAL EXAMPLE.\n") f.write("# You should take portions of this example and edit the options to make\n") f.write("# them suitable for your use.\n") f.write("# This file is useful to know all the available options.\n") f.write('kibot:\n version: 1\n') # Preflights f.write('\npreflight:\n') prefs = BasePreFlight.get_registered() for n, o in OrderedDict(sorted(prefs.items())).items(): if o.__doc__: lines = trim(o.__doc__.rstrip()+'.') for ln in lines: f.write(' # '+ln.rstrip()+'\n') example = o.get_example() if not example.startswith("\n"): example = ' '+example f.write(' {}:{}\n'.format(n, 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 = GS.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'] 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(quoted(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)) def global2human(name): return '`'+name+'`' if name != 'global' else 'general use' class MyEncoder(json.JSONEncoder): """ Simple JSON encoder for objects """ def default(self, o): return o.__dict__ def print_dependencies(markdown=True, jsn=False): # Compute the importance of each dependency for dep in RegDependency.get_registered().values(): importance = 0 for r in dep.roles: local = r.output != 'global' if r.mandatory: importance += LOCAL_MANDATORY if local else GLOBAL_MANDATORY else: importance += LOCAL_OPTIONAL if local else GLOBAL_OPTIONAL dep.importance = importance # The JSON output is just a dump if jsn: print(json.dumps(RegDependency.get_registered(), cls=MyEncoder, indent=4, sort_keys=True)) return # Now print them sorted by importance (and by name as a second criteria) for name, dep in sorted(sorted(RegDependency.get_registered().items(), key=lambda x: x[0].lower()), # noqa C414 key=lambda x: x[1].importance, reverse=True): deb = '' if markdown: dtype = PY_LOGO if dep.is_python else TOOL_LOGO is_pypi_dep = PYPI_LOGO if dep.pypi_name.lower() in __pypi_deps__ else '' has_dowloader = ' '+AUTO_DOWN if dep.downloader is not None else '' if dep.is_python: url = 'https://pypi.org/project/{}/'.format(name) if is_pypi_dep: is_pypi_dep = ' [{}]({})'.format(is_pypi_dep, url) else: url = dep.url name = '[**{}**]({})'.format(name, url) dtype = ' [{}]({})'.format(dtype, url) if dep.in_debian: deb = ' [{}](https://packages.debian.org/bullseye/{})'.format(DEB_LOGO, dep.deb_package) else: dtype = ' (Python module)' if dep.is_python else ' (Tool)' is_pypi_dep = ' (PyPi dependency)' if dep.pypi_name.lower() in __pypi_deps__ else '' has_dowloader = ' (Auto-download)' if dep.downloader is not None else '' if dep.in_debian: deb = ' (Debian: {})'.format(dep.deb_package) needed = [] optional = [] version = None for r in dep.roles: if r.mandatory: needed.append(global2human(r.output)) else: optional.append(r) if r.version and (version is None or r.version > version): version = r.version ver = ' v'+'.'.join(map(str, version)) if version else '' print(name+ver+dtype+is_pypi_dep+deb+has_dowloader) if needed: if len(needed) == 1: if needed[0] == 'general use': print('- Mandatory') else: print('- Mandatory for '+needed[0]) else: print('- Mandatory for: '+', '.join(sorted(needed))) if optional: if len(optional) == 1: o = optional[0] desc = o.desc[0].lower()+o.desc[1:] print('- Optional to {} for {}'.format(desc, global2human(o.output))) else: print('- Optional to:') for o in optional: ver = '' if o.version: ver = ' (v'+'.'.join(map(str, o.version))+')' print(' - {} for {}{}'.format(o.desc, global2human(o.output), ver)) print()