import logging
import os
from typing import TYPE_CHECKING
import yaml
from cowbird.utils import get_logger, print_log, raise_log
if TYPE_CHECKING:
# pylint: disable=W0611,unused-import
from typing import List, Union
from cowbird.typedefs import ConfigDict
[docs]LOGGER = get_logger(__name__)
[docs]class ConfigError(RuntimeError):
"""
Generic error during configuration loading.
"""
[docs]def _load_config(path_or_dict, section, allow_missing=False):
# type: (Union[str, ConfigDict], str, bool) -> ConfigDict
"""
Loads a file path or dictionary as YAML/JSON configuration.
"""
try:
if isinstance(path_or_dict, str):
cfg = yaml.safe_load(open(path_or_dict, "r"))
else:
cfg = path_or_dict
return _expand_all(cfg[section])
except KeyError:
msg = "Config file section [{!s}] not found.".format(section)
if allow_missing:
print_log(msg, level=logging.WARNING, logger=LOGGER)
return {}
raise_log(msg, exception=ConfigError, logger=LOGGER)
except Exception as exc:
raise_log("Invalid config file [{!r}]".format(exc),
exception=ConfigError, logger=LOGGER)
[docs]def get_all_configs(path_or_dict, section, allow_missing=False):
# type: (Union[str, ConfigDict], str, bool) -> List[ConfigDict]
"""
Loads all configuration files specified by the path (if a directory),
a single configuration (if a file) or directly
returns the specified dictionary section (if a configuration dictionary).
:returns:
- list of configurations loaded if input was a directory path
- list of single configuration if input was a file path
- list of single configuration if input was a JSON dict
- empty list if none of the other cases where matched
.. note::
Order of file loading will be resolved by alphabetically sorted filename
if specifying a directory path.
"""
if isinstance(path_or_dict, str):
if os.path.isdir(path_or_dict):
dir_path = os.path.abspath(path_or_dict)
known_extensions = [".cfg", ".yml", ".yaml", ".json"]
cfg_names = list(sorted({fn for fn in os.listdir(dir_path)
if any(fn.endswith(ext) for ext in
known_extensions)}))
return [_load_config(os.path.join(dir_path, fn),
section,
allow_missing) for fn in cfg_names]
if os.path.isfile(path_or_dict):
return [_load_config(path_or_dict, section, allow_missing)]
elif isinstance(path_or_dict, dict):
return [_load_config(path_or_dict, section, allow_missing)]
return []
[docs]def _expand_all(config):
# type: (ConfigDict) -> ConfigDict
"""
Applies environment variable expansion recursively to all applicable fields of a configuration definition.
"""
if isinstance(config, dict):
for cfg in list(config):
cfg_key = os.path.expandvars(cfg)
if cfg_key != cfg:
config[cfg_key] = config.pop(cfg)
config[cfg_key] = _expand_all(config[cfg_key])
elif isinstance(config, (list, set)):
for i, cfg in enumerate(config):
config[i] = _expand_all(cfg)
elif isinstance(config, str):
config = os.path.expandvars(str(config))
elif isinstance(config, (int, bool, float, type(None))):
pass
else:
raise NotImplementedError("unknown parsing of config of type: {}".
format(type(config)))
return config