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:
parent
ee11ecf8e7
commit
45ecb1d02a
46
README.md
46
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`
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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] != '_')
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue