From 45ecb1d02a105daeb121487bf31af0a5148c3a40 Mon Sep 17 00:00:00 2001 From: "Salvador E. Tropea" Date: Sun, 5 Jul 2020 12:40:57 -0300 Subject: [PATCH] Added a mechanism to specify suboptions. Now the legacy drill.map.type and drill.report.filename are specified in this way. The BaseOutput class now inherits from Optionable. Suboptions are just Optionable classes. Also: added traceback print when an error is reported and we are in debug mode. --- README.md | 46 ++++++---- docs/Makefile | 2 +- docs/samples/generic_plot.kiplot.yaml | 24 +++-- kiplot/config_reader.py | 80 +++++++++++----- kiplot/optionable.py | 127 ++++++++++++++++++++++++++ kiplot/out_any_drill.py | 100 ++++++++------------ kiplot/out_base.py | 71 +------------- kiplot/out_ibom.py | 3 +- tests/test_plot/test_yaml_errors.py | 22 ++--- 9 files changed, 284 insertions(+), 191 deletions(-) create mode 100644 kiplot/optionable.py diff --git a/README.md b/README.md index 1235ab02..afca5fe2 100644 --- a/README.md +++ b/README.md @@ -228,13 +228,17 @@ Most options are the same you'll find in the KiCad dialogs. You can create a map file for documentation purposes. This output is what you get from the 'File/Fabrication output/Drill Files' menu in pcbnew. * Options: - - `map`: [string=None] format for a graphical drill map. The valid formats are hpgl, ps, gerber, dxf, svg and pdf. - Not generated unless a format is specified. + - `map`: [dict|string] [hpgl,ps,gerber,dxf,svg,pdf] format for a graphical drill map. + Not generated unless a format is specified. + * 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. - `pth_and_npth_single_file`: [boolean=true] generate one file for both, plated holes and non-plated holes, instead of two separated files. - - `report`: [string=None] name of the drill report. Not generated unless a name is specified. + - `report`: [dict|string] name of the drill report. Not generated unless a name is specified. + * Options: + - `filename`: [string=''] name of the drill report. Not generated unless a name is specified. - `use_aux_axis_as_origin`: [boolean=false] use the auxiliar axis as origin for coordinates. * Gerber drill format @@ -243,9 +247,13 @@ Most options are the same you'll find in the KiCad dialogs. You can create a map file for documentation purposes. This output is what you get from the 'File/Fabrication output/Drill Files' menu in pcbnew. * Options: - - `map`: [string=None] format for a graphical drill map. The valid formats are hpgl, ps, gerber, dxf, svg and pdf. - Not generated unless a format is specified. - - `report`: [string=None] name of the drill report. Not generated unless a name is specified. + - `map`: [dict|string] [hpgl,ps,gerber,dxf,svg,pdf] format for a graphical drill map. + Not generated unless a format is specified. + * Options: + - `type`: [string='pdf'] [hpgl,ps,gerber,dxf,svg,pdf] format for a graphical drill map. + - `report`: [dict|string] name of the drill report. Not generated unless a name is specified. + * Options: + - `filename`: [string=''] name of the drill report. Not generated unless a name is specified. - `use_aux_axis_as_origin`: [boolean=false] use the auxiliar axis as origin for coordinates. * Gerber format @@ -254,7 +262,7 @@ Most options are the same you'll find in the KiCad dialogs. This output is what you get from the File/Plot menu in pcbnew. * Options: - `create_gerber_job_file`: [boolean=true] creates a file with information about all the generated gerbers. - You can use it in gerbview to load all gerbers at once. + You can use it in gerbview to load all gerbers at once. - `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. @@ -303,7 +311,7 @@ Most options are the same you'll find in the KiCad dialogs. - `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. + blacklisted. - `extra_fields`: [string=''] Comma separated list of extra fields to pull from netlist or xml file. - `hide_pads`: [boolean=false] Hide footprint pads by default. - `hide_silkscreen`: [boolean=false] Hide silkscreen by default. @@ -312,13 +320,13 @@ Most options are the same you'll find in the KiCad dialogs. - `include_tracks`: [boolean=false] Include track/zone information in output. F.Cu and B.Cu layers only. - `layer_view`: [string='FB'] [F,FB,B] Default layer view. - `name_format`: [string='ibom'] Output file name format supports substitutions: - %f : original pcb file name without extension. - %p : pcb/project title from pcb metadata. - %c : company from pcb metadata. - %r : revision from pcb metadata. - %d : pcb date from metadata if available, file modification date otherwise. - %D : bom generation date. - %T : bom generation time. Extension .html will be added automatically. + %f : original pcb file name without extension. + %p : pcb/project title from pcb metadata. + %c : company from pcb metadata. + %r : revision from pcb metadata. + %d : pcb date from metadata if available, file modification date otherwise. + %D : bom generation date. + %T : bom generation time. Extension .html will be added automatically. - `netlist_file`: [string=''] Path to netlist or xml file. - `no_blacklist_virtual`: [boolean=false] Do not blacklist virtual components. - `no_redraw_on_drag`: [boolean=false] Do not redraw pcb on drag by default. @@ -340,9 +348,9 @@ Most options are the same you'll find in the KiCad dialogs. - `number`: [number=1] Number of boards to build (components multiplier). - `separator`: [string=','] CSV Separator. - `variant`: [string=''] Board variant(s), used to determine which components - are output to the BoM. To specify multiple variants, - with a BOM file exported for each variant, separate - variants with the ';' (semicolon) character. + are output to the BoM. To specify multiple variants, + with a BOM file exported for each variant, separate + variants with the ';' (semicolon) character. * PDF (Portable Document Format) * Type: `pdf` @@ -410,7 +418,7 @@ Most options are the same you'll find in the KiCad dialogs. - `sketch_plot`: [boolean=false] don't fill objects, just draw the outline. - `tent_vias`: [boolean=true] cover the vias. - `width_adjust`: [number=0] this width factor is intended to compensate PS printers/plotters that do not strictly obey line width settings. - Only used to plot pads and tracks. + Only used to plot pads and tracks. * STEP (ISO 10303-21 Clear Text Encoding of the Exchange Structure) * Type: `step` diff --git a/docs/Makefile b/docs/Makefile index 4338c67b..583bfafa 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -2,7 +2,7 @@ all: ../README.md samples/generic_plot.kiplot.yaml -../README.md: README.in replace_tags.pl ../kiplot/out_*.py ../kiplot/pre_*.py ../kiplot/__main__.py +../README.md: README.in replace_tags.pl ../kiplot/out_*.py ../kiplot/pre_*.py ../kiplot/__main__.py ../kiplot/config_reader.py cat README.in | perl replace_tags.pl > ../README.md samples/generic_plot.kiplot.yaml: ../kiplot/out_*.py ../kiplot/pre_*.py ../kiplot/config_reader.py diff --git a/docs/samples/generic_plot.kiplot.yaml b/docs/samples/generic_plot.kiplot.yaml index 16dae8d5..148b8d5a 100644 --- a/docs/samples/generic_plot.kiplot.yaml +++ b/docs/samples/generic_plot.kiplot.yaml @@ -120,9 +120,11 @@ outputs: type: 'excellon' dir: 'Example/excellon_dir' options: - # [string=None] format for a graphical drill map. The valid formats are hpgl, ps, gerber, dxf, svg and pdf. + # [dict|string] [hpgl,ps,gerber,dxf,svg,pdf] format for a graphical drill map. # Not generated unless a format is specified - map: None + map: + # [string='pdf'] [hpgl,ps,gerber,dxf,svg,pdf] format for a graphical drill map + type: 'pdf' # [boolean=true] use metric units instead of inches metric_units: true # [boolean=false] use a minimal header in the file @@ -131,8 +133,10 @@ outputs: mirror_y_axis: false # [boolean=true] generate one file for both, plated holes and non-plated holes, instead of two separated files pth_and_npth_single_file: true - # [string=None] name of the drill report. Not generated unless a name is specified - report: None + # [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 + filename: '' # [boolean=false] use the auxiliar axis as origin for coordinates use_aux_axis_as_origin: false @@ -144,11 +148,15 @@ outputs: type: 'gerb_drill' dir: 'Example/gerb_drill_dir' options: - # [string=None] format for a graphical drill map. The valid formats are hpgl, ps, gerber, dxf, svg and pdf. + # [dict|string] [hpgl,ps,gerber,dxf,svg,pdf] format for a graphical drill map. # Not generated unless a format is specified - map: None - # [string=None] name of the drill report. Not generated unless a name is specified - report: None + map: + # [string='pdf'] [hpgl,ps,gerber,dxf,svg,pdf] format for a graphical drill map + type: 'pdf' + # [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 + filename: '' # [boolean=false] use the auxiliar axis as origin for coordinates use_aux_axis_as_origin: false diff --git a/kiplot/config_reader.py b/kiplot/config_reader.py index 876ac536..71e69ec8 100644 --- a/kiplot/config_reader.py +++ b/kiplot/config_reader.py @@ -3,12 +3,15 @@ Class to read KiPlot config files """ import os -from sys import (exit, maxsize) +from sys import (exit, maxsize, exc_info) +from traceback import print_tb from collections import OrderedDict from .error import (KiPlotConfigurationError) +from .gs import GS from .kiplot import (Layer, load_board) from .misc import (NO_YAML_MODULE, EXIT_BAD_CONFIG, EXIT_BAD_ARGS, EXAMPLE_CFG, WONT_OVERWRITE) +from .optionable import Optionable from .out_base import BaseOutput from .pre_base import BasePreFlight # Logger @@ -25,6 +28,10 @@ except ImportError: # pragma: no cover def config_error(msg): + if GS.debug_enabled: + logger.error('Trace stack:') + (type, value, traceback) = exc_info() + print_tb(traceback) logger.error(msg) exit(EXIT_BAD_CONFIG) @@ -225,16 +232,30 @@ def trim(docstring): return trimmed -def print_output_options(name, cl): - obj = cl('', name, '') - print(' * Options:') +def print_output_options(name, cl, indent): + ind_str = indent*' ' + if issubclass(cl, BaseOutput): + obj = cl('', name, '') + else: + obj = cl(name, '') + print(ind_str+'* Options:') num_opts = 0 - for k, v in BaseOutput.get_attrs_gen(obj): + for k, v in Optionable.get_attrs_gen(obj): help = getattr(obj, '_help_'+k) - print(' - `{}`: {}.'.format(k, help.rstrip() if help else 'Undocumented')) + if help is None: + help = 'Undocumented' + lines = help.split('\n') + preface = ind_str+' - `{}`: '.format(k) + clines = len(lines) + print('{}{}{}'.format(preface, lines[0].strip(), '.' if clines == 1 else '')) + ind_help = len(preface)*' ' + for ln in range(1, clines): + print('{}{}'.format(ind_help+lines[ln].strip(), '.' if ln+1 == clines else '')) num_opts = num_opts+1 + if isinstance(v, type): + print_output_options('', v, indent+4) if num_opts == 0: - print(' - No available options') + print(ind_str+' - No available options') def print_one_out_help(details, n, o): @@ -247,7 +268,7 @@ def print_one_out_help(details, n, o): print(' * Description: '+lines[1]) for ln in range(2, len(lines)): print(' '+lines[ln]) - print_output_options(n, o) + print_output_options(n, o, 2) else: print('* {} [{}]'.format(lines[0], n)) @@ -280,6 +301,33 @@ def print_preflights_help(): print('- {}: {}.'.format(n, help.rstrip())) +def print_example_options(f, cls, name, indent, po): + ind_str = indent*' ' + if issubclass(cls, BaseOutput): + obj = cls('', name, '') + else: + obj = cls(name, '') + if po: + obj.read_vals_from_po(po) + for k, v in Optionable.get_attrs_gen(obj): + help = getattr(obj, '_help_'+k) + if help: + help_lines = help.split('\n') + for hl in help_lines: + f.write(ind_str+'# '+hl.strip()+'\n') + val = getattr(obj, k) + if isinstance(val, str): + val = "'{}'".format(val) + elif isinstance(val, bool): + val = str(val).lower() + if isinstance(val, type): + f.write(ind_str+'{}:\n'.format(k)) + print_example_options(f, val, '', indent+2, None) + else: + f.write(ind_str+'{}: {}\n'.format(k, val)) + return obj + + def create_example(pcb_file, out_dir, copy_options): if not os.path.exists(out_dir): os.makedirs(out_dir) @@ -326,21 +374,7 @@ def create_example(pcb_file, out_dir, copy_options): f.write(" type: '{}'\n".format(n)) f.write(" dir: 'Example/{}_dir'\n".format(n)) f.write(" options:\n") - obj = cls('', n, '') - if po: - obj.read_vals_from_po(po) - for k, v in BaseOutput.get_attrs_gen(obj): - help = getattr(obj, '_help_'+k) - if help: - help_lines = help.split('\n') - for hl in help_lines: - f.write(' # '+hl.strip()+'\n') - val = getattr(obj, k) - if isinstance(val, str): - val = "'{}'".format(val) - elif isinstance(val, bool): - val = str(val).lower() - f.write(' {}: {}\n'.format(k, val)) + obj = print_example_options(f, cls, n, 6, po) if '_layers' in obj.__dict__: f.write(' layers:\n') for layer in layers: diff --git a/kiplot/optionable.py b/kiplot/optionable.py new file mode 100644 index 00000000..4b5dad8e --- /dev/null +++ b/kiplot/optionable.py @@ -0,0 +1,127 @@ +import inspect +from re import (compile) +from .error import KiPlotConfigurationError +from . import log + +logger = log.get_logger(__name__) + + +def filter(v): + return inspect.isclass(v) or not (callable(v) or isinstance(v, (dict, list))) + + +class Optionable(object): + """ A class to validate and hold configuration options. + Is configured from a dict and the collected values are stored in its attributes. """ + _str_values_re = compile(r"\[string=.*\] \[([^\]]+)\]") + _num_range_re = compile(r"\[number=.*\] \[(-?\d+),(-?\d+)\]") + + def __init__(self, name, description): + self._name = name + self._description = description + self._unkown_is_error = True + + @staticmethod + def _check_str(key, val, doc): + if not isinstance(val, str): + raise KiPlotConfigurationError("Option `{}` must be a string".format(key)) + # If the docstring specifies the allowed values in the form [v1,v2...] enforce it + m = Optionable._str_values_re.match(doc) + if m: + vals = m.group(1).split(',') + if val not in vals: + raise KiPlotConfigurationError("Option `{}` must be any of {} not `{}`".format(key, vals, val)) + + @staticmethod + def _check_num(key, val, doc): + if not isinstance(val, (int, float)): + raise KiPlotConfigurationError("Option `{}` must be a number".format(key)) + # If the docstring specifies a range in the form [from-to] enforce it + m = Optionable._num_range_re.match(doc) + if m: + min = float(m.group(1)) + max = float(m.group(2)) + if val < min or val > max: + raise KiPlotConfigurationError("Option `{}` outside its range [{},{}]".format(key, min, max)) + + @staticmethod + def _check_bool(key, val): + if not isinstance(val, bool): + raise KiPlotConfigurationError("Option `{}` must be true/false".format(key)) + + @staticmethod + def _typeof(v): + if isinstance(v, bool): + return 'boolean' + if isinstance(v, (int, float)): + return 'number' + if isinstance(v, str): + return 'string' + if isinstance(v, dict): + return 'dict' + if isinstance(v, list): + return 'list' + return 'None' + + def _perform_config_mapping(self): + """ Map the options to class attributes """ + attrs = Optionable.get_attrs_for(self) + for k, v in self._options.items(): + # Map known attributes and avoid mapping private ones + if (k[0] == '_') or (k not in attrs): + if self._unkown_is_error: + raise KiPlotConfigurationError("Unknown option `{}`".format(k)) + logger.warning("Unknown option `{}`".format(k)) + continue + # Check the data type + cur_val = getattr(self, k) + cur_doc = getattr(self, '_help_'+k).lstrip() + if isinstance(cur_val, bool): + Optionable._check_bool(k, v) + elif isinstance(cur_val, (int, float)): + Optionable._check_num(k, v, cur_doc) + elif isinstance(cur_val, str): + Optionable._check_str(k, v, cur_doc) + elif isinstance(cur_val, type): + # A class, so we need more information i.e. "[dict|string]" + if cur_doc[0] == '[': + # Separate the valid types for this key + valid = cur_doc[1:].split(']')[0].split('|') + # Get the type used by the user as a string + v_type = Optionable._typeof(v) + if v_type not in valid: + # Not a valid type for this key + if v_type == 'None': + raise KiPlotConfigurationError("Empty option `{}`".format(k)) + raise KiPlotConfigurationError("Option `{}` must be any of {} not `{}`".format(k, valid, v_type)) + # We know the type matches, now apply validations + if isinstance(v, (int, float)) and not isinstance(v, bool): + # Note: booleans are also instance of int + Optionable._check_num(k, v, cur_doc) + elif isinstance(v, str): + Optionable._check_str(k, v, cur_doc) + elif isinstance(v, dict): + # Dicts are solved using Optionable classes + new_val = v + # Create an object for the valid class + v = cur_val(k, cur_doc) + # Delegate the validation to the object + v.config(new_val) + # Seems to be ok, map it + setattr(self, k, v) + + def config(self, options): + self._options = options + if options: + self._perform_config_mapping() + + @staticmethod + def get_attrs_for(obj): + """ Returns all attributes """ + return dict(inspect.getmembers(obj, filter)) + + @staticmethod + def get_attrs_gen(obj): + """ Returns a (key, val) iterator on public attributes """ + attrs = Optionable.get_attrs_for(obj) + return ((k, v) for k, v in attrs.items() if k[0] != '_') diff --git a/kiplot/out_any_drill.py b/kiplot/out_any_drill.py index 0970a342..0c2739af 100644 --- a/kiplot/out_any_drill.py +++ b/kiplot/out_any_drill.py @@ -1,14 +1,30 @@ import os from pcbnew import (PLOT_FORMAT_HPGL, PLOT_FORMAT_POST, PLOT_FORMAT_GERBER, PLOT_FORMAT_DXF, PLOT_FORMAT_SVG, PLOT_FORMAT_PDF, wxPoint) +from .optionable import Optionable from .out_base import BaseOutput -from .error import KiPlotConfigurationError from kiplot.macros import macros, document # noqa: F401 from . import log logger = log.get_logger(__name__) +class DrillMap(Optionable): + def __init__(self, name, description): + super(DrillMap, self).__init__(name, description) + with document: + self.type = 'pdf' + """ [hpgl,ps,gerber,dxf,svg,pdf] format for a graphical drill map """ + + +class DrillReport(Optionable): + def __init__(self, name, description): + super(DrillReport, self).__init__(name, description) + with document: + self.filename = '' + """ name of the drill report. Not generated unless a name is specified """ + + class AnyDrill(BaseOutput): def __init__(self, name, type, description): super(AnyDrill, self).__init__(name, type, description) @@ -16,11 +32,11 @@ class AnyDrill(BaseOutput): with document: self.use_aux_axis_as_origin = False """ use the auxiliar axis as origin for coordinates """ - self._map = None - """ [string=None] format for a graphical drill map. The valid formats are hpgl, ps, gerber, dxf, svg and pdf. + self.map = DrillMap + """ [dict|string] [hpgl,ps,gerber,dxf,svg,pdf] format for a graphical drill map. Not generated unless a format is specified """ - self._report = None - """ [string=None] name of the drill report. Not generated unless a name is specified """ # pragma: no cover + self.report = DrillReport + """ [dict|string] name of the drill report. Not generated unless a name is specified """ # pragma: no cover # Mappings to KiCad values self._map_map = { 'hpgl': PLOT_FORMAT_HPGL, @@ -31,60 +47,20 @@ class AnyDrill(BaseOutput): 'pdf': PLOT_FORMAT_PDF } - @property - def map(self): - return self._map - - @map.setter - def map(self, val): - # In the original "version 1" of the format this is a dict with one key named `type`. - # Currently we spect a string, but we support the old mechanism. - if val is None: - raise KiPlotConfigurationError("Empty drill `map` section") - # Setting from a dict - if isinstance(val, dict): - if 'type' not in val: - raise KiPlotConfigurationError("drill `map` must contain a `type`") - type = val['type'] - if not isinstance(type, str): - raise KiPlotConfigurationError("drill `map` `type` must be a string") - val = type - elif not isinstance(val, str): - raise KiPlotConfigurationError("drill `map` must be a string") - if val == 'None': - val = None - elif val not in self._map_map: - raise KiPlotConfigurationError("Unknown drill `map` `type`: {}".format(val)) - self._map = val - - @property - def report(self): - return self._report - - @report.setter - def report(self, val): - # In the original "version 1" of the format this is a dict with one key named `filename`. - # Currently we spect a string, but we support the old mechanism. - if val is None: - raise KiPlotConfigurationError("Empty drill `report` section") - # Setting from a dict - if isinstance(val, dict): - if 'filename' not in val: - raise KiPlotConfigurationError("drill `report` must contain a `filename`") - filename = val['filename'] - if not isinstance(filename, str): - raise KiPlotConfigurationError("drill `report` `filename` must be a string") - val = filename - elif not isinstance(val, str): - raise KiPlotConfigurationError("drill `report` must be a string") - if val == 'None': - val = None - self._report = val - def config(self, outdir, options, layers): super().config(outdir, options, layers) - if self._map: - self._map = self._map_map[self._map] + # Solve the map for both cases + if isinstance(self.map, str): + self.map = self._map_map[self.map] + elif isinstance(self.map, DrillMap): + self.map = self._map_map[self.map.type] + else: + self.map = None + # Solve the report for both cases + if isinstance(self.report, DrillReport): + self.report = self.report.filename + elif not isinstance(self.report, str): + self.report = None def run(self, output_dir, board): # dialog_gendrill.cpp:357 @@ -95,14 +71,14 @@ class AnyDrill(BaseOutput): drill_writer = self._configure_writer(board, offset) logger.debug("Generating drill files in "+output_dir) - gen_map = self._map is not None + gen_map = self.map is not None if gen_map: - drill_writer.SetMapFileFormat(self._map) - logger.debug("Generating drill map type {} in {}".format(self._map, output_dir)) + drill_writer.SetMapFileFormat(self.map) + logger.debug("Generating drill map type {} in {}".format(self.map, output_dir)) # We always generate the drill file drill_writer.CreateDrillandMapFilesSet(output_dir, True, gen_map) - if self._report is not None: - drill_report_file = os.path.join(output_dir, self._report) + if self.report: + drill_report_file = os.path.join(output_dir, self.report) logger.debug("Generating drill report: "+drill_report_file) drill_writer.GenDrillReportFile(drill_report_file) diff --git a/kiplot/out_base.py b/kiplot/out_base.py index bee9a737..b7b1a3c6 100644 --- a/kiplot/out_base.py +++ b/kiplot/out_base.py @@ -1,83 +1,22 @@ -import inspect -from re import (compile) -from .error import KiPlotConfigurationError +from .optionable import Optionable from . import log logger = log.get_logger(__name__) -def filter(v): - return not (callable(v) or inspect.isclass(v) or isinstance(v, (dict, list))) - - -class BaseOutput(object): +class BaseOutput(Optionable): _registered = {} def __init__(self, name, type, description): - self._name = name + super(BaseOutput, self).__init__(name, description) self._type = type - self._description = description self._sch_related = False - - def _perform_config_mapping(self): - """ Map the options to class attributes """ - attrs = BaseOutput.get_attrs_for(self) - num_range_re = compile(r"\[number=.*\] \[(-?\d+),(-?\d+)\]") - str_values_re = compile(r"\[string=.*\] \[([^\]]+)\]") - for k, v in self._options.items(): - # Map known attributes and avoid mapping private ones - if (k[0] == '_') or (k not in attrs): - # raise KiPlotConfigurationError("Unknown option `{}` {}".format(k, attrs)) - logger.warning("Unknown option `{}`".format(k)) - continue - # Check the data type - cur_val = getattr(self, k) - cur_doc = getattr(self, '_help_'+k).lstrip() - if isinstance(cur_val, bool): - if not isinstance(v, bool): - raise KiPlotConfigurationError("Option `{}` must be true/false".format(k)) - elif isinstance(cur_val, (int, float)): - # Note: booleans are also instance of int - if not isinstance(v, (int, float)): - raise KiPlotConfigurationError("Option `{}` must be a number".format(k)) - # If the docstring specifies a range in the form [from-to] enforce it - m = num_range_re.match(cur_doc) - if m: - min = float(m.group(1)) - max = float(m.group(2)) - if v < min or v > max: - raise KiPlotConfigurationError("Option `{}` outside its range [{},{}]".format(k, min, max)) - elif isinstance(cur_val, str): - if not isinstance(v, str): - raise KiPlotConfigurationError("Option `{}` must be a string".format(k)) - # If the docstring specifies the allowed values in the form [v1,v2...] enforce it - m = str_values_re.match(cur_doc) - if m: - vals = m.group(1).split(',') - if v not in vals: - raise KiPlotConfigurationError("Option `{}` must be any of {}".format(k, vals)) - elif isinstance(v, list): - raise KiPlotConfigurationError("list not yet supported for `{}`".format(k)) - # Seems to be ok, map it - setattr(self, k, v) + self._unkown_is_error = False def config(self, outdir, options, layers): self._outdir = outdir - self._options = options self._layers = layers - if options: - self._perform_config_mapping() - - @staticmethod - def get_attrs_for(obj): - """ Returns all attributes """ - return dict(inspect.getmembers(obj, filter)) - - @staticmethod - def get_attrs_gen(obj): - """ Returns a (key, val) iterator on public attributes """ - attrs = BaseOutput.get_attrs_for(obj) - return ((k, v) for k, v in attrs.items() if k[0] != '_') + super(BaseOutput, self).config(options) @staticmethod def attr2longopt(attr): diff --git a/kiplot/out_ibom.py b/kiplot/out_ibom.py index 0f878fc7..8e6eae77 100644 --- a/kiplot/out_ibom.py +++ b/kiplot/out_ibom.py @@ -3,6 +3,7 @@ from subprocess import (check_output, STDOUT, CalledProcessError) from .misc import (CMD_IBOM, URL_IBOM, BOM_ERROR) from .gs import (GS) from .kiplot import (check_script) +from .optionable import Optionable from kiplot.macros import macros, document, output_class # noqa: F401 from . import log @@ -84,7 +85,7 @@ class IBoM(BaseOutput): # noqa: F821 os.environ['INTERACTIVE_HTML_BOM_NO_DISPLAY'] = '' cmd = [CMD_IBOM, GS.pcb_file, '--dest-dir', output_dir, '--no-browser', ] # Convert attributes into options - for k, v in BaseOutput.get_attrs_gen(self): # noqa: F821 + for k, v in Optionable.get_attrs_gen(self): if not v: continue cmd.append(BaseOutput.attr2longopt(k)) # noqa: F821 diff --git a/tests/test_plot/test_yaml_errors.py b/tests/test_plot/test_yaml_errors.py index c2fb4262..83a7370a 100644 --- a/tests/test_plot/test_yaml_errors.py +++ b/tests/test_plot/test_yaml_errors.py @@ -88,63 +88,63 @@ def test_wrong_version_3(): def test_drill_map_no_type_1(): ctx = context.TestContext('ErrorDrillMapNoType1', '3Rs', 'error_drill_map_no_type', None) ctx.run(EXIT_BAD_CONFIG) - assert ctx.search_err("Empty drill `map` section") + assert ctx.search_err("Empty option .?map.?") ctx.clean_up() def test_drill_map_no_type_2(): ctx = context.TestContext('ErrorDrillMapNoType2', '3Rs', 'error_drill_map_no_type_2', None) ctx.run(EXIT_BAD_CONFIG) - assert ctx.search_err("drill `map` must contain a `type`") + assert ctx.search_err("Unknown option .?types.?") ctx.clean_up() def test_drill_map_wrong_type_1(): ctx = context.TestContext('ErrorDrillMapWrongType1', '3Rs', 'error_drill_map_wrong_type', None) ctx.run(EXIT_BAD_CONFIG) - assert ctx.search_err("Unknown drill `map` `type`: bogus") + assert ctx.search_err("Option .?type.? must be any of") ctx.clean_up() def test_drill_map_wrong_type_2(): ctx = context.TestContext('ErrorDrillMapWrongType2', '3Rs', 'error_drill_map_wrong_type_2', None) ctx.run(EXIT_BAD_CONFIG) - assert ctx.search_err("drill `map` `type` must be a string") + assert ctx.search_err("Option .?type.? must be a string") ctx.clean_up() def test_drill_map_wrong_type_3(): ctx = context.TestContext('ErrorDrillMapWrongType3', '3Rs', 'error_drill_map_wrong_type_3', None) ctx.run(EXIT_BAD_CONFIG) - assert ctx.search_err("drill `map` must be a string") + assert ctx.search_err("Option .?map.? must be any of") ctx.clean_up() def test_drill_report_no_type_1(): ctx = context.TestContext('ErrorDrillReportNoType1', '3Rs', 'error_drill_report_no_type', None) ctx.run(EXIT_BAD_CONFIG) - assert ctx.search_err("Empty drill `report` section") + assert ctx.search_err("Empty option .?report.?") ctx.clean_up() def test_drill_report_no_type_2(): ctx = context.TestContext('ErrorDrillReportNoType2', '3Rs', 'error_drill_report_no_type_2', None) ctx.run(EXIT_BAD_CONFIG) - assert ctx.search_err("drill `report` must contain a `filename`") + assert ctx.search_err("Unknown option .?filenames.?") ctx.clean_up() def test_drill_report_wrong_type_2(): ctx = context.TestContext('ErrorDrillReportWrongType2', '3Rs', 'error_drill_report_wrong_type_2', None) ctx.run(EXIT_BAD_CONFIG) - assert ctx.search_err("drill `report` `filename` must be a string") + assert ctx.search_err("Option .?filename.? must be a string") ctx.clean_up() def test_drill_report_wrong_type_3(): ctx = context.TestContext('ErrorDrillReportWrongType3', '3Rs', 'error_drill_report_wrong_type_3', None) ctx.run(EXIT_BAD_CONFIG) - assert ctx.search_err("drill `report` must be a string") + assert ctx.search_err("Option .?report.? must be any of") ctx.clean_up() @@ -222,7 +222,7 @@ def test_out_unknown_attr(): def test_no_layers(): ctx = context.TestContext('ErrorNoLayers', '3Rs', 'error_no_layers', None) ctx.run(EXIT_BAD_CONFIG) - assert ctx.search_err("Missing `layers` list") + assert ctx.search_err("Missing .?layers.? list") ctx.clean_up() @@ -236,7 +236,7 @@ def test_error_step_origin(): def test_error_step_min_distance(): ctx = context.TestContext('ErrorStepMinDistance', 'bom', 'error_step_min_distance', None) ctx.run(EXIT_BAD_CONFIG) - assert ctx.search_err("`min_distance` must be a number") + assert ctx.search_err(".?min_distance.? must be a number") ctx.clean_up()