From bc4a8ac167760a54088a00f14bd638d92778157c Mon Sep 17 00:00:00 2001 From: Claudio Satriano Date: Mon, 22 Jul 2024 16:43:44 +0200 Subject: [PATCH 1/8] `_Config()` class: use the term "paramter" instead of "option" From eae9947eaf62d37edea3dd962f152f949edf3ed1 Mon Sep 17 00:00:00 2001 From: Kris Vanneste Date: Fri, 20 Feb 2026 16:51:52 +0100 Subject: [PATCH 2/8] Added write method to _Config class. --- sourcespec2/setup/config.py | 50 +++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/sourcespec2/setup/config.py b/sourcespec2/setup/config.py index 5bb092c5..b5213202 100644 --- a/sourcespec2/setup/config.py +++ b/sourcespec2/setup/config.py @@ -623,6 +623,56 @@ def _repr_html_(self): help_texts=help_texts ) + def write(self, config_file): + """ + Write configuration to file + + :param config_file: full path to configuration file + :type config_file: str + """ + import numpy as np + + configspec = parse_configspec() + config_obj = get_default_config_obj(configspec) + + ## Override defaults with configured parameters + config = {} + for key in config_obj.keys(): + value = self.get(key) + ## Hack: convert float lists to strings + if isinstance(value, (list, np.ndarray)): + config[key] = ', '.join([str(val) for val in value]) + if len(value) == 1: + config[key] += ',' + if value is not None: + config[key] = value + ## Station-specific fequency ranges + ## Is there a way to group them with the general bp_freq_* specs? + for key, value in self.items(): + if (key[:6] in ('freq1_', 'freq2_') + or key[:11] in ('bp_freqmin_', 'bp_freqmax_')): + if not key.split('_')[-1] in ('acc', 'shortp', 'broadb', 'disp'): + if value is not None: + config[key] = value + config_obj.update(config) + + ## Copy comments + config_obj.initial_comment = configspec.initial_comment + config_obj.final_comment = configspec.final_comment + config_obj.comments = configspec.comments + config_obj.inline_comments = configspec.inline_comments + ## Station-specific fequency ranges + ## Is there a way to group them with the general bp_freq_* specs? + for key in config.keys(): + if (key[:6] in ('freq1_', 'freq2_') + or key[:11] in ('bp_freqmin_', 'bp_freqmax_')): + if not key.split('_')[-1] in ('acc', 'shortp', 'broadb', 'disp'): + config_obj.comments[key] = '' + config_obj.inline_comments[key] = '' + + with open(config_file, 'wb') as fp: + config_obj.write(fp) + # Global config object, initialized with default values # API users should use this object to access configuration parameters From 8d0f9f1f7500dc86c83352a1d2ac83ab31ffaf96 Mon Sep 17 00:00:00 2001 From: Kris Vanneste Date: Sun, 22 Feb 2026 18:54:06 +0100 Subject: [PATCH 3/8] Removed redundant code in write method of _Config and call _write_config_to_file function to write the file. Added read method to _Config. --- sourcespec2/setup/config.py | 31 +++++++++++++++---------------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/sourcespec2/setup/config.py b/sourcespec2/setup/config.py index b5213202..94835471 100644 --- a/sourcespec2/setup/config.py +++ b/sourcespec2/setup/config.py @@ -623,6 +623,18 @@ def _repr_html_(self): help_texts=help_texts ) + def read(self, config_file): + """ + Read from configuration file + + :param config_file: full path to configuration file + :type config_file: str + """ + from .configobj_helpers import read_config_file + + config_obj = read_config_file(config_file) + self.update(config_obj.dict()) + def write(self, config_file): """ Write configuration to file @@ -630,7 +642,9 @@ def write(self, config_file): :param config_file: full path to configuration file :type config_file: str """ + # TODO: maybe add option to write options as well? import numpy as np + from .configure_cli import _write_config_to_file configspec = parse_configspec() config_obj = get_default_config_obj(configspec) @@ -656,22 +670,7 @@ def write(self, config_file): config[key] = value config_obj.update(config) - ## Copy comments - config_obj.initial_comment = configspec.initial_comment - config_obj.final_comment = configspec.final_comment - config_obj.comments = configspec.comments - config_obj.inline_comments = configspec.inline_comments - ## Station-specific fequency ranges - ## Is there a way to group them with the general bp_freq_* specs? - for key in config.keys(): - if (key[:6] in ('freq1_', 'freq2_') - or key[:11] in ('bp_freqmin_', 'bp_freqmax_')): - if not key.split('_')[-1] in ('acc', 'shortp', 'broadb', 'disp'): - config_obj.comments[key] = '' - config_obj.inline_comments[key] = '' - - with open(config_file, 'wb') as fp: - config_obj.write(fp) + _write_config_to_file(config_obj, config_file) # Global config object, initialized with default values From ec9e3ef188f8dbeb553e312bc3392bcc9fd7de7c Mon Sep 17 00:00:00 2001 From: Kris Vanneste Date: Mon, 23 Feb 2026 11:33:07 +0100 Subject: [PATCH 4/8] Validate configuration in read method of _Config. --- sourcespec2/setup/config.py | 1 + 1 file changed, 1 insertion(+) diff --git a/sourcespec2/setup/config.py b/sourcespec2/setup/config.py index 94835471..9f72e67b 100644 --- a/sourcespec2/setup/config.py +++ b/sourcespec2/setup/config.py @@ -634,6 +634,7 @@ def read(self, config_file): config_obj = read_config_file(config_file) self.update(config_obj.dict()) + self.validate() def write(self, config_file): """ From 5f7f19ce89061385e64cb11d125e0a62bc51bd4b Mon Sep 17 00:00:00 2001 From: Kris Vanneste Date: Mon, 23 Feb 2026 13:57:41 +0100 Subject: [PATCH 5/8] Make sure strings containing lists are split into string lists in read method of _Config. --- sourcespec2/setup/config.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/sourcespec2/setup/config.py b/sourcespec2/setup/config.py index 9f72e67b..0cdd9c77 100644 --- a/sourcespec2/setup/config.py +++ b/sourcespec2/setup/config.py @@ -634,6 +634,11 @@ def read(self, config_file): config_obj = read_config_file(config_file) self.update(config_obj.dict()) + ## Force splitting into string lists + for key, val in self.items(): + if isinstance(val, type('')): + if ',' in val: + self.__setitem__(key, [s.strip() for s in val.split(',')]) self.validate() def write(self, config_file): @@ -648,8 +653,10 @@ def write(self, config_file): from .configure_cli import _write_config_to_file configspec = parse_configspec() - config_obj = get_default_config_obj(configspec) + #config_obj = ConfigObj(self, configspec=configspec, encoding='utf8') + + config_obj = get_default_config_obj(configspec) ## Override defaults with configured parameters config = {} for key in config_obj.keys(): From 64e97fdec4b03392c10d7f3e5f19287d8f133b4f Mon Sep 17 00:00:00 2001 From: Claudio Satriano Date: Mon, 2 Mar 2026 19:49:18 +0100 Subject: [PATCH 6/8] read_config_file(): raise IOError instead of exiting on error This allows using it safely from a non-CLI context, such as a notebook --- sourcespec2/setup/configobj_helpers.py | 9 ++++----- sourcespec2/setup/configure_cli.py | 13 ++++++++++--- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/sourcespec2/setup/configobj_helpers.py b/sourcespec2/setup/configobj_helpers.py index 61167e58..7099ff94 100644 --- a/sourcespec2/setup/configobj_helpers.py +++ b/sourcespec2/setup/configobj_helpers.py @@ -10,7 +10,6 @@ (http://www.cecill.info/licences.en.html) """ import os -import sys from .configobj import ConfigObj from .configobj.validate import Validator @@ -26,6 +25,8 @@ def read_config_file(config_file, configspec=None): :return: ConfigObj object :rtype: ConfigObj + + :raises IOError: if ConfigObj is unable to read the file """ kwargs = { 'configspec': configspec, @@ -41,11 +42,9 @@ def read_config_file(config_file, configspec=None): try: config_obj = ConfigObj(config_file, **kwargs) except IOError as err: - sys.stderr.write(f'{err}\n') - sys.exit(1) + raise IOError(f'{err}') from err except Exception as err: - sys.stderr.write(f'Unable to read "{config_file}": {err}\n') - sys.exit(1) + raise IOError(f'Unable to read "{config_file}": {err}') from err return config_obj diff --git a/sourcespec2/setup/configure_cli.py b/sourcespec2/setup/configure_cli.py index 1d4d6325..7fff8511 100644 --- a/sourcespec2/setup/configure_cli.py +++ b/sourcespec2/setup/configure_cli.py @@ -106,7 +106,11 @@ def _update_config_file(config_file, configspec): :param configspec: The configuration specification :type configspec: ConfigObj """ - config_obj = read_config_file(config_file, configspec) + try: + config_obj = read_config_file(config_file, configspec) + except IOError as err: + sys.stderr.write(f'{err}\n') + sys.exit(1) val = Validator() config_obj.validate(val) mod_time = datetime.fromtimestamp(os.path.getmtime(config_file)) @@ -119,7 +123,6 @@ def _update_config_file(config_file, configspec): if ans not in ['y', 'Y']: sys.exit(0) config_new = ConfigObj(configspec=configspec, default_encoding='utf8') - config_new = read_config_file(None, configspec) config_new.validate(val) config_new.defaults = [] config_new.comments = configspec.comments @@ -325,7 +328,11 @@ def configure_cli(options=None, progname='source_spec', config_overrides=None): if getattr(options, 'config_file', None): options.config_file = _fix_and_expand_path(options.config_file) - config_obj = read_config_file(options.config_file, configspec) + try: + config_obj = read_config_file(options.config_file, configspec) + except IOError as err: + sys.stderr.write(f'{err}\n') + sys.exit(1) # Apply overrides if config_overrides is not None: try: From a476db06fa59a41eaed34571dec0f442e85be7e7 Mon Sep 17 00:00:00 2001 From: Claudio Satriano Date: Mon, 2 Mar 2026 19:56:31 +0100 Subject: [PATCH 7/8] Move write_config_to_file() to configobj_helpers.py --- sourcespec2/setup/config.py | 8 +++++--- sourcespec2/setup/configobj_helpers.py | 22 ++++++++++++++++++++ sourcespec2/setup/configure_cli.py | 28 ++++---------------------- 3 files changed, 31 insertions(+), 27 deletions(-) diff --git a/sourcespec2/setup/config.py b/sourcespec2/setup/config.py index 0cdd9c77..f5165cec 100644 --- a/sourcespec2/setup/config.py +++ b/sourcespec2/setup/config.py @@ -13,7 +13,10 @@ import contextlib import warnings from collections import defaultdict -from .configobj_helpers import parse_configspec, get_default_config_obj +from .configobj_helpers import ( + read_config_file, parse_configspec, get_default_config_obj, + write_config_to_file +) from .mandatory_deprecated import ( mandatory_config_params, check_deprecated_config_params ) @@ -630,7 +633,6 @@ def read(self, config_file): :param config_file: full path to configuration file :type config_file: str """ - from .configobj_helpers import read_config_file config_obj = read_config_file(config_file) self.update(config_obj.dict()) @@ -678,7 +680,7 @@ def write(self, config_file): config[key] = value config_obj.update(config) - _write_config_to_file(config_obj, config_file) + write_config_to_file(_config_obj, config_file) # Global config object, initialized with default values diff --git a/sourcespec2/setup/configobj_helpers.py b/sourcespec2/setup/configobj_helpers.py index 7099ff94..65526839 100644 --- a/sourcespec2/setup/configobj_helpers.py +++ b/sourcespec2/setup/configobj_helpers.py @@ -10,6 +10,7 @@ (http://www.cecill.info/licences.en.html) """ import os +from io import BytesIO from .configobj import ConfigObj from .configobj.validate import Validator @@ -78,3 +79,24 @@ def get_default_config_obj(configspec): config_obj.comments = configspec.comments config_obj.final_comment = configspec.final_comment return config_obj + + +def write_config_to_file(config_obj, filepath): + """ + Write config object to file, removing trailing commas from + force_list entries. + + :param config_obj: ConfigObj instance to write + :param str filepath: Path to the file to write + """ + buffer = BytesIO() + config_obj.write(buffer) + with open(filepath, 'w', encoding='utf8') as fp: + for line in buffer.getvalue().decode('utf8').splitlines(keepends=True): + # Remove trailing comma before newline if present, + # but only if line is not a comment + line = line.rstrip('\n\r') + if line.endswith(',') and not line.lstrip().startswith('#'): + line = line.rstrip(',') + fp.write(line + '\n') + diff --git a/sourcespec2/setup/configure_cli.py b/sourcespec2/setup/configure_cli.py index 7fff8511..83f2df55 100644 --- a/sourcespec2/setup/configure_cli.py +++ b/sourcespec2/setup/configure_cli.py @@ -15,12 +15,12 @@ import shutil import uuid import json -from io import BytesIO from copy import copy from datetime import datetime from .config import config from .configobj_helpers import ( - read_config_file, parse_configspec, get_default_config_obj + read_config_file, parse_configspec, get_default_config_obj, + write_config_to_file ) from .library_versions import library_versions from .configobj import ConfigObj @@ -49,26 +49,6 @@ class MultipleInstanceError(Exception): IPSHELL = None -def _write_config_to_file(config_obj, filepath): - """ - Write config object to file, removing trailing commas from - force_list entries. - - :param config_obj: ConfigObj instance to write - :param str filepath: Path to the file to write - """ - buffer = BytesIO() - config_obj.write(buffer) - with open(filepath, 'w', encoding='utf8') as fp: - for line in buffer.getvalue().decode('utf8').splitlines(keepends=True): - # Remove trailing comma before newline if present, - # but only if line is not a comment - line = line.rstrip('\n\r') - if line.endswith(',') and not line.lstrip().startswith('#'): - line = line.rstrip(',') - fp.write(line + '\n') - - def _write_sample_config(configspec, progname): """ Write a sample configuration file. @@ -87,7 +67,7 @@ def _write_sample_config(configspec, progname): ) write_file = ans in ['y', 'Y'] if write_file: - _write_config_to_file(config_obj, configfile) + write_config_to_file(config_obj, configfile) print(f'Sample config file written to: {configfile}') note = """ Note that the default config parameters are suited for a M<5 earthquake @@ -202,7 +182,7 @@ def _update_config_file(config_file, configspec): config_new['layer_top_depths'] = 'None' config_new['rho'] = 'None' shutil.copyfile(config_file, config_file_old) - _write_config_to_file(config_new, config_file) + write_config_to_file(config_new, config_file) print(f'{config_file}: updated') From 04aacc99f9d36709c8b970edd028e158ae0a235a Mon Sep 17 00:00:00 2001 From: Claudio Satriano Date: Mon, 2 Mar 2026 19:57:23 +0100 Subject: [PATCH 8/8] Optimize code in _Config() class The write() method of the _Config class has been optimized to avoid unnecessary conversions and to handle the free_surface_amplification parameter. --- sourcespec2/setup/config.py | 71 +++++++++++++++++-------------------- 1 file changed, 33 insertions(+), 38 deletions(-) diff --git a/sourcespec2/setup/config.py b/sourcespec2/setup/config.py index f5165cec..d9156144 100644 --- a/sourcespec2/setup/config.py +++ b/sourcespec2/setup/config.py @@ -201,8 +201,8 @@ def _set_defaults(self): self['INSTR_CODES_VEL'] = ['H', 'L', 'P'] self['INSTR_CODES_ACC'] = ['N', ] # Initialize config object to the default values - configspec = parse_configspec() - config_obj = get_default_config_obj(configspec) + self._configspec = parse_configspec() + config_obj = get_default_config_obj(self._configspec) self.update(config_obj.dict()) # Store the key order from configspec for use in repr methods # Internal keys go first, then configspec keys in order @@ -297,7 +297,9 @@ def update(self, other): :raises ValueError: If an error occurs while parsing the parameters """ for key, value in other.items(): - self[key] = value + # Skip internal attributes (those starting with '_') + if not key.startswith('_'): + self[key] = value # Set to None all the 'None' strings for key, value in self.items(): if value == 'None': @@ -633,14 +635,8 @@ def read(self, config_file): :param config_file: full path to configuration file :type config_file: str """ - - config_obj = read_config_file(config_file) - self.update(config_obj.dict()) - ## Force splitting into string lists - for key, val in self.items(): - if isinstance(val, type('')): - if ',' in val: - self.__setitem__(key, [s.strip() for s in val.split(',')]) + _config_obj = read_config_file(config_file, self._configspec) + self.update(_config_obj.dict()) self.validate() def write(self, config_file): @@ -651,35 +647,34 @@ def write(self, config_file): :type config_file: str """ # TODO: maybe add option to write options as well? - import numpy as np - from .configure_cli import _write_config_to_file - - configspec = parse_configspec() - - #config_obj = ConfigObj(self, configspec=configspec, encoding='utf8') - - config_obj = get_default_config_obj(configspec) - ## Override defaults with configured parameters - config = {} - for key in config_obj.keys(): + # pylint: disable=import-outside-toplevel + from collections.abc import Iterable + _config_obj = get_default_config_obj(self._configspec) + for key in _config_obj.keys(): value = self.get(key) - ## Hack: convert float lists to strings - if isinstance(value, (list, np.ndarray)): - config[key] = ', '.join([str(val) for val in value]) - if len(value) == 1: - config[key] += ',' - if value is not None: - config[key] = value - ## Station-specific fequency ranges - ## Is there a way to group them with the general bp_freq_* specs? + if ( + key == 'free_surface_amplification' + and isinstance(value, Iterable) + and not isinstance(value, (str, dict)) + ): + # Special formatting for free_surface_amplification + _str_list = ', '.join( + f'{statid}: {val}' for statid, val in value + ).strip() + _config_obj[key] = _str_list + elif value is not None: + _config_obj[key] = value + # Station-specific fequency ranges + # TODO: Is there a way to group them with the general bp_freq_* specs? for key, value in self.items(): - if (key[:6] in ('freq1_', 'freq2_') - or key[:11] in ('bp_freqmin_', 'bp_freqmax_')): - if not key.split('_')[-1] in ('acc', 'shortp', 'broadb', 'disp'): - if value is not None: - config[key] = value - config_obj.update(config) - + if ( + key.startswith( + ('freq1_', 'freq2_', 'bp_freqmin_', 'bp_freqmax_')) + and not + key.endswith(('_acc', '_shortp', '_broadb', '_disp')) + and value is not None + ): + _config_obj[key] = value write_config_to_file(_config_obj, config_file)