From b94f46cef1af09e5164f5d74668b3c7a8b438d4f Mon Sep 17 00:00:00 2001 From: JarbasAI <33701864+JarbasAl@users.noreply.github.com> Date: Tue, 5 Jul 2022 23:03:56 +0200 Subject: [PATCH] migrate full ovos_utils.configuration module (#7) migrate full ovos_utils.configuration module --- ovos_config/__init__.py | 4 +- ovos_config/base.py | 272 +++++++++++++++++++++++++++ ovos_config/config.py | 239 +++-------------------- ovos_config/locale.py | 14 +- ovos_config/locations.py | 93 ++++++++- ovos_config/ovos.py | 154 ++++++++++++++- ovos_config/utils.py | 2 +- test/unittests/test_configuration.py | 6 +- 8 files changed, 555 insertions(+), 229 deletions(-) create mode 100644 ovos_config/base.py diff --git a/ovos_config/__init__.py b/ovos_config/__init__.py index 0a3db76..b2c3784 100644 --- a/ovos_config/__init__.py +++ b/ovos_config/__init__.py @@ -2,5 +2,5 @@ from ovos_config.locale import set_default_lf_lang, setup_locale, \ set_default_tz, set_default_lang, get_default_tz, get_default_lang, \ get_config_tz, get_primary_lang_code, load_languages, load_language -from ovos_config.locations import SYSTEM_CONFIG, USER_CONFIG, DEFAULT_CONFIG -from ovos_utils.configuration import get_ovos_config, get_xdg_config_locations +from ovos_config.locations import SYSTEM_CONFIG, USER_CONFIG, DEFAULT_CONFIG, get_xdg_config_locations +from ovos_config.ovos import get_ovos_config diff --git a/ovos_config/base.py b/ovos_config/base.py new file mode 100644 index 0000000..fc48513 --- /dev/null +++ b/ovos_config/base.py @@ -0,0 +1,272 @@ +# Copyright 2017 Mycroft AI Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +import json +import re +from os.path import exists, isfile + +import yaml + +from ovos_config.locations import USER_CONFIG, SYSTEM_CONFIG, WEB_CONFIG_CACHE, DEFAULT_CONFIG +from ovos_utils import camel_case_split +from ovos_utils.json_helper import load_commented_json, merge_dict +from ovos_utils.log import LOG + +try: + import mycroft.api as selene_api +except ImportError: + # TODO https://github.com/OpenVoiceOS/selene_api + selene_api = None + + +def is_remote_list(values): + """Check if list corresponds to a backend formatted collection of dicts + """ + for v in values: + if not isinstance(v, dict): + return False + if "@type" not in v.keys(): + return False + return True + + +def translate_remote(config, setting): + """Translate config names from server to equivalents for mycroft-core. + + Args: + config: base config to populate + settings: remote settings to be translated + """ + IGNORED_SETTINGS = ["uuid", "@type", "active", "user", "device"] + + for k, v in setting.items(): + if k not in IGNORED_SETTINGS: + # Translate the CamelCase values stored remotely into the + # Python-style names used within mycroft-core. + key = re.sub(r"Setting(s)?", "", k) + key = camel_case_split(key).replace(" ", "_").lower() + if isinstance(v, dict): + config[key] = config.get(key, {}) + translate_remote(config[key], v) + elif isinstance(v, list): + if is_remote_list(v): + if key not in config: + config[key] = {} + translate_list(config[key], v) + else: + config[key] = v + else: + config[key] = v + + +def translate_list(config, values): + """Translate list formatted by mycroft server. + + Args: + config (dict): target config + values (list): list from mycroft server config + """ + for v in values: + module = v["@type"] + if v.get("active"): + config["module"] = module + config[module] = config.get(module, {}) + translate_remote(config[module], v) + + +class LocalConf(dict): + """Config dictionary from file.""" + allow_overwrite = True + + def __init__(self, path): + super().__init__(self) + self.path = path + if path: + self.load_local(path) + + def _get_file_format(self, path=None): + """The config file format + supported file extensions: + - json (.json) + - commented json (.conf) + - yaml (.yaml/.yml) + + returns "yaml" or "json" + """ + path = path or self.path + if not path: + return "dict" + if path.endswith(".yml") or path.endswith(".yaml"): + return "yaml" + else: + return "json" + + def load_local(self, path=None): + """Load local json file into self. + + Args: + path (str): file to load + """ + path = path or self.path + if not path: + LOG.error(f"in memory configuration, nothing to load") + return + if exists(path) and isfile(path): + try: + if self._get_file_format(path) == "yaml": + with open(path) as f: + config = yaml.safe_load(f) + else: + config = load_commented_json(path) + for key in config: + self.__setitem__(key, config[key]) + LOG.debug(f"Configuration {path} loaded") + except Exception as e: + LOG.exception(f"Error loading configuration '{path}'") + else: + LOG.debug(f"Configuration '{path}' not defined, skipping") + + def reload(self): + self.load_local(self.path) + + def store(self, path=None): + """Cache the received settings locally. + + The cache will be used if the remote is unreachable to load settings + that are as close to the user's as possible. + """ + path = path or self.path + if not path: + LOG.error(f"in memory configuration, no save location") + return + if self._get_file_format(path) == "yaml": + with open(path, 'w') as f: + yaml.dump(dict(self), f, allow_unicode=True, + default_flow_style=False, sort_keys=False) + else: + with open(path, 'w') as f: + json.dump(self, f, indent=2) + + def merge(self, conf): + merge_dict(self, conf) + + +class ReadOnlyConfig(LocalConf): + """ read only """ + + def __init__(self, path, allow_overwrite=False): + super().__init__(path) + self.allow_overwrite = allow_overwrite + + def reload(self): + old = self.allow_overwrite + self.allow_overwrite = True + super().reload() + self.allow_overwrite = old + + def __setitem__(self, key, value): + if not self.allow_overwrite: + raise PermissionError(f"{self.path} is read only! it can not be modified at runtime") + super().__setitem__(key, value) + + def __setattr__(self, key, value): + if not self.allow_overwrite: + raise PermissionError(f"{self.path} is read only! it can not be modified at runtime") + super().__setattr__(key, value) + + def merge(self, *args, **kwargs): + if not self.allow_overwrite: + raise PermissionError(f"{self.path} is read only! it can not be modified at runtime") + super().merge(*args, **kwargs) + + def store(self, path=None): + if not self.allow_overwrite: + raise PermissionError(f"{self.path} is read only! it can not be modified at runtime") + super().store(path) + + +class MycroftDefaultConfig(ReadOnlyConfig): + def __init__(self): + super().__init__(DEFAULT_CONFIG) + + def set_root_config_path(self, root_config): + # in case we got it wrong / non standard + self.path = root_config + self.reload() + + +class MycroftSystemConfig(ReadOnlyConfig): + def __init__(self, allow_overwrite=False): + super().__init__(SYSTEM_CONFIG, allow_overwrite) + + +class RemoteConf(LocalConf): + """Config dictionary fetched from mycroft.ai.""" + + def __init__(self, cache=WEB_CONFIG_CACHE): + super(RemoteConf, self).__init__(cache) + + def reload(self): + if selene_api is None: + self.load_local(self.path) + return + try: + if not selene_api.is_paired(): + self.load_local(self.path) + return + + if selene_api.is_backend_disabled(): + # disable options that require backend + config = { + "server": { + "metrics": False, + "sync_skill_settings": False + }, + "skills": {"upload_skill_manifest": False}, + "opt_in": False + } + for key in config: + self.__setitem__(key, config[key]) + else: + api = selene_api.DeviceApi() + setting = api.get_settings() + location = None + try: + location = api.get_location() + except Exception as e: + LOG.error(f"Exception fetching remote location: {e}") + if exists(self.path) and isfile(self.path): + location = load_commented_json(self.path).get('location') + + if location: + setting["location"] = location + # Remove server specific entries + config = {} + translate_remote(config, setting) + + for key in config: + self.__setitem__(key, config[key]) + self.store(self.path) + + except Exception as e: + LOG.error(f"Exception fetching remote configuration: {e}") + self.load_local(self.path) + + +class MycroftUserConfig(LocalConf): + def __init__(self): + super().__init__(USER_CONFIG) + + +MycroftXDGConfig = MycroftUserConfig diff --git a/ovos_config/config.py b/ovos_config/config.py index b5c4a56..cc4fbb8 100644 --- a/ovos_config/config.py +++ b/ovos_config/config.py @@ -13,219 +13,18 @@ # limitations under the License. # import json -import re -import yaml from os.path import isfile -from mycroft_bus_client.message import Message -from ovos_config.locations import * -from ovos_utils.configuration import get_xdg_config_locations, get_xdg_config_save_path -from ovos_utils.network_utils import is_connected -from ovos_utils import camel_case_split -from ovos_utils.json_helper import load_commented_json, merge_dict -from ovos_utils.log import LOG -from ovos_utils.json_helper import flattened_delete -from ovos_config.utils import FileWatcher - -try: - import mycroft.api as selene_api -except ImportError: - # TODO https://github.com/OpenVoiceOS/selene_api - selene_api = None - - -def is_remote_list(values): - """Check if list corresponds to a backend formatted collection of dicts - """ - for v in values: - if not isinstance(v, dict): - return False - if "@type" not in v.keys(): - return False - return True - - -def translate_remote(config, setting): - """Translate config names from server to equivalents for mycroft-core. - - Args: - config: base config to populate - settings: remote settings to be translated - """ - IGNORED_SETTINGS = ["uuid", "@type", "active", "user", "device"] - - for k, v in setting.items(): - if k not in IGNORED_SETTINGS: - # Translate the CamelCase values stored remotely into the - # Python-style names used within mycroft-core. - key = re.sub(r"Setting(s)?", "", k) - key = camel_case_split(key).replace(" ", "_").lower() - if isinstance(v, dict): - config[key] = config.get(key, {}) - translate_remote(config[key], v) - elif isinstance(v, list): - if is_remote_list(v): - if key not in config: - config[key] = {} - translate_list(config[key], v) - else: - config[key] = v - else: - config[key] = v - - -def translate_list(config, values): - """Translate list formatted by mycroft server. - - Args: - config (dict): target config - values (list): list from mycroft server config - """ - for v in values: - module = v["@type"] - if v.get("active"): - config["module"] = module - config[module] = config.get(module, {}) - translate_remote(config[module], v) - - -class LocalConf(dict): - """Config dictionary from file.""" - - def __init__(self, path): - super().__init__(self) - self.path = path - if path: - self.load_local(path) - - def _get_file_format(self, path=None): - """The config file format - supported file extensions: - - json (.json) - - commented json (.conf) - - yaml (.yaml/.yml) - - returns "yaml" or "json" - """ - path = path or self.path - if not path: - return "dict" - if path.endswith(".yml") or path.endswith(".yaml"): - return "yaml" - else: - return "json" - - def load_local(self, path=None): - """Load local json file into self. - - Args: - path (str): file to load - """ - path = path or self.path - if not path: - LOG.error(f"in memory configuration, nothing to load") - return - if exists(path) and isfile(path): - try: - if self._get_file_format(path) == "yaml": - with open(path) as f: - config = yaml.safe_load(f) - if isinstance(config, str): - LOG.error(f"yaml config parsed as str. '{path}'") - with open(path) as f: - contents = f.read() - LOG.debug(f"{path}:\n{contents}") - return - else: - config = load_commented_json(path) - for key in config: - self.__setitem__(key, config[key]) - LOG.debug(f"Configuration {path} loaded") - except json.JSONDecodeError as e: - LOG.error(f"Error parsing json configuration '{path}': {e}") - with open(path) as f: - contents = f.read() - LOG.debug(f"{path}:\n{contents}") - except Exception as e: - LOG.exception(f"Error loading configuration '{path}'") - else: - LOG.debug(f"Configuration '{path}' not defined, skipping") +from time import sleep - def reload(self): - self.load_local(self.path) - - def store(self, path=None): - """Cache the received settings locally. - - The cache will be used if the remote is unreachable to load settings - that are as close to the user's as possible. - """ - path = path or self.path - if not path: - LOG.error(f"in memory configuration, no save location") - return - if self._get_file_format(path) == "yaml": - with open(path, 'w') as f: - yaml.dump(dict(self), f, allow_unicode=True, - default_flow_style=False, sort_keys=False) - else: - with open(path, 'w') as f: - json.dump(self, f, indent=2) +from mycroft_bus_client import Message - def merge(self, conf): - merge_dict(self, conf) - - -class RemoteConf(LocalConf): - """Config dictionary fetched from mycroft.ai.""" - - def __init__(self, cache=WEB_CONFIG_CACHE): - super(RemoteConf, self).__init__(cache) - - def reload(self): - if selene_api is None: - self.load_local(self.path) - return - try: - if not selene_api.is_paired(): - self.load_local(self.path) - return - - if selene_api.is_backend_disabled(): - # disable options that require backend - config = { - "server": { - "metrics": False, - "sync_skill_settings": False - }, - "skills": {"upload_skill_manifest": False}, - "opt_in": False - } - for key in config: - self.__setitem__(key, config[key]) - else: - api = selene_api.DeviceApi() - setting = api.get_settings() - location = None - try: - location = api.get_location() - except Exception as e: - LOG.error(f"Exception fetching remote location: {e}") - if exists(self.path) and isfile(self.path): - location = load_commented_json(self.path).get('location') - - if location: - setting["location"] = location - # Remove server specific entries - config = {} - translate_remote(config, setting) - - for key in config: - self.__setitem__(key, config[key]) - self.store(self.path) +from ovos_config.base import LocalConf, MycroftDefaultConfig, MycroftSystemConfig, MycroftUserConfig, RemoteConf +from ovos_config.locations import OLD_USER_CONFIG, get_xdg_config_save_path, get_xdg_config_locations +from ovos_config.utils import FileWatcher - except Exception as e: - LOG.error(f"Exception fetching remote configuration: {e}") - self.load_local(self.path) +from ovos_utils.json_helper import flattened_delete, merge_dict +from ovos_utils.log import LOG +from ovos_utils.network_utils import is_connected def _log_old_location_deprecation(old_user_config=OLD_USER_CONFIG): @@ -242,8 +41,8 @@ class Configuration(dict): """Namespace for operations on the configuration singleton.""" __patch = LocalConf(None) # Patch config that skills can update to override config bus = None - default = LocalConf(DEFAULT_CONFIG) - system = LocalConf(SYSTEM_CONFIG) + default = MycroftDefaultConfig() + system = MycroftSystemConfig() remote = RemoteConf() # This includes both the user config and # /etc/xdg/mycroft/mycroft.conf @@ -492,7 +291,7 @@ def _on_file_change(path): Configuration.reload() except: LOG.exception("Failed to load configuration, syntax seems invalid!") - + for handler in Configuration._callbacks: try: handler() @@ -564,3 +363,19 @@ def clear_cache(message=None): """DEPRECATED - there is no cache anymore """ Configuration.updated(message) + +def read_mycroft_config(): + """ returns a stateless dict with the loaded configuration """ + return dict(Configuration()) + + +def update_mycroft_config(config, path=None): + """ updates user config file with the contents of provided dict + if a path is provided that location will be used instead of MycroftUserConfig""" + if path is None: + conf = MycroftUserConfig() + else: + conf = LocalConf(path) + conf.merge(config) + conf.store() + return conf diff --git a/ovos_config/locale.py b/ovos_config/locale.py index 68e173e..b732ff6 100644 --- a/ovos_config/locale.py +++ b/ovos_config/locale.py @@ -10,19 +10,27 @@ except ImportError: LF = None -_lang = "en-us" +_lang = None _default_tz = None -def get_primary_lang_code(): +def get_primary_lang_code(config=None): + global _lang if LF: return LF.get_primary_lang_code() + if not _lang: + config = config or ovos_config.Configuration() + _lang = config.get("lang", "en-us") return _lang.split("-")[0] -def get_default_lang(): +def get_default_lang(config=None): + global _lang if LF: return LF.get_default_lang() + if not _lang: + config = config or ovos_config.Configuration() + _lang = config.get("lang", "en-us") return _lang diff --git a/ovos_config/locations.py b/ovos_config/locations.py index a34876c..bfb73b8 100644 --- a/ovos_config/locations.py +++ b/ovos_config/locations.py @@ -12,16 +12,101 @@ # See the License for the specific language governing permissions and # limitations under the License. import os -from os.path import join, dirname, expanduser, exists +from os.path import join, dirname, expanduser, exists, isfile from time import sleep +from ovos_utils.system import search_mycroft_core_location, is_running_from_module +from ovos_config.ovos import get_xdg_base, is_using_xdg, get_config_filename, get_ovos_config +from ovos_utils.xdg_utils import xdg_config_dirs, xdg_config_home, xdg_data_dirs, xdg_data_home, xdg_cache_home -from ovos_utils.configuration import get_xdg_config_save_path, get_webcache_location, get_xdg_base, \ - get_config_filename, find_default_config as _fdc + +def get_xdg_config_dirs(folder=None): + """ return list of possible XDG config dirs taking into account ovos.conf """ + folder = folder or get_xdg_base() + xdg_dirs = xdg_config_dirs() + [xdg_config_home()] + return [join(path, folder) for path in xdg_dirs] + + +def get_xdg_data_dirs(folder=None): + """ return list of possible XDG data dirs taking into account ovos.conf """ + folder = folder or get_xdg_base() + return [join(path, folder) for path in xdg_data_dirs()] + + +def get_xdg_config_save_path(folder=None): + """ return base XDG config save path taking into account ovos.conf """ + folder = folder or get_xdg_base() + return join(xdg_config_home(), folder) + + +def get_xdg_data_save_path(folder=None): + """ return base XDG data save path taking into account ovos.conf """ + folder = folder or get_xdg_base() + return join(xdg_data_home(), folder) + + +def get_xdg_cache_save_path(folder=None): + """ return base XDG cache save path taking into account ovos.conf """ + folder = folder or get_xdg_base() + return join(xdg_cache_home(), folder) + + +def find_user_config(): + """ return user config full file path taking into account ovos.conf """ + path = join(get_xdg_config_save_path(), get_config_filename()) + if isfile(path): + return path + old, path = get_config_locations(default=False, web_cache=False, + system=False, old_user=True, + user=True) + if isfile(path): + return path + if isfile(old): + return old + return path + + +def get_config_locations(default=True, web_cache=True, system=True, + old_user=True, user=True): + """return list of all possible config files paths sorted by priority taking into account ovos.conf""" + locs = [] + ovos_cfg = get_ovos_config() + if default: + locs.append(ovos_cfg["default_config_path"]) + if system: + locs.append(f"/etc/{ovos_cfg['base_folder']}/{ovos_cfg['config_filename']}") + if web_cache: + locs.append(get_webcache_location()) + if old_user: + locs.append(f"~/.{ovos_cfg['base_folder']}/{ovos_cfg['config_filename']}") + if user: + locs.append(f"{get_xdg_config_save_path()}/{ovos_cfg['config_filename']}") + return locs + + +def get_webcache_location(): + """ return remote config cache full file path taking into account ovos.conf """ + return join(get_xdg_config_save_path(), 'web_cache.json') + + +def get_xdg_config_locations(): + """ return list of possible XDG config full file paths taking into account ovos.conf """ + # This includes both the user config and + # /etc/xdg/mycroft/mycroft.conf + xdg_paths = list(reversed( + [join(p, get_config_filename()) + for p in get_xdg_config_dirs()] + )) + return xdg_paths def find_default_config(): + """ find where mycroft is installed and return the path to the default mycroft.conf + if mycroft is not found then return the bundled file in ovos_config package""" try: - return _fdc() + mycroft_root = search_mycroft_core_location() + if not mycroft_root: + raise FileNotFoundError("Couldn't find mycroft core root folder.") + return join(mycroft_root, "mycroft", "configuration", "mycroft.conf") except FileNotFoundError: # mycroft-core not found return join(dirname(__file__), "mycroft.conf") diff --git a/ovos_config/ovos.py b/ovos_config/ovos.py index c73233f..6eea25d 100644 --- a/ovos_config/ovos.py +++ b/ovos_config/ovos.py @@ -11,9 +11,6 @@ Examples config: { - // check xdg directories OR only check old style hardcoded paths - "xdg": true, - // the "name of the core", // eg, OVOS, Neon, Chatterbox... // all XDG paths should respect this @@ -62,4 +59,153 @@ } } """ -from ovos_utils.configuration import get_ovos_config +from os.path import isfile, join, dirname + +from json_database import JsonStorage + +import ovos_config.locations as _oloc +from ovos_utils.json_helper import load_commented_json, merge_dict +from ovos_utils.log import LOG +from ovos_utils.system import is_running_from_module + + +def get_ovos_config(): + """ load ovos.conf + goes trough all possible ovos.conf paths and loads them in order + + submodule overrides are applied to the final config if overrides are defined for the caller module + eg, if neon-core is calling this method then neon config overrides are loaded + + """ + # populate default values + config = {"xdg": True, + "base_folder": "mycroft", + "config_filename": "mycroft.conf"} + try: + config["default_config_path"] = _oloc.find_default_config() + except FileNotFoundError: # not a mycroft device + config["default_config_path"] = join(dirname(__file__), "mycroft.conf") + + # load ovos.conf + for path in get_ovos_default_config_paths(): + try: + config = merge_dict(config, load_commented_json(path)) + except: + # tolerate bad json TODO proper exception (?) + pass + + # let's check for derivatives specific configs + # the assumption is that these cores are exclusive to each other, + # this will never find more than one override + # TODO this works if using dedicated .venvs what about system installs? + cores = config.get("module_overrides") or {} + for k in cores: + if is_running_from_module(k): + config = merge_dict(config, cores[k]) + break + else: + subcores = config.get("submodule_mappings") or {} + for k in subcores: + if is_running_from_module(k): + config = merge_dict(config, cores[subcores[k]]) + break + + return config + + +def save_ovos_config(new_config): + """ update ovos.conf contents at ~/.config/OpenVoiceOS/ovos.conf """ + OVOS_CONFIG = join(_oloc.get_xdg_config_save_path("OpenVoiceOS"), + "ovos.conf") + cfg = JsonStorage(OVOS_CONFIG) + cfg.update(new_config) + cfg.store() + return cfg + + +def get_ovos_default_config_paths(): + """ return a list of all existing ovos.conf file locations by order of precedence + + eg. ["/etc/OpenVoiceOS/ovos.conf", "/home/user/.config/OpenVoiceOS/ovos.conf"] + + """ + paths = [] + if isfile("/etc/OpenVoiceOS/ovos.conf"): + paths.append("/etc/OpenVoiceOS/ovos.conf") + elif isfile("/etc/mycroft/ovos.conf"): + LOG.warning("found /etc/mycroft/ovos.conf\n" + "This location has been DEPRECATED!\n" + "Please move your config to /etc/OpenVoiceOS/ovos.conf") + paths.append("/etc/mycroft/ovos.conf") + + # This includes both the user config and + # /etc/xdg/OpenVoiceOS/ovos.conf + for p in _oloc.get_xdg_config_dirs("OpenVoiceOS"): + if isfile(join(p, "ovos.conf")): + paths.append(join(p, "ovos.conf")) + + return paths + + +def is_using_xdg(): + """ BACKWARDS COMPAT: logs warning and always returns True""" + LOG.warning("is_using_xdg has been deprecated! XDG specs are always honoured, this method will be removed in a future release") + return True + + +def get_xdg_base(): + """ base folder name to be used when building paths of the format {$XDG_XXX}/{base} + + different derivative cores may change this folder, this value is derived from ovos.conf + eg, "mycroft", "hivemind", "neon" .... + """ + + return get_ovos_config().get("base_folder") or "mycroft" + + +def set_xdg_base(folder_name): + """ base folder name to be used when building paths of the format {$XDG_XXX}/{base} + + different derivative cores may change this folder, this value is derived from ovos.conf + eg, "mycroft", "hivemind", "neon" .... + + NOTE: this value will be set globally, per core overrides in ovos.conf take precedence + """ + LOG.info(f"XDG base folder set to: '{folder_name}'") + save_ovos_config({"base_folder": folder_name}) + + +def set_config_filename(file_name, core_folder=None): + """ base config file name to be used when building paths + + different derivative cores may change this filename, this value is derived from ovos.conf + eg, "mycroft.conf", "hivemind.json", "neon.yaml" .... + + NOTE: this value will be set globally, per core overrides in ovos.conf take precedence + """ + if core_folder: + set_xdg_base(core_folder) + LOG.info(f"config filename set to: '{file_name}'") + save_ovos_config({"config_filename": file_name}) + + +def get_config_filename(): + """ base config file name to be used when building paths + + different derivative cores may change this filename, this value is derived from ovos.conf + eg, "mycroft.conf", "hivemind.json", "neon.yaml" .... + """ + return get_ovos_config().get("config_filename") or "mycroft.conf" + + +def set_default_config(file_path=None): + """ full path to default config file to be used + NOTE: this is a full path, not a directory! "config_filename" parameter is not used here + + different derivative cores may change this file, this value is derived from ovos.conf + + NOTE: this value will be set globally, per core overrides in ovos.conf take precedence + """ + file_path = file_path or _oloc.find_default_config() + LOG.info(f"default config file changed to: {file_path}") + save_ovos_config({"default_config_path": file_path}) diff --git a/ovos_config/utils.py b/ovos_config/utils.py index c9bd341..272e6fb 100644 --- a/ovos_config/utils.py +++ b/ovos_config/utils.py @@ -3,8 +3,8 @@ import time from os.path import dirname -from watchdog.observers import Observer from watchdog.events import FileSystemEventHandler +from watchdog.observers import Observer class FileWatcher: diff --git a/test/unittests/test_configuration.py b/test/unittests/test_configuration.py index 38f1ce3..ee64b68 100644 --- a/test/unittests/test_configuration.py +++ b/test/unittests/test_configuration.py @@ -32,9 +32,9 @@ def test_remote(self, mock_api): self.assertEqual(rc['location']['city']['name'], 'Stockholm') @patch('json.dump') - @patch('ovos_config.config.exists') - @patch('ovos_config.config.isfile') - @patch('ovos_config.config.load_commented_json') + @patch('ovos_config.base.exists') + @patch('ovos_config.base.isfile') + @patch('ovos_config.base.load_commented_json') def test_local(self, mock_json_loader, mock_isfile, mock_exists, mock_json_dump): local_conf = {'answer': 42, 'falling_objects': ['flower pot', 'whale']}