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.
This commit is contained in:
Salvador E. Tropea 2020-07-05 12:40:57 -03:00
parent ee11ecf8e7
commit 45ecb1d02a
9 changed files with 284 additions and 191 deletions

View File

@ -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`

View File

@ -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

View File

@ -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

View File

@ -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:

127
kiplot/optionable.py Normal file
View File

@ -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] != '_')

View File

@ -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)

View File

@ -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):

View File

@ -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

View File

@ -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()