Added pattern expansion in the `dir` option for outputs

Closes #58
This commit is contained in:
Salvador E. Tropea 2021-03-12 21:14:39 -03:00
parent eab8550c11
commit 1b48e614a7
37 changed files with 253 additions and 195 deletions

View File

@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
### Added
- `erc_warnings` pre-flight option to consider ERC warnings as errors.
- Pattern expansion in the `dir` option for outputs (#58)
### Changed
- Errors and warnings from KiAuto now are printed as errors and warnings.

View File

@ -118,7 +118,7 @@ class CfgYamlReader(object):
o_var = reg_class.get_class_for(otype)()
o_var.set_tree(o_tree)
try:
o_var.config()
o_var.config(None)
except KiPlotConfigurationError as e:
config_error("In section `"+name_type+"`: "+str(e))
return o_var
@ -167,7 +167,7 @@ class CfgYamlReader(object):
glb = GS.global_opts_class()
glb.set_tree(gb)
try:
glb.config()
glb.config(None)
except KiPlotConfigurationError as e:
config_error("In `global` section: "+str(e))

View File

@ -43,8 +43,8 @@ class DrillMarks(AnyLayerOptions):
raise KiPlotConfigurationError("Unknown drill mark type: {}".format(val))
self._drill_marks = val
def config(self):
super().config()
def config(self, parent):
super().config(parent)
self._drill_marks = DrillMarks._drill_marks_map[self._drill_marks]
def _configure_plot_ctrl(self, po, output_dir):

View File

@ -118,8 +118,8 @@ class BaseFilter(RegFilter):
self.comment = ''
""" A comment for documentation purposes """
def config(self):
super().config()
def config(self, parent):
super().config(parent)
if self.name[0] == '_' and not self._internal:
raise KiPlotConfigurationError('Filter names starting with `_` are reserved ({})'.format(self.name))
@ -184,7 +184,7 @@ class BaseFilter(RegFilter):
filter = RegFilter.get_class_for(tree['type'])()
filter._internal = True
filter.set_tree(tree)
filter.config()
filter.config(None)
RegOutput.add_filter(filter)
return filter

View File

@ -83,8 +83,8 @@ class Generic(BaseFilter): # noqa: F821
col = col[:-1]
return col
def config(self):
super().config()
def config(self, parent):
super().config(parent)
# include_only
if isinstance(self.include_only, type):
self.include_only = None

View File

@ -64,8 +64,8 @@ class Rot_Footprint(BaseFilter): # noqa: F821
""" [list(list(string))] A list of pairs regular expression/rotation.
Components matching the regular expression will be rotated the indicated angle """
def config(self):
super().config()
def config(self, parent):
super().config(parent)
self._rot = []
if isinstance(self.rotations, list):
for r in self.rotations:

View File

@ -25,8 +25,8 @@ class Var_Rename(BaseFilter): # noqa: F821
self.variant_to_value = False
""" Rename fields matching the variant to the value of the component """
def config(self):
super().config()
def config(self, parent):
super().config(parent)
if not self.separator:
self.separator = ':'

View File

@ -37,8 +37,8 @@ class Globals(FiltersOptions):
return new_val
return current
def config(self):
super().config()
def config(self, parent):
super().config(parent)
GS.global_output = self.set_global(GS.global_output, self.output, 'output')
GS.global_variant = self.set_global(GS.global_variant, self.variant, 'variant')
GS.global_kiauto_wait_start = self.set_global(GS.global_kiauto_wait_start, self.kiauto_wait_start, 'kiauto_wait_start')

View File

@ -326,7 +326,7 @@ def config_output(out, dry=False):
if out.is_sch():
load_sch()
try:
out.config()
out.config(None)
except KiPlotConfigurationError as e:
config_error("In section '"+out.name+"' ("+out.type+"): "+str(e))
@ -334,7 +334,8 @@ def config_output(out, dry=False):
def run_output(out):
GS.current_output = out.name
try:
out.run(get_output_dir(out.dir))
out_dir = out.expand_dirname(out.dir)
out.run(get_output_dir(out_dir))
out._done = True
except PlotError as e:
logger.error("In output `"+str(out)+"`: "+str(e))

View File

@ -105,8 +105,8 @@ class Layer(Optionable):
self._unkown_is_error = True
self._protel_extension = None
def config(self):
super().config()
def config(self, parent):
super().config(parent)
if not self.layer:
raise KiPlotConfigurationError("Missing or empty `layer` attribute for layer entry ({})".format(self._tree))
if not self.description:

View File

@ -31,6 +31,9 @@ class Optionable(object):
self._error_context = ''
self._tree = {}
self._configured = False
# File/directory pattern expansion
self._expand_id = ''
self._expand_ext = ''
super().__init__()
if GS.global_output is not None and getattr(self, 'output', None):
setattr(self, 'output', GS.global_output)
@ -146,7 +149,7 @@ class Optionable(object):
v = cur_val()
# Delegate the validation to the object
v.set_tree(new_val)
v.config()
v.config(self)
elif isinstance(v, list):
new_val = []
for element in v:
@ -157,7 +160,7 @@ class Optionable(object):
if isinstance(element, dict):
nv = cur_val()
nv.set_tree(element)
nv.config()
nv.config(self)
new_val.append(nv)
else:
new_val.append(element)
@ -168,7 +171,8 @@ class Optionable(object):
def set_tree(self, tree):
self._tree = tree
def config(self):
def config(self, parent):
self._parent = parent
if self._tree and not self._configured:
self._perform_config_mapping()
self._configured = True
@ -189,7 +193,7 @@ class Optionable(object):
return self.variant.file_id
return ''
def expand_filename(self, out_dir, name, id='', ext=''):
def expand_filename_pcb(self, name):
""" Expands %* values in filenames.
Uses data from the PCB. """
if GS.board:
@ -200,18 +204,18 @@ class Optionable(object):
name = name.replace('%D', GS.today)
name = name.replace('%F', GS.pcb_no_ext)
name = name.replace('%f', GS.pcb_basename)
name = name.replace('%i', id)
name = name.replace('%i', self._expand_id)
name = name.replace('%p', GS.pcb_title)
name = name.replace('%r', GS.pcb_rev)
name = name.replace('%T', GS.time)
name = name.replace('%v', self._find_variant() if self else '')
name = name.replace('%x', ext)
name = name.replace('%x', self._expand_ext)
# sanitize the name to avoid characters illegal in file systems
name = name.replace('\\', '/')
name = re.sub(r'[?%*:|"<>]', '_', name)
return os.path.abspath(os.path.join(out_dir, name))
return name
def expand_filename_sch(self, out_dir, name, id='', ext=''):
def expand_filename_sch(self, name):
""" Expands %* values in filenames.
Uses data from the SCH. """
if GS.sch_file:
@ -222,16 +226,16 @@ class Optionable(object):
name = name.replace('%D', GS.today)
name = name.replace('%F', GS.sch_no_ext)
name = name.replace('%f', GS.sch_basename)
name = name.replace('%i', id)
name = name.replace('%i', self._expand_id)
name = name.replace('%p', GS.sch_title)
name = name.replace('%r', GS.sch_rev)
name = name.replace('%T', GS.time)
name = name.replace('%v', self._find_variant() if self else '')
name = name.replace('%x', ext)
name = name.replace('%x', self._expand_ext)
# sanitize the name to avoid characters illegal in file systems
name = name.replace('\\', '/')
name = re.sub(r'[?%*:|"<>]', '_', name)
return os.path.abspath(os.path.join(out_dir, name))
return name
class BaseOptions(Optionable):
@ -243,3 +247,17 @@ class BaseOptions(Optionable):
def read_vals_from_po(self, po):
""" Set attributes from a PCB_PLOT_PARAMS (plot options) """
return
def expand_filename(self, dir, name, id, ext):
cur_id = self._expand_id
cur_ext = self._expand_ext
self._expand_id = id
self._expand_ext = ext
if self._parent._sch_related:
name = self.expand_filename_sch(name)
else:
name = self.expand_filename_pcb(name)
res = os.path.abspath(os.path.join(dir, name))
self._expand_id = cur_id
self._expand_ext = cur_ext
return res

View File

@ -73,8 +73,8 @@ class AnyDrill(BaseOptions):
self._map_ext = {'hpgl': 'plt', 'ps': 'ps', 'gerber': 'gbr', 'dxf': 'dxf', 'svg': 'svg', 'pdf': 'pdf'}
self._unified_output = False
def config(self):
super().config()
def config(self, parent):
super().config(parent)
# Solve the map for both cases
if isinstance(self.map, str):
self.map_ext = self._map_ext[self.map]
@ -91,6 +91,8 @@ class AnyDrill(BaseOptions):
self.report = self.report.filename
elif not isinstance(self.report, str):
self.report = None
self._expand_id = 'drill'
self._expand_ext = self._ext
def solve_id(self, d):
if not d:
@ -127,6 +129,8 @@ class AnyDrill(BaseOptions):
return filenames
def run(self, output_dir):
if self.output:
output_dir = os.path.dirname(output_dir)
# dialog_gendrill.cpp:357
if self.use_aux_axis_as_origin:
offset = get_aux_origin(GS.board)
@ -145,6 +149,7 @@ class AnyDrill(BaseOptions):
files = self.get_file_names(output_dir)
for k_f, f in files.items():
if f:
logger.debug("Renaming {} -> {}".format(k_f, f))
os.rename(k_f, f)
# Generate the report
if self.report:
@ -152,7 +157,7 @@ class AnyDrill(BaseOptions):
logger.debug("Generating drill report: "+drill_report_file)
drill_writer.GenDrillReportFile(drill_report_file)
def get_targets(self, parent, out_dir):
def get_targets(self, out_dir):
targets = []
files = self.get_file_names(out_dir)
for k_f, f in files.items():

View File

@ -72,8 +72,8 @@ class AnyLayerOptions(VariantOptions):
""" [list(dict)] A list of customized reports for the manufacturer """
super().__init__()
def config(self):
super().config()
def config(self, parent):
super().config(parent)
if isinstance(self.custom_reports, type):
self.custom_reports = []
@ -243,8 +243,8 @@ class AnyLayer(BaseOutput):
""" [list(dict)|list(string)|string] [all,selected,copper,technical,user]
List of PCB layers to plot """
def config(self):
super().config()
def config(self, parent):
super().config(parent)
# We need layers
if isinstance(self.layers, type):
raise KiPlotConfigurationError("Missing `layers` list")

View File

@ -3,6 +3,7 @@
# Copyright (c) 2020-2021 Instituto Nacional de Tecnología Industrial
# License: GPL-3.0
# Project: KiBot (formerly KiPlot)
import os
from .gs import GS
from .kiplot import load_sch, get_board_comps_data
from .misc import Rect, KICAD_VERSION_5_99, W_WRONGPASTE
@ -53,7 +54,7 @@ class BaseOutput(RegOutput):
if not (hasattr(self, "options") and hasattr(self.options, "get_targets")):
logger.error("Output {} doesn't implement get_targets(), plese report it".format(self))
return []
return self.options.get_targets(self, out_dir)
return self.options.get_targets(out_dir)
def get_dependencies(self):
""" Returns a list of files needed to create this output """
@ -63,16 +64,28 @@ class BaseOutput(RegOutput):
return [GS.sch_file]
return [GS.pcb_file]
def config(self):
super().config()
def config(self, parent):
super().config(parent)
if getattr(self, 'options', None) and isinstance(self.options, type):
# No options, get the defaults
self.options = self.options()
# Configure them using an empty tree
self.options.config()
self.options.config(self)
def expand_dirname(self, out_dir):
if self._sch_related:
return self.options.expand_filename_sch(out_dir)
return self.options.expand_filename_pcb(out_dir)
def expand_filename(self, out_dir, name):
if self._sch_related:
name = self.options.expand_filename_sch(name)
else:
name = self.options.expand_filename_pcb(name)
return os.path.abspath(os.path.join(out_dir, name))
def run(self, output_dir):
self.options.run(output_dir)
self.options.run(self.expand_filename(output_dir, self.options.output))
class BoMRegex(Optionable):
@ -103,8 +116,8 @@ class VariantOptions(BaseOptions):
super().__init__()
self._comps = None
def config(self):
super().config()
def config(self, parent):
super().config(parent)
self.variant = RegOutput.check_variant(self.variant)
self.dnf_filter = BaseFilter.solve_filter(self.dnf_filter, 'dnf_filter')

View File

@ -50,8 +50,8 @@ class BoMColumns(Optionable):
self._field_example = 'Row'
self._name_example = 'Line'
def config(self):
super().config()
def config(self, parent):
super().config(parent)
if not self.field:
raise KiPlotConfigurationError("Missing or empty `field` in columns list ({})".format(str(self._tree)))
# Ensure this is None or a list
@ -92,8 +92,8 @@ class BoMLinkable(Optionable):
self.title = 'KiBot Bill of Materials'
""" BoM title """
def config(self):
super().config()
def config(self, parent):
super().config(parent)
# digikey_link
if isinstance(self.digikey_link, type):
self.digikey_link = []
@ -121,8 +121,8 @@ class BoMHTML(BoMLinkable):
""" Page style. Internal styles: modern-blue, modern-green, modern-red and classic.
Or you can provide a CSS file name. Please use .css as file extension. """
def config(self):
super().config()
def config(self, parent):
super().config(parent)
# Style
if not self.style:
self.style = 'modern-blue'
@ -157,8 +157,8 @@ class BoMXLSX(BoMLinkable):
self.style = 'modern-blue'
""" Head style: modern-blue, modern-green, modern-red and classic. """
def config(self):
super().config()
def config(self, parent):
super().config(parent)
# Style
if not self.style:
self.style = 'modern-blue'
@ -200,8 +200,8 @@ class Aggregate(Optionable):
self.number = 1
""" Number of boards to build (components multiplier). Use negative to substract """
def config(self):
super().config()
def config(self, parent):
super().config(parent)
if not self.file:
raise KiPlotConfigurationError("Missing or empty `file` in aggregate list ({})".format(str(self._tree)))
if not self.name:
@ -323,26 +323,28 @@ class BoMOptions(BaseOptions):
# Delegate any filter to the variant
self.variant.set_def_filters(self.exclude_filter, self.dnf_filter, self.dnc_filter)
self.exclude_filter = self.dnf_filter = self.dnc_filter = None
self.variant.config() # Fill or adjust any detail
self.variant.config(self) # Fill or adjust any detail
def config(self):
super().config()
def config(self, parent):
super().config(parent)
self.format = self._guess_format()
self._expand_id = 'bom'
self._expand_ext = self.format.lower()
# HTML options
if self.format == 'html' and isinstance(self.html, type):
# If no options get the defaults
self.html = BoMHTML()
self.html.config()
self.html.config(self)
# CSV options
if self.format in ['csv', 'tsv', 'txt'] and isinstance(self.csv, type):
# If no options get the defaults
self.csv = BoMCSV()
self.csv.config()
self.csv.config(self)
# XLSX options
if self.format == 'xlsx' and isinstance(self.xlsx, type):
# If no options get the defaults
self.xlsx = BoMXLSX()
self.xlsx.config()
self.xlsx.config(self)
# group_fields
if isinstance(self.group_fields, type):
self.group_fields = ColumnList.DEFAULT_GROUPING
@ -434,9 +436,8 @@ class BoMOptions(BaseOptions):
comps.extend(new_comps)
prj.source = os.path.basename(prj.file)
def run(self, output_dir):
def run(self, output):
format = self.format.lower()
output = self.expand_filename_sch(output_dir, self.output, 'bom', format)
# Add some info needed for the output to the config object.
# So all the configuration is contained in one object.
self.source = GS.sch_basename
@ -482,8 +483,8 @@ class BoMOptions(BaseOptions):
c.ref = c.ref[l_id:]
c.ref_id = ''
def get_targets(self, parent, out_dir):
return [self.expand_filename_sch(out_dir, self.output, 'bom', self.format.lower())]
def get_targets(self, out_dir):
return [self._parent.expand_filename(out_dir, self.output)]
@output_class

View File

@ -62,11 +62,13 @@ class CompressOptions(BaseOptions):
""" [list(dict)] Which files will be included """
super().__init__()
def config(self):
super().config()
def config(self, parent):
super().config(parent)
if isinstance(self.files, type):
self.files = []
logger.warning(W_EMPTYZIP+'No files provided, creating an empty archive')
self._expand_id = parent.name
self._expand_ext = self.solve_extension()
def create_zip(self, output, files):
extra = {}
@ -113,7 +115,7 @@ class CompressOptions(BaseOptions):
ext += '.'+sub_ext
return ext
def get_files(self, output, parent, no_out_run=False):
def get_files(self, output, no_out_run=False):
output_real = os.path.realpath(output)
files = OrderedDict()
for f in self.files:
@ -126,7 +128,7 @@ class CompressOptions(BaseOptions):
files_list = out.get_targets(get_output_dir(out.dir, dry=True))
break
if files_list is None:
logger.error('Unknown output `{}` selected in {}'.format(f.from_output, parent))
logger.error('Unknown output `{}` selected in {}'.format(f.from_output, self._parent))
exit(WRONG_ARGUMENTS)
if not no_out_run:
for file in files_list:
@ -156,20 +158,19 @@ class CompressOptions(BaseOptions):
files[fname_real] = dest
return files
def get_targets(self, parent, out_dir):
return [self.expand_filename(out_dir, self.output, parent.name, self.solve_extension())]
def get_targets(self, out_dir):
return [self._parent.expand_filename(out_dir, self.output)]
def get_dependencies(self, parent):
output = self.get_targets(parent, GS.out_dir)[0]
files = self.get_files(output, parent, no_out_run=True)
def get_dependencies(self):
output = self.get_targets(GS.out_dir)[0]
files = self.get_files(output, no_out_run=True)
return files.keys()
def run(self, output_dir, parent):
def run(self, output):
# Output file name
output = self.get_targets(parent, output_dir)[0]
logger.debug('Collecting files')
# Collect the files
files = self.get_files(output, parent)
files = self.get_files(output)
logger.debug('Generating `{}` archive'.format(output))
if self.format == 'ZIP':
self.create_zip(output, files)
@ -191,7 +192,4 @@ class Compress(BaseOutput): # noqa: F821
""" [dict] Options for the `compress` output """
def get_dependencies(self):
return self.options.get_dependencies(self)
def run(self, output_dir):
self.options.run(output_dir, self)
return self.options.get_dependencies()

View File

@ -90,15 +90,17 @@ class IBoMOptions(VariantOptions):
super().__init__()
self.add_to_doc('variant', WARNING_MIX)
self.add_to_doc('dnf_filter', WARNING_MIX)
self._expand_id = 'ibom'
self._expand_ext = 'html'
def config(self):
super().config()
def config(self, parent):
super().config(parent)
self.netlist_file = self.expand_filename('', self.netlist_file, 'ibom', 'xml')
def get_targets(self, parent, out_dir):
def get_targets(self, out_dir):
if self.output:
return [self.expand_filename(out_dir, self.output, 'ibom', 'html')]
logger.warning(W_EXTNAME+'{} uses a name generated by the external tool.'.format(parent))
logger.warning(W_EXTNAME+'{} uses a name generated by the external tool.'.format(self._parent))
logger.warning(W_EXTNAME+'Please use a name generated by KiBot or specify the name explicitly.')
return []
@ -108,20 +110,23 @@ class IBoMOptions(VariantOptions):
files.append(self.netlist_file)
return files
def run(self, output_dir):
super().run(output_dir)
def run(self, name):
super().run(name)
tool = search_as_plugin(CMD_IBOM, ['InteractiveHtmlBom', 'InteractiveHtmlBom/InteractiveHtmlBom'])
check_script(tool, URL_IBOM)
logger.debug('Doing Interactive BoM')
# Tell ibom we don't want to use the screen
os.environ['INTERACTIVE_HTML_BOM_NO_DISPLAY'] = ''
cmd = [tool, GS.pcb_file, '--dest-dir', output_dir, '--no-browser', ]
# Solve the output name
output = None
if self.output:
output = self.expand_filename(output_dir, self.output, 'ibom', 'html')
output = name
self.name_format = 'ibom'
output_dir = os.path.dirname(name)
cur = os.path.join(output_dir, 'ibom.html')
else:
output_dir = name
cmd = [tool, GS.pcb_file, '--dest-dir', output_dir, '--no-browser', ]
# Apply variants/filters
to_remove = ','.join(self.get_not_fitted_refs())
if self.blacklist and to_remove:

View File

@ -55,8 +55,8 @@ class KiBoMColumns(Optionable):
self._field_example = 'Row'
self._name_example = 'Line'
def config(self):
super().config()
def config(self, parent):
super().config(parent)
if not self.field:
raise KiPlotConfigurationError("Missing or empty `field` in columns list ({})".format(str(self._tree)))
if isinstance(self.join, type):
@ -207,8 +207,8 @@ class KiBoMConfig(Optionable):
logger.debug('Output from command:\n'+cmd_output.decode())
return columns
def config(self):
super().config()
def config(self, parent):
super().config(parent)
# digikey_link
if isinstance(self.digikey_link, type):
self.digikey_link = None
@ -341,9 +341,10 @@ class KiBoMOptions(BaseOptions):
self.format = 'HTML'
""" [HTML,CSV,XML,XLSX] format for the BoM """
super().__init__()
self._expand_id = 'bom'
def config(self):
super().config()
def config(self, parent):
super().config(parent)
if isinstance(self.conf, type):
self.conf = 'bom.ini'
elif isinstance(self.conf, str):
@ -354,25 +355,28 @@ class KiBoMOptions(BaseOptions):
conf = os.path.abspath(os.path.join(GS.out_dir, CONFIG_FILENAME))
self.conf.save(conf)
self.conf = conf
self._expand_ext = self.format.lower()
def get_targets(self, parent, out_dir):
def get_targets(self, out_dir):
if self.output:
return [self.expand_filename_sch(out_dir, self.output, 'bom', self.format.lower())]
logger.warning(W_EXTNAME+'{} uses a name generated by the external tool.'.format(parent))
return [self.expand_filename(out_dir, self.output, 'bom', self.format.lower())]
logger.warning(W_EXTNAME+'{} uses a name generated by the external tool.'.format(self._parent))
logger.warning(W_EXTNAME+'Please use a name generated by KiBot or specify the name explicitly.')
return []
def run(self, output_dir):
def run(self, name):
check_script(CMD_KIBOM, URL_KIBOM, '1.8.0')
format = self.format.lower()
prj = GS.sch_no_ext
config = os.path.join(GS.sch_dir, self.conf)
if self.output:
force_output = True
output = self.expand_filename_sch(output_dir, self.output, 'bom', format)
output = name
output_dir = os.path.dirname(name)
else:
force_output = False
output = os.path.basename(prj)+'.'+format
output_dir = name
logger.debug('Doing BoM, format {} prj: {} config: {} output: {}'.format(format, prj, config, output))
cmd = [CMD_KIBOM,
'-n', str(self.number),

View File

@ -55,8 +55,8 @@ class PcbDrawStyle(Optionable):
if not self._color_re.match(color):
raise KiPlotConfigurationError('Invalid color for `{}` use `#rrggbb` with hex digits'.format(name))
def config(self):
super().config()
def config(self, parent):
super().config(parent)
self.validate_color('board')
self.validate_color('copper')
self.validate_color('board')
@ -78,7 +78,7 @@ class PcbDrawRemap(Optionable):
def __init__(self):
super().__init__()
def config(self):
def config(self, parent):
pass
@ -139,8 +139,8 @@ class PcbDrawOptions(VariantOptions):
""" Name for the generated file """
super().__init__()
def config(self):
super().config()
def config(self, parent):
super().config(parent)
# Libs
if isinstance(self.libs, type):
self.libs = None
@ -174,6 +174,8 @@ class PcbDrawOptions(VariantOptions):
self.style = None
elif isinstance(self.style, PcbDrawStyle):
self.style = self.style.to_dict()
self._expand_id = 'bottom' if self.bottom else 'top'
self._expand_ext = self.format
def _create_remap(self):
with NamedTemporaryFile(mode='w', delete=False) as f:
@ -227,14 +229,12 @@ class PcbDrawOptions(VariantOptions):
cmd.append(svg)
return svg
def get_targets(self, parent, out_dir):
return [self.expand_filename(out_dir, self.output, 'bottom' if self.bottom else 'top', self.format)]
def get_targets(self, out_dir):
return [self._parent.expand_filename(out_dir, self.output)]
def run(self, output_dir):
super().run(output_dir)
def run(self, name):
super().run(name)
check_script(PCBDRAW, URL_PCBDRAW, '0.6.0')
# Output file name
output = self.expand_filename(output_dir, self.output, 'bottom' if self.bottom else 'top', self.format)
# Base command with overwrite
cmd = [PCBDRAW]
# Add user options
@ -278,7 +278,7 @@ class PcbDrawOptions(VariantOptions):
tmp_remap = None
# The board & output
cmd.append(GS.pcb_file)
svg = self._append_output(cmd, output)
svg = self._append_output(cmd, name)
# Execute and inform is successful
_run_command(cmd, tmp_remap, tmp_style)
if svg is not None:
@ -288,7 +288,7 @@ class PcbDrawOptions(VariantOptions):
cmd = [CONVERT, '-trim', png]
if self.format == 'jpg':
cmd += ['-quality', '85%']
cmd.append(output)
cmd.append(name)
_run_command(cmd, png)

View File

@ -42,6 +42,7 @@ class PDF_Pcb_PrintOptions(VariantOptions):
self.mirror = False
""" Print mirrored (X axis inverted). ONLY for KiCad 6 """
super().__init__()
self._expand_ext = 'pdf'
@property
def drill_marks(self):
@ -53,8 +54,8 @@ class PDF_Pcb_PrintOptions(VariantOptions):
raise KiPlotConfigurationError("Unknown drill mark type: {}".format(val))
self._drill_marks = val
def config(self):
super().config()
def config(self, parent):
super().config(parent)
self._drill_marks = PDF_Pcb_PrintOptions._drill_marks_map[self._drill_marks]
@staticmethod
@ -85,18 +86,13 @@ class PDF_Pcb_PrintOptions(VariantOptions):
self.restore_paste_and_glue(board, comps_hash)
return fname, fproj
def get_targets(self, out_dir, layers):
layers = Layer.solve(layers)
id = '+'.join([la.suffix for la in layers])
return [self.expand_filename(out_dir, self.output, id, 'pdf')]
def get_targets(self, out_dir):
return [self._parent.expand_filename(out_dir, self.output)]
def run(self, output_dir, layers):
super().run(layers)
def run(self, output):
super().run(self._layers)
check_script(CMD_PCBNEW_PRINT_LAYERS, URL_PCBNEW_PRINT_LAYERS, '1.5.2')
layers = Layer.solve(layers)
# Output file name
id = '+'.join([la.suffix for la in layers])
output = self.expand_filename(output_dir, self.output, id, 'pdf')
cmd = [CMD_PCBNEW_PRINT_LAYERS, 'export', '--output_name', output]
if BasePreFlight.get_option('check_zone_fills'):
cmd.append('-f')
@ -110,10 +106,10 @@ class PDF_Pcb_PrintOptions(VariantOptions):
if self.mirror:
cmd.append('--mirror')
board_name, proj_name = self.filter_components(GS.board)
cmd.extend([board_name, output_dir])
cmd.extend([board_name, os.path.dirname(output)])
cmd, video_remove = add_extra_options(cmd)
# Add the layers
cmd.extend([la.layer for la in layers])
cmd.extend([la.layer for la in self._layers])
# Execute it
ret = exec_with_retry(cmd)
# Remove the temporal PCB
@ -129,6 +125,11 @@ class PDF_Pcb_PrintOptions(VariantOptions):
if os.path.isfile(video_name):
os.remove(video_name)
def set_layers(self, layers):
layers = Layer.solve(layers)
self._layers = layers
self._expand_id = '+'.join([la.suffix for la in layers])
@output_class
class PDF_Pcb_Print(BaseOutput): # noqa: F821
@ -145,14 +146,9 @@ class PDF_Pcb_Print(BaseOutput): # noqa: F821
""" [list(dict)|list(string)|string] [all,selected,copper,technical,user]
List of PCB layers to include in the PDF """
def config(self):
super().config()
def config(self, parent):
super().config(parent)
# We need layers
if isinstance(self.layers, type):
raise KiPlotConfigurationError("Missing `layers` list")
def get_targets(self, out_dir):
return self.options.get_targets(out_dir, self.layers)
def run(self, output_dir):
self.options.run(output_dir, self.layers)
self.options.set_layers(self.layers)

View File

@ -23,16 +23,17 @@ class PDF_Sch_PrintOptions(VariantOptions):
""" Filename for the output PDF (%i=schematic %x=pdf) """
super().__init__()
self.add_to_doc('variant', "Not fitted components are crossed")
self._expand_id = 'schematic'
self._expand_ext = 'pdf'
def get_targets(self, parent, out_dir):
id = 'schematic'
ext = 'pdf'
def get_targets(self, out_dir):
if self.output:
return [self.expand_filename_sch(out_dir, self.output, id, ext)]
return [self.expand_filename_sch(out_dir, '%f.%x', id, ext)]
return [self._parent.expand_filename(out_dir, self.output)]
return [self._parent.expand_filename(out_dir, '%f.%x')]
def run(self, output_dir):
super().run(output_dir)
def run(self, name):
super().run(name)
output_dir = os.path.dirname(name)
check_eeschema_do()
if self._comps:
# Save it to a temporal dir
@ -53,18 +54,15 @@ class PDF_Sch_PrintOptions(VariantOptions):
logger.error(CMD_EESCHEMA_DO+' returned %d', ret)
exit(PDF_SCH_PRINT)
if self.output:
id = 'schematic'
ext = 'pdf'
cur = self.expand_filename_sch(output_dir, '%f.%x', id, ext)
new = self.expand_filename_sch(output_dir, self.output, id, ext)
logger.debug('Moving '+cur+' -> '+new)
os.rename(cur, new)
cur = self._parent.expand_filename(output_dir, '%f.%x')
logger.debug('Moving '+cur+' -> '+name)
os.rename(cur, name)
# Remove the temporal dir if needed
if sch_dir:
logger.debug('Removing temporal variant dir `{}`'.format(sch_dir))
rmtree(sch_dir)
if video_remove:
video_name = os.path.join(GS.out_dir, 'export_eeschema_screencast.ogv')
video_name = os.path.join(output_dir, 'export_eeschema_screencast.ogv')
if os.path.isfile(video_name):
os.remove(video_name)

View File

@ -5,6 +5,7 @@
# License: GPL-3.0
# Project: KiBot (formerly KiPlot)
# Adapted from: https://github.com/johnbeard/kiplot/pull/10
import os
from re import compile
from datetime import datetime
from pcbnew import IU_PER_MM, IU_PER_MILS
@ -44,8 +45,8 @@ class PosColumns(Optionable):
self._id_example = 'Ref'
self._name_example = 'Reference'
def config(self):
super().config()
def config(self, parent):
super().config(parent)
if not self.id:
raise KiPlotConfigurationError("Missing or empty `id` in columns list ({})".format(str(self._tree)))
@ -68,9 +69,10 @@ class PositionOptions(VariantOptions):
self.bottom_negative_x = False
""" Use negative X coordinates for footprints on bottom layer """
super().__init__()
self._expand_id = 'position'
def config(self):
super().config()
def config(self, parent):
super().config(parent)
if isinstance(self.columns, type):
# Default list of columns
self.columns = OrderedDict([('Ref', 'Ref'), ('Val', 'Val'), ('Package', 'Package'), ('PosX', 'PosX'),
@ -86,6 +88,7 @@ class PositionOptions(VariantOptions):
new_name = col.name if col.name else new_col
new_columns[new_col] = new_name
self.columns = new_columns
self._expand_ext = 'pos' if self.format == 'ASCII' else 'csv'
def _do_position_plot_ascii(self, output_dir, columns, modulesStr, maxSizes):
topf = None
@ -200,15 +203,16 @@ class PositionOptions(VariantOptions):
return PositionOptions.is_pure_smd_5, PositionOptions.is_not_virtual_5
return PositionOptions.is_pure_smd_6, PositionOptions.is_not_virtual_6 # pragma: no cover (Ki6)
def get_targets(self, parent, out_dir):
ext = 'pos' if self.format == 'ASCII' else 'csv'
def get_targets(self, out_dir):
ext = self._expand_ext
if self.separate_files_for_front_and_back:
return [self.expand_filename(out_dir, self.output, 'top_pos', ext),
self.expand_filename(out_dir, self.output, 'bottom_pos', ext)]
return [self.expand_filename(out_dir, self.output, 'both_pos', ext)]
def run(self, output_dir):
super().run(output_dir)
def run(self, fname):
super().run(fname)
output_dir = os.path.dirname(fname)
columns = self.columns.values()
# Note: the parser already checked the units are milimeters or inches
conv = 1.0
@ -223,11 +227,13 @@ class PositionOptions(VariantOptions):
quote_char = '"' if self.format == 'CSV' else ''
for m in sorted(GS.board.GetModules(), key=lambda c: _ref_key(c.GetReference())):
ref = m.GetReference()
logger.debug('P&P ref: {}'.format(ref))
value = None
# Apply any filter or variant data
if comps_hash:
c = comps_hash.get(ref, None)
if c:
logger.debug('fit: {} include: {}'.format(c.fitted, c.included))
if not c.fitted or not c.included:
continue
value = c.value

View File

@ -12,7 +12,7 @@ class Sch_Variant_Options(VariantOptions):
def __init__(self):
super().__init__()
def get_targets(self, parent, out_dir):
def get_targets(self, out_dir):
return GS.sch.file_names_variant(out_dir)
def run(self, output_dir):
@ -33,3 +33,7 @@ class Sch_Variant(BaseOutput): # noqa: F821
self.options = Sch_Variant_Options
""" [dict] Options for the `sch_variant` output """
self._sch_related = True
def run(self, output_dir):
# No output member, just a dir
self.options.run(output_dir)

View File

@ -43,6 +43,8 @@ class STEPOptions(VariantOptions):
# Temporal dir used to store the downloaded files
self._tmp_dir = None
super().__init__()
self._expand_id = '3D'
self._expand_ext = 'step'
@property
def origin(self):
@ -197,13 +199,11 @@ class STEPOptions(VariantOptions):
models.push_front(model)
return fname
def get_targets(self, parent, out_dir):
return [self.expand_filename(out_dir, self.output, '3D', 'step')]
def get_targets(self, out_dir):
return [self._parent.expand_filename(out_dir, self.output)]
def run(self, output_dir):
super().run(output_dir)
# Output file name
output = self.get_targets(None, output_dir)[0]
def run(self, output):
super().run(output)
# Make units explicit
if self.metric_units:
units = 'mm'

View File

@ -24,16 +24,17 @@ class SVG_Sch_PrintOptions(VariantOptions):
""" Filename for the output SVG (%i=schematic %x=svg) """
super().__init__()
self.add_to_doc('variant', "Not fitted components are crossed")
self._expand_id = 'schematic'
self._expand_ext = 'svg'
def get_targets(self, parent, out_dir):
id = 'schematic'
ext = 'svg'
def get_targets(self, out_dir):
if self.output:
return [self.expand_filename_sch(out_dir, self.output, id, ext)]
return [self.expand_filename_sch(out_dir, '%f.%x', id, ext)]
return [self._parent.expand_filename(out_dir, self.output)]
return [self._parent.expand_filename(out_dir, '%f.%x')]
def run(self, output_dir):
super().run(output_dir)
def run(self, name):
super().run(name)
output_dir = os.path.dirname(name)
check_eeschema_do()
if self._comps:
# Save it to a temporal dir
@ -50,18 +51,15 @@ class SVG_Sch_PrintOptions(VariantOptions):
logger.error(CMD_EESCHEMA_DO+' returned %d', ret)
exit(SVG_SCH_PRINT)
if self.output:
id = 'schematic'
ext = 'svg'
cur = self.expand_filename_sch(output_dir, '%f.%x', id, ext)
new = self.expand_filename_sch(output_dir, self.output, id, ext)
logger.debug('Moving '+cur+' -> '+new)
os.rename(cur, new)
cur = self._parent.expand_filename(output_dir, '%f.%x')
logger.debug('Moving '+cur+' -> '+name)
os.rename(cur, name)
# Remove the temporal dir if needed
if sch_dir:
logger.debug('Removing temporal variant dir `{}`'.format(sch_dir))
rmtree(sch_dir)
if video_remove:
video_name = os.path.join(GS.out_dir, 'export_eeschema_screencast.ogv')
video_name = os.path.join(output_dir, 'export_eeschema_screencast.ogv')
if os.path.isfile(video_name):
os.remove(video_name)

View File

@ -104,3 +104,6 @@ class BasePreFlight(Registrable):
def get_targets(self):
""" Returns a list of targets generated by this preflight """
return []
def _find_variant(self):
return ''

View File

@ -26,12 +26,15 @@ class Run_DRC(BasePreFlight): # noqa: F821
raise KiPlotConfigurationError('must be boolean')
self._enabled = value
self._pcb_related = True
self._expand_id = 'drc'
self._expand_ext = 'txt'
def get_targets(self):
""" Returns a list of targets generated by this preflight """
load_board()
out_pattern = GS.global_output if GS.global_output is not None else GS.def_global_output
return [Optionable.expand_filename(None, GS.out_dir, out_pattern, 'drc', 'txt')]
name = Optionable.expand_filename_pcb(self, out_pattern)
return [os.path.abspath(os.path.join(GS.out_dir, name))]
def run(self):
check_script(CMD_PCBNEW_RUN_DRC, URL_PCBNEW_RUN_DRC, '1.4.0')

View File

@ -26,12 +26,15 @@ class Run_ERC(BasePreFlight): # noqa: F821
raise KiPlotConfigurationError('must be boolean')
self._enabled = value
self._sch_related = True
self._expand_id = 'erc'
self._expand_ext = 'txt'
def get_targets(self):
""" Returns a list of targets generated by this preflight """
load_sch()
out_pattern = GS.global_output if GS.global_output is not None else GS.def_global_output
return [Optionable.expand_filename_sch(None, GS.out_dir, out_pattern, 'erc', 'txt')]
name = Optionable.expand_filename_sch(self, out_pattern)
return [os.path.abspath(os.path.join(GS.out_dir, name))]
def run(self):
check_eeschema_do()

View File

@ -47,8 +47,8 @@ class FiltersOptions(Optionable):
""" [list(dict)] DRC/ERC errors to be ignored """
self._filter_what = 'DRC/ERC errors'
def config(self):
super().config()
def config(self, parent):
super().config(parent)
parsed = None
self.unparsed = None
if not isinstance(self.filters, type):
@ -82,7 +82,7 @@ class Filters(BasePreFlight): # noqa: F821
def __init__(self, name, value):
f = FiltersOptions()
f.set_tree({'filters': value})
f.config()
f.config(self)
super().__init__(name, f.filters)
def get_example():

View File

@ -36,8 +36,8 @@ class BaseVariant(RegVariant):
""" [string|list(string)=''] Name of the filter to mark components as 'Do Not Change'.
Use '_kibom_dnc' for the default KiBoM behavior """
def config(self):
super().config()
def config(self, parent):
super().config(parent)
self.pre_transform = BaseFilter.solve_filter(self.pre_transform, 'pre_transform')
def filter(self, comps):

View File

@ -46,8 +46,8 @@ class IBoM(BaseVariant): # noqa: F821
val = []
return val
def config(self):
super().config()
def config(self, parent):
super().config(parent)
self.exclude_filter = BaseFilter.solve_filter(self.exclude_filter, 'exclude_filter', IFILT_MECHANICAL)
self.dnf_filter = BaseFilter.solve_filter(self.dnf_filter, 'dnf_filter')
self.dnc_filter = BaseFilter.solve_filter(self.dnc_filter, 'dnc_filter')

View File

@ -39,9 +39,9 @@ class KiBoM(BaseVariant): # noqa: F821
self._def_dnf_filter = dnf_filter
self._def_dnc_filter = dnc_filter
def config(self):
def config(self, parent):
# Now we can let the parent initialize the filters
super().config()
super().config(parent)
# Variants, ensure a list
if isinstance(self.variant, type):
self.variant = []

View File

@ -15,7 +15,7 @@ class TestOptions(BaseOptions):
self.bar = 'nope'
""" nothing """ # pragma: no cover
def get_targets(self, parent, out_dir):
def get_targets(self, out_dir):
return ['dummy']

View File

@ -70,7 +70,7 @@ def run_compress(ctx, test_import_fail=False):
# Create a compress object with the dummy file as source
out = RegOutput.get_class_for('compress')()
out.set_tree({'options': {'format': 'RAR', 'files': [{'source': ctx.get_out_path('*')}]}})
out.config()
out.config(None)
# Setup the GS output dir, needed for the output path
GS.out_dir = '.'
# Run the compression and catch the error
@ -168,7 +168,7 @@ def test_ibom_parse_fail(test_dir, caplog, monkeypatch):
# Create an ibom object
out = RegOutput.get_class_for('ibom')()
out.set_tree({})
out.config()
out.config(None)
with pytest.raises(SystemExit) as pytest_wrapped_e:
out.run('')
assert pytest_wrapped_e.type == SystemExit
@ -224,7 +224,7 @@ def test_pre_xrc_fail(test_dir, caplog, monkeypatch):
pre_erc.run()
out = RegOutput.get_class_for('pdf_pcb_print')()
out.set_tree({'layers': 'all'})
out.config()
out.config(None)
with pytest.raises(SystemExit) as e3:
out.run('')
assert e1.type == SystemExit
@ -264,7 +264,7 @@ def test_step_fail(test_dir, caplog, monkeypatch):
# Create a compress object with the dummy file as source
out = RegOutput.get_class_for('step')()
out.set_tree({})
out.config()
out.config(None)
with pytest.raises(SystemExit) as e:
out.run('')
# Check we exited because rar isn't installed

View File

@ -69,7 +69,7 @@ def test_pcbdraw_miss_rsvg(caplog, monkeypatch):
o.style = ''
o.remap = None
o.format = 'jpg'
o.config()
o.config(None)
cov.load()
cov.start()
o.run('')
@ -88,7 +88,7 @@ def test_pcbdraw_miss_convert(caplog, monkeypatch):
o.style = ''
o.remap = None
o.format = 'jpg'
o.config()
o.config(None)
cov.load()
cov.start()
o.run('')

View File

@ -88,6 +88,7 @@ def test_3Rs_position_1(test_dir):
def test_3Rs_position_neg_x(test_dir):
ctx = context.TestContext(test_dir, '3Rs_position_neg_x', '3Rs', 'simple_position_neg_x', POS_DIR)
ctx.run()
ctx.sub_dir = os.path.join(ctx.sub_dir, 'position')
pos_top = ctx.get_pos_top_filename()
pos_bot = ctx.get_pos_bot_filename()
ctx.expect_out_file(pos_top)

View File

@ -7,7 +7,7 @@ outputs:
- name: 'position'
comment: "Pick and place file"
type: position
dir: positiondir
dir: positiondir/%i
options:
format: ASCII # CSV or ASCII format
units: millimeters # millimeters or inches