Skip to content
62 changes: 58 additions & 4 deletions sourcespec2/setup/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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':
Expand Down Expand Up @@ -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
Expand Down
31 changes: 26 additions & 5 deletions sourcespec2/setup/configobj_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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,
Expand All @@ -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


Expand Down Expand Up @@ -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')

41 changes: 14 additions & 27 deletions sourcespec2/setup/configure_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand All @@ -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
Expand All @@ -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))
Expand All @@ -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
Expand Down Expand Up @@ -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')


Expand Down Expand Up @@ -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:
Expand Down