From a1455e0f4634cae45127a175994d9293981ca9c8 Mon Sep 17 00:00:00 2001 From: "Salvador E. Tropea" Date: Sat, 29 Aug 2020 19:32:04 -0300 Subject: [PATCH] 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 | 82 ++++++++++------------- 4 files changed, 139 insertions(+), 53 deletions(-) diff --git a/README.md b/README.md index 87600084..6740c4ea 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 e90939eb..936aee86 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 7c2c3a6c..c18964a0 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 @@ class BaseFilter(RegFilter): """ 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 1c557fb2..5b080bde 100644 --- a/kibot/out_bom.py +++ b/kibot/out_bom.py @@ -16,7 +16,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_generic import Generic +from .fil_base import BaseFilter from . import log logger = log.get_logger(__name__) @@ -218,14 +218,14 @@ class BoMOptions(BaseOptions): 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 @@ class BoMOptions(BaseOptions): 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) + @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 - 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_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 @@ class BoMOptions(BaseOptions): 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