diff --git a/README.md b/README.md index 3ab5e653..2f12bebc 100644 --- a/README.md +++ b/README.md @@ -271,7 +271,7 @@ Currently the only type available is `generic`. #### Supported filters: - generic: Generic filter - This filter is based on regular exressions. + This filter is based on regular expressions. 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. * Valid keys: @@ -312,6 +312,12 @@ Currently the only type available is `generic`. Use `dnf_list` for ['dnf', 'dnl', 'dnp', 'do not fit', 'do not load', 'do not place', 'no stuff', 'nofit', 'noload', 'noplace', 'nostuff', 'not fitted', 'not loaded', 'not placed']. Use `dnc_list` for ['dnc', 'do not change', 'fixed', 'no change']. - `name`: [string=''] Used to identify this particular filter definition. +- var_rename: Var_Rename + This filter implements the VARIANT:FIELD=VALUE renamer to get FIELD=VALUE when VARIANT is in use. + * Valid keys: + - `comment`: [string=''] A comment for documentation purposes. + - `name`: [string=''] Used to identify this particular filter definition. + - `separator`: [string=':'] Separator used between the variant and the field name. diff --git a/kibot/fil_base.py b/kibot/fil_base.py index faf1bb36..65c3e62d 100644 --- a/kibot/fil_base.py +++ b/kibot/fil_base.py @@ -4,7 +4,7 @@ # License: GPL-3.0 # Project: KiBot (formerly KiPlot) from .registrable import RegFilter, Registrable, RegOutput -from .misc import IFILL_MECHANICAL +from .misc import IFILT_MECHANICAL, IFILT_VAR_RENAME from .error import KiPlotConfigurationError from .bom.columnlist import ColumnList from .macros import macros, document # noqa: F401 @@ -65,11 +65,11 @@ class NotFilter(Registrable): return not self._filter.filter(comp) -def reset_filters(comps): - for c in comps: - c.included = True - c.fitted = True - c.fixed = False +def apply_pre_transform(comps, filter): + if filter: + logger.debug('Applying transform filter `{}`'.format(filter.name)) + for c in comps: + filter.filter(c) def apply_exclude_filter(comps, filter): @@ -125,6 +125,14 @@ class BaseFilter(RegFilter): logger.debug('Creating internal filter: '+str(o_tree)) return o_tree + @staticmethod + def _create_var_rename(name): + o_tree = {'name': name} + o_tree['type'] = 'var_rename' + o_tree['comment'] = 'Internal default variant field renamer filter' + logger.debug('Creating internal filter: '+str(o_tree)) + return o_tree + @staticmethod def _create_kibom_dnx(name): type = name[7:10] @@ -146,10 +154,12 @@ class BaseFilter(RegFilter): @staticmethod def _create_internal_filter(name): - if name == IFILL_MECHANICAL: + if name == IFILT_MECHANICAL: tree = BaseFilter._create_mechanical(name) elif name.startswith('_kibom_dn') and len(name) >= 10: tree = BaseFilter._create_kibom_dnx(name) + elif name == IFILT_VAR_RENAME: + tree = BaseFilter._create_var_rename(name) else: return None filter = RegFilter.get_class_for(tree['type'])() diff --git a/kibot/fil_generic.py b/kibot/fil_generic.py index cf12b39f..0d4dc9e3 100644 --- a/kibot/fil_generic.py +++ b/kibot/fil_generic.py @@ -28,7 +28,7 @@ class DNFList(Optionable): @filter_class class Generic(BaseFilter): # noqa: F821 """ Generic filter - This filter is based on regular exressions. + This filter is based on regular expressions. 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): diff --git a/kibot/fil_var_rename.py b/kibot/fil_var_rename.py new file mode 100644 index 00000000..e69072b5 --- /dev/null +++ b/kibot/fil_var_rename.py @@ -0,0 +1,50 @@ +# -*- 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 VARIANT:FIELD=VALUE renamer to get FIELD=VALUE when VARIANT is in use. +""" +# 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__) + + +@filter_class +class Var_Rename(BaseFilter): # noqa: F821 + """ Var_Rename + This filter implements the VARIANT:FIELD=VALUE renamer to get FIELD=VALUE when VARIANT is in use """ + def __init__(self): + super().__init__() + with document: + self.separator = ':' + """ Separator used between the variant and the field name """ + + def config(self): + super().config() + if not self.separator: + self.separator = ':' + + def filter(self, comp): + """ Look for fields containing VARIANT:FIELD used to change fields according to the variant """ + if not GS.variant: + # No variant in use, nothing to do + return + for variant in GS.variant: + for name, value in comp.get_user_fields(): + res = name.strip().split(self.separator) + if len(res) == 2: + f_variant = res[0].lower() + f_field = res[1].lower() + if f_variant == variant: + if GS.debug_level > 2: + logger.debug('ref: {} value: {} -> {}'.format(comp.ref, comp.get_field_value(f_field), value)) + comp.set_field(f_field, value) diff --git a/kibot/gs.py b/kibot/gs.py index f6fef1df..f6ad70ea 100644 --- a/kibot/gs.py +++ b/kibot/gs.py @@ -55,6 +55,8 @@ class GS(object): pcb_date = None pcb_rev = None pcb_comp = None + # Current variant/s + variant = None # Global defaults # This is used as default value for classes supporting "output" option def_global_output = '%f-%i%v.%x' diff --git a/kibot/misc.py b/kibot/misc.py index 84c3ca6d..714a3cff 100644 --- a/kibot/misc.py +++ b/kibot/misc.py @@ -72,7 +72,8 @@ AUTO_SCALE = 0 KICAD_VERSION_5_99 = 5099000 # Internal filter names -IFILL_MECHANICAL = '_mechanical' +IFILT_MECHANICAL = '_mechanical' +IFILT_VAR_RENAME = '_var_rename' # KiCad 5 GUI values for the attribute UI_THT = 0 # 1 for KiCad 6 UI_SMD = 1 # 2 for KiCad 6 diff --git a/kibot/out_base.py b/kibot/out_base.py index 80da1a73..39d0842e 100644 --- a/kibot/out_base.py +++ b/kibot/out_base.py @@ -3,6 +3,7 @@ # Copyright (c) 2020 Instituto Nacional de TecnologĂ­a Industrial # License: GPL-3.0 # Project: KiBot (formerly KiPlot) +from copy import deepcopy from .gs import GS from .kiplot import load_sch from .misc import Rect, KICAD_VERSION_5_99, W_WRONGPASTE @@ -13,7 +14,7 @@ else: from pcbnew import EDGE_MODULE, wxPoint, LSET from .registrable import RegOutput from .optionable import Optionable, BaseOptions -from .fil_base import BaseFilter, apply_fitted_filter, reset_filters +from .fil_base import BaseFilter, apply_fitted_filter from .macros import macros, document # noqa: F401 from . import log @@ -259,9 +260,9 @@ class VariantOptions(BaseOptions): return load_sch() # Get the components list from the schematic - comps = GS.sch.get_components() + # Note: we work with a copy to avoid changes by filters affecting other outputs + comps = deepcopy(GS.sch.get_components()) # Apply the filter - reset_filters(comps) apply_fitted_filter(comps, self.dnf_filter) # Apply the variant if self.variant: diff --git a/kibot/out_bom.py b/kibot/out_bom.py index 502570a1..aad3a563 100644 --- a/kibot/out_bom.py +++ b/kibot/out_bom.py @@ -8,6 +8,7 @@ Internal BoM (Bill of Materials) output for KiBot. This is somehow compatible with KiBoM. """ import os +from copy import deepcopy from .gs import GS from .optionable import Optionable, BaseOptions from .registrable import RegOutput @@ -17,7 +18,7 @@ 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 .fil_base import BaseFilter, apply_exclude_filter, apply_fitted_filter, apply_fixed_filter, reset_filters +from .fil_base import BaseFilter, apply_exclude_filter, apply_fitted_filter, apply_fixed_filter from . import log # To debug the `with document` we can use: # from .mcpyrate.debug import macros, step_expansion @@ -373,8 +374,9 @@ class BoMOptions(BaseOptions): # Get the components list from the schematic comps = GS.sch.get_components() get_board_comps_data(comps) + # We work with a copy to avoid changes by filters affecting other outputs + comps = deepcopy(comps) # Apply all the filters - reset_filters(comps) apply_exclude_filter(comps, self.exclude_filter) apply_fitted_filter(comps, self.dnf_filter) apply_fixed_filter(comps, self.dnc_filter) diff --git a/kibot/var_base.py b/kibot/var_base.py index ed7cd499..8d9ddf50 100644 --- a/kibot/var_base.py +++ b/kibot/var_base.py @@ -5,7 +5,7 @@ # Project: KiBot (formerly KiPlot) from .registrable import RegVariant from .optionable import Optionable -from .fil_base import apply_exclude_filter, apply_fitted_filter, apply_fixed_filter +from .fil_base import apply_exclude_filter, apply_fitted_filter, apply_fixed_filter, BaseFilter, apply_pre_transform from .macros import macros, document # noqa: F401 @@ -23,6 +23,9 @@ class BaseVariant(RegVariant): self.file_id = '' """ Text to use as the """ # * Filters + self.pre_transform = Optionable + """ [string|list(string)=''] Name of the filter to transform fields before applying other filters. + Use '_var_rename' to transform VARIANT:FIELD fields """ self.exclude_filter = Optionable """ [string|list(string)=''] Name of the filter to exclude components from BoM processing. Use '_mechanical' for the default KiBoM behavior """ @@ -33,8 +36,13 @@ class BaseVariant(RegVariant): """ [string|list(string)=''] Name of the filter to mark components as 'Do Not Change'. Use '_kibom_dnc' for the default KiBoM behavior """ + def config(self): + super().config() + self.pre_transform = BaseFilter.solve_filter(self.pre_transform, 'pre_transform') + def filter(self, comps): # Apply all the filters + apply_pre_transform(comps, self.pre_transform) apply_exclude_filter(comps, self.exclude_filter) apply_fitted_filter(comps, self.dnf_filter) apply_fixed_filter(comps, self.dnc_filter) diff --git a/kibot/var_ibom.py b/kibot/var_ibom.py index cede5ede..6e20a7c4 100644 --- a/kibot/var_ibom.py +++ b/kibot/var_ibom.py @@ -8,7 +8,7 @@ Implements the IBoM variants mechanism. """ from .optionable import Optionable from .gs import GS -from .misc import IFILL_MECHANICAL +from .misc import IFILT_MECHANICAL from .fil_base import BaseFilter from .macros import macros, document, variant_class # noqa: F401 from . import log @@ -48,7 +48,7 @@ class IBoM(BaseVariant): # noqa: F821 def config(self): super().config() - self.exclude_filter = BaseFilter.solve_filter(self.exclude_filter, 'exclude_filter', IFILL_MECHANICAL) + self.exclude_filter = BaseFilter.solve_filter(self.exclude_filter, 'exclude_filter', IFILT_MECHANICAL) self.dnf_filter = BaseFilter.solve_filter(self.dnf_filter, 'dnf_filter') self.dnc_filter = BaseFilter.solve_filter(self.dnc_filter, 'dnc_filter') self.variants_blacklist = self._force_list(self.variants_blacklist) @@ -67,6 +67,7 @@ class IBoM(BaseVariant): # noqa: F821 return False def filter(self, comps): + GS.variant = self.variants_whitelist super().filter(comps) logger.debug("Applying IBoM style variants `{}`".format(self.name)) # Make black/white lists case insensitive diff --git a/kibot/var_kibom.py b/kibot/var_kibom.py index ce813669..36cc5e4a 100644 --- a/kibot/var_kibom.py +++ b/kibot/var_kibom.py @@ -8,7 +8,7 @@ Implements the KiBoM variants mechanism. """ from .optionable import Optionable from .gs import GS -from .misc import IFILL_MECHANICAL +from .misc import IFILT_MECHANICAL from .fil_base import BaseFilter from .macros import macros, document, variant_class # noqa: F401 from . import log @@ -32,10 +32,6 @@ class KiBoM(BaseVariant): # noqa: F821 """ Name of the field used to clasify components """ self.variant = Optionable """ [string|list(string)=''] Board variant(s) """ - self.field_changer = False - """ Enable the VARIANT.FIELD to FIELD rename mechanism """ - self.field_variant_separator = '.' - """ Separator used for the VARIANT.FIELD rename mechanism """ def set_def_filters(self, exclude_filter, dnf_filter, dnc_filter): """ Filters delegated to the variant """ @@ -61,7 +57,7 @@ class KiBoM(BaseVariant): # noqa: F821 # 3) KiBoM default behavior # exclude_filter if not self._def_exclude_filter: - self._def_exclude_filter = IFILL_MECHANICAL + self._def_exclude_filter = IFILT_MECHANICAL self.exclude_filter = BaseFilter.solve_filter(self.exclude_filter, 'exclude_filter', self._def_exclude_filter) # dnf_filter if not self._def_dnf_filter: @@ -97,21 +93,8 @@ class KiBoM(BaseVariant): # noqa: F821 # No match return not exclusive - def _rename_var_fields(self, comp): - """ Look for fields containing VARIANT:FIELD used to change fields according to the variant """ - for variant in self.variant: - for name, value in comp.get_user_fields(): - res = name.strip().split(self.field_variant_separator) - if len(res) == 2: - f_variant = res[0].lower() - f_field = res[1].lower() - if f_variant == variant: - if GS.debug_level > 2: - logger.debug('ref: {} value: {} -> {}'.format(comp.ref, comp.get_field_value(f_field), value)) - comp.set_field(f_field, value) - - def filter(self, comps): + GS.variant = self.variant super().filter(comps) logger.debug("Applying KiBoM style variants `{}`".format(self.name)) for c in comps: @@ -121,8 +104,6 @@ class KiBoM(BaseVariant): # noqa: F821 value = c.value.lower() config = c.get_field_value(self.config_field).lower() c.fitted = self._variant_comp_is_fitted(value, config) - if c.fitted and self.field_changer: - self._rename_var_fields(c) - elif not c.fitted and GS.debug_level > 2: + 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 841cf56e..e0762767 100644 --- a/tests/test_plot/test_int_bom.py +++ b/tests/test_plot/test_int_bom.py @@ -1226,6 +1226,18 @@ def test_int_bom_variant_t2b(): ctx.clean_up() +def test_int_bom_variant_t2c(): + prj = 'kibom-variant_2' + ctx = context.TestContextSCH('test_int_bom_variant_t2c', prj, 'int_bom_var_t2c_csv', BOM_DIR) + ctx.run() + rows, header, info = ctx.load_csv(prj+'-bom_(test).csv') + ref_column = header.index(REF_COLUMN_NAME) + val_column = header.index(VALUE_COLUMN_NAME) + check_kibom_test_netlist(rows, ref_column, 2, ['R2'], ['R1', 'C1', 'C2']) + check_value(rows, ref_column, 'R1', val_column, '3k3') + 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) diff --git a/tests/yaml_samples/int_bom_var_t2_csv.kibot.yaml b/tests/yaml_samples/int_bom_var_t2_csv.kibot.yaml index 40a6e63c..b6534d19 100644 --- a/tests/yaml_samples/int_bom_var_t2_csv.kibot.yaml +++ b/tests/yaml_samples/int_bom_var_t2_csv.kibot.yaml @@ -2,6 +2,11 @@ kibot: version: 1 +filters: + - name: 'Variant rename' + type: var_rename + separator: ':' + variants: - name: 'production' comment: 'Production variant' @@ -14,8 +19,7 @@ variants: type: kibom file_id: '_(test)' variant: test - field_changer: true - field_variant_separator: ':' + pre_transform: 'Variant rename' outputs: - name: 'bom_internal' diff --git a/tests/yaml_samples/int_bom_var_t2c_csv.kibot.yaml b/tests/yaml_samples/int_bom_var_t2c_csv.kibot.yaml new file mode 100644 index 00000000..8d9f0615 --- /dev/null +++ b/tests/yaml_samples/int_bom_var_t2c_csv.kibot.yaml @@ -0,0 +1,37 @@ +# 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 + pre_transform: '_var_rename' + +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