diff --git a/sourcespec2/setup/config.py b/sourcespec2/setup/config.py index 5bb092c5..d9156144 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 ) @@ -198,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 @@ -294,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': @@ -623,6 +628,55 @@ 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 + """ + _config_obj = read_config_file(config_file, self._configspec) + self.update(_config_obj.dict()) + self.validate() + + def write(self, config_file): + """ + Write configuration to file + + :param config_file: full path to configuration file + :type config_file: str + """ + # TODO: maybe add option to write options as well? + # 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) + 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.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) + # Global config object, initialized with default values # API users should use this object to access configuration parameters diff --git a/sourcespec2/setup/configobj_helpers.py b/sourcespec2/setup/configobj_helpers.py index 61167e58..65526839 100644 --- a/sourcespec2/setup/configobj_helpers.py +++ b/sourcespec2/setup/configobj_helpers.py @@ -10,7 +10,7 @@ (http://www.cecill.info/licences.en.html) """ import os -import sys +from io import BytesIO from .configobj import ConfigObj from .configobj.validate import Validator @@ -26,6 +26,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 +43,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 @@ -79,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 1d4d6325..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 @@ -106,7 +86,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 +103,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 @@ -199,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') @@ -325,7 +308,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: