KiBot/kibot/config_reader.py

879 lines
35 KiB
Python

# -*- 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()