From ab8550796d3e219e11451f7019e66e7e361e6fb3 Mon Sep 17 00:00:00 2001 From: "Salvador E. Tropea" Date: Fri, 28 Aug 2020 16:33:25 -0300 Subject: [PATCH 01/21] Added debug information to the docs generation. This is run during the lint run, before even knowing if the code is valid. Having debug information is quite useful here. --- docs/Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/Makefile b/docs/Makefile index f1454b832..0e023e9f1 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -7,5 +7,5 @@ all: ../README.md samples/generic_plot.kibot.yaml samples/generic_plot.kibot.yaml: ../kibot/out_*.py ../kibot/pre_*.py ../kibot/config_reader.py rm -f example.kibot.yaml - ../src/kibot --example + ../src/kibot -v --example mv example.kibot.yaml $@ From 3be45edf07e2c0d2ebfd4ad7628cf5ff7c4a75cf Mon Sep 17 00:00:00 2001 From: "Salvador E. Tropea" Date: Fri, 28 Aug 2020 16:46:24 -0300 Subject: [PATCH 02/21] Added the basic infraestructure for variants. Now variants are defined separately so we can apply the variants for more than one output. It will also allow different variant semantics. --- kibot/config_reader.py | 54 +++++++++++++++++++++++++++++++++++++----- kibot/kiplot.py | 2 +- kibot/macros.py | 11 +++++++++ kibot/out_base.py | 4 +--- kibot/pre_base.py | 24 ++++--------------- kibot/reg_out.py | 30 ----------------------- kibot/registrable.py | 54 ++++++++++++++++++++++++++++++++++++++++++ kibot/var_base.py | 24 +++++++++++++++++++ 8 files changed, 144 insertions(+), 59 deletions(-) delete mode 100644 kibot/reg_out.py create mode 100644 kibot/registrable.py create mode 100644 kibot/var_base.py diff --git a/kibot/config_reader.py b/kibot/config_reader.py index c9aef6ca0..487503d0e 100644 --- a/kibot/config_reader.py +++ b/kibot/config_reader.py @@ -17,7 +17,7 @@ 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 .registrable import RegOutput, RegVariant from .pre_base import BasePreFlight # Logger @@ -80,6 +80,50 @@ def _parse_output(self, o_tree): return o_out + def _parse_outputs(self, v): + outputs = [] + if isinstance(v, list): + for o in v: + outputs.append(self._parse_output(o)) + else: + config_error("`outputs` must be a list") + return outputs + + def _parse_variant(self, o_tree): + try: + name = o_tree['name'] + if not name: + raise KeyError + except KeyError: + config_error("Variant needs a name in: "+str(o_tree)) + try: + otype = o_tree['type'] + except KeyError: + config_error("Variant `"+name+"` needs a type") + # Is a valid type? + if not RegVariant.is_registered(otype): + config_error("Unknown variant type: `{}`".format(otype)) + # Load it + name_type = "`"+name+"` ("+otype+")" + logger.debug("Parsing variant "+name_type) + o_var = RegVariant.get_class_for(otype)() + o_var.set_tree(o_tree) + try: + o_var.config() + except KiPlotConfigurationError as e: + config_error("In section `"+name_type+"`: "+str(e)) + return o_var + + def _parse_variants(self, v): + variants = {} + if isinstance(v, list): + for o in v: + o_var = self._parse_variant(o) + variants[o_var.name] = o_var + else: + config_error("`variants` must be a list") + return variants + def _parse_preflight(self, pf): logger.debug("Parsing preflight options: {}".format(pf)) if not isinstance(pf, dict): @@ -131,12 +175,10 @@ def read(self, fstream): self._parse_preflight(v) elif k == 'global': self._parse_global(v) + elif k == 'variants': + RegOutput.set_variants(self._parse_variants(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") + outputs = self._parse_outputs(v) else: config_error('Unknown section `{}` in config.'.format(k)) if version is None: diff --git a/kibot/kiplot.py b/kibot/kiplot.py index 2864d89e2..193e2f3e5 100644 --- a/kibot/kiplot.py +++ b/kibot/kiplot.py @@ -56,7 +56,7 @@ def _import(name, path): def _load_actions(path): logger.debug("Importing from "+path) - lst = glob(os.path.join(path, 'out_*.py')) + glob(os.path.join(path, 'pre_*.py')) + lst = glob(os.path.join(path, 'out_*.py')) + glob(os.path.join(path, 'pre_*.py')) + glob(os.path.join(path, 'var_*.py')) for p in lst: name = os.path.splitext(os.path.basename(p))[0] logger.debug("- Importing "+name) diff --git a/kibot/macros.py b/kibot/macros.py index 6a604fecf..4c0804e58 100644 --- a/kibot/macros.py +++ b/kibot/macros.py @@ -122,6 +122,17 @@ def output_class(tree, **kw): return _do_wrap_class_register(tree, 'out_base', 'BaseOutput') +def variant_class(tree, **kw): + """A decorator to wrap a class with: + + from .var_base import BaseVariant + ... Class definition + BaseVariant.register(CLASS_NAME_LOWER_STRING, CLASS_NAME) + + Allowing to register the class as a variant. """ + return _do_wrap_class_register(tree, 'var_base', 'BaseVariant') + + def pre_class(tree, **kw): """A decorator to wrap a class with: diff --git a/kibot/out_base.py b/kibot/out_base.py index 35bda345e..86751e9ad 100644 --- a/kibot/out_base.py +++ b/kibot/out_base.py @@ -3,7 +3,7 @@ # Copyright (c) 2020 Instituto Nacional de Tecnología Industrial # License: GPL-3.0 # Project: KiBot (formerly KiPlot) -from .reg_out import RegOutput +from .registrable import RegOutput from .macros import macros, document # noqa: F401 from . import log @@ -11,8 +11,6 @@ class BaseOutput(RegOutput): - _registered = {} - def __init__(self): super().__init__() with document: diff --git a/kibot/pre_base.py b/kibot/pre_base.py index 3e94f73fe..ae4f3372b 100644 --- a/kibot/pre_base.py +++ b/kibot/pre_base.py @@ -3,40 +3,26 @@ # Copyright (c) 2020 Instituto Nacional de Tecnología Industrial # License: GPL-3.0 # Project: KiBot (formerly KiPlot) -from .gs import (GS) -from .log import (get_logger) +from .gs import GS +from .registrable import Registrable +from .log import get_logger logger = get_logger(__name__) -class BasePreFlight(object): +class BasePreFlight(Registrable): _registered = {} _in_use = {} _options = {} def __init__(self, name, value): + super().__init__() self._value = value self._name = name self._sch_related = False self._pcb_related = False self._enabled = True - @staticmethod - def register(name, aclass): - BasePreFlight._registered[name] = aclass - - @staticmethod - def is_registered(name): - return name in BasePreFlight._registered - - @staticmethod - def get_registered(): - return BasePreFlight._registered - - @staticmethod - def get_class_for(name): - return BasePreFlight._registered[name] - @staticmethod def add_preflight(o_pre): BasePreFlight._in_use[o_pre._name] = o_pre diff --git a/kibot/reg_out.py b/kibot/reg_out.py deleted file mode 100644 index 2f8d8dda0..000000000 --- a/kibot/reg_out.py +++ /dev/null @@ -1,30 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) 2020 Salvador E. Tropea -# Copyright (c) 2020 Instituto Nacional de Tecnología Industrial -# License: GPL-3.0 -# Project: KiBot (formerly KiPlot) -from .optionable import Optionable - - -class RegOutput(Optionable): - """ This class adds the mechanism to register outputs """ - _registered = {} - - def __init__(self): - super().__init__() - - @staticmethod - def register(name, aclass): - RegOutput._registered[name] = aclass - - @staticmethod - def is_registered(name): - return name in RegOutput._registered - - @staticmethod - def get_class_for(name): - return RegOutput._registered[name] - - @staticmethod - def get_registered(): - return RegOutput._registered diff --git a/kibot/registrable.py b/kibot/registrable.py new file mode 100644 index 000000000..6028ec041 --- /dev/null +++ b/kibot/registrable.py @@ -0,0 +1,54 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020 Salvador E. Tropea +# Copyright (c) 2020 Instituto Nacional de Tecnología Industrial +# License: GPL-3.0 +# Project: KiBot (formerly KiPlot) +from .optionable import Optionable + + +class Registrable(object): + """ This class adds the mechanism to register plug-ins """ + def __init__(self): + super().__init__() + + @classmethod + def register(cl, name, aclass): + cl._registered[name] = aclass + + @classmethod + def is_registered(cl, name): + return name in cl._registered + + @classmethod + def get_class_for(cl, name): + return cl._registered[name] + + @classmethod + def get_registered(cl): + return cl._registered + + +class RegOutput(Optionable, Registrable): + """ An optionable that is also registrable. + Used by BaseOutput. + Here because it doesn't need macros. """ + _registered = {} + # List of defined variants + _def_variants = {} + + def __init__(self): + super().__init__() + + @staticmethod + def set_variants(variants): + RegOutput._def_variants = variants + + +class RegVariant(Optionable, Registrable): + """ An optionable that is also registrable. + Used by BaseVariant. + Here because it doesn't need macros. """ + _registered = {} + + def __init__(self): + super().__init__() diff --git a/kibot/var_base.py b/kibot/var_base.py new file mode 100644 index 000000000..88bc2a1db --- /dev/null +++ b/kibot/var_base.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020 Salvador E. Tropea +# Copyright (c) 2020 Instituto Nacional de Tecnología Industrial +# License: GPL-3.0 +# Project: KiBot (formerly KiPlot) +from .registrable import RegVariant +from .macros import macros, document # noqa: F401 + + +class BaseVariant(RegVariant): + def __init__(self): + super().__init__() + with document: + self.name = '' + """ Used to identify this particular variant definition """ + self.type = '' + """ Type of variant """ + self.comment = '' + """ A comment for documentation purposes """ + self.file_id = '' + """ Text to use as the """ + + def __str__(self): + return "'{}' ({}) [{}]".format(self.comment, self.name, self.type) From a19c6157b71ec324b419046ed31ee0240577273f Mon Sep 17 00:00:00 2001 From: "Salvador E. Tropea" Date: Fri, 28 Aug 2020 16:48:02 -0300 Subject: [PATCH 03/21] Added support to see the variant in the output file name. Now the variant can affect the output file name. --- kibot/gs.py | 6 +++++- kibot/optionable.py | 25 +++++++++++++++++-------- 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/kibot/gs.py b/kibot/gs.py index e1e859cc9..6141bc2ee 100644 --- a/kibot/gs.py +++ b/kibot/gs.py @@ -50,8 +50,12 @@ class GS(object): pcb_rev = None pcb_comp = None # Global defaults + # This is used as default value for classes supporting "output" option + def_global_output = '%f-%i%v.%x' + # This value will overwrite GS.def_global_output if defined + # Classes supporting global "output" option must call super().__init__() + # after defining its own options to allow Optionable do the overwrite. global_output = None - def_global_output = '%f-%i.%x' @staticmethod def set_sch(name): diff --git a/kibot/optionable.py b/kibot/optionable.py index ed3ee5bb0..6b2059faf 100644 --- a/kibot/optionable.py +++ b/kibot/optionable.py @@ -166,20 +166,28 @@ def get_attrs_gen(self): attrs = self.get_attrs_for() return ((k, v) for k, v in attrs.items() if k[0] != '_') + def _find_variant(self): + """ Returns the text to add for the current variant. + If no variant is defined an empty string is returned. """ + if hasattr(self, 'variant') and self.variant and hasattr(self.variant, 'file_id'): + return self.variant.file_id + return '' + def expand_filename(self, out_dir, name, id='', ext=''): """ Expands %* values in filenames. Uses data from the PCB. """ if GS.board: GS.load_pcb_title_block() # Do the replacements - name = name.replace('%f', GS.pcb_basename) - name = name.replace('%p', GS.pcb_title) name = name.replace('%c', GS.pcb_comp) - name = name.replace('%r', GS.pcb_rev) name = name.replace('%d', GS.pcb_date) name = name.replace('%D', GS.today) - name = name.replace('%T', GS.time) + name = name.replace('%f', GS.pcb_basename) name = name.replace('%i', id) + name = name.replace('%p', GS.pcb_title) + name = name.replace('%r', GS.pcb_rev) + name = name.replace('%T', GS.time) + name = name.replace('%v', self._find_variant()) name = name.replace('%x', ext) # sanitize the name to avoid characters illegal in file systems name = name.replace('\\', '/') @@ -192,14 +200,15 @@ def expand_filename_sch(self, out_dir, name, id='', ext=''): if GS.sch_file: GS.load_sch_title_block() # Do the replacements - name = name.replace('%f', GS.sch_basename) - name = name.replace('%p', GS.sch_title) name = name.replace('%c', GS.sch_comp) - name = name.replace('%r', GS.sch_rev) name = name.replace('%d', GS.sch_date) name = name.replace('%D', GS.today) - name = name.replace('%T', GS.time) + name = name.replace('%f', GS.sch_basename) name = name.replace('%i', id) + name = name.replace('%p', GS.sch_title) + name = name.replace('%r', GS.sch_rev) + name = name.replace('%T', GS.time) + name = name.replace('%v', self._find_variant()) name = name.replace('%x', ext) # sanitize the name to avoid characters illegal in file systems name = name.replace('\\', '/') From 0bdce78004758f114b8e6d279d0d50c25310ae89 Mon Sep 17 00:00:00 2001 From: "Salvador E. Tropea" Date: Fri, 28 Aug 2020 16:49:20 -0300 Subject: [PATCH 04/21] Implemented the new variants mechanism in the internal BoM. --- kibot/bom/bom.py | 104 +------------ kibot/bom/csv_writer.py | 2 +- kibot/bom/html_writer.py | 2 +- kibot/bom/xlsx_writer.py | 2 +- kibot/bom/xml_writer.py | 2 +- kibot/misc.py | 25 ++++ kibot/out_bom.py | 37 +++-- kibot/var_kibom.py | 141 ++++++++++++++++++ tests/test_plot/test_int_bom.py | 93 ++++-------- .../int_bom_var_production_csv.kibot.yaml | 12 -- .../int_bom_var_t1_csv.kibot.yaml | 63 ++++++++ .../int_bom_var_t2_csv.kibot.yaml | 36 +++++ .../int_bom_var_test_csv.kibot.yaml | 12 -- .../int_bom_var_v1_csv.kibot.yaml | 12 -- .../int_bom_var_v1v3_csv.kibot.yaml | 12 -- .../int_bom_var_v2_csv.kibot.yaml | 12 -- .../int_bom_var_v3_csv.kibot.yaml | 12 -- 17 files changed, 326 insertions(+), 253 deletions(-) create mode 100644 kibot/var_kibom.py delete mode 100644 tests/yaml_samples/int_bom_var_production_csv.kibot.yaml create mode 100644 tests/yaml_samples/int_bom_var_t1_csv.kibot.yaml create mode 100644 tests/yaml_samples/int_bom_var_t2_csv.kibot.yaml delete mode 100644 tests/yaml_samples/int_bom_var_test_csv.kibot.yaml delete mode 100644 tests/yaml_samples/int_bom_var_v1_csv.kibot.yaml delete mode 100644 tests/yaml_samples/int_bom_var_v1v3_csv.kibot.yaml delete mode 100644 tests/yaml_samples/int_bom_var_v2_csv.kibot.yaml delete mode 100644 tests/yaml_samples/int_bom_var_v3_csv.kibot.yaml diff --git a/kibot/bom/bom.py b/kibot/bom/bom.py index 2b6c0530c..72a457e7b 100644 --- a/kibot/bom/bom.py +++ b/kibot/bom/bom.py @@ -14,33 +14,10 @@ from .units import compare_values, comp_match from .bom_writer import write_bom from .columnlist import ColumnList +from ..misc import DNF from .. import log logger = log.get_logger(__name__) -# Supported values for "do not fit" -DNF = { - "dnf": 1, - "dnl": 1, - "dnp": 1, - "do not fit": 1, - "do not place": 1, - "do not load": 1, - "nofit": 1, - "nostuff": 1, - "noplace": 1, - "noload": 1, - "not fitted": 1, - "not loaded": 1, - "not placed": 1, - "no stuff": 1, -} -# String matches for marking a component as "do not change" or "fixed" -DNC = { - "dnc": 1, - "do not change": 1, - "no change": 1, - "fixed": 1 -} # RV == Resistor Variable or Varistor # RN == Resistor 'N'(Pack) # RT == Thermistor @@ -412,88 +389,11 @@ def group_components(cfg, components): return groups -def comp_is_fixed(value, config, variants): - """ Determine if a component is FIXED or not. - Fixed components shouldn't be replaced without express authorization. - value: component value (lowercase). - config: content of the 'Config' field (lowercase). - variants: list of variants to match. """ - # Check the value field first - if value in DNC: - return True - # Empty is not fixed - if not config: - return False - # Also support space separated list (simple cases) - opts = config.split(" ") - for opt in opts: - if opt in DNC: - return True - # Normal separator is "," - opts = config.split(",") - for opt in opts: - if opt in DNC: - return True - return False - - -def comp_is_fitted(value, config, variants): - """ Determine if a component will be or not. - value: component value (lowercase). - config: content of the 'Config' field (lowercase). - variants: list of variants to match. """ - # Check the value field first - if value in DNF: - return False - # Empty value means part is fitted - if not config: - return True - # Also support space separated list (simple cases) - opts = config.split(" ") - for opt in opts: - if opt in DNF: - return False - # Variants logic - opts = config.split(",") - # Only fit for ... - exclusive = False - for opt in opts: - opt = opt.strip() - # Any option containing a DNF is not fitted - if opt in DNF: - return False - # Options that start with '-' are explicitly removed from certain configurations - if opt.startswith("-") and opt[1:] in variants: - return False - # Options that start with '+' are fitted only for certain configurations - if opt.startswith("+"): - exclusive = True - if opt[1:] in variants: - return True - # No match - return not exclusive - - def do_bom(file_name, ext, comps, cfg): - # Make the config field name lowercase - cfg.fit_field = cfg.fit_field.lower() - f_config = cfg.fit_field - # Make the variants lowercase - variants = [v.lower() for v in cfg.variant] # Solve `fixed` and `fitted` attributes for all components - for c in comps: - value = c.value.lower() - config = c.get_field_value(f_config).lower() - c.fitted = comp_is_fitted(value, config, variants) - if cfg.debug_level > 2: - logger.debug('ref: {} value: {} config: {} variants: {} -> fitted {}'. - format(c.ref, value, config, variants, c.fitted)) - c.fixed = comp_is_fixed(value, config, variants) + cfg.variant.filter(comps) # Group components according to group_fields groups = group_components(cfg, comps) - # Give a name to empty variant - if not variants: - cfg.variant = ['default'] # Create the BoM logger.debug("Saving BOM File: "+file_name) write_bom(file_name, ext, groups, cfg.columns, cfg) diff --git a/kibot/bom/csv_writer.py b/kibot/bom/csv_writer.py index 748dc85ce..dca727656 100644 --- a/kibot/bom/csv_writer.py +++ b/kibot/bom/csv_writer.py @@ -53,7 +53,7 @@ def write_csv(filename, ext, groups, headings, head_names, cfg): if not cfg.csv.hide_pcb_info: writer.writerow(["Project info:"]) writer.writerow(["Schematic:", cfg.source]) - writer.writerow(["Variant:", ' + '.join(cfg.variant)]) + writer.writerow(["Variant:", cfg.variant.name]) writer.writerow(["Revision:", cfg.revision]) writer.writerow(["Date:", cfg.date]) writer.writerow(["KiCad Version:", cfg.kicad_version]) diff --git a/kibot/bom/html_writer.py b/kibot/bom/html_writer.py index 93778fc06..cf13eff49 100644 --- a/kibot/bom/html_writer.py +++ b/kibot/bom/html_writer.py @@ -213,7 +213,7 @@ def write_html(filename, groups, headings, head_names, cfg): html.write(' \n') if not cfg.html.hide_pcb_info: html.write(" Schematic: {}
\n".format(cfg.source)) - html.write(" Variant: {}
\n".format(', '.join(cfg.variant))) + html.write(" Variant: {}
\n".format(cfg.variant.name)) html.write(" Revision: {}
\n".format(cfg.revision)) html.write(" Date: {}
\n".format(cfg.date)) html.write(" KiCad Version: {}
\n".format(cfg.kicad_version)) diff --git a/kibot/bom/xlsx_writer.py b/kibot/bom/xlsx_writer.py index 551e65a04..5874f9f1f 100644 --- a/kibot/bom/xlsx_writer.py +++ b/kibot/bom/xlsx_writer.py @@ -313,7 +313,7 @@ def write_xlsx(filename, groups, col_fields, head_names, cfg): rc = r_info_start if not cfg.xlsx.hide_pcb_info: rc = add_info(worksheet, column_widths, rc, col1, fmt_info, "Schematic:", cfg.source) - rc = add_info(worksheet, column_widths, rc, col1, fmt_info, "Variant:", ' + '.join(cfg.variant)) + rc = add_info(worksheet, column_widths, rc, col1, fmt_info, "Variant:", cfg.variant.name) rc = add_info(worksheet, column_widths, rc, col1, fmt_info, "Revision:", cfg.revision) rc = add_info(worksheet, column_widths, rc, col1, fmt_info, "Date:", cfg.date) rc = add_info(worksheet, column_widths, rc, col1, fmt_info, "KiCad Version:", cfg.kicad_version) diff --git a/kibot/bom/xml_writer.py b/kibot/bom/xml_writer.py index 5bb2f3e00..fa5148966 100644 --- a/kibot/bom/xml_writer.py +++ b/kibot/bom/xml_writer.py @@ -26,7 +26,7 @@ def write_xml(filename, groups, headings, head_names, cfg): attrib['Schematic_Source'] = cfg.source attrib['Schematic_Revision'] = cfg.revision attrib['Schematic_Date'] = cfg.date - attrib['PCB_Variant'] = ', '.join(cfg.variant) + attrib['PCB_Variant'] = cfg.variant.name attrib['KiCad_Version'] = cfg.kicad_version attrib['Component_Groups'] = str(cfg.n_groups) attrib['Component_Count'] = str(cfg.n_total) diff --git a/kibot/misc.py b/kibot/misc.py index 515d74362..8947524e1 100644 --- a/kibot/misc.py +++ b/kibot/misc.py @@ -45,3 +45,28 @@ URL_PCBDRAW = 'https://github.com/INTI-CMNB/pcbdraw' EXAMPLE_CFG = 'example.kibot.yaml' AUTO_SCALE = 0 + +# Supported values for "do not fit" +DNF = { + "dnf": 1, + "dnl": 1, + "dnp": 1, + "do not fit": 1, + "do not place": 1, + "do not load": 1, + "nofit": 1, + "nostuff": 1, + "noplace": 1, + "noload": 1, + "not fitted": 1, + "not loaded": 1, + "not placed": 1, + "no stuff": 1, +} +# String matches for marking a component as "do not change" or "fixed" +DNC = { + "dnc": 1, + "do not change": 1, + "no change": 1, + "fixed": 1 +} diff --git a/kibot/out_bom.py b/kibot/out_bom.py index 81557193d..615e38664 100644 --- a/kibot/out_bom.py +++ b/kibot/out_bom.py @@ -11,10 +11,12 @@ from re import compile, IGNORECASE from .gs import GS from .optionable import Optionable, BaseOptions +from .registrable import RegOutput from .error import KiPlotConfigurationError from .macros import macros, document, output_class # noqa: F401 from .bom.columnlist import ColumnList, BoMError from .bom.bom import do_bom +from .var_kibom import KiBoM from . import log logger = log.get_logger(__name__) @@ -207,8 +209,8 @@ def __init__(self): with document: self.number = 1 """ Number of boards to build (components multiplier) """ - self.variant = Optionable - """ [string|list(string)=''] Board variant(s), used to determine which components + self.variant = '' + """ Board variant(s), used to determine which components are output to the BoM. """ self.output = GS.def_global_output """ filename for the output (%i=bom)""" @@ -228,7 +230,8 @@ def __init__(self): self.merge_blank_fields = True """ Component groups with blank fields will be merged into the most compatible group, where possible """ self.fit_field = 'Config' - """ Field name used to determine if a particular part is to be fitted (also DNC and variants) """ + """ Field name used to determine if a particular part is to be fitted (also DNC, not for variants). + This value is used only when no variants are specified """ self.group_fields = GroupFields """ [list(string)] List of fields used for sorting individual components into groups. Components which match (comparing *all* fields) will be grouped together. @@ -316,16 +319,18 @@ def _fix_ref_field(field): col = col[:-1] return col - @staticmethod - def _normalize_variant(variant): - if isinstance(variant, type): - variant = [] - elif isinstance(variant, str): - if variant: - variant = [variant] - else: - variant = [] - return variant + def _normalize_variant(self): + """ Replaces the name of the variant by an object handling it. """ + if self.variant: + if self.variant not in RegOutput._def_variants: + raise KiPlotConfigurationError("Unknown variant name `{}`".format(self.variant)) + self.variant = RegOutput._def_variants[self.variant] + else: + # If no variant is specified use the KiBoM variant class with basic functionality + self.variant = KiBoM() + self.variant.config_field = self.fit_field + self.variant.variant = [] + self.variant.name = 'default' def config(self): super().config() @@ -372,8 +377,10 @@ def config(self): for r in self.exclude_any: r.column = self._fix_ref_field(r.column) r.regex = compile(r.regex, flags=IGNORECASE) - # Variants, ensure a list - self.variant = self._normalize_variant(self.variant) + # Make the config field name lowercase + self.fit_field = self.fit_field.lower() + # Variants, make it an object + self._normalize_variant() # Columns self.column_rename = {} self.join = [] diff --git a/kibot/var_kibom.py b/kibot/var_kibom.py new file mode 100644 index 000000000..2b606c4ad --- /dev/null +++ b/kibot/var_kibom.py @@ -0,0 +1,141 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020 Salvador E. Tropea +# Copyright (c) 2020 Instituto Nacional de Tecnología Industrial +# License: GPL-3.0 +# Project: KiBot (formerly KiPlot) +""" +Implements the KiBoM variants mechanism. +""" +from .optionable import Optionable +from .gs import GS +from .misc import DNF, DNC +from .macros import macros, document, variant_class # noqa: F401 +from . import log + +logger = log.get_logger(__name__) + + +@variant_class +class KiBoM(BaseVariant): # noqa: F821 + """ KiBoM variant style + The Config field (configurable) contains a comma separated list of variant directives. + -VARIANT excludes a component from VARIANT. + +VARIANT includes the component only if we are using this variant. """ + def __init__(self): + super().__init__() + with document: + self.config_field = 'Config' + """ Name of the field used to clasify components """ + self.variant = Optionable + """ [string|list(string)=''] Board variant(s) """ + + def config(self): + super().config() + # Variants, ensure a list + if isinstance(self.variant, type): + self.variant = [] + elif isinstance(self.variant, str): + if self.variant: + self.variant = [self.variant] + else: + self.variant = [] + self.variant = [v.lower() for v in self.variant] + # Config field must be lowercase + self.config_field = self.config_field.lower() + + @staticmethod + def basic_comp_is_fitted(value, config): + """ Basic `fitted` criteria, no variants. + value: component value (lowercase). + config: content of the 'Config' field (lowercase). """ + # Check the value field first + if value in DNF: + return False + # Empty value means part is fitted + if not config: + return True + # Also support space separated list (simple cases) + opts = config.split(" ") + for opt in opts: + if opt in DNF: + return False + # Normal separator is "," + opts = config.split(",") + for opt in opts: + if opt in DNF: + return False + return True + + @staticmethod + def basic_comp_is_fixed(value, config): + """ Basic `fixed` criteria, no variants + Fixed components shouldn't be replaced without express authorization. + value: component value (lowercase). + config: content of the 'Config' field (lowercase). """ + # Check the value field first + if value in DNC: + return True + # Empty is not fixed + if not config: + return False + # Also support space separated list (simple cases) + opts = config.split(" ") + for opt in opts: + if opt in DNC: + return True + # Normal separator is "," + opts = config.split(",") + for opt in opts: + if opt in DNC: + return True + return False + + @staticmethod + def _base_filter(comps, f_config): + """ Fill the `fixed` and `fitted` using the basic criteria. + No variant is applied in this step. """ + logger.debug("- Generic KiBoM rules") + for c in comps: + value = c.value.lower() + config = c.get_field_value(f_config).lower() + c.fitted = KiBoM.basic_comp_is_fitted(value, config) + if GS.debug_level > 2: + logger.debug('ref: {} value: {} config: {} -> fitted {}'. + format(c.ref, value, config, c.fitted)) + c.fixed = KiBoM.basic_comp_is_fixed(value, config) + + def variant_comp_is_fitted(self, value, config): + """ Apply the variants to determine if this component will be fitted. + value: component value (lowercase). + config: content of the 'Config' field (lowercase). """ + # Variants logic + opts = config.split(",") + # Only fit for ... + exclusive = False + for opt in opts: + opt = opt.strip() + # Options that start with '-' are explicitly removed from certain configurations + if opt.startswith("-") and opt[1:] in self.variant: + return False + # Options that start with '+' are fitted only for certain configurations + if opt.startswith("+"): + exclusive = True + if opt[1:] in self.variant: + return True + # No match + return not exclusive + + def filter(self, comps): + logger.debug("Applying KiBoM style filter `{}`".format(self.name)) + self._base_filter(comps, self.config_field) + logger.debug("- Variant specific rules") + for c in comps: + if not c.fitted: + # Don't check if we already discarded it during the basic test + continue + value = c.value.lower() + config = c.get_field_value(self.config_field).lower() + c.fitted = self.variant_comp_is_fitted(value, config) + if not c.fitted and GS.debug_level > 2: + logger.debug('ref: {} value: {} config: {} variant: {} -> False'. + format(c.ref, value, config, self.variant)) diff --git a/tests/test_plot/test_int_bom.py b/tests/test_plot/test_int_bom.py index fa4e5bac7..f80f536f9 100644 --- a/tests/test_plot/test_int_bom.py +++ b/tests/test_plot/test_int_bom.py @@ -81,6 +81,7 @@ len(KIBOM_TEST_COMPONENTS), 1, len(KIBOM_TEST_COMPONENTS)] +VARIANTE_PRJ_INFO = ['kibom-variante', 'default', 'A', '2020-03-12', None] LINK_HEAD = ['References', 'Part', 'Value', 'Quantity Per PCB', 'digikey#', 'digikey_alt#', 'manf#'] LINKS_COMPONENTS = ['J1', 'J2', 'R1'] LINKS_EXCLUDE = ['C1'] @@ -1158,82 +1159,54 @@ def test_int_bom_missing_lib(): ctx.clean_up() -def test_int_bom_variant_t1_1(): +def test_int_bom_variant_t1(): prj = 'kibom-variante' - ctx = context.TestContextSCH('test_int_bom_variant_t1_1', prj, 'int_bom_var_v1_csv', BOM_DIR) + ctx = context.TestContextSCH('test_int_bom_variant_t1', prj, 'int_bom_var_t1_csv', BOM_DIR) ctx.run() + # No variant + logging.debug("* No variant") rows, header, info = ctx.load_csv(prj+'-bom.csv') ref_column = header.index(REF_COLUMN_NAME) + check_kibom_test_netlist(rows, ref_column, 2, ['R4'], ['R1', 'R2', 'R3']) + VARIANTE_PRJ_INFO[1] = 'default' + check_csv_info(info, VARIANTE_PRJ_INFO, [4, 20, 3, 1, 3]) + # V1 + logging.debug("* t1_v1 variant") + rows, header, info = ctx.load_csv(prj+'-bom_(V1).csv') check_kibom_test_netlist(rows, ref_column, 2, ['R3', 'R4'], ['R1', 'R2']) ctx.search_err(r'Field Config of component (.*) contains extra spaces') - ctx.clean_up() - - -def test_int_bom_variant_t1_2(): - prj = 'kibom-variante' - ctx = context.TestContextSCH('test_int_bom_variant_t1_2', prj, 'int_bom_var_v2_csv', BOM_DIR) - ctx.run() - rows, header, info = ctx.load_csv(prj+'-bom.csv') - ref_column = header.index(REF_COLUMN_NAME) + VARIANTE_PRJ_INFO[1] = 't1_v1' + check_csv_info(info, VARIANTE_PRJ_INFO, [4, 20, 2, 1, 2]) + # V2 + logging.debug("* t1_v2 variant") + rows, header, info = ctx.load_csv(prj+'-bom_(V2).csv') check_kibom_test_netlist(rows, ref_column, 1, ['R2', 'R4'], ['R1', 'R3']) - ctx.clean_up() - - -def test_int_bom_variant_t1_3(): - prj = 'kibom-variante' - ctx = context.TestContextSCH('test_int_bom_variant_t1_3', prj, 'int_bom_var_v3_csv', BOM_DIR) - ctx.run() - rows, header, info = ctx.load_csv(prj+'-bom.csv') - ref_column = header.index(REF_COLUMN_NAME) + VARIANTE_PRJ_INFO[1] = 't1_v2' + check_csv_info(info, VARIANTE_PRJ_INFO, [3, 20, 2, 1, 2]) + # V3 + logging.debug("* t1_v3 variant") + rows, header, info = ctx.load_csv(prj+'-bom_V3.csv') check_kibom_test_netlist(rows, ref_column, 1, ['R2', 'R3'], ['R1', 'R4']) - ctx.clean_up() - - -def test_int_bom_variant_t1_4(): - prj = 'kibom-variante' - ctx = context.TestContextSCH('test_int_bom_variant_t1_4', prj, 'int_bom_simple_csv', BOM_DIR) - ctx.run() - rows, header, info = ctx.load_csv(prj+'-bom.csv') - ref_column = header.index(REF_COLUMN_NAME) - check_kibom_test_netlist(rows, ref_column, 2, ['R4'], ['R1', 'R2', 'R3']) - ctx.clean_up() - - -def test_int_bom_variant_t1_5(): - prj = 'kibom-variante' - ctx = context.TestContextSCH('test_int_bom_variant_t1_5', prj, 'int_bom_var_v1v3_csv', BOM_DIR) - ctx.run() - rows, header, info = ctx.load_csv(prj+'-bom.csv') - ref_column = header.index(REF_COLUMN_NAME) + VARIANTE_PRJ_INFO[1] = 't1_v3' + check_csv_info(info, VARIANTE_PRJ_INFO, [3, 20, 2, 1, 2]) + # V1,V3 + logging.debug("* `bla bla` variant") + rows, header, info = ctx.load_csv(prj+'-bom_bla_bla.csv') check_kibom_test_netlist(rows, ref_column, 1, ['R2', 'R3'], ['R1', 'R4']) + VARIANTE_PRJ_INFO[1] = 'bla bla' + check_csv_info(info, VARIANTE_PRJ_INFO, [3, 20, 2, 1, 2]) ctx.clean_up() -def test_int_bom_variant_t2_1(): +def test_int_bom_variant_t2(): prj = 'kibom-variant_2' - ctx = context.TestContextSCH('test_int_bom_variant_t2_1', prj, 'int_bom_var_production_csv', BOM_DIR) + ctx = context.TestContextSCH('test_int_bom_variant_t2', prj, 'int_bom_var_t2_csv', BOM_DIR) ctx.run() rows, header, info = ctx.load_csv(prj+'-bom.csv') ref_column = header.index(REF_COLUMN_NAME) + check_kibom_test_netlist(rows, ref_column, 1, ['C1', 'C2'], ['R1', 'R2']) + rows, header, info = ctx.load_csv(prj+'-bom_(production).csv') check_kibom_test_netlist(rows, ref_column, 2, ['C1'], ['R1', 'R2', 'C2']) - ctx.clean_up() - - -def test_int_bom_variant_t2_2(): - prj = 'kibom-variant_2' - ctx = context.TestContextSCH('test_int_bom_variant_t2_2', prj, 'int_bom_var_test_csv', BOM_DIR) - ctx.run() - rows, header, info = ctx.load_csv(prj+'-bom.csv') - ref_column = header.index(REF_COLUMN_NAME) + rows, header, info = ctx.load_csv(prj+'-bom_(test).csv') check_kibom_test_netlist(rows, ref_column, 2, ['R2'], ['R1', 'C1', 'C2']) ctx.clean_up() - - -def test_int_bom_variant_t2_3(): - prj = 'kibom-variant_2' - ctx = context.TestContextSCH('test_int_bom_variant_t2_3', prj, 'int_bom_simple_csv', BOM_DIR) - ctx.run() - rows, header, info = ctx.load_csv(prj+'-bom.csv') - ref_column = header.index(REF_COLUMN_NAME) - check_kibom_test_netlist(rows, ref_column, 1, ['C1', 'C2'], ['R1', 'R2']) - ctx.clean_up() diff --git a/tests/yaml_samples/int_bom_var_production_csv.kibot.yaml b/tests/yaml_samples/int_bom_var_production_csv.kibot.yaml deleted file mode 100644 index 08696d295..000000000 --- a/tests/yaml_samples/int_bom_var_production_csv.kibot.yaml +++ /dev/null @@ -1,12 +0,0 @@ -# Example KiBot config file -kibot: - version: 1 - -outputs: - - name: 'bom_internal' - comment: "Bill of Materials in CSV format" - type: bom - dir: BoM - options: - variant: production - diff --git a/tests/yaml_samples/int_bom_var_t1_csv.kibot.yaml b/tests/yaml_samples/int_bom_var_t1_csv.kibot.yaml new file mode 100644 index 000000000..aa9974d22 --- /dev/null +++ b/tests/yaml_samples/int_bom_var_t1_csv.kibot.yaml @@ -0,0 +1,63 @@ +# Example KiBot config file +kibot: + version: 1 + + +variants: + - name: 't1_v1' + comment: 'Test 1 Variant V1' + type: kibom + file_id: '_(V1)' + variant: V1 + + - name: 't1_v2' + comment: 'Test 1 Variant V2' + type: kibom + file_id: '_(V2)' + variant: V2 + + - name: 't1_v3' + comment: 'Test 1 Variant V3' + type: kibom + file_id: '_V3' + variant: V3 + + - name: 'bla bla' + comment: 'Test 1 Variant V1+V3' + type: kibom + file_id: '_bla_bla' + variant: ['V1', 'V3'] + +outputs: + - name: 'bom_internal' + comment: "Bill of Materials in CSV format" + type: bom + dir: BoM + + - name: 'bom_internal_v1' + comment: "Bill of Materials in CSV format for variant t1_v1" + type: bom + dir: BoM + options: + variant: t1_v1 + + - name: 'bom_internal_v2' + comment: "Bill of Materials in CSV format for variant t1_v2" + type: bom + dir: BoM + options: + variant: t1_v2 + + - name: 'bom_internal_v3' + comment: "Bill of Materials in CSV format for variant t1_v3" + type: bom + dir: BoM + options: + variant: t1_v3 + + - name: 'bom_internal_bla_bla' + comment: "Bill of Materials in CSV format for variant `bla bla`" + type: bom + dir: BoM + options: + variant: 'bla bla' diff --git a/tests/yaml_samples/int_bom_var_t2_csv.kibot.yaml b/tests/yaml_samples/int_bom_var_t2_csv.kibot.yaml new file mode 100644 index 000000000..0c79bfbce --- /dev/null +++ b/tests/yaml_samples/int_bom_var_t2_csv.kibot.yaml @@ -0,0 +1,36 @@ +# Example KiBot config file +kibot: + version: 1 + +variants: + - name: 'production' + comment: 'Production variant' + type: kibom + file_id: '_(production)' + variant: production + + - name: 'test' + comment: 'Test variant' + type: kibom + file_id: '_(test)' + variant: test + +outputs: + - name: 'bom_internal' + comment: "Bill of Materials in CSV format" + type: bom + dir: BoM + + - name: 'bom_internal_production' + comment: "Bill of Materials in CSV format for production" + type: bom + dir: BoM + options: + variant: production + + - name: 'bom_internal_test' + comment: "Bill of Materials in CSV format for test" + type: bom + dir: BoM + options: + variant: test diff --git a/tests/yaml_samples/int_bom_var_test_csv.kibot.yaml b/tests/yaml_samples/int_bom_var_test_csv.kibot.yaml deleted file mode 100644 index 8f857ba73..000000000 --- a/tests/yaml_samples/int_bom_var_test_csv.kibot.yaml +++ /dev/null @@ -1,12 +0,0 @@ -# Example KiBot config file -kibot: - version: 1 - -outputs: - - name: 'bom_internal' - comment: "Bill of Materials in CSV format" - type: bom - dir: BoM - options: - variant: test - diff --git a/tests/yaml_samples/int_bom_var_v1_csv.kibot.yaml b/tests/yaml_samples/int_bom_var_v1_csv.kibot.yaml deleted file mode 100644 index 9a2cbf12e..000000000 --- a/tests/yaml_samples/int_bom_var_v1_csv.kibot.yaml +++ /dev/null @@ -1,12 +0,0 @@ -# Example KiBot config file -kibot: - version: 1 - -outputs: - - name: 'bom_internal' - comment: "Bill of Materials in CSV format" - type: bom - dir: BoM - options: - variant: V1 - diff --git a/tests/yaml_samples/int_bom_var_v1v3_csv.kibot.yaml b/tests/yaml_samples/int_bom_var_v1v3_csv.kibot.yaml deleted file mode 100644 index f1b0b8af7..000000000 --- a/tests/yaml_samples/int_bom_var_v1v3_csv.kibot.yaml +++ /dev/null @@ -1,12 +0,0 @@ -# Example KiBot config file -kibot: - version: 1 - -outputs: - - name: 'bom_internal' - comment: "Bill of Materials in CSV format" - type: bom - dir: BoM - options: - variant: ['V1', 'V3'] - diff --git a/tests/yaml_samples/int_bom_var_v2_csv.kibot.yaml b/tests/yaml_samples/int_bom_var_v2_csv.kibot.yaml deleted file mode 100644 index 1c7396822..000000000 --- a/tests/yaml_samples/int_bom_var_v2_csv.kibot.yaml +++ /dev/null @@ -1,12 +0,0 @@ -# Example KiBot config file -kibot: - version: 1 - -outputs: - - name: 'bom_internal' - comment: "Bill of Materials in CSV format" - type: bom - dir: BoM - options: - variant: V2 - diff --git a/tests/yaml_samples/int_bom_var_v3_csv.kibot.yaml b/tests/yaml_samples/int_bom_var_v3_csv.kibot.yaml deleted file mode 100644 index 707cbf58c..000000000 --- a/tests/yaml_samples/int_bom_var_v3_csv.kibot.yaml +++ /dev/null @@ -1,12 +0,0 @@ -# Example KiBot config file -kibot: - version: 1 - -outputs: - - name: 'bom_internal' - comment: "Bill of Materials in CSV format" - type: bom - dir: BoM - options: - variant: V3 - From cbf24200a578319ee253cd9a0d892a54d0e49f65 Mon Sep 17 00:00:00 2001 From: "Salvador E. Tropea" Date: Fri, 28 Aug 2020 16:50:02 -0300 Subject: [PATCH 05/21] Updated the list of file to exclude from Python cache. Also sorted the list. --- kibot/__main__.py | 47 +++++++++++++++++++++++++---------------------- 1 file changed, 25 insertions(+), 22 deletions(-) diff --git a/kibot/__main__.py b/kibot/__main__.py index 4940682ac..eca944473 100644 --- a/kibot/__main__.py +++ b/kibot/__main__.py @@ -72,35 +72,38 @@ from .docopt import docopt logger = None -has_macro = ['pre_filters', +has_macro = [ + 'layer', + 'drill_marks', + 'out_any_drill', 'out_any_layer', - 'out_svg_sch_print', - 'pre_erc', + 'out_base', + 'out_bom', + 'out_dxf', + 'out_excellon', 'out_gerb_drill', - 'drill_marks', - 'pre_update_xml', - 'out_pdf_sch_print', + 'out_gerber', 'out_hpgl', - 'out_dxf', - 'out_pdf_pcb_print', + 'out_ibom', + 'out_kibom', + 'out_pcbdraw', 'out_pdf', + 'out_pdf_pcb_print', + 'out_pdf_sch_print', + 'out_position', + 'out_ps', + 'out_step', 'out_svg', - 'out_pcbdraw', - 'pre_ignore_unconnected', + 'out_svg_sch_print', 'pre_check_zone_fills', - 'out_gerber', - 'out_any_drill', - 'out_step', - 'out_ps', 'pre_drc', - 'out_excellon', - 'out_bom', - 'out_base', - 'out_ibom', - 'out_kibom', - 'out_position', - 'layer', - ] + 'pre_erc', + 'pre_filters', + 'pre_ignore_unconnected', + 'pre_update_xml', + 'var_base', + 'var_kibom', + ] def list_pre_and_outs(logger, outputs): From 5ecb253525bafd6c0a418cfa3a8e251dd7bd63cb Mon Sep 17 00:00:00 2001 From: "Salvador E. Tropea" Date: Fri, 28 Aug 2020 16:50:41 -0300 Subject: [PATCH 06/21] Updated the README and example. Mainly because of the addition of the variant to file names. --- README.md | 47 +++++++-------- docs/samples/generic_plot.kibot.yaml | 85 ++++++++++++++-------------- 2 files changed, 67 insertions(+), 65 deletions(-) diff --git a/README.md b/README.md index f8a5b6a04..bea4bd4ef 100644 --- a/README.md +++ b/README.md @@ -398,7 +398,8 @@ Next time you need this list just use an alias, like this: - *field*: Alias for column. - `regex`: [string=''] Regular expression to match. - *regexp*: Alias for regex. - - `fit_field`: [string='Config'] Field name used to determine if a particular part is to be fitted (also DNC and variants). + - `fit_field`: [string='Config'] Field name used to determine if a particular part is to be fitted (also DNC, not for variants). + This value is used only when no variants are specified. - `format`: [string=''] [HTML,CSV,TXT,TSV,XML,XLSX] format for the BoM. If empty defaults to CSV or a guess according to the options.. - `group_connectors`: [boolean=true] Connectors with the same footprints will be grouped together, independent of the name of the connector. @@ -433,11 +434,11 @@ Next time you need this list just use an alias, like this: - `normalize_locale`: [boolean=false] When normalizing values use the locale decimal point. - `normalize_values`: [boolean=false] Try to normalize the R, L and C values, producing uniform units and prefixes. - `number`: [number=1] Number of boards to build (components multiplier). - - `output`: [string='%f-%i.%x'] filename for the output (%i=bom). Affected by global options. + - `output`: [string='%f-%i%v.%x'] filename for the output (%i=bom). Affected by global options. - `test_regex`: [boolean=true] Each component group will be tested against a number of regular-expressions (see `include_only` and `exclude_any`). - `use_alt`: [boolean=false] Print grouped references in the alternate compressed style eg: R1-R7,R18. - - `variant`: [string|list(string)=''] Board variant(s), used to determine which components + - `variant`: [string=''] Board variant(s), used to determine which components are output to the BoM.. - `xlsx`: [dict] Options for the XLSX format. * Valid keys: @@ -474,7 +475,7 @@ Next time you need this list just use an alias, like this: - `exclude_pads_from_silkscreen`: [boolean=false] do not plot the component pads in the silk screen. - `force_plot_invisible_refs_vals`: [boolean=false] include references and values even when they are marked as invisible. - `metric_units`: [boolean=false] use mm instead of inches. - - `output`: [string='%f-%i.%x'] output file name, the default KiCad name if empty. Affected by global options. + - `output`: [string='%f-%i%v.%x'] output file name, the default KiCad name if empty. Affected by global options. - `plot_footprint_refs`: [boolean=true] include the footprint references. - `plot_footprint_values`: [boolean=true] include the footprint values. - `plot_sheet_reference`: [boolean=false] currently without effect. @@ -497,12 +498,12 @@ Next time you need this list just use an alias, like this: - `map`: [dict|string] [hpgl,ps,gerber,dxf,svg,pdf] format for a graphical drill map. Not generated unless a format is specified. * Valid keys: - - `output`: [string='%f-%i.%x'] name for the map file, KiCad defaults if empty (%i='PTH_drill_map'). Affected by global options. + - `output`: [string='%f-%i%v.%x'] name for the map file, KiCad defaults if empty (%i='PTH_drill_map'). Affected by global options. - `type`: [string='pdf'] [hpgl,ps,gerber,dxf,svg,pdf] format for a graphical drill map. - `metric_units`: [boolean=true] use metric units instead of inches. - `minimal_header`: [boolean=false] use a minimal header in the file. - `mirror_y_axis`: [boolean=false] invert the Y axis. - - `output`: [string='%f-%i.%x'] name for the drill file, KiCad defaults if empty (%i='PTH_drill'). Affected by global options. + - `output`: [string='%f-%i%v.%x'] name for the drill file, KiCad defaults if empty (%i='PTH_drill'). Affected by global options. - `pth_and_npth_single_file`: [boolean=true] generate one file for both, plated holes and non-plated holes, instead of two separated files. - `report`: [dict|string] name of the drill report. Not generated unless a name is specified. * Valid keys: @@ -524,9 +525,9 @@ Next time you need this list just use an alias, like this: - `map`: [dict|string] [hpgl,ps,gerber,dxf,svg,pdf] format for a graphical drill map. Not generated unless a format is specified. * Valid keys: - - `output`: [string='%f-%i.%x'] name for the map file, KiCad defaults if empty (%i='PTH_drill_map'). Affected by global options. + - `output`: [string='%f-%i%v.%x'] name for the map file, KiCad defaults if empty (%i='PTH_drill_map'). Affected by global options. - `type`: [string='pdf'] [hpgl,ps,gerber,dxf,svg,pdf] format for a graphical drill map. - - `output`: [string='%f-%i.%x'] name for the drill file, KiCad defaults if empty (%i='PTH_drill'). Affected by global options. + - `output`: [string='%f-%i%v.%x'] name for the drill file, KiCad defaults if empty (%i='PTH_drill'). Affected by global options. - `report`: [dict|string] name of the drill report. Not generated unless a name is specified. * Valid keys: - `filename`: [string=''] name of the drill report. Not generated unless a name is specified. @@ -554,10 +555,10 @@ Next time you need this list just use an alias, like this: - `exclude_edge_layer`: [boolean=true] do not include the PCB edge layer. - `exclude_pads_from_silkscreen`: [boolean=false] do not plot the component pads in the silk screen. - `force_plot_invisible_refs_vals`: [boolean=false] include references and values even when they are marked as invisible. - - `gerber_job_file`: [string='%f-%i.%x'] name for the gerber job file (%i='job', %x='gbrjob'). + - `gerber_job_file`: [string='%f-%i%v.%x'] name for the gerber job file (%i='job', %x='gbrjob'). Affected by global options. - `gerber_precision`: [number=4.6] this the gerber coordinate format, can be 4.5 or 4.6. - `line_width`: [number=0.1] [0.02,2] line_width for objects without width [mm]. - - `output`: [string='%f-%i.%x'] output file name, the default KiCad name if empty. Affected by global options. + - `output`: [string='%f-%i%v.%x'] output file name, the default KiCad name if empty. Affected by global options. - `plot_footprint_refs`: [boolean=true] include the footprint references. - `plot_footprint_values`: [boolean=true] include the footprint values. - `plot_sheet_reference`: [boolean=false] currently without effect. @@ -589,7 +590,7 @@ Next time you need this list just use an alias, like this: - `exclude_pads_from_silkscreen`: [boolean=false] do not plot the component pads in the silk screen. - `force_plot_invisible_refs_vals`: [boolean=false] include references and values even when they are marked as invisible. - `mirror_plot`: [boolean=false] plot mirrored. - - `output`: [string='%f-%i.%x'] output file name, the default KiCad name if empty. Affected by global options. + - `output`: [string='%f-%i%v.%x'] output file name, the default KiCad name if empty. Affected by global options. - `pen_number`: [number=1] [1,16] pen number. - `pen_speed`: [number=20] [1,99] pen speed. - `pen_width`: [number=15] [0,100] pen diameter in MILS, useful to fill areas. However, it is in mm in HPGL files. @@ -640,7 +641,7 @@ Next time you need this list just use an alias, like this: - `no_blacklist_virtual`: [boolean=false] Do not blacklist virtual components. - `no_redraw_on_drag`: [boolean=false] Do not redraw pcb on drag by default. - `normalize_field_case`: [boolean=false] Normalize extra field name case. E.g. 'MPN' and 'mpn' will be considered the same field. - - `output`: [string='%f-%i.%x'] Filename for the output, use '' to use the IBoM filename (%i=ibom, %x=html). Affected by global options. + - `output`: [string='%f-%i%v.%x'] Filename for the output, use '' to use the IBoM filename (%i=ibom, %x=html). Affected by global options. - `show_fabrication`: [boolean=false] Show fabrication layer by default. - `sort_order`: [string='C,R,L,D,U,Y,X,F,SW,A,~,HS,CNN,J,P,NT,MH'] Default sort order for components. Must contain '~' once. - `variant_field`: [string=''] Name of the extra field that stores board variant for component. @@ -729,7 +730,7 @@ Next time you need this list just use an alias, like this: - `use_alt`: [boolean=false] Print grouped references in the alternate compressed style eg: R1-R7,R18. - `format`: [string='HTML'] [HTML,CSV,XML,XLSX] format for the BoM. - `number`: [number=1] Number of boards to build (components multiplier). - - `output`: [string='%f-%i.%x'] filename for the output (%i=bom). Affected by global options. + - `output`: [string='%f-%i%v.%x'] filename for the output (%i=bom). Affected by global options. - `separator`: [string=','] CSV Separator. - `variant`: [string=''] Board variant(s), used to determine which components are output to the BoM. To specify multiple variants, @@ -754,7 +755,7 @@ Next time you need this list just use an alias, like this: - `libs`: [list(string)=[]] list of libraries. - `mirror`: [boolean=false] mirror the board. - `no_drillholes`: [boolean=false] do not make holes transparent. - - `output`: [string='%f-%i.%x'] name for the generated file. Affected by global options. + - `output`: [string='%f-%i%v.%x'] name for the generated file. Affected by global options. - `placeholder`: [boolean=false] show placeholder for missing components. - `remap`: [dict|None] replacements for PCB references using components (lib:component). - `show_components`: [string|list(string)=none] [none,all] list of components to draw, can be also a string for none or all. @@ -802,12 +803,12 @@ Next time you need this list just use an alias, like this: - `line_width`: [number=0.1] [0.02,2] for objects without width [mm]. - `mirror_plot`: [boolean=false] plot mirrored. - `negative_plot`: [boolean=false] invert black and white. - - `output`: [string='%f-%i.%x'] output file name, the default KiCad name if empty. Affected by global options. + - `output`: [string='%f-%i%v.%x'] output file name, the default KiCad name if empty. Affected by global options. - `plot_footprint_refs`: [boolean=true] include the footprint references. - `plot_footprint_values`: [boolean=true] include the footprint values. - `plot_sheet_reference`: [boolean=false] currently without effect. - `tent_vias`: [boolean=true] cover the vias. - - `output`: [string='%f-%i.%x'] output file name, the default KiCad name if empty. Affected by global options. + - `output`: [string='%f-%i%v.%x'] output file name, the default KiCad name if empty. Affected by global options. - `plot_footprint_refs`: [boolean=true] include the footprint references. - `plot_footprint_values`: [boolean=true] include the footprint values. - `plot_sheet_reference`: [boolean=false] currently without effect. @@ -830,7 +831,7 @@ Next time you need this list just use an alias, like this: - `name`: [string=''] Used to identify this particular output definition. - `options`: [dict] Options for the `pdf_pcb_print` output. * Valid keys: - - `output`: [string='%f-%i.%x'] filename for the output PDF (%i=layers, %x=pdf). Affected by global options. + - `output`: [string='%f-%i%v.%x'] filename for the output PDF (%i=layers, %x=pdf). Affected by global options. - *output_name*: Alias for output. * PDF Schematic Print (Portable Document Format) @@ -844,7 +845,7 @@ Next time you need this list just use an alias, like this: - `name`: [string=''] Used to identify this particular output definition. - `options`: [dict] Options for the `pdf_sch_print` output. * Valid keys: - - `output`: [string='%f-%i.%x'] filename for the output PDF (%i=schematic %x=pdf). Affected by global options. + - `output`: [string='%f-%i%v.%x'] filename for the output PDF (%i=schematic %x=pdf). Affected by global options. * Pick & place * Type: `position` @@ -858,7 +859,7 @@ Next time you need this list just use an alias, like this: * Valid keys: - `format`: [string='ASCII'] [ASCII,CSV] format for the position file. - `only_smd`: [boolean=true] only include the surface mount components. - - `output`: [string='%f-%i.%x'] output file name (%i='top_pos'|'bottom_pos'|'both_pos', %x='pos'|'csv'). Affected by global options. + - `output`: [string='%f-%i%v.%x'] output file name (%i='top_pos'|'bottom_pos'|'both_pos', %x='pos'|'csv'). Affected by global options. - `separate_files_for_front_and_back`: [boolean=true] generate two separated files, one for the top and another for the bottom. - `units`: [string='millimeters'] [millimeters,inches] units used for the positions. @@ -886,7 +887,7 @@ Next time you need this list just use an alias, like this: - `line_width`: [number=0.15] [0.02,2] for objects without width [mm]. - `mirror_plot`: [boolean=false] plot mirrored. - `negative_plot`: [boolean=false] invert black and white. - - `output`: [string='%f-%i.%x'] output file name, the default KiCad name if empty. Affected by global options. + - `output`: [string='%f-%i%v.%x'] output file name, the default KiCad name if empty. Affected by global options. - `plot_footprint_refs`: [boolean=true] include the footprint references. - `plot_footprint_values`: [boolean=true] include the footprint values. - `plot_sheet_reference`: [boolean=false] currently without effect. @@ -915,7 +916,7 @@ Next time you need this list just use an alias, like this: - `origin`: [string='grid'] determines the coordinates origin. Using grid the coordinates are the same as you have in the design sheet. The drill option uses the auxiliar reference defined by the user. You can define any other origin using the format 'X,Y', i.e. '3.2,-10'. - - `output`: [string='%f-%i.%x'] name for the generated STEP file (%i='3D' %x='step'). Affected by global options. + - `output`: [string='%f-%i%v.%x'] name for the generated STEP file (%i='3D' %x='step'). Affected by global options. * SVG (Scalable Vector Graphics) * Type: `svg` @@ -941,7 +942,7 @@ Next time you need this list just use an alias, like this: - `line_width`: [number=0.25] [0.02,2] for objects without width [mm]. - `mirror_plot`: [boolean=false] plot mirrored. - `negative_plot`: [boolean=false] invert black and white. - - `output`: [string='%f-%i.%x'] output file name, the default KiCad name if empty. Affected by global options. + - `output`: [string='%f-%i%v.%x'] output file name, the default KiCad name if empty. Affected by global options. - `plot_footprint_refs`: [boolean=true] include the footprint references. - `plot_footprint_values`: [boolean=true] include the footprint values. - `plot_sheet_reference`: [boolean=false] currently without effect. @@ -957,7 +958,7 @@ Next time you need this list just use an alias, like this: - `name`: [string=''] Used to identify this particular output definition. - `options`: [dict] Options for the `svg_sch_print` output. * Valid keys: - - `output`: [string='%f-%i.%x'] filename for the output SVG (%i=schematic %x=svg). Affected by global options. + - `output`: [string='%f-%i%v.%x'] filename for the output SVG (%i=schematic %x=svg). Affected by global options. ## Using KiBot diff --git a/docs/samples/generic_plot.kibot.yaml b/docs/samples/generic_plot.kibot.yaml index 45395df92..3f9ced520 100644 --- a/docs/samples/generic_plot.kibot.yaml +++ b/docs/samples/generic_plot.kibot.yaml @@ -92,7 +92,8 @@ outputs: # [string=''] Regular expression to match regex: '' # `regexp` is an alias for `regex` - # [string='Config'] Field name used to determine if a particular part is to be fitted (also DNC and variants) + # [string='Config'] Field name used to determine if a particular part is to be fitted (also DNC, not for variants). + # This value is used only when no variants are specified fit_field: 'Config' # [string=''] [HTML,CSV,TXT,TSV,XML,XLSX] format for the BoM. # If empty defaults to CSV or a guess according to the options. @@ -148,14 +149,14 @@ outputs: normalize_values: false # [number=1] Number of boards to build (components multiplier) number: 1 - # [string='%f-%i.%x'] filename for the output (%i=bom). Affected by global options - output: '%f-%i.%x' + # [string='%f-%i%v.%x'] filename for the output (%i=bom). Affected by global options + output: '%f-%i%v.%x' # [boolean=true] Each component group will be tested against a number of regular-expressions # (see `include_only` and `exclude_any`) test_regex: true # [boolean=false] Print grouped references in the alternate compressed style eg: R1-R7,R18 use_alt: false - # [string|list(string)=''] Board variant(s), used to determine which components + # [string=''] Board variant(s), used to determine which components # are output to the BoM. variant: '' # [dict] Options for the XLSX format @@ -200,8 +201,8 @@ outputs: force_plot_invisible_refs_vals: false # [boolean=false] use mm instead of inches metric_units: false - # [string='%f-%i.%x'] output file name, the default KiCad name if empty. Affected by global options - output: '%f-%i.%x' + # [string='%f-%i%v.%x'] output file name, the default KiCad name if empty. Affected by global options + output: '%f-%i%v.%x' # [boolean=true] include the footprint references plot_footprint_refs: true # [boolean=true] include the footprint values @@ -229,8 +230,8 @@ outputs: # [dict|string] [hpgl,ps,gerber,dxf,svg,pdf] format for a graphical drill map. # Not generated unless a format is specified map: - # [string='%f-%i.%x'] name for the map file, KiCad defaults if empty (%i='PTH_drill_map'). Affected by global options - output: '%f-%i.%x' + # [string='%f-%i%v.%x'] name for the map file, KiCad defaults if empty (%i='PTH_drill_map'). Affected by global options + output: '%f-%i%v.%x' # [string='pdf'] [hpgl,ps,gerber,dxf,svg,pdf] format for a graphical drill map type: 'pdf' # [boolean=true] use metric units instead of inches @@ -239,8 +240,8 @@ outputs: minimal_header: false # [boolean=false] invert the Y axis mirror_y_axis: false - # [string='%f-%i.%x'] name for the drill file, KiCad defaults if empty (%i='PTH_drill'). Affected by global options - output: '%f-%i.%x' + # [string='%f-%i%v.%x'] name for the drill file, KiCad defaults if empty (%i='PTH_drill'). Affected by global options + output: '%f-%i%v.%x' # [boolean=true] generate one file for both, plated holes and non-plated holes, instead of two separated files pth_and_npth_single_file: true # [dict|string] name of the drill report. Not generated unless a name is specified @@ -262,12 +263,12 @@ outputs: # [dict|string] [hpgl,ps,gerber,dxf,svg,pdf] format for a graphical drill map. # Not generated unless a format is specified map: - # [string='%f-%i.%x'] name for the map file, KiCad defaults if empty (%i='PTH_drill_map'). Affected by global options - output: '%f-%i.%x' + # [string='%f-%i%v.%x'] name for the map file, KiCad defaults if empty (%i='PTH_drill_map'). Affected by global options + output: '%f-%i%v.%x' # [string='pdf'] [hpgl,ps,gerber,dxf,svg,pdf] format for a graphical drill map type: 'pdf' - # [string='%f-%i.%x'] name for the drill file, KiCad defaults if empty (%i='PTH_drill'). Affected by global options - output: '%f-%i.%x' + # [string='%f-%i%v.%x'] name for the drill file, KiCad defaults if empty (%i='PTH_drill'). Affected by global options + output: '%f-%i%v.%x' # [dict|string] name of the drill report. Not generated unless a name is specified report: # [string=''] name of the drill report. Not generated unless a name is specified. @@ -292,14 +293,14 @@ outputs: exclude_pads_from_silkscreen: false # [boolean=false] include references and values even when they are marked as invisible force_plot_invisible_refs_vals: false - # [string='%f-%i.%x'] name for the gerber job file (%i='job', %x='gbrjob') - gerber_job_file: '%f-%i.%x' + # [string='%f-%i%v.%x'] name for the gerber job file (%i='job', %x='gbrjob'). Affected by global options + gerber_job_file: '%f-%i%v.%x' # [number=4.6] this the gerber coordinate format, can be 4.5 or 4.6 gerber_precision: 4.6 # [number=0.1] [0.02,2] line_width for objects without width [mm] line_width: 0.1 - # [string='%f-%i.%x'] output file name, the default KiCad name if empty. Affected by global options - output: '%f-%i.%x' + # [string='%f-%i%v.%x'] output file name, the default KiCad name if empty. Affected by global options + output: '%f-%i%v.%x' # [boolean=true] include the footprint references plot_footprint_refs: true # [boolean=true] include the footprint values @@ -337,8 +338,8 @@ outputs: force_plot_invisible_refs_vals: false # [boolean=false] plot mirrored mirror_plot: false - # [string='%f-%i.%x'] output file name, the default KiCad name if empty. Affected by global options - output: '%f-%i.%x' + # [string='%f-%i%v.%x'] output file name, the default KiCad name if empty. Affected by global options + output: '%f-%i%v.%x' # [number=1] [1,16] pen number pen_number: 1 # [number=20] [1,99] pen speed @@ -415,8 +416,8 @@ outputs: no_redraw_on_drag: false # [boolean=false] Normalize extra field name case. E.g. 'MPN' and 'mpn' will be considered the same field normalize_field_case: false - # [string='%f-%i.%x'] Filename for the output, use '' to use the IBoM filename (%i=ibom, %x=html). Affected by global options - output: '%f-%i.%x' + # [string='%f-%i%v.%x'] Filename for the output, use '' to use the IBoM filename (%i=ibom, %x=html). Affected by global options + output: '%f-%i%v.%x' # [boolean=false] Show fabrication layer by default show_fabrication: false # [string='C,R,L,D,U,Y,X,F,SW,A,~,HS,CNN,J,P,NT,MH'] Default sort order for components. Must contain '~' once @@ -530,8 +531,8 @@ outputs: format: 'HTML' # [number=1] Number of boards to build (components multiplier) number: 1 - # [string='%f-%i.%x'] filename for the output (%i=bom). Affected by global options - output: '%f-%i.%x' + # [string='%f-%i%v.%x'] filename for the output (%i=bom). Affected by global options + output: '%f-%i%v.%x' # [string=','] CSV Separator separator: ',' # [string=''] Board variant(s), used to determine which components @@ -562,8 +563,8 @@ outputs: mirror: false # [boolean=false] do not make holes transparent no_drillholes: false - # [string='%f-%i.%x'] name for the generated file. Affected by global options - output: '%f-%i.%x' + # [string='%f-%i%v.%x'] name for the generated file. Affected by global options + output: '%f-%i%v.%x' # [boolean=false] show placeholder for missing components placeholder: false # [dict|None] replacements for PCB references using components (lib:component) @@ -620,8 +621,8 @@ outputs: mirror_plot: false # [boolean=false] invert black and white negative_plot: false - # [string='%f-%i.%x'] output file name, the default KiCad name if empty. Affected by global options - output: '%f-%i.%x' + # [string='%f-%i%v.%x'] output file name, the default KiCad name if empty. Affected by global options + output: '%f-%i%v.%x' # [boolean=true] include the footprint references plot_footprint_refs: true # [boolean=true] include the footprint values @@ -640,8 +641,8 @@ outputs: type: 'pdf_pcb_print' dir: 'Example/pdf_pcb_print_dir' options: - # [string='%f-%i.%x'] filename for the output PDF (%i=layers, %x=pdf). Affected by global options - output: '%f-%i.%x' + # [string='%f-%i%v.%x'] filename for the output PDF (%i=layers, %x=pdf). Affected by global options + output: '%f-%i%v.%x' # `output_name` is an alias for `output` layers: all @@ -653,8 +654,8 @@ outputs: type: 'pdf_sch_print' dir: 'Example/pdf_sch_print_dir' options: - # [string='%f-%i.%x'] filename for the output PDF (%i=schematic %x=pdf). Affected by global options - output: '%f-%i.%x' + # [string='%f-%i%v.%x'] filename for the output PDF (%i=schematic %x=pdf). Affected by global options + output: '%f-%i%v.%x' # Pick & place: # This output is what you get from the 'File/Fabrication output/Footprint poistion (.pos) file' menu in pcbnew. @@ -667,8 +668,8 @@ outputs: format: 'ASCII' # [boolean=true] only include the surface mount components only_smd: true - # [string='%f-%i.%x'] output file name (%i='top_pos'|'bottom_pos'|'both_pos', %x='pos'|'csv'). Affected by global options - output: '%f-%i.%x' + # [string='%f-%i%v.%x'] output file name (%i='top_pos'|'bottom_pos'|'both_pos', %x='pos'|'csv'). Affected by global options + output: '%f-%i%v.%x' # [boolean=true] generate two separated files, one for the top and another for the bottom separate_files_for_front_and_back: true # [string='millimeters'] [millimeters,inches] units used for the positions @@ -697,8 +698,8 @@ outputs: mirror_plot: false # [boolean=false] invert black and white negative_plot: false - # [string='%f-%i.%x'] output file name, the default KiCad name if empty. Affected by global options - output: '%f-%i.%x' + # [string='%f-%i%v.%x'] output file name, the default KiCad name if empty. Affected by global options + output: '%f-%i%v.%x' # [boolean=true] include the footprint references plot_footprint_refs: true # [boolean=true] include the footprint values @@ -738,8 +739,8 @@ outputs: # The drill option uses the auxiliar reference defined by the user. # You can define any other origin using the format 'X,Y', i.e. '3.2,-10' origin: 'grid' - # [string='%f-%i.%x'] name for the generated STEP file (%i='3D' %x='step'). Affected by global options - output: '%f-%i.%x' + # [string='%f-%i%v.%x'] name for the generated STEP file (%i='3D' %x='step'). Affected by global options + output: '%f-%i%v.%x' # SVG (Scalable Vector Graphics): # Unlike bitmaps SVG drawings can be scaled without losing resolution. @@ -763,8 +764,8 @@ outputs: mirror_plot: false # [boolean=false] invert black and white negative_plot: false - # [string='%f-%i.%x'] output file name, the default KiCad name if empty. Affected by global options - output: '%f-%i.%x' + # [string='%f-%i%v.%x'] output file name, the default KiCad name if empty. Affected by global options + output: '%f-%i%v.%x' # [boolean=true] include the footprint references plot_footprint_refs: true # [boolean=true] include the footprint values @@ -782,6 +783,6 @@ outputs: type: 'svg_sch_print' dir: 'Example/svg_sch_print_dir' options: - # [string='%f-%i.%x'] filename for the output SVG (%i=schematic %x=svg). Affected by global options - output: '%f-%i.%x' + # [string='%f-%i%v.%x'] filename for the output SVG (%i=schematic %x=svg). Affected by global options + output: '%f-%i%v.%x' From c227b10f86c89e527a7010583fdcbb2bd651d46b Mon Sep 17 00:00:00 2001 From: "Salvador E. Tropea" Date: Fri, 28 Aug 2020 20:11:07 -0300 Subject: [PATCH 07/21] Interpret as error using unknown fields in variants. --- kibot/var_base.py | 1 + 1 file changed, 1 insertion(+) diff --git a/kibot/var_base.py b/kibot/var_base.py index 88bc2a1db..43bda00ff 100644 --- a/kibot/var_base.py +++ b/kibot/var_base.py @@ -10,6 +10,7 @@ class BaseVariant(RegVariant): def __init__(self): super().__init__() + self._unkown_is_error = True with document: self.name = '' """ Used to identify this particular variant definition """ From 249caad348ffed32c7214f1a8daa64974e5f5d0f Mon Sep 17 00:00:00 2001 From: "Salvador E. Tropea" Date: Fri, 28 Aug 2020 20:12:21 -0300 Subject: [PATCH 08/21] Implemented the IBoM variants styles. The core functionality is now tested in the internal BoM. The IBoM output needs adjust to allow using the variants with it. --- kibot/out_ibom.py | 29 +- kibot/var_ibom.py | 97 +++++++ tests/board_samples/kibom-variant_3.kicad_pcb | 268 ++++++++++++++++++ tests/board_samples/kibom-variant_3.sch | 68 +++++ tests/board_samples/kibom-variant_3.xml | 130 +++++++++ tests/test_plot/test_int_bom.py | 16 ++ .../int_bom_var_t2i_csv.kibot.yaml | 56 ++++ 7 files changed, 650 insertions(+), 14 deletions(-) create mode 100644 kibot/var_ibom.py create mode 100644 tests/board_samples/kibom-variant_3.kicad_pcb create mode 100644 tests/board_samples/kibom-variant_3.sch create mode 100644 tests/board_samples/kibom-variant_3.xml create mode 100644 tests/yaml_samples/int_bom_var_t2i_csv.kibot.yaml diff --git a/kibot/out_ibom.py b/kibot/out_ibom.py index 08f747648..17383c3c8 100644 --- a/kibot/out_ibom.py +++ b/kibot/out_ibom.py @@ -52,27 +52,28 @@ def __init__(self): """ Include netlist information in output. """ self.sort_order = 'C,R,L,D,U,Y,X,F,SW,A,~,HS,CNN,J,P,NT,MH' """ Default sort order for components. Must contain '~' once """ - self.blacklist = '' - """ List of comma separated blacklisted components or prefixes with *. E.g. 'X1,MH*' """ + # self.blacklist = '' + # """ List of comma separated blacklisted components or prefixes with *. E.g. 'X1,MH*' """ self.no_blacklist_virtual = False """ Do not blacklist virtual components """ - self.blacklist_empty_val = False - """ Blacklist components with empty value """ + # self.blacklist_empty_val = False + # """ Blacklist components with empty value """ self.netlist_file = '' """ Path to netlist or xml file """ self.extra_fields = '' - """ Comma separated list of extra fields to pull from netlist or xml file """ + """ Comma separated list of extra fields to pull from netlist or xml file. + These are extra columns in the BoM """ self.normalize_field_case = False """ Normalize extra field name case. E.g. 'MPN' and 'mpn' will be considered the same field """ - self.variant_field = '' - """ Name of the extra field that stores board variant for component """ - self.variants_whitelist = '' - """ List of board variants to include in the BOM """ - self.variants_blacklist = '' - """ List of board variants to exclude from the BOM """ - self.dnp_field = '' - """ Name of the extra field that indicates do not populate status. Components with this field not empty will be - blacklisted """ + # self.variant_field = '' + # """ Name of the extra field that stores board variant for component """ + # self.variants_whitelist = '' + # """ List of board variants to include in the BOM """ + # self.variants_blacklist = '' + # """ List of board variants to exclude from the BOM """ + # self.dnp_field = '' + # """ Name of the extra field that indicates do not populate status. Components with this field not empty will be + # blacklisted """ super().__init__() def run(self, output_dir, board): diff --git a/kibot/var_ibom.py b/kibot/var_ibom.py new file mode 100644 index 000000000..2e1673f83 --- /dev/null +++ b/kibot/var_ibom.py @@ -0,0 +1,97 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020 Salvador E. Tropea +# Copyright (c) 2020 Instituto Nacional de Tecnología Industrial +# License: GPL-3.0 +# Project: KiBot (formerly KiPlot) +""" +Implements the IBoM variants mechanism. +""" +from .optionable import Optionable +from .gs import GS +from .macros import macros, document, variant_class # noqa: F401 +from . import log + +logger = log.get_logger(__name__) + + +@variant_class +class IBoM(BaseVariant): # noqa: F821 + """ IBoM variant style + The Config field (configurable) contains a value. + If this value matches with a value in the whitelist is included. + If this value matches with a value in the blacklist is excluded. """ + def __init__(self): + super().__init__() + with document: + self.variant_field = 'Config' + """ Name of the field that stores board variant for component """ + self.variants_blacklist = Optionable + """ [string|list(string)=''] List of board variants to exclude from the BOM """ + self.variants_whitelist = Optionable + """ [string|list(string)=''] List of board variants to include in the BOM """ + self.blacklist = Optionable + """ [string|list(string)=''] List of comma separated blacklisted components or prefixes with *. E.g. 'X1,MH*' """ + self.blacklist_empty_val = False + """ Blacklist components with empty value """ + self.dnp_field = '' + """ Name of the extra field that indicates do not populate status. + Components with this field not empty will be blacklisted """ + + @staticmethod + def _force_list(val): + if isinstance(val, type): + # Not used + val = [] + elif isinstance(val, str): + # A string + if val: + val = [v.strip() for v in val.split(',')] + else: + # Empty string + val = [] + return val + + def config(self): + super().config() + self.variants_blacklist = self._force_list(self.variants_blacklist) + self.variants_whitelist = self._force_list(self.variants_whitelist) + self.blacklist = self._force_list(self.blacklist) + + def skip_component(self, c): + """ Skip blacklisted components. + This is what IBoM does internally """ + if c.ref in self.blacklist: + return True + if c.ref_prefix + '*' in self.blacklist: + return True + # Remove components with empty value + if self.blacklist_empty_val and c.value in ['', '~']: + return True + # Skip virtual components if needed + # TODO: We currently lack this information + # if config.blacklist_virtual and m.attr == 'Virtual': + # return True + # Skip components with dnp field not empty + if self.dnp_field and c.get_field_value(self.dnp_field): + return True + # Apply variants white/black lists + if self.variant_field: + ref_variant = c.get_field_value(self.variant_field).lower() + # skip components with wrong variant field + if self.variants_whitelist and ref_variant not in self.variants_whitelist: + return True + if self.variants_blacklist and ref_variant and ref_variant in self.variants_blacklist: + return True + return False + + def filter(self, comps): + logger.debug("Applying IBoM style filter `{}`".format(self.name)) + # Make black/white lists case insensitive + self.variants_whitelist = [v.lower() for v in self.variants_whitelist] + self.variants_blacklist = [v.lower() for v in self.variants_blacklist] + # Apply to all the components + for c in comps: + c.fitted = not self.skip_component(c) + c.fixed = False + if not c.fitted and GS.debug_level > 2: + logger.debug('ref: {} value: {} -> False'.format(c.ref, c.value)) diff --git a/tests/board_samples/kibom-variant_3.kicad_pcb b/tests/board_samples/kibom-variant_3.kicad_pcb new file mode 100644 index 000000000..7cb862b7f --- /dev/null +++ b/tests/board_samples/kibom-variant_3.kicad_pcb @@ -0,0 +1,268 @@ +(kicad_pcb (version 20171130) (host pcbnew 5.1.6+dfsg1-1) + + (general + (thickness 1.6) + (drawings 4) + (tracks 0) + (zones 0) + (modules 4) + (nets 9) + ) + + (page A4) + (layers + (0 F.Cu signal) + (31 B.Cu signal) + (32 B.Adhes user) + (33 F.Adhes user) + (34 B.Paste user) + (35 F.Paste user) + (36 B.SilkS user) + (37 F.SilkS user) + (38 B.Mask user) + (39 F.Mask user) + (40 Dwgs.User user) + (41 Cmts.User user) + (42 Eco1.User user) + (43 Eco2.User user) + (44 Edge.Cuts user) + (45 Margin user) + (46 B.CrtYd user) + (47 F.CrtYd user) + (48 B.Fab user) + (49 F.Fab user) + ) + + (setup + (last_trace_width 0.25) + (trace_clearance 0.2) + (zone_clearance 0.508) + (zone_45_only no) + (trace_min 0.2) + (via_size 0.8) + (via_drill 0.4) + (via_min_size 0.4) + (via_min_drill 0.3) + (uvia_size 0.3) + (uvia_drill 0.1) + (uvias_allowed no) + (uvia_min_size 0.2) + (uvia_min_drill 0.1) + (edge_width 0.1) + (segment_width 0.2) + (pcb_text_width 0.3) + (pcb_text_size 1.5 1.5) + (mod_edge_width 0.15) + (mod_text_size 1 1) + (mod_text_width 0.15) + (pad_size 1.524 1.524) + (pad_drill 0.762) + (pad_to_mask_clearance 0) + (aux_axis_origin 139.89 89.63) + (visible_elements FFFFFF7F) + (pcbplotparams + (layerselection 0x010fc_ffffffff) + (usegerberextensions false) + (usegerberattributes true) + (usegerberadvancedattributes true) + (creategerberjobfile true) + (excludeedgelayer true) + (linewidth 0.100000) + (plotframeref false) + (viasonmask false) + (mode 1) + (useauxorigin false) + (hpglpennumber 1) + (hpglpenspeed 20) + (hpglpendiameter 15.000000) + (psnegative false) + (psa4output false) + (plotreference true) + (plotvalue true) + (plotinvisibletext false) + (padsonsilk false) + (subtractmaskfromsilk false) + (outputformat 1) + (mirror false) + (drillshape 1) + (scaleselection 1) + (outputdirectory "")) + ) + + (net 0 "") + (net 1 "Net-(C1-Pad2)") + (net 2 "Net-(C1-Pad1)") + (net 3 "Net-(C2-Pad2)") + (net 4 "Net-(C2-Pad1)") + (net 5 "Net-(R1-Pad2)") + (net 6 "Net-(R1-Pad1)") + (net 7 "Net-(R2-Pad2)") + (net 8 "Net-(R2-Pad1)") + + (net_class Default "Esta es la clase de red por defecto." + (clearance 0.2) + (trace_width 0.25) + (via_dia 0.8) + (via_drill 0.4) + (uvia_dia 0.3) + (uvia_drill 0.1) + (add_net "Net-(C1-Pad1)") + (add_net "Net-(C1-Pad2)") + (add_net "Net-(C2-Pad1)") + (add_net "Net-(C2-Pad2)") + (add_net "Net-(R1-Pad1)") + (add_net "Net-(R1-Pad2)") + (add_net "Net-(R2-Pad1)") + (add_net "Net-(R2-Pad2)") + ) + + (module Resistor_SMD:R_0805_2012Metric (layer F.Cu) (tedit 5B36C52B) (tstamp 5F496A8B) + (at 141.57 90.58) + (descr "Resistor SMD 0805 (2012 Metric), square (rectangular) end terminal, IPC_7351 nominal, (Body size source: https://docs.google.com/spreadsheets/d/1BsfQQcO9C6DZCsRaXUlFlo91Tg2WpOkGARC1WS5S8t0/edit?usp=sharing), generated with kicad-footprint-generator") + (tags resistor) + (path /5F43D4BB) + (attr smd) + (fp_text reference R2 (at 0 -1.65) (layer F.SilkS) + (effects (font (size 1 1) (thickness 0.15))) + ) + (fp_text value 1000 (at 0 1.65) (layer F.Fab) + (effects (font (size 1 1) (thickness 0.15))) + ) + (fp_text user %R (at 0 0) (layer F.Fab) + (effects (font (size 0.5 0.5) (thickness 0.08))) + ) + (fp_line (start -1 0.6) (end -1 -0.6) (layer F.Fab) (width 0.1)) + (fp_line (start -1 -0.6) (end 1 -0.6) (layer F.Fab) (width 0.1)) + (fp_line (start 1 -0.6) (end 1 0.6) (layer F.Fab) (width 0.1)) + (fp_line (start 1 0.6) (end -1 0.6) (layer F.Fab) (width 0.1)) + (fp_line (start -0.258578 -0.71) (end 0.258578 -0.71) (layer F.SilkS) (width 0.12)) + (fp_line (start -0.258578 0.71) (end 0.258578 0.71) (layer F.SilkS) (width 0.12)) + (fp_line (start -1.68 0.95) (end -1.68 -0.95) (layer F.CrtYd) (width 0.05)) + (fp_line (start -1.68 -0.95) (end 1.68 -0.95) (layer F.CrtYd) (width 0.05)) + (fp_line (start 1.68 -0.95) (end 1.68 0.95) (layer F.CrtYd) (width 0.05)) + (fp_line (start 1.68 0.95) (end -1.68 0.95) (layer F.CrtYd) (width 0.05)) + (pad 2 smd roundrect (at 0.9375 0) (size 0.975 1.4) (layers F.Cu F.Paste F.Mask) (roundrect_rratio 0.25) + (net 7 "Net-(R2-Pad2)")) + (pad 1 smd roundrect (at -0.9375 0) (size 0.975 1.4) (layers F.Cu F.Paste F.Mask) (roundrect_rratio 0.25) + (net 8 "Net-(R2-Pad1)")) + (model ${KISYS3DMOD}/Resistor_SMD.3dshapes/R_0805_2012Metric.wrl + (at (xyz 0 0 0)) + (scale (xyz 1 1 1)) + (rotate (xyz 0 0 0)) + ) + ) + + (module Resistor_SMD:R_0805_2012Metric (layer F.Cu) (tedit 5B36C52B) (tstamp 5F496A7A) + (at 141.57 87.63) + (descr "Resistor SMD 0805 (2012 Metric), square (rectangular) end terminal, IPC_7351 nominal, (Body size source: https://docs.google.com/spreadsheets/d/1BsfQQcO9C6DZCsRaXUlFlo91Tg2WpOkGARC1WS5S8t0/edit?usp=sharing), generated with kicad-footprint-generator") + (tags resistor) + (path /5F43D144) + (attr smd) + (fp_text reference R1 (at 0 -1.65) (layer F.SilkS) + (effects (font (size 1 1) (thickness 0.15))) + ) + (fp_text value 1k (at 0 1.65) (layer F.Fab) + (effects (font (size 1 1) (thickness 0.15))) + ) + (fp_text user %R (at 0 0) (layer F.Fab) + (effects (font (size 0.5 0.5) (thickness 0.08))) + ) + (fp_line (start -1 0.6) (end -1 -0.6) (layer F.Fab) (width 0.1)) + (fp_line (start -1 -0.6) (end 1 -0.6) (layer F.Fab) (width 0.1)) + (fp_line (start 1 -0.6) (end 1 0.6) (layer F.Fab) (width 0.1)) + (fp_line (start 1 0.6) (end -1 0.6) (layer F.Fab) (width 0.1)) + (fp_line (start -0.258578 -0.71) (end 0.258578 -0.71) (layer F.SilkS) (width 0.12)) + (fp_line (start -0.258578 0.71) (end 0.258578 0.71) (layer F.SilkS) (width 0.12)) + (fp_line (start -1.68 0.95) (end -1.68 -0.95) (layer F.CrtYd) (width 0.05)) + (fp_line (start -1.68 -0.95) (end 1.68 -0.95) (layer F.CrtYd) (width 0.05)) + (fp_line (start 1.68 -0.95) (end 1.68 0.95) (layer F.CrtYd) (width 0.05)) + (fp_line (start 1.68 0.95) (end -1.68 0.95) (layer F.CrtYd) (width 0.05)) + (pad 2 smd roundrect (at 0.9375 0) (size 0.975 1.4) (layers F.Cu F.Paste F.Mask) (roundrect_rratio 0.25) + (net 5 "Net-(R1-Pad2)")) + (pad 1 smd roundrect (at -0.9375 0) (size 0.975 1.4) (layers F.Cu F.Paste F.Mask) (roundrect_rratio 0.25) + (net 6 "Net-(R1-Pad1)")) + (model ${KISYS3DMOD}/Resistor_SMD.3dshapes/R_0805_2012Metric.wrl + (at (xyz 0 0 0)) + (scale (xyz 1 1 1)) + (rotate (xyz 0 0 0)) + ) + ) + + (module Capacitor_SMD:C_0805_2012Metric (layer F.Cu) (tedit 5B36C52B) (tstamp 5F496A69) + (at 137.16 90.58) + (descr "Capacitor SMD 0805 (2012 Metric), square (rectangular) end terminal, IPC_7351 nominal, (Body size source: https://docs.google.com/spreadsheets/d/1BsfQQcO9C6DZCsRaXUlFlo91Tg2WpOkGARC1WS5S8t0/edit?usp=sharing), generated with kicad-footprint-generator") + (tags capacitor) + (path /5F43CE1C) + (attr smd) + (fp_text reference C2 (at 0 -1.65) (layer F.SilkS) + (effects (font (size 1 1) (thickness 0.15))) + ) + (fp_text value "1000 pF" (at 0 1.65) (layer F.Fab) + (effects (font (size 1 1) (thickness 0.15))) + ) + (fp_text user %R (at 0 0) (layer F.Fab) + (effects (font (size 0.5 0.5) (thickness 0.08))) + ) + (fp_line (start -1 0.6) (end -1 -0.6) (layer F.Fab) (width 0.1)) + (fp_line (start -1 -0.6) (end 1 -0.6) (layer F.Fab) (width 0.1)) + (fp_line (start 1 -0.6) (end 1 0.6) (layer F.Fab) (width 0.1)) + (fp_line (start 1 0.6) (end -1 0.6) (layer F.Fab) (width 0.1)) + (fp_line (start -0.258578 -0.71) (end 0.258578 -0.71) (layer F.SilkS) (width 0.12)) + (fp_line (start -0.258578 0.71) (end 0.258578 0.71) (layer F.SilkS) (width 0.12)) + (fp_line (start -1.68 0.95) (end -1.68 -0.95) (layer F.CrtYd) (width 0.05)) + (fp_line (start -1.68 -0.95) (end 1.68 -0.95) (layer F.CrtYd) (width 0.05)) + (fp_line (start 1.68 -0.95) (end 1.68 0.95) (layer F.CrtYd) (width 0.05)) + (fp_line (start 1.68 0.95) (end -1.68 0.95) (layer F.CrtYd) (width 0.05)) + (pad 2 smd roundrect (at 0.9375 0) (size 0.975 1.4) (layers F.Cu F.Paste F.Mask) (roundrect_rratio 0.25) + (net 3 "Net-(C2-Pad2)")) + (pad 1 smd roundrect (at -0.9375 0) (size 0.975 1.4) (layers F.Cu F.Paste F.Mask) (roundrect_rratio 0.25) + (net 4 "Net-(C2-Pad1)")) + (model ${KISYS3DMOD}/Capacitor_SMD.3dshapes/C_0805_2012Metric.wrl + (at (xyz 0 0 0)) + (scale (xyz 1 1 1)) + (rotate (xyz 0 0 0)) + ) + ) + + (module Capacitor_SMD:C_0805_2012Metric (layer F.Cu) (tedit 5B36C52B) (tstamp 5F496A58) + (at 137.16 87.63) + (descr "Capacitor SMD 0805 (2012 Metric), square (rectangular) end terminal, IPC_7351 nominal, (Body size source: https://docs.google.com/spreadsheets/d/1BsfQQcO9C6DZCsRaXUlFlo91Tg2WpOkGARC1WS5S8t0/edit?usp=sharing), generated with kicad-footprint-generator") + (tags capacitor) + (path /5F43BEC2) + (attr smd) + (fp_text reference C1 (at 0 -1.65) (layer F.SilkS) + (effects (font (size 1 1) (thickness 0.15))) + ) + (fp_text value 1nF (at 0 1.65) (layer F.Fab) + (effects (font (size 1 1) (thickness 0.15))) + ) + (fp_text user %R (at 0 0) (layer F.Fab) + (effects (font (size 0.5 0.5) (thickness 0.08))) + ) + (fp_line (start -1 0.6) (end -1 -0.6) (layer F.Fab) (width 0.1)) + (fp_line (start -1 -0.6) (end 1 -0.6) (layer F.Fab) (width 0.1)) + (fp_line (start 1 -0.6) (end 1 0.6) (layer F.Fab) (width 0.1)) + (fp_line (start 1 0.6) (end -1 0.6) (layer F.Fab) (width 0.1)) + (fp_line (start -0.258578 -0.71) (end 0.258578 -0.71) (layer F.SilkS) (width 0.12)) + (fp_line (start -0.258578 0.71) (end 0.258578 0.71) (layer F.SilkS) (width 0.12)) + (fp_line (start -1.68 0.95) (end -1.68 -0.95) (layer F.CrtYd) (width 0.05)) + (fp_line (start -1.68 -0.95) (end 1.68 -0.95) (layer F.CrtYd) (width 0.05)) + (fp_line (start 1.68 -0.95) (end 1.68 0.95) (layer F.CrtYd) (width 0.05)) + (fp_line (start 1.68 0.95) (end -1.68 0.95) (layer F.CrtYd) (width 0.05)) + (pad 2 smd roundrect (at 0.9375 0) (size 0.975 1.4) (layers F.Cu F.Paste F.Mask) (roundrect_rratio 0.25) + (net 1 "Net-(C1-Pad2)")) + (pad 1 smd roundrect (at -0.9375 0) (size 0.975 1.4) (layers F.Cu F.Paste F.Mask) (roundrect_rratio 0.25) + (net 2 "Net-(C1-Pad1)")) + (model ${KISYS3DMOD}/Capacitor_SMD.3dshapes/C_0805_2012Metric.wrl + (at (xyz 0 0 0)) + (scale (xyz 1 1 1)) + (rotate (xyz 0 0 0)) + ) + ) + + (gr_line (start 133.35 83.82) (end 133.35 93.98) (layer Edge.Cuts) (width 0.1) (tstamp 5F496ACC)) + (gr_line (start 146.05 83.82) (end 133.35 83.82) (layer Edge.Cuts) (width 0.1)) + (gr_line (start 146.05 93.98) (end 146.05 83.82) (layer Edge.Cuts) (width 0.1)) + (gr_line (start 133.35 93.98) (end 146.05 93.98) (layer Edge.Cuts) (width 0.1)) + +) diff --git a/tests/board_samples/kibom-variant_3.sch b/tests/board_samples/kibom-variant_3.sch new file mode 100644 index 000000000..dec1d1d62 --- /dev/null +++ b/tests/board_samples/kibom-variant_3.sch @@ -0,0 +1,68 @@ +EESchema Schematic File Version 4 +EELAYER 30 0 +EELAYER END +$Descr A4 11693 8268 +encoding utf-8 +Sheet 1 1 +Title "KiBom Test Schematic" +Date "2020-03-12" +Rev "A" +Comp "https://github.com/SchrodingersGat/KiBom" +Comment1 "" +Comment2 "" +Comment3 "" +Comment4 "" +$EndDescr +Text Notes 500 750 0 79 ~ 0 +This schematic serves as a test-file for the KiBot export script.\nHere we implement the IBoM variants style. +$Comp +L Device:C C1 +U 1 1 5F43BEC2 +P 1000 1700 +F 0 "C1" H 1115 1746 50 0000 L CNN +F 1 "1nF" H 1115 1655 50 0000 L CNN +F 2 "Capacitor_SMD:C_0805_2012Metric" H 1038 1550 50 0001 C CNN +F 3 "~" H 1000 1700 50 0001 C CNN +F 4 "T2" H 1000 1700 50 0001 C CNN "Config" + 1 1000 1700 + 1 0 0 -1 +$EndComp +$Comp +L Device:C C2 +U 1 1 5F43CE1C +P 1450 1700 +F 0 "C2" H 1565 1746 50 0000 L CNN +F 1 "1000 pF" H 1565 1655 50 0000 L CNN +F 2 "Capacitor_SMD:C_0805_2012Metric" H 1488 1550 50 0001 C CNN +F 3 "~" H 1450 1700 50 0001 C CNN +F 4 "T3" H 1450 1700 50 0001 C CNN "Config" + 1 1450 1700 + 1 0 0 -1 +$EndComp +$Comp +L Device:R R1 +U 1 1 5F43D144 +P 2100 1700 +F 0 "R1" H 2170 1746 50 0000 L CNN +F 1 "1k" H 2170 1655 50 0000 L CNN +F 2 "Resistor_SMD:R_0805_2012Metric" V 2030 1700 50 0001 C CNN +F 3 "~" H 2100 1700 50 0001 C CNN +F 4 "default" H 2100 1700 50 0001 C CNN "Config" + 1 2100 1700 + 1 0 0 -1 +$EndComp +$Comp +L Device:R R2 +U 1 1 5F43D4BB +P 2500 1700 +F 0 "R2" H 2570 1746 50 0000 L CNN +F 1 "1000" H 2570 1655 50 0000 L CNN +F 2 "Resistor_SMD:R_0805_2012Metric" V 2430 1700 50 0001 C CNN +F 3 "~" H 2500 1700 50 0001 C CNN +F 4 "T1" H 2500 1700 50 0001 C CNN "Config" + 1 2500 1700 + 1 0 0 -1 +$EndComp +Text Notes 5950 3200 0 118 ~ 0 +The test tests the following \nvariants matrix:\n production test default\nC1 X\nC2 X X\nR1 X X X\nR2 X X\n\nproduction: blacklist T2\ntest: blacklist T1\ndefault: whitelist T1,default \n blacklist T2,T3 +$EndSCHEMATC diff --git a/tests/board_samples/kibom-variant_3.xml b/tests/board_samples/kibom-variant_3.xml new file mode 100644 index 000000000..bd41b6727 --- /dev/null +++ b/tests/board_samples/kibom-variant_3.xml @@ -0,0 +1,130 @@ + + + + /home/salvador/0Data/Eccosur/kibot/tests/board_samples/var3/kibom-variant_3.sch + vie 28 ago 2020 18:18:02 + Eeschema 5.1.6+dfsg1-1 + + + KiBom Test Schematic + https://github.com/SchrodingersGat/KiBom + A + 2020-03-12 + kibom-variant_3.sch + + + + + + + + + + 1nF + Capacitor_SMD:C_0805_2012Metric + ~ + + T2 + + + + 5F43BEC2 + + + 1000 pF + Capacitor_SMD:C_0805_2012Metric + ~ + + T3 + + + + 5F43CE1C + + + 1k + Resistor_SMD:R_0805_2012Metric + ~ + + default + + + + 5F43D144 + + + 1000 + Resistor_SMD:R_0805_2012Metric + ~ + + T1 + + + + 5F43D4BB + + + + + Unpolarized capacitor + ~ + + C_* + + + C + C + + + + + + + + Resistor + ~ + + R_* + + + R + R + + + + + + + + + + /usr/share/kicad/library/Device.lib + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/test_plot/test_int_bom.py b/tests/test_plot/test_int_bom.py index f80f536f9..2706dd873 100644 --- a/tests/test_plot/test_int_bom.py +++ b/tests/test_plot/test_int_bom.py @@ -1210,3 +1210,19 @@ def test_int_bom_variant_t2(): rows, header, info = ctx.load_csv(prj+'-bom_(test).csv') check_kibom_test_netlist(rows, ref_column, 2, ['R2'], ['R1', 'C1', 'C2']) ctx.clean_up() + + +def test_int_bom_variant_t2i(): + prj = 'kibom-variant_3' + ctx = context.TestContextSCH('test_int_bom_variant_t2i', prj, 'int_bom_var_t2i_csv', BOM_DIR) + ctx.run() + rows, header, info = ctx.load_csv(prj+'-bom.csv') + ref_column = header.index(REF_COLUMN_NAME) + check_kibom_test_netlist(rows, ref_column, 1, ['C1', 'C2'], ['R1', 'R2']) + rows, header, info = ctx.load_csv(prj+'-bom_[2].csv') + check_kibom_test_netlist(rows, ref_column, 1, ['C1', 'C2'], ['R1', 'R2']) + rows, header, info = ctx.load_csv(prj+'-bom_(production).csv') + check_kibom_test_netlist(rows, ref_column, 2, ['C1'], ['R1', 'R2', 'C2']) + rows, header, info = ctx.load_csv(prj+'-bom_(test).csv') + check_kibom_test_netlist(rows, ref_column, 2, ['R2'], ['R1', 'C1', 'C2']) + ctx.clean_up() diff --git a/tests/yaml_samples/int_bom_var_t2i_csv.kibot.yaml b/tests/yaml_samples/int_bom_var_t2i_csv.kibot.yaml new file mode 100644 index 000000000..e14042fd2 --- /dev/null +++ b/tests/yaml_samples/int_bom_var_t2i_csv.kibot.yaml @@ -0,0 +1,56 @@ +# Example KiBot config file +kibot: + version: 1 + +variants: + - name: 'production' + comment: 'Production variant' + type: ibom + file_id: '_(production)' + variants_blacklist: T2 + + - name: 'test' + comment: 'Test variant' + type: ibom + file_id: '_(test)' + variants_blacklist: T1 + + - name: 'default' + comment: 'Default variant' + type: ibom + variants_blacklist: T2,T3 + + - name: 'default2' + comment: 'Default variant 2' + type: ibom + file_id: '_[2]' + variants_whitelist: T1, Default + +outputs: + - name: 'bom_internal' + comment: "Bill of Materials in CSV format" + type: bom + dir: BoM + options: + variant: default + + - name: 'bom_internal2' + comment: "Bill of Materials in CSV format (2)" + type: bom + dir: BoM + options: + variant: default2 + + - name: 'bom_internal_production' + comment: "Bill of Materials in CSV format for production" + type: bom + dir: BoM + options: + variant: production + + - name: 'bom_internal_test' + comment: "Bill of Materials in CSV format for test" + type: bom + dir: BoM + options: + variant: test From cc006c6260ae8edbfa8009c0d5af142056c3e324 Mon Sep 17 00:00:00 2001 From: "Salvador E. Tropea" Date: Fri, 28 Aug 2020 20:15:12 -0300 Subject: [PATCH 09/21] Updated README and generic example. Mainly because I moved IBoM options to IBoM variants. --- README.md | 10 ++-------- docs/samples/generic_plot.kibot.yaml | 20 ++++---------------- 2 files changed, 6 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index bea4bd4ef..e02c3a817 100644 --- a/README.md +++ b/README.md @@ -555,7 +555,7 @@ Next time you need this list just use an alias, like this: - `exclude_edge_layer`: [boolean=true] do not include the PCB edge layer. - `exclude_pads_from_silkscreen`: [boolean=false] do not plot the component pads in the silk screen. - `force_plot_invisible_refs_vals`: [boolean=false] include references and values even when they are marked as invisible. - - `gerber_job_file`: [string='%f-%i%v.%x'] name for the gerber job file (%i='job', %x='gbrjob'). Affected by global options. + - `gerber_job_file`: [string='%f-%i.%x'] name for the gerber job file (%i='job', %x='gbrjob'). - `gerber_precision`: [number=4.6] this the gerber coordinate format, can be 4.5 or 4.6. - `line_width`: [number=0.1] [0.02,2] line_width for objects without width [mm]. - `output`: [string='%f-%i%v.%x'] output file name, the default KiCad name if empty. Affected by global options. @@ -612,15 +612,12 @@ Next time you need this list just use an alias, like this: - `name`: [string=''] Used to identify this particular output definition. - `options`: [dict] Options for the `ibom` output. * Valid keys: - - `blacklist`: [string=''] List of comma separated blacklisted components or prefixes with *. E.g. 'X1,MH*'. - - `blacklist_empty_val`: [boolean=false] Blacklist components with empty value. - `board_rotation`: [number=0] Board rotation in degrees (-180 to 180). Will be rounded to multiple of 5. - `bom_view`: [string='left-right'] [bom-only,left-right,top-bottom] Default BOM view. - `checkboxes`: [string='Sourced,Placed'] Comma separated list of checkbox columns. - `dark_mode`: [boolean=false] Default to dark mode. - - `dnp_field`: [string=''] Name of the extra field that indicates do not populate status. Components with this field not empty will be - blacklisted. - `extra_fields`: [string=''] Comma separated list of extra fields to pull from netlist or xml file. + These are extra columns in the BoM. - `hide_pads`: [boolean=false] Hide footprint pads by default. - `hide_silkscreen`: [boolean=false] Hide silkscreen by default. - `highlight_pin1`: [boolean=false] Highlight pin1 by default. @@ -644,9 +641,6 @@ Next time you need this list just use an alias, like this: - `output`: [string='%f-%i%v.%x'] Filename for the output, use '' to use the IBoM filename (%i=ibom, %x=html). Affected by global options. - `show_fabrication`: [boolean=false] Show fabrication layer by default. - `sort_order`: [string='C,R,L,D,U,Y,X,F,SW,A,~,HS,CNN,J,P,NT,MH'] Default sort order for components. Must contain '~' once. - - `variant_field`: [string=''] Name of the extra field that stores board variant for component. - - `variants_blacklist`: [string=''] List of board variants to exclude from the BOM. - - `variants_whitelist`: [string=''] List of board variants to include in the BOM. * KiBoM (KiCad Bill of Materials) * Type: `kibom` diff --git a/docs/samples/generic_plot.kibot.yaml b/docs/samples/generic_plot.kibot.yaml index 3f9ced520..600cc1c8b 100644 --- a/docs/samples/generic_plot.kibot.yaml +++ b/docs/samples/generic_plot.kibot.yaml @@ -293,8 +293,8 @@ outputs: exclude_pads_from_silkscreen: false # [boolean=false] include references and values even when they are marked as invisible force_plot_invisible_refs_vals: false - # [string='%f-%i%v.%x'] name for the gerber job file (%i='job', %x='gbrjob'). Affected by global options - gerber_job_file: '%f-%i%v.%x' + # [string='%f-%i.%x'] name for the gerber job file (%i='job', %x='gbrjob') + gerber_job_file: '%f-%i.%x' # [number=4.6] this the gerber coordinate format, can be 4.5 or 4.6 gerber_precision: 4.6 # [number=0.1] [0.02,2] line_width for objects without width [mm] @@ -368,10 +368,6 @@ outputs: type: 'ibom' dir: 'Example/ibom_dir' options: - # [string=''] List of comma separated blacklisted components or prefixes with *. E.g. 'X1,MH*' - blacklist: '' - # [boolean=false] Blacklist components with empty value - blacklist_empty_val: false # [number=0] Board rotation in degrees (-180 to 180). Will be rounded to multiple of 5 board_rotation: 0 # [string='left-right'] [bom-only,left-right,top-bottom] Default BOM view @@ -380,10 +376,8 @@ outputs: checkboxes: 'Sourced,Placed' # [boolean=false] Default to dark mode dark_mode: false - # [string=''] Name of the extra field that indicates do not populate status. Components with this field not empty will be - # blacklisted - dnp_field: '' - # [string=''] Comma separated list of extra fields to pull from netlist or xml file + # [string=''] Comma separated list of extra fields to pull from netlist or xml file. + # These are extra columns in the BoM extra_fields: '' # [boolean=false] Hide footprint pads by default hide_pads: false @@ -422,12 +416,6 @@ outputs: show_fabrication: false # [string='C,R,L,D,U,Y,X,F,SW,A,~,HS,CNN,J,P,NT,MH'] Default sort order for components. Must contain '~' once sort_order: 'C,R,L,D,U,Y,X,F,SW,A,~,HS,CNN,J,P,NT,MH' - # [string=''] Name of the extra field that stores board variant for component - variant_field: '' - # [string=''] List of board variants to exclude from the BOM - variants_blacklist: '' - # [string=''] List of board variants to include in the BOM - variants_whitelist: '' # KiBoM (KiCad Bill of Materials): # For more information: https://github.com/INTI-CMNB/KiBoM From 59a18723107a76409b6098dd88e1a6648a8dc237 Mon Sep 17 00:00:00 2001 From: "Salvador E. Tropea" Date: Fri, 28 Aug 2020 20:16:22 -0300 Subject: [PATCH 10/21] Added my notes about KiBoM and IBoM variants. --- experiments/variants/IBoM/README.md | 35 ++++++++++++++++++++++++++ experiments/variants/KiBOM/README.md | 37 ++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+) create mode 100644 experiments/variants/IBoM/README.md create mode 100644 experiments/variants/KiBOM/README.md diff --git a/experiments/variants/IBoM/README.md b/experiments/variants/IBoM/README.md new file mode 100644 index 000000000..275c4a94a --- /dev/null +++ b/experiments/variants/IBoM/README.md @@ -0,0 +1,35 @@ +# IBoM variants + +This is an analysis and test of the *variants* implementation of [IBoM](https://github.com/openscopeproject/InteractiveHtmlBom) + +## What goes inside the SCH + +- The field used for variants must be specified using `--variant-field` +- The field can contain only one value. So you create some kind of component groups. + +## What goes outside the SCH + +- Two optional lists are passed to create the variant. +- Components without a group are always included. +- Whitelist: only the groups listed here are included. + - If this list is empty all groups are included, unless listed in the blacklist. +- Blacklist: groups listed here are excluded. + +## Where is in the code? + +In core/ibom.py function skip_component. + +## Conclusion + +### Advantages + +- The `Config` field is simple. +- You have "exclude from" and "include only" options. + +### Disadvantages + +- Critical part of the information is outside the project. + + + + diff --git a/experiments/variants/KiBOM/README.md b/experiments/variants/KiBOM/README.md new file mode 100644 index 000000000..ac39d1f0c --- /dev/null +++ b/experiments/variants/KiBOM/README.md @@ -0,0 +1,37 @@ +# KiBOM variants + +This is an analysis and test of the *variants* implementation of [KiBOM](https://github.com/SchrodingersGat/KiBoM) + +## What goes inside the SCH + +- The variants are implemented using the `Config` field (the name can be configured). +- By default a component is included in all variants. +- You can exclude a component from one variant adding `-VARIANT_NAME` to the `Config`. + - If you want to exclude this component from more than one variant just add more `-VARIANT_NAME` entries. Comma separated. +- If a component will be included **only** in one variant you can use `+VARIANT_NAME`. + - Again you can add more than one `+VARIANT_NAME` entry. So the component will be included only if generating one of the selected variants. + +## What goes outside the SCH + +- When you generate the BoM you can select one or more variants, again comma separated. +- The `-VARIANT_NAME` and `+VARIANT_NAME` is tested using a list of all the indicated variants. +- By default the list of variants is ['default']. So *default* is like a special variant. + +## Where is in the code? + +The Component.isFitted() method implements the functionality. + +## Conclusion + +### Advantages + +- Almost all the information is inside the project. +- You have "exclude from" and "include only" options. + +### Disadvantages + +- The `Config` field could become large. + + + + From 006072e842bc5dcd647d7ca49d61684bab6b64a8 Mon Sep 17 00:00:00 2001 From: "Salvador E. Tropea" Date: Sat, 29 Aug 2020 17:31:35 -0300 Subject: [PATCH 11/21] Documented computed component flags. fitted, in_bom (new) and fixed --- kibot/kicad/v5_sch.py | 13 +++++++++++++ kibot/out_ibom.py | 29 ++++++++++++++--------------- 2 files changed, 27 insertions(+), 15 deletions(-) diff --git a/kibot/kicad/v5_sch.py b/kibot/kicad/v5_sch.py index bb21781b8..47bf6b465 100644 --- a/kibot/kicad/v5_sch.py +++ b/kibot/kicad/v5_sch.py @@ -591,6 +591,19 @@ def parse(line): class SchematicComponent(object): + """ Class for a component in the schematic. + Here are special members currently computed elsehere: + - fitted: equivalent to 'Exclude from board' but inverted + - Solded: only if True + - BoM normal: only if True + - BoM DNF: only if False + - in_bom: equivalent to 'Exclude from BoM' but inverted + - Solded: doesn't affected + - BoM normal: only if True + - BoM DNF: only if True (and fitted is False) + - fixed: means you can't change it by a replacement without authorization + Is just a flag and doesn't affect much. + """ ref_re = re.compile(r'([^\d]+)([\?\d]+)') def __init__(self): diff --git a/kibot/out_ibom.py b/kibot/out_ibom.py index 17383c3c8..08f747648 100644 --- a/kibot/out_ibom.py +++ b/kibot/out_ibom.py @@ -52,28 +52,27 @@ def __init__(self): """ Include netlist information in output. """ self.sort_order = 'C,R,L,D,U,Y,X,F,SW,A,~,HS,CNN,J,P,NT,MH' """ Default sort order for components. Must contain '~' once """ - # self.blacklist = '' - # """ List of comma separated blacklisted components or prefixes with *. E.g. 'X1,MH*' """ + self.blacklist = '' + """ List of comma separated blacklisted components or prefixes with *. E.g. 'X1,MH*' """ self.no_blacklist_virtual = False """ Do not blacklist virtual components """ - # self.blacklist_empty_val = False - # """ Blacklist components with empty value """ + self.blacklist_empty_val = False + """ Blacklist components with empty value """ self.netlist_file = '' """ Path to netlist or xml file """ self.extra_fields = '' - """ Comma separated list of extra fields to pull from netlist or xml file. - These are extra columns in the BoM """ + """ Comma separated list of extra fields to pull from netlist or xml file """ self.normalize_field_case = False """ Normalize extra field name case. E.g. 'MPN' and 'mpn' will be considered the same field """ - # self.variant_field = '' - # """ Name of the extra field that stores board variant for component """ - # self.variants_whitelist = '' - # """ List of board variants to include in the BOM """ - # self.variants_blacklist = '' - # """ List of board variants to exclude from the BOM """ - # self.dnp_field = '' - # """ Name of the extra field that indicates do not populate status. Components with this field not empty will be - # blacklisted """ + self.variant_field = '' + """ Name of the extra field that stores board variant for component """ + self.variants_whitelist = '' + """ List of board variants to include in the BOM """ + self.variants_blacklist = '' + """ List of board variants to exclude from the BOM """ + self.dnp_field = '' + """ Name of the extra field that indicates do not populate status. Components with this field not empty will be + blacklisted """ super().__init__() def run(self, output_dir, board): From ad7ed9183a6ae884b0e46481b40ae645709602e7 Mon Sep 17 00:00:00 2001 From: "Salvador E. Tropea" Date: Sat, 29 Aug 2020 17:38:32 -0300 Subject: [PATCH 12/21] Added the concept of `filters` Closely related to variants, but more abstract. Will allow much more configurability. --- kibot/__main__.py | 2 ++ kibot/config_reader.py | 31 ++++++++++++++++++++++--------- kibot/kiplot.py | 3 ++- kibot/macros.py | 11 +++++++++++ kibot/registrable.py | 39 +++++++++++++++++++++++++++++++++++++++ kibot/var_base.py | 3 --- 6 files changed, 76 insertions(+), 13 deletions(-) diff --git a/kibot/__main__.py b/kibot/__main__.py index eca944473..7ff7b4c33 100644 --- a/kibot/__main__.py +++ b/kibot/__main__.py @@ -75,6 +75,8 @@ has_macro = [ 'layer', 'drill_marks', + 'fil_base', + 'fil_generic', 'out_any_drill', 'out_any_layer', 'out_base', diff --git a/kibot/config_reader.py b/kibot/config_reader.py index 487503d0e..3726dcd00 100644 --- a/kibot/config_reader.py +++ b/kibot/config_reader.py @@ -17,7 +17,7 @@ from .kiplot import (load_board) from .misc import (NO_YAML_MODULE, EXIT_BAD_ARGS, EXAMPLE_CFG, WONT_OVERWRITE) from .gs import GS -from .registrable import RegOutput, RegVariant +from .registrable import RegOutput, RegVariant, RegFilter from .pre_base import BasePreFlight # Logger @@ -89,24 +89,25 @@ def _parse_outputs(self, v): config_error("`outputs` must be a list") return outputs - def _parse_variant(self, o_tree): + def _parse_variant(self, o_tree, kind, reg_class): + kind_f = kind[0].upper()+kind[1:] try: name = o_tree['name'] if not name: raise KeyError except KeyError: - config_error("Variant needs a name in: "+str(o_tree)) + config_error(kind_f+" needs a name in: "+str(o_tree)) try: otype = o_tree['type'] except KeyError: - config_error("Variant `"+name+"` needs a type") + config_error(kind_f+" `"+name+"` needs a type") # Is a valid type? - if not RegVariant.is_registered(otype): - config_error("Unknown variant type: `{}`".format(otype)) + if not reg_class.is_registered(otype): + config_error("Unknown {} type: `{}`".format(kind, otype)) # Load it name_type = "`"+name+"` ("+otype+")" - logger.debug("Parsing variant "+name_type) - o_var = RegVariant.get_class_for(otype)() + logger.debug("Parsing "+kind+" "+name_type) + o_var = reg_class.get_class_for(otype)() o_var.set_tree(o_tree) try: o_var.config() @@ -118,12 +119,22 @@ def _parse_variants(self, v): variants = {} if isinstance(v, list): for o in v: - o_var = self._parse_variant(o) + o_var = self._parse_variant(o, 'variant', RegVariant) variants[o_var.name] = o_var else: config_error("`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(o, 'filter', RegFilter) + filters[o_fil.name] = o_fil + else: + config_error("`filters` must be a list") + return filters + def _parse_preflight(self, pf): logger.debug("Parsing preflight options: {}".format(pf)) if not isinstance(pf, dict): @@ -177,6 +188,8 @@ def read(self, fstream): self._parse_global(v) elif k == 'variants': RegOutput.set_variants(self._parse_variants(v)) + elif k == 'filters': + RegOutput.set_filters(self._parse_filters(v)) elif k == 'outputs': outputs = self._parse_outputs(v) else: diff --git a/kibot/kiplot.py b/kibot/kiplot.py index 193e2f3e5..693f9f07b 100644 --- a/kibot/kiplot.py +++ b/kibot/kiplot.py @@ -56,7 +56,8 @@ def _import(name, path): def _load_actions(path): logger.debug("Importing from "+path) - lst = glob(os.path.join(path, 'out_*.py')) + glob(os.path.join(path, 'pre_*.py')) + glob(os.path.join(path, 'var_*.py')) + lst = glob(os.path.join(path, 'out_*.py')) + glob(os.path.join(path, 'pre_*.py')) + lst += glob(os.path.join(path, 'var_*.py')) + glob(os.path.join(path, 'fil_*.py')) for p in lst: name = os.path.splitext(os.path.basename(p))[0] logger.debug("- Importing "+name) diff --git a/kibot/macros.py b/kibot/macros.py index 4c0804e58..dd647f790 100644 --- a/kibot/macros.py +++ b/kibot/macros.py @@ -133,6 +133,17 @@ def variant_class(tree, **kw): return _do_wrap_class_register(tree, 'var_base', 'BaseVariant') +def filter_class(tree, **kw): + """A decorator to wrap a class with: + + from .fil_base import BaseFilter + ... Class definition + BaseFilter.register(CLASS_NAME_LOWER_STRING, CLASS_NAME) + + Allowing to register the class as a variant. """ + return _do_wrap_class_register(tree, 'fil_base', 'BaseFilter') + + def pre_class(tree, **kw): """A decorator to wrap a class with: diff --git a/kibot/registrable.py b/kibot/registrable.py index 6028ec041..f44c88347 100644 --- a/kibot/registrable.py +++ b/kibot/registrable.py @@ -27,12 +27,17 @@ def get_class_for(cl, name): def get_registered(cl): return cl._registered + def __str__(self): + return "'{}' ({}) [{}]".format(self.comment, self.name, self.type) + class RegOutput(Optionable, Registrable): """ An optionable that is also registrable. Used by BaseOutput. Here because it doesn't need macros. """ _registered = {} + # List of defined filters + _def_filters = {} # List of defined variants _def_variants = {} @@ -43,6 +48,30 @@ def __init__(self): def set_variants(variants): RegOutput._def_variants = variants + @staticmethod + def is_variant(name): + return name in RegOutput._def_variants + + @staticmethod + def get_variant(name): + return RegOutput._def_variants[name] + + @staticmethod + def set_filters(filters): + RegOutput._def_filters = filters + + @staticmethod + def is_filter(name): + return name in RegOutput._def_filters + + @staticmethod + def get_filter(name): + return RegOutput._def_filters[name] + + @staticmethod + def add_filter(obj): + RegOutput._def_filters[obj.name] = obj + class RegVariant(Optionable, Registrable): """ An optionable that is also registrable. @@ -52,3 +81,13 @@ class RegVariant(Optionable, Registrable): def __init__(self): super().__init__() + + +class RegFilter(Optionable, Registrable): + """ An optionable that is also registrable. + Used by BaseFilter. + Here because it doesn't need macros. """ + _registered = {} + + def __init__(self): + super().__init__() diff --git a/kibot/var_base.py b/kibot/var_base.py index 43bda00ff..976789fd3 100644 --- a/kibot/var_base.py +++ b/kibot/var_base.py @@ -20,6 +20,3 @@ def __init__(self): """ A comment for documentation purposes """ self.file_id = '' """ Text to use as the """ - - def __str__(self): - return "'{}' ({}) [{}]".format(self.comment, self.name, self.type) From 6af9faf9092b0605c29e971c27a2012dcdaa37a3 Mon Sep 17 00:00:00 2001 From: "Salvador E. Tropea" Date: Sat, 29 Aug 2020 17:39:56 -0300 Subject: [PATCH 13/21] Created the base filter class and the generic filter. Moved all the KiBoM and IBoM filter functionality that was in their variants to this generic mechanism. --- kibot/fil_base.py | 20 +++++ kibot/fil_generic.py | 181 +++++++++++++++++++++++++++++++++++++++++++ kibot/optionable.py | 4 + kibot/out_base.py | 20 ++++- kibot/var_ibom.py | 31 ++------ kibot/var_kibom.py | 74 ++---------------- 6 files changed, 232 insertions(+), 98 deletions(-) create mode 100644 kibot/fil_base.py create mode 100644 kibot/fil_generic.py diff --git a/kibot/fil_base.py b/kibot/fil_base.py new file mode 100644 index 000000000..7c2c3a6c0 --- /dev/null +++ b/kibot/fil_base.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020 Salvador E. Tropea +# Copyright (c) 2020 Instituto Nacional de Tecnología Industrial +# License: GPL-3.0 +# Project: KiBot (formerly KiPlot) +from .registrable import RegFilter +from .macros import macros, document # noqa: F401 + + +class BaseFilter(RegFilter): + def __init__(self): + super().__init__() + self._unkown_is_error = True + with document: + self.name = '' + """ Used to identify this particular filter definition """ + self.type = '' + """ Type of filter """ + self.comment = '' + """ A comment for documentation purposes """ diff --git a/kibot/fil_generic.py b/kibot/fil_generic.py new file mode 100644 index 000000000..1d173dd00 --- /dev/null +++ b/kibot/fil_generic.py @@ -0,0 +1,181 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020 Salvador E. Tropea +# Copyright (c) 2020 Instituto Nacional de Tecnología Industrial +# License: GPL-3.0 +# Project: KiBot (formerly KiPlot) +""" +Implements the KiBoM and IBoM filters. +""" +from re import compile, IGNORECASE +from .optionable import Optionable +from .bom.columnlist import ColumnList +from .gs import GS +from .misc import DNF, DNC +from .macros import macros, document, filter_class # noqa: F401 +from .out_base import BoMRegex +from . import log + +logger = log.get_logger(__name__) + + +class DNFList(Optionable): + _default = DNF + + def __init__(self): + super().__init__() + + +@filter_class +class Generic(BaseFilter): # noqa: F821 + """ Generic filter + This filter is based on regular exressions. + It also provides some shortcuts for common situations. + Note that matches aren't case sensitive and spaces at the beggining and the end are removed. """ + def __init__(self): + super().__init__() + with document: + self.invert = False + """ Invert the result of the filter """ + self.include_only = BoMRegex + """ [list(dict)] A series of regular expressions used to include parts. + If there are any regex defined here, only components that match against ANY of them will be included. + Column/field names are case-insensitive. + If empty this rule is ignored """ + self.exclude_any = BoMRegex + """ [list(dict)] A series of regular expressions used to exclude parts. + If a component matches ANY of these, it will be excluded. + Column names are case-insensitive """ + self.keys = DNFList + """ [string|list(string)=dnf_list] [dnc_list,dnf_list] List of keys to match. + The `dnf_list` and `dnc_list` internal lists can be specified as strings """ + self.exclude_value = False + """ Exclude components if their 'Value' is any of the keys """ + self.config_field = 'Config' + """ Name of the field used to clasify components """ + self.config_separators = ' ,' + """ Characters used to separate options inside the config field """ + self.exclude_config = False + """ Exclude components containing a key value in the config field. + Separators are applied """ + self.exclude_field = False + """ Exclude components if a field is named as any of the keys """ + self.exclude_empty_val = False + """ Exclude components with empty 'Value' """ + self.exclude_refs = Optionable + """ [list(string)] List of references to be excluded. + Use R* for all references with R prefix """ + # Skip virtual components if needed + # TODO: We currently lack this information + # if config.blacklist_virtual and m.attr == 'Virtual': + # return True + self.add_to_doc('keys', 'Use `dnf_list` for '+str(DNF)) + self.add_to_doc('keys', 'Use `dnc_list` for '+str(DNC)) + + @staticmethod + def _fix_field(field): + """ References -> Reference """ + col = field.lower() + if col == ColumnList.COL_REFERENCE_L: + col = col[:-1] + return col + + def config(self): + super().config() + # include_only + if isinstance(self.include_only, type): + self.include_only = None + else: + for r in self.include_only: + r.column = self._fix_field(r.column) + r.regex = compile(r.regex, flags=IGNORECASE) + # exclude_any + if isinstance(self.exclude_any, type): + self.exclude_any = None + else: + for r in self.exclude_any: + r.column = self._fix_field(r.column) + r.regex = compile(r.regex, flags=IGNORECASE) + # keys + if isinstance(self.keys, type): + self.keys = DNF + elif isinstance(self.keys, str): + self.keys = DNF if self.keys == 'dnf_list' else DNC + else: + # Ensure lowercase + self.keys = [v.lower() for v in self.keys] + # Config field must be lowercase + self.config_field = self.config_field.lower() + # exclude_refs + if isinstance(self.exclude_refs, type): + self.exclude_refs = None + + def test_reg_include(self, c): + """ Reject components that doesn't match the provided regex. + So we include only the components that matches any of the regexs. """ + if not self.include_only: # Nothing to match against, means include all + return True + for reg in self.include_only: + field_value = c.get_field_value(reg.column) + if reg.regex.search(field_value): + if GS.debug_level > 1: + logger.debug("Including '{ref}': Field '{field}' ({value}) matched '{re}'".format( + ref=c.ref, field=reg.column, value=field_value, re=reg.regex)) + # Found a match + return True + # Default, could not find a match + return False + + def test_reg_exclude(self, c): + """ Test if this part should be included, based on any regex expressions provided in the preferences """ + if not self.exclude_any: # Nothing to match against, means don't exclude any + return False + for reg in self.exclude_any: + field_value = c.get_field_value(reg.column) + if reg.regex.search(field_value): + if GS.debug_level > 1: + logger.debug("Excluding '{ref}': Field '{field}' ({value}) matched '{re}'".format( + ref=c.ref, field=reg.column, value=field_value, re=reg.regex)) + # Found a match + return True + # Default, could not find any matches + return False + + def filter(self, comp): + exclude = self.invert + value = comp.value.strip().lower() + # Exclude components with empty 'Value' + if self.exclude_empty_val and (value == '' or value == '~'): + return exclude + # List of references to be excluded + if self.exclude_refs and (comp.ref in self.exclude_refs or comp.ref_prefix+'*' in self.exclude_refs): + return exclude + # All stuff where keys are involved + if self.keys: + # Exclude components if their 'Value' is any of the keys + if self.exclude_value and value in self.keys: + return exclude + # Exclude components if a field is named as any of the keys + if self.exclude_field: + for k in self.keys: + if k in comp.dfields: + return exclude + # Exclude components containing a key value in the config field. + if self.exclude_config: + config = comp.get_field_value(self.config_field).strip().lower() + if self.config_separators: + # Try with all the separators + for sep in self.config_separators: + opts = config.split(sep) + # Try with all the extracted values + for opt in opts: + if opt.strip() in self.keys: + return exclude + else: # No separator + if config in self.keys: + return exclude + # Regular expressions + if not self.test_reg_include(comp): + return exclude + if self.test_reg_exclude(comp): + return exclude + return not exclude diff --git a/kibot/optionable.py b/kibot/optionable.py index 6b2059faf..e795a684b 100644 --- a/kibot/optionable.py +++ b/kibot/optionable.py @@ -67,6 +67,10 @@ def get_doc(self, name): return getattr(self, '_help_'+alias).strip(), alias, True return doc, name, False + def add_to_doc(self, name, text): + doc = getattr(self, '_help_'+name).strip() + setattr(self, '_help_'+name, doc+'.\n'+text) + @staticmethod def _typeof(v): if isinstance(v, bool): diff --git a/kibot/out_base.py b/kibot/out_base.py index 86751e9ad..c3a30b596 100644 --- a/kibot/out_base.py +++ b/kibot/out_base.py @@ -4,6 +4,7 @@ # License: GPL-3.0 # Project: KiBot (formerly KiPlot) from .registrable import RegOutput +from .optionable import Optionable from .macros import macros, document # noqa: F401 from . import log @@ -29,9 +30,6 @@ def __init__(self): def attr2longopt(attr): return '--'+attr.replace('_', '-') - def __str__(self): - return "'{}' ({}) [{}]".format(self.comment, self.name, self.type) - def is_sch(self): """ True for outputs that works on the schematic """ return self._sch_related @@ -50,3 +48,19 @@ def config(self): def run(self, output_dir, board): self.options.run(output_dir, board) + + +class BoMRegex(Optionable): + """ Implements the pair column/regex """ + def __init__(self): + super().__init__() + self._unkown_is_error = True + with document: + self.column = '' + """ Name of the column to apply the regular expression """ + self.regex = '' + """ Regular expression to match """ + self.field = None + """ {column} """ + self.regexp = None + """ {regex} """ diff --git a/kibot/var_ibom.py b/kibot/var_ibom.py index 2e1673f83..4c0e31f16 100644 --- a/kibot/var_ibom.py +++ b/kibot/var_ibom.py @@ -29,13 +29,6 @@ def __init__(self): """ [string|list(string)=''] List of board variants to exclude from the BOM """ self.variants_whitelist = Optionable """ [string|list(string)=''] List of board variants to include in the BOM """ - self.blacklist = Optionable - """ [string|list(string)=''] List of comma separated blacklisted components or prefixes with *. E.g. 'X1,MH*' """ - self.blacklist_empty_val = False - """ Blacklist components with empty value """ - self.dnp_field = '' - """ Name of the extra field that indicates do not populate status. - Components with this field not empty will be blacklisted """ @staticmethod def _force_list(val): @@ -55,25 +48,9 @@ def config(self): super().config() self.variants_blacklist = self._force_list(self.variants_blacklist) self.variants_whitelist = self._force_list(self.variants_whitelist) - self.blacklist = self._force_list(self.blacklist) def skip_component(self, c): - """ Skip blacklisted components. - This is what IBoM does internally """ - if c.ref in self.blacklist: - return True - if c.ref_prefix + '*' in self.blacklist: - return True - # Remove components with empty value - if self.blacklist_empty_val and c.value in ['', '~']: - return True - # Skip virtual components if needed - # TODO: We currently lack this information - # if config.blacklist_virtual and m.attr == 'Virtual': - # return True - # Skip components with dnp field not empty - if self.dnp_field and c.get_field_value(self.dnp_field): - return True + """ Skip components that doesn't belong to this variant. """ # Apply variants white/black lists if self.variant_field: ref_variant = c.get_field_value(self.variant_field).lower() @@ -85,13 +62,15 @@ def skip_component(self, c): return False def filter(self, comps): - logger.debug("Applying IBoM style filter `{}`".format(self.name)) + logger.debug("Applying IBoM style variants `{}`".format(self.name)) # Make black/white lists case insensitive self.variants_whitelist = [v.lower() for v in self.variants_whitelist] self.variants_blacklist = [v.lower() for v in self.variants_blacklist] # Apply to all the components for c in comps: + if not (c.fitted and c.in_bom): + # Don't check if we already discarded it + continue c.fitted = not self.skip_component(c) - c.fixed = False if not c.fitted and GS.debug_level > 2: logger.debug('ref: {} value: {} -> False'.format(c.ref, c.value)) diff --git a/kibot/var_kibom.py b/kibot/var_kibom.py index 2b606c4ad..069c88e7a 100644 --- a/kibot/var_kibom.py +++ b/kibot/var_kibom.py @@ -8,7 +8,6 @@ """ from .optionable import Optionable from .gs import GS -from .misc import DNF, DNC from .macros import macros, document, variant_class # noqa: F401 from . import log @@ -43,68 +42,7 @@ def config(self): # Config field must be lowercase self.config_field = self.config_field.lower() - @staticmethod - def basic_comp_is_fitted(value, config): - """ Basic `fitted` criteria, no variants. - value: component value (lowercase). - config: content of the 'Config' field (lowercase). """ - # Check the value field first - if value in DNF: - return False - # Empty value means part is fitted - if not config: - return True - # Also support space separated list (simple cases) - opts = config.split(" ") - for opt in opts: - if opt in DNF: - return False - # Normal separator is "," - opts = config.split(",") - for opt in opts: - if opt in DNF: - return False - return True - - @staticmethod - def basic_comp_is_fixed(value, config): - """ Basic `fixed` criteria, no variants - Fixed components shouldn't be replaced without express authorization. - value: component value (lowercase). - config: content of the 'Config' field (lowercase). """ - # Check the value field first - if value in DNC: - return True - # Empty is not fixed - if not config: - return False - # Also support space separated list (simple cases) - opts = config.split(" ") - for opt in opts: - if opt in DNC: - return True - # Normal separator is "," - opts = config.split(",") - for opt in opts: - if opt in DNC: - return True - return False - - @staticmethod - def _base_filter(comps, f_config): - """ Fill the `fixed` and `fitted` using the basic criteria. - No variant is applied in this step. """ - logger.debug("- Generic KiBoM rules") - for c in comps: - value = c.value.lower() - config = c.get_field_value(f_config).lower() - c.fitted = KiBoM.basic_comp_is_fitted(value, config) - if GS.debug_level > 2: - logger.debug('ref: {} value: {} config: {} -> fitted {}'. - format(c.ref, value, config, c.fitted)) - c.fixed = KiBoM.basic_comp_is_fixed(value, config) - - def variant_comp_is_fitted(self, value, config): + def _variant_comp_is_fitted(self, value, config): """ Apply the variants to determine if this component will be fitted. value: component value (lowercase). config: content of the 'Config' field (lowercase). """ @@ -126,16 +64,14 @@ def variant_comp_is_fitted(self, value, config): return not exclusive def filter(self, comps): - logger.debug("Applying KiBoM style filter `{}`".format(self.name)) - self._base_filter(comps, self.config_field) - logger.debug("- Variant specific rules") + logger.debug("Applying KiBoM style variants `{}`".format(self.name)) for c in comps: - if not c.fitted: - # Don't check if we already discarded it during the basic test + if not (c.fitted and c.in_bom): + # Don't check if we already discarded it continue value = c.value.lower() config = c.get_field_value(self.config_field).lower() - c.fitted = self.variant_comp_is_fitted(value, config) + c.fitted = self._variant_comp_is_fitted(value, config) if not c.fitted and GS.debug_level > 2: logger.debug('ref: {} value: {} config: {} variant: {} -> False'. format(c.ref, value, config, self.variant)) From c0a1867dd545f51072d67a6f36e21e433d57833a Mon Sep 17 00:00:00 2001 From: "Salvador E. Tropea" Date: Sat, 29 Aug 2020 17:41:57 -0300 Subject: [PATCH 14/21] Added support for filters to the internal BoM generator. - Now we can configure more details, like the DNC. - We can also mix KiBoM and IBoM strategies. - Adapted the test examples that used filtering. --- kibot/bom/bom.py | 46 +---- kibot/out_bom.py | 191 ++++++++---------- tests/test_plot/test_int_bom.py | 26 +-- .../int_bom_exclude_any.kibot.yaml | 42 ++-- .../int_bom_include_only.kibot.yaml | 12 +- .../int_bom_no_include_only.kibot.yaml | 14 -- 6 files changed, 139 insertions(+), 192 deletions(-) delete mode 100644 tests/yaml_samples/int_bom_no_include_only.kibot.yaml diff --git a/kibot/bom/bom.py b/kibot/bom/bom.py index 72a457e7b..1a4f1f146 100644 --- a/kibot/bom/bom.py +++ b/kibot/bom/bom.py @@ -263,37 +263,6 @@ def get_row(self, columns): return row -def test_reg_exclude(cfg, c): - """ Test if this part should be included, based on any regex expressions provided in the preferences """ - for reg in cfg.exclude_any: - field_value = c.get_field_value(reg.column) - if reg.regex.search(field_value): - if cfg.debug_level > 1: - logger.debug("Excluding '{ref}': Field '{field}' ({value}) matched '{re}'".format( - ref=c.ref, field=reg.column, value=field_value, re=reg.regex)) - # Found a match - return True - # Default, could not find any matches - return False - - -def test_reg_include(cfg, c): - """ Reject components that doesn't match the provided regex. - So we include only the components that matches any of the regexs. """ - if not cfg.include_only: # Nothing to match against, means include all - return True - for reg in cfg.include_only: - field_value = c.get_field_value(reg.column) - if reg.regex.search(field_value): - if cfg.debug_level > 1: - logger.debug("Including '{ref}': Field '{field}' ({value}) matched '{re}'".format( - ref=c.ref, field=reg.column, value=field_value, re=reg.regex)) - # Found a match - return True - # Default, could not find a match - return False - - def get_value_sort(comp): """ Try to better sort R, L and C components """ res = comp.value_sort @@ -325,12 +294,8 @@ def group_components(cfg, components): groups = [] # Iterate through each component, and test whether a group for these already exists for c in components: - if cfg.test_regex: - # Skip components if they do not meet regex requirements - if not test_reg_include(cfg, c): - continue - if test_reg_exclude(cfg, c): - continue + if not c.in_bom: # Skip components marked as excluded from BoM + continue # Cache the value used to sort if c.ref_prefix in RLC_PREFIX and c.value.lower() not in DNF: c.value_sort = comp_match(c.value, c.ref_prefix) @@ -390,7 +355,12 @@ def group_components(cfg, components): def do_bom(file_name, ext, comps, cfg): - # Solve `fixed` and `fitted` attributes for all components + # Apply all the filters + for c in comps: + c.in_bom = cfg.exclude_filter.filter(c) + c.fitted = cfg.dnf_filter.filter(c) + c.fixed = cfg.dnc_filter.filter(c) + # Apply the variant cfg.variant.filter(comps) # Group components according to group_fields groups = group_components(cfg, comps) diff --git a/kibot/out_bom.py b/kibot/out_bom.py index 615e38664..1c557fb29 100644 --- a/kibot/out_bom.py +++ b/kibot/out_bom.py @@ -8,7 +8,6 @@ This is somehow compatible with KiBoM. """ import os -from re import compile, IGNORECASE from .gs import GS from .optionable import Optionable, BaseOptions from .registrable import RegOutput @@ -17,6 +16,7 @@ from .bom.columnlist import ColumnList, BoMError from .bom.bom import do_bom from .var_kibom import KiBoM +from .fil_generic import Generic from . import log logger = log.get_logger(__name__) @@ -30,26 +30,6 @@ ] -# String matches for marking a component as "do not fit" -class BoMRegex(Optionable): - """ Implements the pair column/regex """ - def __init__(self): - super().__init__() - self._unkown_is_error = True - with document: - self.column = '' - """ Name of the column to apply the regular expression """ - self.regex = '' - """ Regular expression to match """ - self.field = None - """ {column} """ - self.regexp = None - """ {regex} """ - -# def __str__(self): -# return self.column+'\t'+self.regex - - class BoMColumns(Optionable): """ Information for the BoM columns """ def __init__(self): @@ -194,15 +174,15 @@ def __init__(self): class BoMOptions(BaseOptions): - DEFAULT_EXCLUDE = [[ColumnList.COL_REFERENCE, '^TP[0-9]*'], - [ColumnList.COL_REFERENCE, '^FID'], - [ColumnList.COL_PART, 'mount.*hole'], - [ColumnList.COL_PART, 'solder.*bridge'], - [ColumnList.COL_PART, 'solder.*jump'], - [ColumnList.COL_PART, 'test.*point'], - [ColumnList.COL_FP, 'test.*point'], - [ColumnList.COL_FP, 'mount.*hole'], - [ColumnList.COL_FP, 'fiducial'], + DEFAULT_EXCLUDE = [{'column': ColumnList.COL_REFERENCE, 'regex': '^TP[0-9]*'}, + {'column': ColumnList.COL_REFERENCE, 'regex': '^FID'}, + {'column': ColumnList.COL_PART, 'regex': 'mount.*hole'}, + {'column': ColumnList.COL_PART, 'regex': 'solder.*bridge'}, + {'column': ColumnList.COL_PART, 'regex': 'solder.*jump'}, + {'column': ColumnList.COL_PART, 'regex': 'test.*point'}, + {'column': ColumnList.COL_FP, 'regex': 'test.*point'}, + {'column': ColumnList.COL_FP, 'regex': 'mount.*hole'}, + {'column': ColumnList.COL_FP, 'regex': 'fiducial'}, ] def __init__(self): @@ -220,18 +200,38 @@ def __init__(self): # Equivalent to KiBoM INI: self.ignore_dnf = True """ Exclude DNF (Do Not Fit) components """ + self.fit_field = 'Config' + """ Field name used for internal filters """ self.use_alt = False """ Print grouped references in the alternate compressed style eg: R1-R7,R18 """ + self.columns = BoMColumns + """ [list(dict)|list(string)] List of columns to display. + Can be just the name of the field """ + self.normalize_values = False + """ Try to normalize the R, L and C values, producing uniform units and prefixes """ + self.normalize_locale = False + """ When normalizing values use the locale decimal point """ + self.html = BoMHTML + """ [dict] Options for the HTML format """ + self.xlsx = BoMXLSX + """ [dict] Options for the XLSX format """ + self.csv = BoMCSV + """ [dict] Options for the CSV, TXT and TSV formats """ + # * Filters + self.exclude_filter = '_mechanical' + """ Name of the filter to exclude components from BoM processing. + The default filter excludes test points, fiducial marks, mounting holes, etc """ + self.dnf_filter = '_kibom_dnf' + """ Name of the filter to mark components as 'Do Not Fit'. + The default filter marks components with a DNF value or DNF in the Config field """ + self.dnc_filter = '_kibom_dnc' + """ Name of the filter to mark components as 'Do Not Change'. + The default filter marks components with a DNC value or DNC in the Config field """ + # * Grouping criteria self.group_connectors = True """ Connectors with the same footprints will be grouped together, independent of the name of the connector """ - self.test_regex = True - """ Each component group will be tested against a number of regular-expressions - (see `include_only` and `exclude_any`) """ self.merge_blank_fields = True """ Component groups with blank fields will be merged into the most compatible group, where possible """ - self.fit_field = 'Config' - """ Field name used to determine if a particular part is to be fitted (also DNC, not for variants). - This value is used only when no variants are specified """ self.group_fields = GroupFields """ [list(string)] List of fields used for sorting individual components into groups. Components which match (comparing *all* fields) will be grouped together. @@ -248,47 +248,6 @@ def __init__(self): - ['sw', 'switch'] - ['zener', 'zenersmall'] - ['d', 'diode', 'd_small'] """ - self.include_only = BoMRegex - """ [list(dict)] A series of regular expressions used to select included parts. - If there are any regex defined here, only components that match against ANY of them will be included. - Column names are case-insensitive. - If empty all the components are included """ - self.exclude_any = BoMRegex - """ [list(dict)] A series of regular expressions used to exclude parts. - If a component matches ANY of these, it will be excluded. - Column names are case-insensitive. - If empty the following list is used: - - column: References - ..regex: '^TP[0-9]*' - - column: References - ..regex: '^FID' - - column: Part - ..regex: 'mount.*hole' - - column: Part - ..regex: 'solder.*bridge' - - column: Part - ..regex: 'solder.*jump' - - column: Part - ..regex: 'test.*point' - - column: Footprint - ..regex: 'test.*point' - - column: Footprint - ..regex: 'mount.*hole' - - column: Footprint - ..regex: 'fiducial' """ - self.columns = BoMColumns - """ [list(dict)|list(string)] List of columns to display. - Can be just the name of the field """ - self.normalize_values = False - """ Try to normalize the R, L and C values, producing uniform units and prefixes """ - self.normalize_locale = False - """ When normalizing values use the locale decimal point """ - self.html = BoMHTML - """ [dict] Options for the HTML format """ - self.xlsx = BoMXLSX - """ [dict] Options for the XLSX format """ - self.csv = BoMCSV - """ [dict] Options for the CSV, TXT and TSV formats """ super().__init__() @staticmethod @@ -311,20 +270,12 @@ def _guess_format(self): # Explicit selection return self.format.lower() - @staticmethod - def _fix_ref_field(field): - """ References -> Reference """ - col = field.lower() - if col == ColumnList.COL_REFERENCE_L: - col = col[:-1] - return col - def _normalize_variant(self): """ Replaces the name of the variant by an object handling it. """ if self.variant: - if self.variant not in RegOutput._def_variants: + if not RegOutput.is_variant(self.variant): raise KiPlotConfigurationError("Unknown variant name `{}`".format(self.variant)) - self.variant = RegOutput._def_variants[self.variant] + self.variant = RegOutput.get_variant(self.variant) else: # If no variant is specified use the KiBoM variant class with basic functionality self.variant = KiBoM() @@ -332,6 +283,44 @@ def _normalize_variant(self): self.variant.variant = [] self.variant.name = 'default' + def _solve_exclude_filter(self): + """ Check we have a valid exclude filter. Create it if needed. """ + if not RegOutput.is_filter(self.exclude_filter): + if self.exclude_filter == '_mechanical': + o = Generic() + o_tree = {'name': '_mechanical', 'type': 'generic', 'comment': 'Internal default mechanical filter'} + o_tree['exclude_any'] = BoMOptions.DEFAULT_EXCLUDE + o.set_tree(o_tree) + o.config() + RegOutput.add_filter(o) + self.exclude_filter = o + return + raise KiPlotConfigurationError("Unknown filter `{}` used for `exclude_filter`".format(self.exclude_filter)) + self.exclude_filter = RegOutput.get_filter(self.exclude_filter) + + def _solve_dnx_filter(self, name, type, invert=False): + real_name = name + if real_name == '_kibom_'+type: + # Allow different internal filters using different config fields + real_name += '_'+self.fit_field + if not RegOutput.is_filter(real_name): + o = Generic() + o_tree = {'name': real_name, 'type': 'generic'} + o_tree['comment'] = 'Internal KiBoM '+type.upper()+' filter ('+self.fit_field+')' + o_tree['config_field'] = self.fit_field + o_tree['exclude_value'] = True + o_tree['exclude_config'] = True + o_tree['keys'] = type+'_list' + if invert: + o_tree['invert'] = True + o.set_tree(o_tree) + o.config() + RegOutput.add_filter(o) + return o + if not RegOutput.is_filter(real_name): + raise KiPlotConfigurationError("Unknown filter `{}` used for `{}_filter`".format(real_name, type)) + return RegOutput.get_filter(real_name) + def config(self): super().config() self.format = self._guess_format() @@ -359,28 +348,16 @@ def config(self): # component_aliases if isinstance(self.component_aliases, type): self.component_aliases = DEFAULT_ALIASES - # include_only - if isinstance(self.include_only, type): - self.include_only = None - else: - for r in self.include_only: - r.regex = compile(r.regex, flags=IGNORECASE) - # exclude_any - if isinstance(self.exclude_any, type): - self.exclude_any = [] - for r in BoMOptions.DEFAULT_EXCLUDE: - o = BoMRegex() - o.column = self._fix_ref_field(r[0]) - o.regex = compile(r[1], flags=IGNORECASE) - self.exclude_any.append(o) - else: - for r in self.exclude_any: - r.column = self._fix_ref_field(r.column) - r.regex = compile(r.regex, flags=IGNORECASE) - # Make the config field name lowercase - self.fit_field = self.fit_field.lower() + # exclude_filter + self._solve_exclude_filter() + # dnf_filter + self.dnf_filter = self._solve_dnx_filter(self.dnf_filter, 'dnf') + # dnc_filter + self.dnc_filter = self._solve_dnx_filter(self.dnc_filter, 'dnc', True) # Variants, make it an object self._normalize_variant() + # Field names are handled in lowercase + self.fit_field = self.fit_field.lower() # Columns self.column_rename = {} self.join = [] diff --git a/tests/test_plot/test_int_bom.py b/tests/test_plot/test_int_bom.py index 2706dd873..8ff12597f 100644 --- a/tests/test_plot/test_int_bom.py +++ b/tests/test_plot/test_int_bom.py @@ -915,19 +915,19 @@ def test_int_bom_include_only(): ctx.clean_up() -def test_int_bom_no_test_regex(): - prj = 'kibom-test' - ext = 'csv' - ctx = context.TestContextSCH('test_int_bom_simple_csv', prj, 'int_bom_no_include_only', BOM_DIR) - ctx.run() - out = prj + '-bom.' + ext - rows, header, info = ctx.load_csv(out) - assert header == KIBOM_TEST_HEAD - ref_column = header.index(REF_COLUMN_NAME) - qty_column = header.index(QTY_COLUMN_NAME) - check_kibom_test_netlist(rows, ref_column, KIBOM_TEST_GROUPS, KIBOM_TEST_EXCLUDE, KIBOM_TEST_COMPONENTS) - check_dnc(rows, 'R7', ref_column, qty_column) - ctx.clean_up() +# def test_int_bom_no_test_regex(): +# prj = 'kibom-test' +# ext = 'csv' +# ctx = context.TestContextSCH('test_int_bom_simple_csv', prj, 'int_bom_no_include_only', BOM_DIR) +# ctx.run() +# out = prj + '-bom.' + ext +# rows, header, info = ctx.load_csv(out) +# assert header == KIBOM_TEST_HEAD +# ref_column = header.index(REF_COLUMN_NAME) +# qty_column = header.index(QTY_COLUMN_NAME) +# check_kibom_test_netlist(rows, ref_column, KIBOM_TEST_GROUPS, KIBOM_TEST_EXCLUDE, KIBOM_TEST_COMPONENTS) +# check_dnc(rows, 'R7', ref_column, qty_column) +# ctx.clean_up() def test_int_bom_sub_sheet_alt(): diff --git a/tests/yaml_samples/int_bom_exclude_any.kibot.yaml b/tests/yaml_samples/int_bom_exclude_any.kibot.yaml index 24c0fd8bb..d5e3706d5 100644 --- a/tests/yaml_samples/int_bom_exclude_any.kibot.yaml +++ b/tests/yaml_samples/int_bom_exclude_any.kibot.yaml @@ -2,26 +2,34 @@ kibot: version: 1 +filters: + - name: 'exclude_any' + type: 'generic' + comment: 'Almost same as KiBoM, no fiducial' + exclude_any: + - column: References + regex: '^TP[0-9]*' + - column: References + regex: '^FID' + - column: Part + regex: 'mount.*hole' + - column: Part + regex: 'solder.*bridge' + - column: Part + regex: 'solder.*jump' + - column: Part + regex: 'test.*point' + - column: Footprint + regex: 'test.*point' + - column: Footprint + regex: 'mount.*hole' + + outputs: - name: 'bom_internal' comment: "Bill of Materials in CSV format" type: bom dir: BoM options: - exclude_any: - - column: References - regex: '^TP[0-9]*' - - column: References - regex: '^FID' - - column: Part - regex: 'mount.*hole' - - column: Part - regex: 'solder.*bridge' - - column: Part - regex: 'solder.*jump' - - column: Part - regex: 'test.*point' - - column: Footprint - regex: 'test.*point' - - column: Footprint - regex: 'mount.*hole' + exclude_filter: 'exclude_any' + diff --git a/tests/yaml_samples/int_bom_include_only.kibot.yaml b/tests/yaml_samples/int_bom_include_only.kibot.yaml index b1515fece..b8520ab2d 100644 --- a/tests/yaml_samples/int_bom_include_only.kibot.yaml +++ b/tests/yaml_samples/int_bom_include_only.kibot.yaml @@ -2,12 +2,18 @@ kibot: version: 1 +filters: + - name: 'include_only' + type: 'generic' + comment: 'Test for include_only' + include_only: + - column: 'Footprint' + regex: '0805' + outputs: - name: 'bom_internal' comment: "Bill of Materials in CSV format" type: bom dir: BoM options: - include_only: - - column: 'Footprint' - regex: '0805' + exclude_filter: 'include_only' diff --git a/tests/yaml_samples/int_bom_no_include_only.kibot.yaml b/tests/yaml_samples/int_bom_no_include_only.kibot.yaml deleted file mode 100644 index 049c479be..000000000 --- a/tests/yaml_samples/int_bom_no_include_only.kibot.yaml +++ /dev/null @@ -1,14 +0,0 @@ -# Example KiBot config file -kibot: - version: 1 - -outputs: - - name: 'bom_internal' - comment: "Bill of Materials in CSV format" - type: bom - dir: BoM - options: - test_regex: false - include_only: - - column: 'Footprint' - regex: '0805' From 4cc5494f5fdf520ba18e61d30ec3b9bb52c2c57b Mon Sep 17 00:00:00 2001 From: "Salvador E. Tropea" Date: Sat, 29 Aug 2020 17:43:49 -0300 Subject: [PATCH 15/21] Updated the README and generic example. - Functionality of IBoM that I reincorporated. - Support for filters in internal BoM. --- README.md | 55 ++++++--------------- docs/samples/generic_plot.kibot.yaml | 71 ++++++++++------------------ 2 files changed, 38 insertions(+), 88 deletions(-) diff --git a/README.md b/README.md index e02c3a817..876000842 100644 --- a/README.md +++ b/README.md @@ -371,35 +371,13 @@ Next time you need this list just use an alias, like this: - `hide_stats_info`: [boolean=false] Hide statistics information. - `quote_all`: [boolean=false] Enclose all values using double quotes. - `separator`: [string=','] CSV Separator. TXT and TSV always use tab as delimiter. - - `exclude_any`: [list(dict)] A series of regular expressions used to exclude parts. - If a component matches ANY of these, it will be excluded. - Column names are case-insensitive. - If empty the following list is used: - - column: References - regex: '^TP[0-9]*' - - column: References - regex: '^FID' - - column: Part - regex: 'mount.*hole' - - column: Part - regex: 'solder.*bridge' - - column: Part - regex: 'solder.*jump' - - column: Part - regex: 'test.*point' - - column: Footprint - regex: 'test.*point' - - column: Footprint - regex: 'mount.*hole' - - column: Footprint - regex: 'fiducial'. - * Valid keys: - - `column`: [string=''] Name of the column to apply the regular expression. - - *field*: Alias for column. - - `regex`: [string=''] Regular expression to match. - - *regexp*: Alias for regex. - - `fit_field`: [string='Config'] Field name used to determine if a particular part is to be fitted (also DNC, not for variants). - This value is used only when no variants are specified. + - `dnc_filter`: [string='_kibom_dnc'] Name of the filter to mark components as 'Do Not Change'. + The default filter marks components with a DNC value or DNC in the Config field. + - `dnf_filter`: [string='_kibom_dnf'] Name of the filter to mark components as 'Do Not Fit'. + The default filter marks components with a DNF value or DNF in the Config field. + - `exclude_filter`: [string='_mechanical'] Name of the filter to exclude components from BoM processing. + The default filter excludes test points, fiducial marks, mounting holes, etc. + - `fit_field`: [string='Config'] Field name used for internal filters. - `format`: [string=''] [HTML,CSV,TXT,TSV,XML,XLSX] format for the BoM. If empty defaults to CSV or a guess according to the options.. - `group_connectors`: [boolean=true] Connectors with the same footprints will be grouped together, independent of the name of the connector. @@ -421,22 +399,11 @@ Next time you need this list just use an alias, like this: Or you can provide a CSS file name. Please use .css as file extension.. - `title`: [string='KiBot Bill of Materials'] BoM title. - `ignore_dnf`: [boolean=true] Exclude DNF (Do Not Fit) components. - - `include_only`: [list(dict)] A series of regular expressions used to select included parts. - If there are any regex defined here, only components that match against ANY of them will be included. - Column names are case-insensitive. - If empty all the components are included. - * Valid keys: - - `column`: [string=''] Name of the column to apply the regular expression. - - *field*: Alias for column. - - `regex`: [string=''] Regular expression to match. - - *regexp*: Alias for regex. - `merge_blank_fields`: [boolean=true] Component groups with blank fields will be merged into the most compatible group, where possible. - `normalize_locale`: [boolean=false] When normalizing values use the locale decimal point. - `normalize_values`: [boolean=false] Try to normalize the R, L and C values, producing uniform units and prefixes. - `number`: [number=1] Number of boards to build (components multiplier). - `output`: [string='%f-%i%v.%x'] filename for the output (%i=bom). Affected by global options. - - `test_regex`: [boolean=true] Each component group will be tested against a number of regular-expressions - (see `include_only` and `exclude_any`). - `use_alt`: [boolean=false] Print grouped references in the alternate compressed style eg: R1-R7,R18. - `variant`: [string=''] Board variant(s), used to determine which components are output to the BoM.. @@ -612,12 +579,15 @@ Next time you need this list just use an alias, like this: - `name`: [string=''] Used to identify this particular output definition. - `options`: [dict] Options for the `ibom` output. * Valid keys: + - `blacklist`: [string=''] List of comma separated blacklisted components or prefixes with *. E.g. 'X1,MH*'. + - `blacklist_empty_val`: [boolean=false] Blacklist components with empty value. - `board_rotation`: [number=0] Board rotation in degrees (-180 to 180). Will be rounded to multiple of 5. - `bom_view`: [string='left-right'] [bom-only,left-right,top-bottom] Default BOM view. - `checkboxes`: [string='Sourced,Placed'] Comma separated list of checkbox columns. - `dark_mode`: [boolean=false] Default to dark mode. + - `dnp_field`: [string=''] Name of the extra field that indicates do not populate status. Components with this field not empty will be + blacklisted. - `extra_fields`: [string=''] Comma separated list of extra fields to pull from netlist or xml file. - These are extra columns in the BoM. - `hide_pads`: [boolean=false] Hide footprint pads by default. - `hide_silkscreen`: [boolean=false] Hide silkscreen by default. - `highlight_pin1`: [boolean=false] Highlight pin1 by default. @@ -641,6 +611,9 @@ Next time you need this list just use an alias, like this: - `output`: [string='%f-%i%v.%x'] Filename for the output, use '' to use the IBoM filename (%i=ibom, %x=html). Affected by global options. - `show_fabrication`: [boolean=false] Show fabrication layer by default. - `sort_order`: [string='C,R,L,D,U,Y,X,F,SW,A,~,HS,CNN,J,P,NT,MH'] Default sort order for components. Must contain '~' once. + - `variant_field`: [string=''] Name of the extra field that stores board variant for component. + - `variants_blacklist`: [string=''] List of board variants to exclude from the BOM. + - `variants_whitelist`: [string=''] List of board variants to include in the BOM. * KiBoM (KiCad Bill of Materials) * Type: `kibom` diff --git a/docs/samples/generic_plot.kibot.yaml b/docs/samples/generic_plot.kibot.yaml index 600cc1c8b..e90939eb4 100644 --- a/docs/samples/generic_plot.kibot.yaml +++ b/docs/samples/generic_plot.kibot.yaml @@ -63,37 +63,16 @@ outputs: quote_all: false # [string=','] CSV Separator. TXT and TSV always use tab as delimiter separator: ',' - # [list(dict)] A series of regular expressions used to exclude parts. - # If a component matches ANY of these, it will be excluded. - # Column names are case-insensitive. - # If empty the following list is used: - # - column: References - # regex: '^TP[0-9]*' - # - column: References - # regex: '^FID' - # - column: Part - # regex: 'mount.*hole' - # - column: Part - # regex: 'solder.*bridge' - # - column: Part - # regex: 'solder.*jump' - # - column: Part - # regex: 'test.*point' - # - column: Footprint - # regex: 'test.*point' - # - column: Footprint - # regex: 'mount.*hole' - # - column: Footprint - # regex: 'fiducial' - exclude_any: - # [string=''] Name of the column to apply the regular expression - column: '' - # `field` is an alias for `column` - # [string=''] Regular expression to match - regex: '' - # `regexp` is an alias for `regex` - # [string='Config'] Field name used to determine if a particular part is to be fitted (also DNC, not for variants). - # This value is used only when no variants are specified + # [string='_kibom_dnc'] Name of the filter to mark components as 'Do Not Change'. + # The default filter marks components with a DNC value or DNC in the Config field + dnc_filter: '_kibom_dnc' + # [string='_kibom_dnf'] Name of the filter to mark components as 'Do Not Fit'. + # The default filter marks components with a DNF value or DNF in the Config field + dnf_filter: '_kibom_dnf' + # [string='_mechanical'] Name of the filter to exclude components from BoM processing. + # The default filter excludes test points, fiducial marks, mounting holes, etc + exclude_filter: '_mechanical' + # [string='Config'] Field name used for internal filters fit_field: 'Config' # [string=''] [HTML,CSV,TXT,TSV,XML,XLSX] format for the BoM. # If empty defaults to CSV or a guess according to the options. @@ -130,17 +109,6 @@ outputs: title: 'KiBot Bill of Materials' # [boolean=true] Exclude DNF (Do Not Fit) components ignore_dnf: true - # [list(dict)] A series of regular expressions used to select included parts. - # If there are any regex defined here, only components that match against ANY of them will be included. - # Column names are case-insensitive. - # If empty all the components are included - include_only: - # [string=''] Name of the column to apply the regular expression - column: '' - # `field` is an alias for `column` - # [string=''] Regular expression to match - regex: '' - # `regexp` is an alias for `regex` # [boolean=true] Component groups with blank fields will be merged into the most compatible group, where possible merge_blank_fields: true # [boolean=false] When normalizing values use the locale decimal point @@ -151,9 +119,6 @@ outputs: number: 1 # [string='%f-%i%v.%x'] filename for the output (%i=bom). Affected by global options output: '%f-%i%v.%x' - # [boolean=true] Each component group will be tested against a number of regular-expressions - # (see `include_only` and `exclude_any`) - test_regex: true # [boolean=false] Print grouped references in the alternate compressed style eg: R1-R7,R18 use_alt: false # [string=''] Board variant(s), used to determine which components @@ -368,6 +333,10 @@ outputs: type: 'ibom' dir: 'Example/ibom_dir' options: + # [string=''] List of comma separated blacklisted components or prefixes with *. E.g. 'X1,MH*' + blacklist: '' + # [boolean=false] Blacklist components with empty value + blacklist_empty_val: false # [number=0] Board rotation in degrees (-180 to 180). Will be rounded to multiple of 5 board_rotation: 0 # [string='left-right'] [bom-only,left-right,top-bottom] Default BOM view @@ -376,8 +345,10 @@ outputs: checkboxes: 'Sourced,Placed' # [boolean=false] Default to dark mode dark_mode: false - # [string=''] Comma separated list of extra fields to pull from netlist or xml file. - # These are extra columns in the BoM + # [string=''] Name of the extra field that indicates do not populate status. Components with this field not empty will be + # blacklisted + dnp_field: '' + # [string=''] Comma separated list of extra fields to pull from netlist or xml file extra_fields: '' # [boolean=false] Hide footprint pads by default hide_pads: false @@ -416,6 +387,12 @@ outputs: show_fabrication: false # [string='C,R,L,D,U,Y,X,F,SW,A,~,HS,CNN,J,P,NT,MH'] Default sort order for components. Must contain '~' once sort_order: 'C,R,L,D,U,Y,X,F,SW,A,~,HS,CNN,J,P,NT,MH' + # [string=''] Name of the extra field that stores board variant for component + variant_field: '' + # [string=''] List of board variants to exclude from the BOM + variants_blacklist: '' + # [string=''] List of board variants to include in the BOM + variants_whitelist: '' # KiBoM (KiCad Bill of Materials): # For more information: https://github.com/INTI-CMNB/KiBoM From a1455e0f4634cae45127a175994d9293981ca9c8 Mon Sep 17 00:00:00 2001 From: "Salvador E. Tropea" Date: Sat, 29 Aug 2020 19:32:04 -0300 Subject: [PATCH 16/21] Added more flexibility to filters. Support for: - Pass all - Negated (in addition to its internal option) - List of filters --- README.md | 6 +- docs/samples/generic_plot.kibot.yaml | 6 +- kibot/fil_base.py | 98 +++++++++++++++++++++++++++- kibot/out_bom.py | 84 +++++++++++------------- 4 files changed, 140 insertions(+), 54 deletions(-) diff --git a/README.md b/README.md index 876000842..6740c4eaa 100644 --- a/README.md +++ b/README.md @@ -371,11 +371,11 @@ Next time you need this list just use an alias, like this: - `hide_stats_info`: [boolean=false] Hide statistics information. - `quote_all`: [boolean=false] Enclose all values using double quotes. - `separator`: [string=','] CSV Separator. TXT and TSV always use tab as delimiter. - - `dnc_filter`: [string='_kibom_dnc'] Name of the filter to mark components as 'Do Not Change'. + - `dnc_filter`: [string|list(string)='_kibom_dnc'] Name of the filter to mark components as 'Do Not Change'. The default filter marks components with a DNC value or DNC in the Config field. - - `dnf_filter`: [string='_kibom_dnf'] Name of the filter to mark components as 'Do Not Fit'. + - `dnf_filter`: [string|list(string)='_kibom_dnf'] Name of the filter to mark components as 'Do Not Fit'. The default filter marks components with a DNF value or DNF in the Config field. - - `exclude_filter`: [string='_mechanical'] Name of the filter to exclude components from BoM processing. + - `exclude_filter`: [string|list(string)='_mechanical'] Name of the filter to exclude components from BoM processing. The default filter excludes test points, fiducial marks, mounting holes, etc. - `fit_field`: [string='Config'] Field name used for internal filters. - `format`: [string=''] [HTML,CSV,TXT,TSV,XML,XLSX] format for the BoM. diff --git a/docs/samples/generic_plot.kibot.yaml b/docs/samples/generic_plot.kibot.yaml index e90939eb4..936aee86b 100644 --- a/docs/samples/generic_plot.kibot.yaml +++ b/docs/samples/generic_plot.kibot.yaml @@ -63,13 +63,13 @@ outputs: quote_all: false # [string=','] CSV Separator. TXT and TSV always use tab as delimiter separator: ',' - # [string='_kibom_dnc'] Name of the filter to mark components as 'Do Not Change'. + # [string|list(string)='_kibom_dnc'] Name of the filter to mark components as 'Do Not Change'. # The default filter marks components with a DNC value or DNC in the Config field dnc_filter: '_kibom_dnc' - # [string='_kibom_dnf'] Name of the filter to mark components as 'Do Not Fit'. + # [string|list(string)='_kibom_dnf'] Name of the filter to mark components as 'Do Not Fit'. # The default filter marks components with a DNF value or DNF in the Config field dnf_filter: '_kibom_dnf' - # [string='_mechanical'] Name of the filter to exclude components from BoM processing. + # [string|list(string)='_mechanical'] Name of the filter to exclude components from BoM processing. # The default filter excludes test points, fiducial marks, mounting holes, etc exclude_filter: '_mechanical' # [string='Config'] Field name used for internal filters diff --git a/kibot/fil_base.py b/kibot/fil_base.py index 7c2c3a6c0..c18964a09 100644 --- a/kibot/fil_base.py +++ b/kibot/fil_base.py @@ -3,10 +3,53 @@ # Copyright (c) 2020 Instituto Nacional de Tecnología Industrial # License: GPL-3.0 # Project: KiBot (formerly KiPlot) -from .registrable import RegFilter +from .registrable import RegFilter, Registrable, RegOutput +from .error import KiPlotConfigurationError from .macros import macros, document # noqa: F401 +class DummyFilter(Registrable): + """ A filter that allows all """ + def __init__(self): + super().__init__() + self.name = 'Dummy' + self.type = 'dummy' + self.comment = 'A filter that does nothing' + + def filter(self, comp): + return True + + +class MultiFilter(Registrable): + """ A filter containing a list of filters. + They are applied in sequence. """ + def __init__(self, filters): + super().__init__() + self.name = ','.join([f.name for f in filters]) + self.type = ','.join([f.type for f in filters]) + self.comment = 'Multi-filter' + self.filters = filters + + def filter(self, comp): + for f in self.filters: + if not f.filter(comp): + return False + return True + + +class NotFilter(Registrable): + """ A filter that returns the inverted result """ + def __init__(self, filter): + super().__init__() + self.name = filter.name + self.type = '!'+filter.type + self.comment = filter.comment + self.filter = filter + + def filter(self, comp): + return not self.filter(comp) + + class BaseFilter(RegFilter): def __init__(self): super().__init__() @@ -18,3 +61,56 @@ def __init__(self): """ Type of filter """ self.comment = '' """ A comment for documentation purposes """ + + @staticmethod + def solve_filter(name, def_key, def_real, creator, target_name): + """ Name can be: + - A class, meaning we have to use a default. + - A string, the name of a filter. + - A list of strings, the name of 1 or more filters. + If any of the names matches def_key we call creator asking to create the filter. + If def_real is not None we pass this name to creator. """ + if isinstance(name, type): + # Nothing specified, use the default + names = [def_key] + elif isinstance(name, str): + # User provided, but only one, make a list + names = [name] + # Here we should have a list of strings + filters = [] + for name in names: + if name[0] == '!': + invert = True + name = name[1:] + else: + invert = False + filter = None + if name == def_key: + # Matched the default name, translate it to the real name + if def_real: + name = def_real + # Is already defined? + if RegOutput.is_filter(name): + filter = RegOutput.get_filter(name) + else: # Nope, create it + tree = creator(name) + filter = RegFilter.get_class_for(tree['type'])() + filter.set_tree(tree) + filter.config() + RegOutput.add_filter(filter) + elif name: + # A filter that is supposed to exist + if not RegOutput.is_filter(name): + raise KiPlotConfigurationError("Unknown filter `{}` used for `{}`".format(name, target_name)) + filter = RegOutput.get_filter(name) + if filter: + if invert: + filters.append(NotFilter(filter)) + else: + filters.append(filter) + # Finished collecting filters + if not filters: + return DummyFilter() + if len(filters) == 1: + return filters[0] + return MultiFilter(filters) diff --git a/kibot/out_bom.py b/kibot/out_bom.py index 1c557fb29..5b080bde3 100644 --- a/kibot/out_bom.py +++ b/kibot/out_bom.py @@ -16,7 +16,7 @@ from .bom.columnlist import ColumnList, BoMError from .bom.bom import do_bom from .var_kibom import KiBoM -from .fil_generic import Generic +from .fil_base import BaseFilter from . import log logger = log.get_logger(__name__) @@ -218,14 +218,14 @@ def __init__(self): self.csv = BoMCSV """ [dict] Options for the CSV, TXT and TSV formats """ # * Filters - self.exclude_filter = '_mechanical' - """ Name of the filter to exclude components from BoM processing. + self.exclude_filter = Optionable + """ [string|list(string)='_mechanical'] Name of the filter to exclude components from BoM processing. The default filter excludes test points, fiducial marks, mounting holes, etc """ - self.dnf_filter = '_kibom_dnf' - """ Name of the filter to mark components as 'Do Not Fit'. + self.dnf_filter = Optionable + """ [string|list(string)='_kibom_dnf'] Name of the filter to mark components as 'Do Not Fit'. The default filter marks components with a DNF value or DNF in the Config field """ - self.dnc_filter = '_kibom_dnc' - """ Name of the filter to mark components as 'Do Not Change'. + self.dnc_filter = Optionable + """ [string|list(string)='_kibom_dnc'] Name of the filter to mark components as 'Do Not Change'. The default filter marks components with a DNC value or DNC in the Config field """ # * Grouping criteria self.group_connectors = True @@ -283,43 +283,30 @@ def _normalize_variant(self): self.variant.variant = [] self.variant.name = 'default' - def _solve_exclude_filter(self): - """ Check we have a valid exclude filter. Create it if needed. """ - if not RegOutput.is_filter(self.exclude_filter): - if self.exclude_filter == '_mechanical': - o = Generic() - o_tree = {'name': '_mechanical', 'type': 'generic', 'comment': 'Internal default mechanical filter'} - o_tree['exclude_any'] = BoMOptions.DEFAULT_EXCLUDE - o.set_tree(o_tree) - o.config() - RegOutput.add_filter(o) - self.exclude_filter = o - return - raise KiPlotConfigurationError("Unknown filter `{}` used for `exclude_filter`".format(self.exclude_filter)) - self.exclude_filter = RegOutput.get_filter(self.exclude_filter) - - def _solve_dnx_filter(self, name, type, invert=False): - real_name = name - if real_name == '_kibom_'+type: - # Allow different internal filters using different config fields - real_name += '_'+self.fit_field - if not RegOutput.is_filter(real_name): - o = Generic() - o_tree = {'name': real_name, 'type': 'generic'} - o_tree['comment'] = 'Internal KiBoM '+type.upper()+' filter ('+self.fit_field+')' - o_tree['config_field'] = self.fit_field - o_tree['exclude_value'] = True - o_tree['exclude_config'] = True - o_tree['keys'] = type+'_list' - if invert: - o_tree['invert'] = True - o.set_tree(o_tree) - o.config() - RegOutput.add_filter(o) - return o - if not RegOutput.is_filter(real_name): - raise KiPlotConfigurationError("Unknown filter `{}` used for `{}_filter`".format(real_name, type)) - return RegOutput.get_filter(real_name) + @staticmethod + def _create_mechanical(name): + o_tree = {'name': name} + o_tree['type'] = 'generic' + o_tree['comment'] = 'Internal default mechanical filter' + o_tree['exclude_any'] = BoMOptions.DEFAULT_EXCLUDE + logger.debug('Creating internal filter: '+str(o_tree)) + return o_tree + + @staticmethod + def _create_kibom_dnx(name): + type = name[7:10] + subtype = name[11:] + o_tree = {'name': name} + o_tree['type'] = 'generic' + o_tree['comment'] = 'Internal KiBoM '+type.upper()+' filter ('+subtype+')' + o_tree['config_field'] = subtype + o_tree['exclude_value'] = True + o_tree['exclude_config'] = True + o_tree['keys'] = type+'_list' + if type[-1] == 'c': + o_tree['invert'] = True + logger.debug('Creating internal filter: '+str(o_tree)) + return o_tree def config(self): super().config() @@ -349,11 +336,14 @@ def config(self): if isinstance(self.component_aliases, type): self.component_aliases = DEFAULT_ALIASES # exclude_filter - self._solve_exclude_filter() + self.exclude_filter = BaseFilter.solve_filter(self.exclude_filter, '_mechanical', None, + BoMOptions._create_mechanical, 'exclude_filter') # dnf_filter - self.dnf_filter = self._solve_dnx_filter(self.dnf_filter, 'dnf') + self.dnf_filter = BaseFilter.solve_filter(self.dnf_filter, '_kibom_dnf', '_kibom_dnf_'+self.fit_field, + BoMOptions._create_kibom_dnx, 'dnf_filter') # dnc_filter - self.dnc_filter = self._solve_dnx_filter(self.dnc_filter, 'dnc', True) + self.dnc_filter = BaseFilter.solve_filter(self.dnc_filter, '_kibom_dnc', '_kibom_dnc_'+self.fit_field, + BoMOptions._create_kibom_dnx, 'dnc_filter') # Variants, make it an object self._normalize_variant() # Field names are handled in lowercase From 58430d86113cab69a9f3ab83aac67b8e0cdaa39e Mon Sep 17 00:00:00 2001 From: "Salvador E. Tropea" Date: Sun, 30 Aug 2020 11:53:21 -0300 Subject: [PATCH 17/21] Fixed errors in Not and Multi Filters implementation. --- kibot/fil_base.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/kibot/fil_base.py b/kibot/fil_base.py index c18964a09..39ccdc6cc 100644 --- a/kibot/fil_base.py +++ b/kibot/fil_base.py @@ -44,10 +44,10 @@ def __init__(self, filter): self.name = filter.name self.type = '!'+filter.type self.comment = filter.comment - self.filter = filter + self._filter = filter def filter(self, comp): - return not self.filter(comp) + return not self._filter.filter(comp) class BaseFilter(RegFilter): @@ -63,23 +63,23 @@ def __init__(self): """ A comment for documentation purposes """ @staticmethod - def solve_filter(name, def_key, def_real, creator, target_name): + def solve_filter(names, def_key, def_real, creator, target_name): """ Name can be: - A class, meaning we have to use a default. - A string, the name of a filter. - A list of strings, the name of 1 or more filters. If any of the names matches def_key we call creator asking to create the filter. If def_real is not None we pass this name to creator. """ - if isinstance(name, type): + if isinstance(names, type): # Nothing specified, use the default names = [def_key] - elif isinstance(name, str): + elif isinstance(names, str): # User provided, but only one, make a list - names = [name] + names = [names] # Here we should have a list of strings filters = [] for name in names: - if name[0] == '!': + if name and name[0] == '!': invert = True name = name[1:] else: From 4ed499531b0f48894e1b840fdea90330ad3e08a8 Mon Sep 17 00:00:00 2001 From: "Salvador E. Tropea" Date: Sun, 30 Aug 2020 11:54:15 -0300 Subject: [PATCH 18/21] Removed dead code. --- kibot/config_reader.py | 10 ++++++---- kibot/fil_generic.py | 4 ++-- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/kibot/config_reader.py b/kibot/config_reader.py index 3726dcd00..83c3cbeff 100644 --- a/kibot/config_reader.py +++ b/kibot/config_reader.py @@ -109,10 +109,12 @@ def _parse_variant(self, o_tree, kind, reg_class): logger.debug("Parsing "+kind+" "+name_type) o_var = reg_class.get_class_for(otype)() o_var.set_tree(o_tree) - try: - o_var.config() - except KiPlotConfigurationError as e: - config_error("In section `"+name_type+"`: "+str(e)) + # No errors yet +# try: +# o_var.config() +# except KiPlotConfigurationError as e: +# config_error("In section `"+name_type+"`: "+str(e)) + o_var.config() return o_var def _parse_variants(self, v): diff --git a/kibot/fil_generic.py b/kibot/fil_generic.py index 1d173dd00..9d6a2fefb 100644 --- a/kibot/fil_generic.py +++ b/kibot/fil_generic.py @@ -21,8 +21,8 @@ class DNFList(Optionable): _default = DNF - def __init__(self): - super().__init__() +# def __init__(self): +# super().__init__() @filter_class From 41c66b1ab498e3112005a2d83b5411383963d7b8 Mon Sep 17 00:00:00 2001 From: "Salvador E. Tropea" Date: Sun, 30 Aug 2020 11:54:34 -0300 Subject: [PATCH 19/21] Added support for '~' as empty value. --- kibot/bom/bom.py | 9 ++++++++- kibot/bom/units.py | 6 +++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/kibot/bom/bom.py b/kibot/bom/bom.py index 1a4f1f146..4f3f76c5a 100644 --- a/kibot/bom/bom.py +++ b/kibot/bom/bom.py @@ -26,8 +26,15 @@ def compare_value(c1, c2, cfg): """ Compare the value of two components """ + c1_value = c1.value.strip().lower() + c2_value = c2.value.strip().lower() + # '~' is the same as empty for KiCad + if c1_value == '~': + c1_value = '' + if c2_value == '~': + c2_value = '' # Simple string comparison - if c1.value.lower() == c2.value.lower(): + if c1_value == c2_value: return True # Otherwise, perform a more complicated value comparison if compare_values(c1, c2): diff --git a/kibot/bom/units.py b/kibot/bom/units.py index 125f1c713..f4d456173 100644 --- a/kibot/bom/units.py +++ b/kibot/bom/units.py @@ -103,8 +103,12 @@ def comp_match(component, ref_prefix): e.g. comp_match('10R2') returns (10, R) e.g. comp_match('3.3mOhm') returns (0.0033, R) """ - original = component + # Remove useless spaces + component = component.strip() + # ~ is the same as empty for KiCad + if component == '~': + component = '' # Convert the decimal point from the current locale to a '.' global decimal_point if decimal_point is None: From 0f0aa71baabfd1a05817a4b7689bcddd6e2cae2c Mon Sep 17 00:00:00 2001 From: "Salvador E. Tropea" Date: Sun, 30 Aug 2020 11:55:35 -0300 Subject: [PATCH 20/21] Added more tests for the filters and variants --- tests/board_samples/kibom-test-4.sch | 116 ++++++++++++++++++ tests/test_plot/test_int_bom.py | 61 ++++++++- tests/test_plot/test_yaml_errors.py | 49 ++++++++ .../yaml_samples/error_fil_no_list.kibot.yaml | 6 + .../yaml_samples/error_fil_unknown.kibot.yaml | 14 +++ .../error_var_empty_name.kibot.yaml | 6 + .../yaml_samples/error_var_no_list.kibot.yaml | 6 + .../yaml_samples/error_var_no_name.kibot.yaml | 5 + .../yaml_samples/error_var_no_type.kibot.yaml | 6 + .../error_var_wrong_type.kibot.yaml | 6 + tests/yaml_samples/int_bom_fil_1.kibot.yaml | 86 +++++++++++++ .../yaml_samples/int_bom_fil_dummy.kibot.yaml | 13 ++ .../int_bom_var_t2i_csv.kibot.yaml | 1 + .../int_bom_var_t2is_csv.kibot.yaml | 27 ++++ .../int_bom_var_t2s_csv.kibot.yaml | 31 +++++ .../int_bom_wrong_variant.kibot.yaml | 11 ++ 16 files changed, 442 insertions(+), 2 deletions(-) create mode 100644 tests/board_samples/kibom-test-4.sch create mode 100644 tests/yaml_samples/error_fil_no_list.kibot.yaml create mode 100644 tests/yaml_samples/error_fil_unknown.kibot.yaml create mode 100644 tests/yaml_samples/error_var_empty_name.kibot.yaml create mode 100644 tests/yaml_samples/error_var_no_list.kibot.yaml create mode 100644 tests/yaml_samples/error_var_no_name.kibot.yaml create mode 100644 tests/yaml_samples/error_var_no_type.kibot.yaml create mode 100644 tests/yaml_samples/error_var_wrong_type.kibot.yaml create mode 100644 tests/yaml_samples/int_bom_fil_1.kibot.yaml create mode 100644 tests/yaml_samples/int_bom_fil_dummy.kibot.yaml create mode 100644 tests/yaml_samples/int_bom_var_t2is_csv.kibot.yaml create mode 100644 tests/yaml_samples/int_bom_var_t2s_csv.kibot.yaml create mode 100644 tests/yaml_samples/int_bom_wrong_variant.kibot.yaml diff --git a/tests/board_samples/kibom-test-4.sch b/tests/board_samples/kibom-test-4.sch new file mode 100644 index 000000000..e583b4026 --- /dev/null +++ b/tests/board_samples/kibom-test-4.sch @@ -0,0 +1,116 @@ +EESchema Schematic File Version 4 +EELAYER 30 0 +EELAYER END +$Descr A4 11693 8268 +encoding utf-8 +Sheet 1 1 +Title "KiBot Filters Test Schematic" +Date "2020-08-30" +Rev "r1" +Comp "https://github.com/INTI-CMNB/KiBot" +Comment1 "" +Comment2 "" +Comment3 "" +Comment4 "" +$EndDescr +$Comp +L Device:R R1 +U 1 1 5E6A2873 +P 2200 2550 +F 0 "R1" V 2280 2550 50 0000 C CNN +F 1 " " V 2200 2550 50 0000 C CNN +F 2 "Resistor_SMD:R_0805_2012Metric" V 2130 2550 50 0001 C CNN +F 3 "~" H 2200 2550 50 0001 C CNN + 1 2200 2550 + 1 0 0 -1 +$EndComp +$Comp +L Device:R R2 +U 1 1 5E6A330D +P 2500 2550 +F 0 "R2" V 2580 2550 50 0000 C CNN +F 1 "~" V 2500 2550 50 0000 C CNN +F 2 "Resistor_SMD:R_0805_2012Metric" V 2430 2550 50 0001 C CNN +F 3 "~" H 2500 2550 50 0001 C CNN + 1 2500 2550 + 1 0 0 -1 +$EndComp +Text Notes 2750 2600 0 50 ~ 0 +Empty value +$Comp +L Device:R R3 +U 1 1 5E6A3CA0 +P 2200 3100 +F 0 "R3" V 2280 3100 50 0000 C CNN +F 1 "4K7" V 2200 3100 50 0000 C CNN +F 2 "Resistor_SMD:R_0805_2012Metric" V 2130 3100 50 0001 C CNN +F 3 "~" H 2200 3100 50 0001 C CNN +F 4 "" V 2200 3100 50 0001 C CNN "K K" + 1 2200 3100 + 1 0 0 -1 +$EndComp +$Comp +L Device:R R4 +U 1 1 5E6A3F38 +P 2500 3100 +F 0 "R4" V 2580 3100 50 0000 C CNN +F 1 "4700" V 2500 3100 50 0000 C CNN +F 2 "Resistor_SMD:R_0805_2012Metric" V 2430 3100 50 0001 C CNN +F 3 "http://www.google.com/" H 2500 3100 50 0001 C CNN +F 4 "" V 2500 3100 50 0001 C CNN "Q,Q" + 1 2500 3100 + 1 0 0 -1 +$EndComp +Text Notes 2750 3150 0 50 ~ 0 +With "K K" and "Q,Q" field +$Comp +L Device:R R5 +U 1 1 5E6A448B +P 2200 3650 +F 0 "R5" V 2280 3650 50 0000 C CNN +F 1 "1k" V 2200 3650 50 0000 C CNN +F 2 "Resistor_SMD:R_0603_1608Metric" V 2130 3650 50 0001 C CNN +F 3 "~" H 2200 3650 50 0001 C CNN +F 4 "K K" V 2200 3650 50 0001 C CNN "BB" + 1 2200 3650 + 1 0 0 -1 +$EndComp +$Comp +L Device:R R6 +U 1 1 5E6A491A +P 2500 3650 +F 0 "R6" V 2580 3650 50 0000 C CNN +F 1 "1000" V 2500 3650 50 0000 C CNN +F 2 "Resistor_SMD:R_0603_1608Metric" V 2430 3650 50 0001 C CNN +F 3 "~" H 2500 3650 50 0001 C CNN +F 4 "Q,Q" V 2500 3650 50 0001 C CNN "BB" + 1 2500 3650 + 1 0 0 -1 +$EndComp +Text Notes 2750 3700 0 50 ~ 0 +BB field containing "K K" and "Q,Q" +Text Notes 600 1250 0 157 ~ 0 +This schematic serves as a test-file for the KiBot export script.\n\nHere we play with filters. +$Comp +L Device:C C1 +U 1 1 5E6A62CC +P 6650 2550 +F 0 "C1" H 6675 2650 50 0000 L CNN +F 1 "10nF" H 6675 2450 50 0000 L CNN +F 2 "Capacitor_SMD:C_0603_1608Metric" H 6688 2400 50 0001 C CNN +F 3 "This is a long text, wrap check" H 6650 2550 50 0001 C CNN + 1 6650 2550 + 1 0 0 -1 +$EndComp +$Comp +L Device:C C2 +U 1 1 5E6A6854 +P 7050 2550 +F 0 "C2" H 7075 2650 50 0000 L CNN +F 1 "10n" H 7075 2450 50 0000 L CNN +F 2 "Capacitor_SMD:C_0603_1608Metric" H 7088 2400 50 0001 C CNN +F 3 "~" H 7050 2550 50 0001 C CNN + 1 7050 2550 + 1 0 0 -1 +$EndComp +$EndSCHEMATC diff --git a/tests/test_plot/test_int_bom.py b/tests/test_plot/test_int_bom.py index 8ff12597f..316da62b3 100644 --- a/tests/test_plot/test_int_bom.py +++ b/tests/test_plot/test_int_bom.py @@ -31,9 +31,7 @@ - No XLSX support Missing: -- Variants - number_boards - - XLSX/HTML colors (for real) For debug information use: @@ -1212,6 +1210,18 @@ def test_int_bom_variant_t2(): ctx.clean_up() +def test_int_bom_variant_t2s(): + prj = 'kibom-variant_2' + ctx = context.TestContextSCH('test_int_bom_variant_t2s', prj, 'int_bom_var_t2s_csv', BOM_DIR) + ctx.run(extra_debug=True) + rows, header, info = ctx.load_csv(prj+'-bom_(dummy).csv') + ref_column = header.index(REF_COLUMN_NAME) + check_kibom_test_netlist(rows, ref_column, 1, ['C1', 'C2'], ['R1', 'R2']) + rows, header, info = ctx.load_csv(prj+'-bom_(dummy2).csv') + check_kibom_test_netlist(rows, ref_column, 1, ['C1', 'C2'], ['R1', 'R2']) + ctx.clean_up() + + def test_int_bom_variant_t2i(): prj = 'kibom-variant_3' ctx = context.TestContextSCH('test_int_bom_variant_t2i', prj, 'int_bom_var_t2i_csv', BOM_DIR) @@ -1226,3 +1236,50 @@ def test_int_bom_variant_t2i(): rows, header, info = ctx.load_csv(prj+'-bom_(test).csv') check_kibom_test_netlist(rows, ref_column, 2, ['R2'], ['R1', 'C1', 'C2']) ctx.clean_up() + + +def test_int_bom_variant_t2is(): + prj = 'kibom-variant_3' + ctx = context.TestContextSCH('test_int_bom_variant_t2is', prj, 'int_bom_var_t2is_csv', BOM_DIR) + ctx.run(extra_debug=True) + rows, header, info = ctx.load_csv('filter_R1.csv') + ref_column = header.index(REF_COLUMN_NAME) + check_kibom_test_netlist(rows, ref_column, 1, ['R2', 'R1'], ['C1', 'C2']) + ctx.clean_up() + + +def test_int_bom_wrong_variant(): + ctx = context.TestContextSCH('test_int_bom_wrong_variant', 'links', 'int_bom_wrong_variant', '') + ctx.run(EXIT_BAD_CONFIG) + assert ctx.search_err("Unknown variant name") + ctx.clean_up() + + +def test_int_bom_fil_dummy(): + prj = 'kibom-test-4' + ctx = context.TestContextSCH('test_int_bom_fil_dummy', prj, 'int_bom_fil_dummy', BOM_DIR) + ctx.run() + rows, header, info = ctx.load_csv(prj+'-bom.csv') + ref_column = header.index(REF_COLUMN_NAME) + check_kibom_test_netlist(rows, ref_column, 4, None, ['R1-R2', 'R3-R4', 'R5-R6', 'C1-C2']) + ctx.clean_up() + + +def test_int_bom_fil_1(): + prj = 'kibom-test-4' + ctx = context.TestContextSCH('test_int_bom_fil_1', prj, 'int_bom_fil_1', BOM_DIR) + ctx.run() + rows, header, info = ctx.load_csv('empty_val.csv') + ref_column = header.index(REF_COLUMN_NAME) + check_kibom_test_netlist(rows, ref_column, 3, None, ['R3-R4', 'R5-R6', 'C1-C2']) + rows, header, info = ctx.load_csv('by_prefix.csv') + check_kibom_test_netlist(rows, ref_column, 3, None, ['R2', 'R3-R4', 'R5-R6']) + rows, header, info = ctx.load_csv('no_kk.csv') + check_kibom_test_netlist(rows, ref_column, 3, None, ['R1-R2', 'R5-R6', 'C1-C2']) + rows, header, info = ctx.load_csv('no_conf_kk.csv') + check_kibom_test_netlist(rows, ref_column, 3, None, ['R1-R2', 'R3-R4', 'C1-C2']) + rows, header, info = ctx.load_csv('no_by_prefix.csv') + check_kibom_test_netlist(rows, ref_column, 2, None, ['R1', 'C1-C2']) + rows, header, info = ctx.load_csv('multi.csv') + check_kibom_test_netlist(rows, ref_column, 1, None, ['C1-C2']) + ctx.clean_up() diff --git a/tests/test_plot/test_yaml_errors.py b/tests/test_plot/test_yaml_errors.py index e532ee02e..864c36213 100644 --- a/tests/test_plot/test_yaml_errors.py +++ b/tests/test_plot/test_yaml_errors.py @@ -529,3 +529,52 @@ def test_error_int_bom_logo_format(): ctx.run(EXIT_BAD_CONFIG) assert ctx.search_err("Only PNG images are supported for the logo") ctx.clean_up() + + +def test_error_var_no_name(): + ctx = context.TestContextSCH('test_error_var_no_name', 'links', 'error_var_no_name', '') + ctx.run(EXIT_BAD_CONFIG) + assert ctx.search_err("Variant needs a name in:") + ctx.clean_up() + + +def test_error_var_empty_name(): + ctx = context.TestContextSCH('test_error_var_empty_name', 'links', 'error_var_empty_name', '') + ctx.run(EXIT_BAD_CONFIG) + assert ctx.search_err("Variant needs a name in:") + ctx.clean_up() + + +def test_error_var_wrong_type(): + ctx = context.TestContextSCH('test_error_var_wrong_type', 'links', 'error_var_wrong_type', '') + ctx.run(EXIT_BAD_CONFIG) + assert ctx.search_err("Unknown variant type") + ctx.clean_up() + + +def test_error_var_no_type(): + ctx = context.TestContextSCH('test_error_var_no_type', 'links', 'error_var_no_type', '') + ctx.run(EXIT_BAD_CONFIG) + assert ctx.search_err("Variant (.*) needs a type") + ctx.clean_up() + + +def test_error_var_no_list(): + ctx = context.TestContextSCH('test_error_var_no_list', 'links', 'error_var_no_list', '') + ctx.run(EXIT_BAD_CONFIG) + assert ctx.search_err(".?variants.? must be a list") + ctx.clean_up() + + +def test_error_fil_no_list(): + ctx = context.TestContextSCH('test_error_fil_no_list', 'links', 'error_fil_no_list', '') + ctx.run(EXIT_BAD_CONFIG) + assert ctx.search_err(".?filters.? must be a list") + ctx.clean_up() + + +def test_error_fil_unknown(): + ctx = context.TestContextSCH('test_error_fil_unknown', 'links', 'error_fil_unknown', '') + ctx.run(EXIT_BAD_CONFIG) + assert ctx.search_err("Unknown filter (.*) used for ") + ctx.clean_up() diff --git a/tests/yaml_samples/error_fil_no_list.kibot.yaml b/tests/yaml_samples/error_fil_no_list.kibot.yaml new file mode 100644 index 000000000..70a0eb5a4 --- /dev/null +++ b/tests/yaml_samples/error_fil_no_list.kibot.yaml @@ -0,0 +1,6 @@ +kibot: + version: 1 + +filters: + name: ok + type: 'generic' diff --git a/tests/yaml_samples/error_fil_unknown.kibot.yaml b/tests/yaml_samples/error_fil_unknown.kibot.yaml new file mode 100644 index 000000000..c362ed6b8 --- /dev/null +++ b/tests/yaml_samples/error_fil_unknown.kibot.yaml @@ -0,0 +1,14 @@ +# Example KiBot config file +kibot: + version: 1 + +outputs: + - name: 'empty_val' + comment: "BoM no empty val" + type: bom + dir: BoM + options: + output: 'empty_val.csv' + use_alt: true + exclude_filter: 'empty val' + diff --git a/tests/yaml_samples/error_var_empty_name.kibot.yaml b/tests/yaml_samples/error_var_empty_name.kibot.yaml new file mode 100644 index 000000000..e5583cddc --- /dev/null +++ b/tests/yaml_samples/error_var_empty_name.kibot.yaml @@ -0,0 +1,6 @@ +kibot: + version: 1 + +variants: + - name: '' + - type: 'generic' diff --git a/tests/yaml_samples/error_var_no_list.kibot.yaml b/tests/yaml_samples/error_var_no_list.kibot.yaml new file mode 100644 index 000000000..af47e04a2 --- /dev/null +++ b/tests/yaml_samples/error_var_no_list.kibot.yaml @@ -0,0 +1,6 @@ +kibot: + version: 1 + +variants: + name: ok + type: 'generic' diff --git a/tests/yaml_samples/error_var_no_name.kibot.yaml b/tests/yaml_samples/error_var_no_name.kibot.yaml new file mode 100644 index 000000000..f91c7cd5b --- /dev/null +++ b/tests/yaml_samples/error_var_no_name.kibot.yaml @@ -0,0 +1,5 @@ +kibot: + version: 1 + +variants: + - type: 'generic' diff --git a/tests/yaml_samples/error_var_no_type.kibot.yaml b/tests/yaml_samples/error_var_no_type.kibot.yaml new file mode 100644 index 000000000..0b9b1f43d --- /dev/null +++ b/tests/yaml_samples/error_var_no_type.kibot.yaml @@ -0,0 +1,6 @@ +kibot: + version: 1 + +variants: + - name: ok + diff --git a/tests/yaml_samples/error_var_wrong_type.kibot.yaml b/tests/yaml_samples/error_var_wrong_type.kibot.yaml new file mode 100644 index 000000000..e66dac946 --- /dev/null +++ b/tests/yaml_samples/error_var_wrong_type.kibot.yaml @@ -0,0 +1,6 @@ +kibot: + version: 1 + +variants: + - name: ok + type: '' diff --git a/tests/yaml_samples/int_bom_fil_1.kibot.yaml b/tests/yaml_samples/int_bom_fil_1.kibot.yaml new file mode 100644 index 000000000..dde543110 --- /dev/null +++ b/tests/yaml_samples/int_bom_fil_1.kibot.yaml @@ -0,0 +1,86 @@ +# Example KiBot config file +kibot: + version: 1 + +filters: + - name: 'empty val' + type: 'generic' + comment: 'Remove components with empty value' + exclude_empty_val: true + + - name: 'by prefix' + type: 'generic' + comment: 'Remove components by prefix' + exclude_refs: + - R1 + - C* + + - name: no_KK_Q,Q + type: 'generic' + comment: 'Remove components with K K and Q,Q fields' + keys: ['K K', 'Q,Q'] + exclude_field: true + + - name: no Conf KK + type: 'generic' + comment: 'Remove components with K K and Q,Q in config' + keys: ['K K', 'Q,Q'] + exclude_config: true + config_separators: '' + config_field: BB + +outputs: + - name: 'empty_val' + comment: "BoM no empty val" + type: bom + dir: BoM + options: + output: 'empty_val.csv' + use_alt: true + exclude_filter: 'empty val' + + - name: 'by_prefix' + comment: "BoM no R1 C*" + type: bom + dir: BoM + options: + output: 'by_prefix.csv' + use_alt: true + exclude_filter: 'by prefix' + + - name: 'no_KK' + comment: "BoM no K K/Q,Q" + type: bom + dir: BoM + options: + output: 'no_kk.csv' + use_alt: true + exclude_filter: 'no_KK_Q,Q' + + - name: 'no conf KK' + comment: "BoM no K K/Q,Q in config" + type: bom + dir: BoM + options: + output: 'no_conf_kk.csv' + use_alt: true + exclude_filter: 'no Conf KK' + + - name: 'no_by_prefix' + comment: "BoM R1 C*" + type: bom + dir: BoM + options: + output: 'no_by_prefix.csv' + use_alt: true + exclude_filter: '!by prefix' + + - name: 'multi' + comment: "BoM C*" + type: bom + dir: BoM + options: + output: 'multi.csv' + use_alt: true + exclude_filter: ['!by prefix', 'empty val'] + diff --git a/tests/yaml_samples/int_bom_fil_dummy.kibot.yaml b/tests/yaml_samples/int_bom_fil_dummy.kibot.yaml new file mode 100644 index 000000000..8d1edc675 --- /dev/null +++ b/tests/yaml_samples/int_bom_fil_dummy.kibot.yaml @@ -0,0 +1,13 @@ +# Example KiBot config file +kibot: + version: 1 + +outputs: + - name: 'bom_internal' + comment: "Bill of Materials in CSV format" + type: bom + dir: BoM + options: + use_alt: true + exclude_filter: '' + diff --git a/tests/yaml_samples/int_bom_var_t2i_csv.kibot.yaml b/tests/yaml_samples/int_bom_var_t2i_csv.kibot.yaml index e14042fd2..580f18ad3 100644 --- a/tests/yaml_samples/int_bom_var_t2i_csv.kibot.yaml +++ b/tests/yaml_samples/int_bom_var_t2i_csv.kibot.yaml @@ -19,6 +19,7 @@ variants: comment: 'Default variant' type: ibom variants_blacklist: T2,T3 + variants_whitelist: '' - name: 'default2' comment: 'Default variant 2' diff --git a/tests/yaml_samples/int_bom_var_t2is_csv.kibot.yaml b/tests/yaml_samples/int_bom_var_t2is_csv.kibot.yaml new file mode 100644 index 000000000..7e3542ea0 --- /dev/null +++ b/tests/yaml_samples/int_bom_var_t2is_csv.kibot.yaml @@ -0,0 +1,27 @@ +# Example KiBot config file +kibot: + version: 1 + +filters: + - name: 'no R1' + type: 'generic' + comment: 'Remove components by prefix' + exclude_refs: + - R1 + +variants: + - name: 'test' + comment: 'Test variant' + type: ibom + file_id: '_(test)' + variants_blacklist: T1 + +outputs: + - name: 'bom filtered' + comment: "Bill of Materials test, no R1" + type: bom + dir: BoM + options: + exclude_filter: no R1 + variant: test + output: filter_R1.csv diff --git a/tests/yaml_samples/int_bom_var_t2s_csv.kibot.yaml b/tests/yaml_samples/int_bom_var_t2s_csv.kibot.yaml new file mode 100644 index 000000000..c055aeaf0 --- /dev/null +++ b/tests/yaml_samples/int_bom_var_t2s_csv.kibot.yaml @@ -0,0 +1,31 @@ +# Example KiBot config file +kibot: + version: 1 + +variants: + - name: 'dummy' + comment: 'Dummy variant' + type: kibom + file_id: '_(dummy)' + variant: '' + + - name: 'dummy2' + comment: 'Dummy 2 variant' + type: kibom + file_id: '_(dummy2)' + +outputs: + - name: 'bom_internal_dummy' + comment: "Bill of Materials in CSV format" + type: bom + dir: BoM + options: + variant: dummy + + - name: 'bom_internal_dummy2' + comment: "Bill of Materials in CSV format" + type: bom + dir: BoM + options: + variant: dummy2 + diff --git a/tests/yaml_samples/int_bom_wrong_variant.kibot.yaml b/tests/yaml_samples/int_bom_wrong_variant.kibot.yaml new file mode 100644 index 000000000..1a52e57ed --- /dev/null +++ b/tests/yaml_samples/int_bom_wrong_variant.kibot.yaml @@ -0,0 +1,11 @@ +# Example KiBot config file +kibot: + version: 1 + +outputs: + - name: 'bom_internal2' + comment: "Bill of Materials in CSV format (2)" + type: bom + dir: BoM + options: + variant: foobar From 15cf4072e2626966041e323fa27594e8c183b2d5 Mon Sep 17 00:00:00 2001 From: "Salvador E. Tropea" Date: Sun, 30 Aug 2020 12:06:13 -0300 Subject: [PATCH 21/21] Added the new functionality to the CHANGELOG --- CHANGELOG.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e9f221ec..b87b838fd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added +- Now variants are separated entities. + - Only the internal BoM currently supports it. + - In the future IBoM will also support it, contact me if you think this is + high priority. +- New filters entities. They implement all the functionality in KiBoM and IBoM. +- Implemented the IBoM variants style. +- The internal BoM format supports filters for: + - Excluding components from processing + - Marking components as "Do Not Fit" + - Marking components as "Do Not Change" +- The internal BoM format supports KiBoM and IBoM style variants ## [0.6.2] - 2020-08-25 ### Changed