From 3eb82bc86dd4d5dcb9f4fefb19f593840b700eb7 Mon Sep 17 00:00:00 2001 From: Diego Capusotto Date: Wed, 15 Dec 2021 17:15:49 -0300 Subject: [PATCH] Changed: The `%d/%sd/%bd` expansion patterns are now affected by the global `date_format`. - Can be disabled using `date_reformat: false`. - Related to #121 --- CHANGELOG.md | 2 + README.md | 5 +- docs/README.in | 5 +- kibot/globals.py | 10 ++- kibot/gs.py | 26 +++++-- kibot/kicad/v5_sch.py | 10 +-- kibot/misc.py | 1 + kibot/optionable.py | 75 +++++++++++--------- tests/test_plot/test_misc.py | 19 +++++ tests/yaml_samples/date_format_1.kiplot.yaml | 16 +++++ 10 files changed, 118 insertions(+), 51 deletions(-) create mode 100644 tests/yaml_samples/date_format_1.kiplot.yaml diff --git a/CHANGELOG.md b/CHANGELOG.md index c437489a..d0d502e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -63,6 +63,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 a context not related to variants. I.e. when a `compress` target expands `%v`. - Now you get an error when defining two outputs with the same name. +- The `%d/%sd/%bd` expansion patterns are now affected by the global `date_format`. + Can be disabled using `date_reformat: false`. (#121) ### Fixed - Position files now defaults to use the auxiliar origin as KiCad. diff --git a/README.md b/README.md index 7a57ad2b..feb82da4 100644 --- a/README.md +++ b/README.md @@ -312,7 +312,10 @@ The behavior of these patterns isn't fully defined in this case and the results #### Date format option -* The **%d**, **%sd** and **%bd** patterns use the date and time from the PCB and schematic. When abscent they use the file timestamp. The `date_time_format` global option controls the format used. +* The **%d**, **%sd** and **%bd** patterns use the date and time from the PCB and schematic. + When abscent they use the file timestamp, and the `date_time_format` global option controls the format used. + When available, and in ISO format, the `date_format` controls the format used. + You can disable this reformatting assigning `false` to the `date_reformat` option. * The **%D** format is controlled by the `date_format` global option. * The **%T** format is controlled by the `time_format` global option. diff --git a/docs/README.in b/docs/README.in index 2a62cc38..2a93de34 100644 --- a/docs/README.in +++ b/docs/README.in @@ -254,7 +254,10 @@ The behavior of these patterns isn't fully defined in this case and the results #### Date format option -* The **%d**, **%sd** and **%bd** patterns use the date and time from the PCB and schematic. When abscent they use the file timestamp. The `date_time_format` global option controls the format used. +* The **%d**, **%sd** and **%bd** patterns use the date and time from the PCB and schematic. + When abscent they use the file timestamp, and the `date_time_format` global option controls the format used. + When available, and in ISO format, the `date_format` controls the format used. + You can disable this reformatting assigning `false` to the `date_reformat` option. * The **%D** format is controlled by the `date_format` global option. * The **%T** format is controlled by the `time_format` global option. diff --git a/kibot/globals.py b/kibot/globals.py index 96939267..d5c47cde 100644 --- a/kibot/globals.py +++ b/kibot/globals.py @@ -31,9 +31,14 @@ class Globals(FiltersOptions): self.date_time_format = '%Y-%m-%d_%H-%M-%S' """ Format used for the PCB and schematic date when using the file timestamp. Uses the `strftime` format """ self.date_format = '%Y-%m-%d' - """ Format used for the day we started the script. Uses the `strftime` format """ + """ Format used for the day we started the script. + Is also used for the PCB/SCH date formatting when `time_reformat` is enabled (default behavior). + Uses the `strftime` format """ self.time_format = '%H-%M-%S' """ Format used for the time we started the script. Uses the `strftime` format """ + self.time_reformat = True + """ Tries to reformat the PCB/SCH date using the `date_format`. + This assumes you let KiCad fill this value and hence the time is in ISO format (YY-MM-DD) """ self.set_doc('filters', " [list(dict)] KiBot warnings to be ignored ") self._filter_what = 'KiBot warnings' self._unkown_is_error = True @@ -41,7 +46,7 @@ class Globals(FiltersOptions): @staticmethod def set_global(current, new_val, opt): - if current: + if current is not None: logger.info('Using command line value `{}` for global option `{}`'.format(current, opt)) return current if new_val: @@ -56,6 +61,7 @@ class Globals(FiltersOptions): GS.global_date_time_format = self.set_global(GS.global_date_time_format, self.date_time_format, 'date_time_format') GS.global_date_format = self.set_global(GS.global_date_format, self.date_format, 'date_format') GS.global_time_format = self.set_global(GS.global_time_format, self.time_format, 'time_format') + GS.global_time_reformat = self.set_global(GS.global_time_reformat, self.time_reformat, 'time_reformat') GS.global_kiauto_wait_start = self.set_global(GS.global_kiauto_wait_start, self.kiauto_wait_start, 'kiauto_wait_start') if GS.global_kiauto_wait_start and int(GS.global_kiauto_wait_start) != GS.global_kiauto_wait_start: GS.global_kiauto_wait_start = int(GS.global_kiauto_wait_start) diff --git a/kibot/gs.py b/kibot/gs.py index 4cb340cf..1836880a 100644 --- a/kibot/gs.py +++ b/kibot/gs.py @@ -4,10 +4,10 @@ # License: GPL-3.0 # Project: KiBot (formerly KiPlot) import os -from datetime import datetime +from datetime import datetime, date from sys import exit -from .misc import (EXIT_BAD_ARGS) -from .log import (get_logger) +from .misc import EXIT_BAD_ARGS, W_DATEFORMAT +from .log import get_logger logger = get_logger(__name__) @@ -87,6 +87,7 @@ class GS(object): global_date_time_format = None global_date_format = None global_time_format = None + global_time_reformat = None test_boolean = True @staticmethod @@ -121,6 +122,20 @@ class GS(object): GS.sch_com3 = GS.sch.comment3 GS.sch_com4 = GS.sch.comment4 + @staticmethod + def format_date(d, fname, what): + if not d: + return datetime.fromtimestamp(os.path.getmtime(fname)).strftime(GS.global_date_time_format) + elif GS.global_time_reformat: + try: + dt = date.fromisoformat(d) + except ValueError as e: + logger.warning(W_DATEFORMAT+"Trying to reformat {} time, but not in ISO format ({})".format(what, d)) + logger.warning(W_DATEFORMAT+"Problem: {}".format(e)) + return d + return dt.strftime(GS.global_date_format) + return d + @staticmethod def load_pcb_title_block(): if GS.pcb_title is not None: @@ -131,10 +146,7 @@ class GS(object): GS.pcb_comp = '' # This is based on InterativeHtmlBom expansion title_block = GS.board.GetTitleBlock() - GS.pcb_date = title_block.GetDate() - if not GS.pcb_date: - file_mtime = os.path.getmtime(GS.pcb_file) - GS.pcb_date = datetime.fromtimestamp(file_mtime).strftime(GS.global_date_time_format) + GS.pcb_date = GS.format_date(title_block.GetDate(), GS.pcb_file, 'PCB') GS.pcb_title = title_block.GetTitle() if not GS.pcb_title: GS.pcb_title = GS.pcb_basename diff --git a/kibot/kicad/v5_sch.py b/kibot/kicad/v5_sch.py index 9324d8f0..6c6bbbbf 100644 --- a/kibot/kicad/v5_sch.py +++ b/kibot/kicad/v5_sch.py @@ -11,7 +11,6 @@ Currently oriented to collect the components for the BoM. # Encapsulate file/line import re import os -from datetime import datetime from copy import deepcopy from collections import OrderedDict from .config import KiConf, un_quote @@ -961,7 +960,10 @@ class SchematicComponent(object): self.footprint_lib = res[0] self.footprint = res[1] else: - raise SchFileError('Footprint with more than one colon', f.value, fr) + if fr: + raise SchFileError('Footprint with more than one colon', f.value, fr) + else: + raise SchError('Footprint with more than one colon (`{}`)'.format(f.value)) basic += 1 elif f.number == 3: self.datasheet = f.value @@ -1498,9 +1500,7 @@ class Schematic(object): # Load the title block self._get_title_block(f) # Fill in some missing info - if not self.date: - file_mtime = os.path.getmtime(fname) - self.date = datetime.fromtimestamp(file_mtime).strftime(GS.global_date_time_format) + self.date = GS.format_date(self.date, fname, 'SCH') if not self.title: self.title = os.path.splitext(os.path.basename(fname))[0] logger.debug("SCH title: `{}`".format(self.title)) diff --git a/kibot/misc.py b/kibot/misc.py index 18994bf9..88ea0c3e 100644 --- a/kibot/misc.py +++ b/kibot/misc.py @@ -212,6 +212,7 @@ W_NEEDSPCB = '(W071) ' W_NOGLOBALS = '(W072) ' W_EMPTREP = '(W073) ' W_BADCHARS = '(W074) ' +W_DATEFORMAT = '(W075) ' class Rect(object): diff --git a/kibot/optionable.py b/kibot/optionable.py index 4bc5c2fc..78c95121 100644 --- a/kibot/optionable.py +++ b/kibot/optionable.py @@ -20,6 +20,11 @@ def filter(v): return inspect.isclass(v) or not (callable(v) or isinstance(v, (dict, list))) +def _cl(text): + """ Eliminates dangerous characters from the text """ + return re.sub(r'[\\\/\?%\*:|"<>]', '_', text) + + class Optionable(object): """ A class to validate and hold configuration outputs/options. Is configured from a dict and the collected values are stored in its attributes. """ @@ -219,40 +224,40 @@ class Optionable(object): """ Expansions common to the PCB and Schematic """ # PCB expansions, explicit if GS.board and '%b' in name: - name = name.replace('%bc', GS.pcb_comp) - name = name.replace('%bd', GS.pcb_date) + name = name.replace('%bc', _cl(GS.pcb_comp)) + name = name.replace('%bd', _cl(GS.pcb_date)) name = name.replace('%bF', GS.pcb_no_ext) name = name.replace('%bf', GS.pcb_basename) - name = name.replace('%bp', GS.pcb_title) - name = name.replace('%br', GS.pcb_rev) - name = name.replace('%bC1', GS.pcb_com1) - name = name.replace('%bC2', GS.pcb_com2) - name = name.replace('%bC3', GS.pcb_com3) - name = name.replace('%bC4', GS.pcb_com4) + name = name.replace('%bp', _cl(GS.pcb_title)) + name = name.replace('%br', _cl(GS.pcb_rev)) + name = name.replace('%bC1', _cl(GS.pcb_com1)) + name = name.replace('%bC2', _cl(GS.pcb_com2)) + name = name.replace('%bC3', _cl(GS.pcb_com3)) + name = name.replace('%bC4', _cl(GS.pcb_com4)) if GS.solved_global_variant: name = name.replace('%g', GS.solved_global_variant.file_id) name = name.replace('%G', GS.solved_global_variant.name) # Schematic expansions, explicit if GS.sch and '%s' in name: - name = name.replace('%sc', GS.sch_comp) - name = name.replace('%sd', GS.sch_date) + name = name.replace('%sc', _cl(GS.sch_comp)) + name = name.replace('%sd', _cl(GS.sch_date)) name = name.replace('%sF', GS.sch_no_ext) name = name.replace('%sf', GS.sch_basename) - name = name.replace('%sp', GS.sch_title) - name = name.replace('%sr', GS.sch_rev) - name = name.replace('%sC1', GS.sch_com1) - name = name.replace('%sC2', GS.sch_com2) - name = name.replace('%sC3', GS.sch_com3) - name = name.replace('%sC4', GS.sch_com4) + name = name.replace('%sp', _cl(GS.sch_title)) + name = name.replace('%sr', _cl(GS.sch_rev)) + name = name.replace('%sC1', _cl(GS.sch_com1)) + name = name.replace('%sC2', _cl(GS.sch_com2)) + name = name.replace('%sC3', _cl(GS.sch_com3)) + name = name.replace('%sC4', _cl(GS.sch_com4)) name = name.replace('%D', GS.n.strftime(GS.global_date_format)) name = name.replace('%T', GS.n.strftime(GS.global_time_format)) if self: name = name.replace('%i', self._expand_id) - name = name.replace('%v', self._find_variant()) - name = name.replace('%V', self._find_variant_name()) + name = name.replace('%v', _cl(self._find_variant())) + name = name.replace('%V', _cl(self._find_variant_name())) name = name.replace('%x', self._expand_ext) if parent and hasattr(parent, 'output_id'): - name = name.replace('%I', parent.output_id) + name = name.replace('%I', _cl(parent.output_id)) return name def expand_filename_both(self, name, is_sch=True): @@ -281,27 +286,27 @@ class Optionable(object): # This member can be called with a preflight object name = Optionable.expand_filename_common(self, name, parent) if GS.board and do_pcb: - name = name.replace('%c', GS.pcb_comp) - name = name.replace('%d', GS.pcb_date) + name = name.replace('%c', _cl(GS.pcb_comp)) + name = name.replace('%d', _cl(GS.pcb_date)) name = name.replace('%F', GS.pcb_no_ext) name = name.replace('%f', GS.pcb_basename) - name = name.replace('%p', GS.pcb_title) - name = name.replace('%r', GS.pcb_rev) - name = name.replace('%C1', GS.pcb_com1) - name = name.replace('%C2', GS.pcb_com2) - name = name.replace('%C3', GS.pcb_com3) - name = name.replace('%C4', GS.pcb_com4) + name = name.replace('%p', _cl(GS.pcb_title)) + name = name.replace('%r', _cl(GS.pcb_rev)) + name = name.replace('%C1', _cl(GS.pcb_com1)) + name = name.replace('%C2', _cl(GS.pcb_com2)) + name = name.replace('%C3', _cl(GS.pcb_com3)) + name = name.replace('%C4', _cl(GS.pcb_com4)) if GS.sch and do_sch: - name = name.replace('%c', GS.sch_comp) - name = name.replace('%d', GS.sch_date) + name = name.replace('%c', _cl(GS.sch_comp)) + name = name.replace('%d', _cl(GS.sch_date)) name = name.replace('%F', GS.sch_no_ext) name = name.replace('%f', GS.sch_basename) - name = name.replace('%p', GS.sch_title) - name = name.replace('%r', GS.sch_rev) - name = name.replace('%C1', GS.sch_com1) - name = name.replace('%C2', GS.sch_com2) - name = name.replace('%C3', GS.sch_com3) - name = name.replace('%C4', GS.sch_com4) + name = name.replace('%p', _cl(GS.sch_title)) + name = name.replace('%r', _cl(GS.sch_rev)) + name = name.replace('%C1', _cl(GS.sch_com1)) + name = name.replace('%C2', _cl(GS.sch_com2)) + name = name.replace('%C3', _cl(GS.sch_com3)) + name = name.replace('%C4', _cl(GS.sch_com4)) # sanitize the name to avoid characters illegal in file systems name = name.replace('\\', '/') name = re.sub(r'[?%*:|"<>]', '_', name) diff --git a/tests/test_plot/test_misc.py b/tests/test_plot/test_misc.py index ad302660..8c28f88b 100644 --- a/tests/test_plot/test_misc.py +++ b/tests/test_plot/test_misc.py @@ -875,3 +875,22 @@ def test_compress_sources_1(test_dir): files = ['source/'+prj+'.kicad_pcb', 'source/'+prj+'.sch', 'source/deeper.sch', 'source/sub-sheet.sch'] ctx.test_compress(prj + '-result.tar.bz2', files) ctx.clean_up() + + +def test_date_format_1(test_dir): + """ Date from SCH reformated """ + prj = 'test_v5' + ctx = context.TestContext(test_dir, 'test_date_format_1', prj, 'date_format_1', '') + ctx.run(extra=[]) + ctx.expect_out_file(POS_DIR+'/test_v5_20200812.csv') + ctx.clean_up() + + +def test_date_format_2(test_dir): + """ Date from SCH reformated """ + prj = 'bom' + ctx = context.TestContext(test_dir, 'test_date_format_2', prj, 'date_format_1', '') + ctx.run(extra=[]) + ctx.expect_out_file(POS_DIR+'/bom_13_07_2020.csv') + assert ctx.search_err('Trying to reformat SCH time, but not in ISO format') + ctx.clean_up() diff --git a/tests/yaml_samples/date_format_1.kiplot.yaml b/tests/yaml_samples/date_format_1.kiplot.yaml new file mode 100644 index 00000000..e889d225 --- /dev/null +++ b/tests/yaml_samples/date_format_1.kiplot.yaml @@ -0,0 +1,16 @@ +kibot: + version: 1 + +global: + output: '%f_%sd.%x' + date_format: '%Y%m%d' + +outputs: + - name: 'position' + type: position + dir: positiondir + options: + format: CSV # CSV or ASCII format + units: millimeters # millimeters or inches + separate_files_for_front_and_back: false + only_smd: true