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. 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. This output is what you get from the 'File/Fabrication output/Drill Files' menu in pcbnew.
* Options: * Options:
- `map`: [string=None] format for a graphical drill map. The valid formats are hpgl, ps, gerber, dxf, svg and pdf. - `map`: [dict|string] [hpgl,ps,gerber,dxf,svg,pdf] format for a graphical drill map.
Not generated unless a format is specified. 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. - `metric_units`: [boolean=true] use metric units instead of inches.
- `minimal_header`: [boolean=false] use a minimal header in the file. - `minimal_header`: [boolean=false] use a minimal header in the file.
- `mirror_y_axis`: [boolean=false] invert the Y axis. - `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. - `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. - `use_aux_axis_as_origin`: [boolean=false] use the auxiliar axis as origin for coordinates.
* Gerber drill format * 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. 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. This output is what you get from the 'File/Fabrication output/Drill Files' menu in pcbnew.
* Options: * Options:
- `map`: [string=None] format for a graphical drill map. The valid formats are hpgl, ps, gerber, dxf, svg and pdf. - `map`: [dict|string] [hpgl,ps,gerber,dxf,svg,pdf] format for a graphical drill map.
Not generated unless a format is specified. Not generated unless a format is specified.
- `report`: [string=None] name of the drill report. Not generated unless a name 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. - `use_aux_axis_as_origin`: [boolean=false] use the auxiliar axis as origin for coordinates.
* Gerber format * 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. This output is what you get from the File/Plot menu in pcbnew.
* Options: * Options:
- `create_gerber_job_file`: [boolean=true] creates a file with information about all the generated gerbers. - `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_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. - `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. - `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. - `checkboxes`: [string='Sourced,Placed'] Comma separated list of checkbox columns.
- `dark_mode`: [boolean=false] Default to dark mode. - `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 - `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. - `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_pads`: [boolean=false] Hide footprint pads by default.
- `hide_silkscreen`: [boolean=false] Hide silkscreen 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. - `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. - `layer_view`: [string='FB'] [F,FB,B] Default layer view.
- `name_format`: [string='ibom'] Output file name format supports substitutions: - `name_format`: [string='ibom'] Output file name format supports substitutions:
%f : original pcb file name without extension. %f : original pcb file name without extension.
%p : pcb/project title from pcb metadata. %p : pcb/project title from pcb metadata.
%c : company from pcb metadata. %c : company from pcb metadata.
%r : revision from pcb metadata. %r : revision from pcb metadata.
%d : pcb date from metadata if available, file modification date otherwise. %d : pcb date from metadata if available, file modification date otherwise.
%D : bom generation date. %D : bom generation date.
%T : bom generation time. Extension .html will be added automatically. %T : bom generation time. Extension .html will be added automatically.
- `netlist_file`: [string=''] Path to netlist or xml file. - `netlist_file`: [string=''] Path to netlist or xml file.
- `no_blacklist_virtual`: [boolean=false] Do not blacklist virtual components. - `no_blacklist_virtual`: [boolean=false] Do not blacklist virtual components.
- `no_redraw_on_drag`: [boolean=false] Do not redraw pcb on drag by default. - `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). - `number`: [number=1] Number of boards to build (components multiplier).
- `separator`: [string=','] CSV Separator. - `separator`: [string=','] CSV Separator.
- `variant`: [string=''] Board variant(s), used to determine which components - `variant`: [string=''] Board variant(s), used to determine which components
are output to the BoM. To specify multiple variants, are output to the BoM. To specify multiple variants,
with a BOM file exported for each variant, separate with a BOM file exported for each variant, separate
variants with the ';' (semicolon) character. variants with the ';' (semicolon) character.
* PDF (Portable Document Format) * PDF (Portable Document Format)
* Type: `pdf` * 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. - `sketch_plot`: [boolean=false] don't fill objects, just draw the outline.
- `tent_vias`: [boolean=true] cover the vias. - `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. - `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) * STEP (ISO 10303-21 Clear Text Encoding of the Exchange Structure)
* Type: `step` * Type: `step`

View File

@ -2,7 +2,7 @@
all: ../README.md samples/generic_plot.kiplot.yaml 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 cat README.in | perl replace_tags.pl > ../README.md
samples/generic_plot.kiplot.yaml: ../kiplot/out_*.py ../kiplot/pre_*.py ../kiplot/config_reader.py samples/generic_plot.kiplot.yaml: ../kiplot/out_*.py ../kiplot/pre_*.py ../kiplot/config_reader.py

View File

@ -120,9 +120,11 @@ outputs:
type: 'excellon' type: 'excellon'
dir: 'Example/excellon_dir' dir: 'Example/excellon_dir'
options: 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 # 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 # [boolean=true] use metric units instead of inches
metric_units: true metric_units: true
# [boolean=false] use a minimal header in the file # [boolean=false] use a minimal header in the file
@ -131,8 +133,10 @@ outputs:
mirror_y_axis: false mirror_y_axis: false
# [boolean=true] generate one file for both, plated holes and non-plated holes, instead of two separated files # [boolean=true] generate one file for both, plated holes and non-plated holes, instead of two separated files
pth_and_npth_single_file: true pth_and_npth_single_file: true
# [string=None] name of the drill report. Not generated unless a name is specified # [dict|string] name of the drill report. Not generated unless a name is specified
report: None 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 # [boolean=false] use the auxiliar axis as origin for coordinates
use_aux_axis_as_origin: false use_aux_axis_as_origin: false
@ -144,11 +148,15 @@ outputs:
type: 'gerb_drill' type: 'gerb_drill'
dir: 'Example/gerb_drill_dir' dir: 'Example/gerb_drill_dir'
options: 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 # Not generated unless a format is specified
map: None map:
# [string=None] name of the drill report. Not generated unless a name is specified # [string='pdf'] [hpgl,ps,gerber,dxf,svg,pdf] format for a graphical drill map
report: None 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 # [boolean=false] use the auxiliar axis as origin for coordinates
use_aux_axis_as_origin: false use_aux_axis_as_origin: false

View File

@ -3,12 +3,15 @@ Class to read KiPlot config files
""" """
import os import os
from sys import (exit, maxsize) from sys import (exit, maxsize, exc_info)
from traceback import print_tb
from collections import OrderedDict from collections import OrderedDict
from .error import (KiPlotConfigurationError) from .error import (KiPlotConfigurationError)
from .gs import GS
from .kiplot import (Layer, load_board) from .kiplot import (Layer, load_board)
from .misc import (NO_YAML_MODULE, EXIT_BAD_CONFIG, EXIT_BAD_ARGS, EXAMPLE_CFG, WONT_OVERWRITE) 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 .out_base import BaseOutput
from .pre_base import BasePreFlight from .pre_base import BasePreFlight
# Logger # Logger
@ -25,6 +28,10 @@ except ImportError: # pragma: no cover
def config_error(msg): def config_error(msg):
if GS.debug_enabled:
logger.error('Trace stack:')
(type, value, traceback) = exc_info()
print_tb(traceback)
logger.error(msg) logger.error(msg)
exit(EXIT_BAD_CONFIG) exit(EXIT_BAD_CONFIG)
@ -225,16 +232,30 @@ def trim(docstring):
return trimmed return trimmed
def print_output_options(name, cl): def print_output_options(name, cl, indent):
obj = cl('', name, '') ind_str = indent*' '
print(' * Options:') if issubclass(cl, BaseOutput):
obj = cl('', name, '')
else:
obj = cl(name, '')
print(ind_str+'* Options:')
num_opts = 0 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) 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 num_opts = num_opts+1
if isinstance(v, type):
print_output_options('', v, indent+4)
if num_opts == 0: if num_opts == 0:
print(' - No available options') print(ind_str+' - No available options')
def print_one_out_help(details, n, o): def print_one_out_help(details, n, o):
@ -247,7 +268,7 @@ def print_one_out_help(details, n, o):
print(' * Description: '+lines[1]) print(' * Description: '+lines[1])
for ln in range(2, len(lines)): for ln in range(2, len(lines)):
print(' '+lines[ln]) print(' '+lines[ln])
print_output_options(n, o) print_output_options(n, o, 2)
else: else:
print('* {} [{}]'.format(lines[0], n)) print('* {} [{}]'.format(lines[0], n))
@ -280,6 +301,33 @@ def print_preflights_help():
print('- {}: {}.'.format(n, help.rstrip())) 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): def create_example(pcb_file, out_dir, copy_options):
if not os.path.exists(out_dir): if not os.path.exists(out_dir):
os.makedirs(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(" type: '{}'\n".format(n))
f.write(" dir: 'Example/{}_dir'\n".format(n)) f.write(" dir: 'Example/{}_dir'\n".format(n))
f.write(" options:\n") f.write(" options:\n")
obj = cls('', n, '') obj = print_example_options(f, cls, n, 6, po)
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))
if '_layers' in obj.__dict__: if '_layers' in obj.__dict__:
f.write(' layers:\n') f.write(' layers:\n')
for layer in layers: 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 import os
from pcbnew import (PLOT_FORMAT_HPGL, PLOT_FORMAT_POST, PLOT_FORMAT_GERBER, PLOT_FORMAT_DXF, PLOT_FORMAT_SVG, from pcbnew import (PLOT_FORMAT_HPGL, PLOT_FORMAT_POST, PLOT_FORMAT_GERBER, PLOT_FORMAT_DXF, PLOT_FORMAT_SVG,
PLOT_FORMAT_PDF, wxPoint) PLOT_FORMAT_PDF, wxPoint)
from .optionable import Optionable
from .out_base import BaseOutput from .out_base import BaseOutput
from .error import KiPlotConfigurationError
from kiplot.macros import macros, document # noqa: F401 from kiplot.macros import macros, document # noqa: F401
from . import log from . import log
logger = log.get_logger(__name__) 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): class AnyDrill(BaseOutput):
def __init__(self, name, type, description): def __init__(self, name, type, description):
super(AnyDrill, self).__init__(name, type, description) super(AnyDrill, self).__init__(name, type, description)
@ -16,11 +32,11 @@ class AnyDrill(BaseOutput):
with document: with document:
self.use_aux_axis_as_origin = False self.use_aux_axis_as_origin = False
""" use the auxiliar axis as origin for coordinates """ """ use the auxiliar axis as origin for coordinates """
self._map = None self.map = DrillMap
""" [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 """ Not generated unless a format is specified """
self._report = None self.report = DrillReport
""" [string=None] name of the drill report. Not generated unless a name is specified """ # pragma: no cover """ [dict|string] name of the drill report. Not generated unless a name is specified """ # pragma: no cover
# Mappings to KiCad values # Mappings to KiCad values
self._map_map = { self._map_map = {
'hpgl': PLOT_FORMAT_HPGL, 'hpgl': PLOT_FORMAT_HPGL,
@ -31,60 +47,20 @@ class AnyDrill(BaseOutput):
'pdf': PLOT_FORMAT_PDF '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): def config(self, outdir, options, layers):
super().config(outdir, options, layers) super().config(outdir, options, layers)
if self._map: # Solve the map for both cases
self._map = self._map_map[self._map] 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): def run(self, output_dir, board):
# dialog_gendrill.cpp:357 # dialog_gendrill.cpp:357
@ -95,14 +71,14 @@ class AnyDrill(BaseOutput):
drill_writer = self._configure_writer(board, offset) drill_writer = self._configure_writer(board, offset)
logger.debug("Generating drill files in "+output_dir) 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: if gen_map:
drill_writer.SetMapFileFormat(self._map) drill_writer.SetMapFileFormat(self.map)
logger.debug("Generating drill map type {} in {}".format(self._map, output_dir)) logger.debug("Generating drill map type {} in {}".format(self.map, output_dir))
# We always generate the drill file # We always generate the drill file
drill_writer.CreateDrillandMapFilesSet(output_dir, True, gen_map) drill_writer.CreateDrillandMapFilesSet(output_dir, True, gen_map)
if self._report is not None: if self.report:
drill_report_file = os.path.join(output_dir, self._report) drill_report_file = os.path.join(output_dir, self.report)
logger.debug("Generating drill report: "+drill_report_file) logger.debug("Generating drill report: "+drill_report_file)
drill_writer.GenDrillReportFile(drill_report_file) drill_writer.GenDrillReportFile(drill_report_file)

View File

@ -1,83 +1,22 @@
import inspect from .optionable import Optionable
from re import (compile)
from .error import KiPlotConfigurationError
from . import log from . import log
logger = log.get_logger(__name__) logger = log.get_logger(__name__)
def filter(v): class BaseOutput(Optionable):
return not (callable(v) or inspect.isclass(v) or isinstance(v, (dict, list)))
class BaseOutput(object):
_registered = {} _registered = {}
def __init__(self, name, type, description): def __init__(self, name, type, description):
self._name = name super(BaseOutput, self).__init__(name, description)
self._type = type self._type = type
self._description = description
self._sch_related = False self._sch_related = False
self._unkown_is_error = 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)
def config(self, outdir, options, layers): def config(self, outdir, options, layers):
self._outdir = outdir self._outdir = outdir
self._options = options
self._layers = layers self._layers = layers
if options: super(BaseOutput, self).config(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] != '_')
@staticmethod @staticmethod
def attr2longopt(attr): 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 .misc import (CMD_IBOM, URL_IBOM, BOM_ERROR)
from .gs import (GS) from .gs import (GS)
from .kiplot import (check_script) from .kiplot import (check_script)
from .optionable import Optionable
from kiplot.macros import macros, document, output_class # noqa: F401 from kiplot.macros import macros, document, output_class # noqa: F401
from . import log from . import log
@ -84,7 +85,7 @@ class IBoM(BaseOutput): # noqa: F821
os.environ['INTERACTIVE_HTML_BOM_NO_DISPLAY'] = '' os.environ['INTERACTIVE_HTML_BOM_NO_DISPLAY'] = ''
cmd = [CMD_IBOM, GS.pcb_file, '--dest-dir', output_dir, '--no-browser', ] cmd = [CMD_IBOM, GS.pcb_file, '--dest-dir', output_dir, '--no-browser', ]
# Convert attributes into options # 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: if not v:
continue continue
cmd.append(BaseOutput.attr2longopt(k)) # noqa: F821 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(): def test_drill_map_no_type_1():
ctx = context.TestContext('ErrorDrillMapNoType1', '3Rs', 'error_drill_map_no_type', None) ctx = context.TestContext('ErrorDrillMapNoType1', '3Rs', 'error_drill_map_no_type', None)
ctx.run(EXIT_BAD_CONFIG) ctx.run(EXIT_BAD_CONFIG)
assert ctx.search_err("Empty drill `map` section") assert ctx.search_err("Empty option .?map.?")
ctx.clean_up() ctx.clean_up()
def test_drill_map_no_type_2(): def test_drill_map_no_type_2():
ctx = context.TestContext('ErrorDrillMapNoType2', '3Rs', 'error_drill_map_no_type_2', None) ctx = context.TestContext('ErrorDrillMapNoType2', '3Rs', 'error_drill_map_no_type_2', None)
ctx.run(EXIT_BAD_CONFIG) 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() ctx.clean_up()
def test_drill_map_wrong_type_1(): def test_drill_map_wrong_type_1():
ctx = context.TestContext('ErrorDrillMapWrongType1', '3Rs', 'error_drill_map_wrong_type', None) ctx = context.TestContext('ErrorDrillMapWrongType1', '3Rs', 'error_drill_map_wrong_type', None)
ctx.run(EXIT_BAD_CONFIG) 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() ctx.clean_up()
def test_drill_map_wrong_type_2(): def test_drill_map_wrong_type_2():
ctx = context.TestContext('ErrorDrillMapWrongType2', '3Rs', 'error_drill_map_wrong_type_2', None) ctx = context.TestContext('ErrorDrillMapWrongType2', '3Rs', 'error_drill_map_wrong_type_2', None)
ctx.run(EXIT_BAD_CONFIG) 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() ctx.clean_up()
def test_drill_map_wrong_type_3(): def test_drill_map_wrong_type_3():
ctx = context.TestContext('ErrorDrillMapWrongType3', '3Rs', 'error_drill_map_wrong_type_3', None) ctx = context.TestContext('ErrorDrillMapWrongType3', '3Rs', 'error_drill_map_wrong_type_3', None)
ctx.run(EXIT_BAD_CONFIG) 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() ctx.clean_up()
def test_drill_report_no_type_1(): def test_drill_report_no_type_1():
ctx = context.TestContext('ErrorDrillReportNoType1', '3Rs', 'error_drill_report_no_type', None) ctx = context.TestContext('ErrorDrillReportNoType1', '3Rs', 'error_drill_report_no_type', None)
ctx.run(EXIT_BAD_CONFIG) ctx.run(EXIT_BAD_CONFIG)
assert ctx.search_err("Empty drill `report` section") assert ctx.search_err("Empty option .?report.?")
ctx.clean_up() ctx.clean_up()
def test_drill_report_no_type_2(): def test_drill_report_no_type_2():
ctx = context.TestContext('ErrorDrillReportNoType2', '3Rs', 'error_drill_report_no_type_2', None) ctx = context.TestContext('ErrorDrillReportNoType2', '3Rs', 'error_drill_report_no_type_2', None)
ctx.run(EXIT_BAD_CONFIG) 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() ctx.clean_up()
def test_drill_report_wrong_type_2(): def test_drill_report_wrong_type_2():
ctx = context.TestContext('ErrorDrillReportWrongType2', '3Rs', 'error_drill_report_wrong_type_2', None) ctx = context.TestContext('ErrorDrillReportWrongType2', '3Rs', 'error_drill_report_wrong_type_2', None)
ctx.run(EXIT_BAD_CONFIG) 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() ctx.clean_up()
def test_drill_report_wrong_type_3(): def test_drill_report_wrong_type_3():
ctx = context.TestContext('ErrorDrillReportWrongType3', '3Rs', 'error_drill_report_wrong_type_3', None) ctx = context.TestContext('ErrorDrillReportWrongType3', '3Rs', 'error_drill_report_wrong_type_3', None)
ctx.run(EXIT_BAD_CONFIG) 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() ctx.clean_up()
@ -222,7 +222,7 @@ def test_out_unknown_attr():
def test_no_layers(): def test_no_layers():
ctx = context.TestContext('ErrorNoLayers', '3Rs', 'error_no_layers', None) ctx = context.TestContext('ErrorNoLayers', '3Rs', 'error_no_layers', None)
ctx.run(EXIT_BAD_CONFIG) ctx.run(EXIT_BAD_CONFIG)
assert ctx.search_err("Missing `layers` list") assert ctx.search_err("Missing .?layers.? list")
ctx.clean_up() ctx.clean_up()
@ -236,7 +236,7 @@ def test_error_step_origin():
def test_error_step_min_distance(): def test_error_step_min_distance():
ctx = context.TestContext('ErrorStepMinDistance', 'bom', 'error_step_min_distance', None) ctx = context.TestContext('ErrorStepMinDistance', 'bom', 'error_step_min_distance', None)
ctx.run(EXIT_BAD_CONFIG) 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() ctx.clean_up()