Source code for spinn_utilities.config_holder

# Copyright (c) 2017 The University of Manchester
#
# 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
#
#     https://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.
from configparser import NoOptionError
import logging
import os
from typing import List, Optional, Tuple

import appdirs

import spinn_utilities.conf_loader as conf_loader
from spinn_utilities.data import UtilsDataView
from spinn_utilities.configs import CamelCaseConfigParser
from spinn_utilities.configs.no_config_found_exception import (
    NoConfigFoundException)
from spinn_utilities.configs.two_user_configs_exception import (
    TwoUserConfigsException)
from spinn_utilities.exceptions import ConfigException
from spinn_utilities.log import (
    FormatAdapter, ConfiguredFilter, ConfiguredFormatter)

logger = FormatAdapter(logging.getLogger(__file__))

# pylint: disable=global-statement
# Any cleaner method than global statements would add extra overhead
__config: Optional[CamelCaseConfigParser] = None
__default_config_files: List[str] = []
__config_file: Optional[str] = None
__missing_config_file: Optional[str] = None
__template: Optional[str] = None
__user_cfg: Optional[str] = None
__unittest_mode: bool = False


[docs] def add_default_cfg(default: str) -> None: """ Adds an extra default configuration file to be read after earlier ones. :param default: Absolute path to the configuration file """ if default not in __default_config_files: __default_config_files.append(default)
[docs] def add_template(template: str) -> None: """ Adds an extra default configuration file to be read after earlier ones. :param template: Absolute path to the template file """ global __template if __template is None: __template = template else: raise ConfigException("Second template")
[docs] def get_default_cfgs() -> Tuple[str, ...]: """ The default configuration files This is a read only values to be used outside of normal operations :returns: The default configuration files as a tuple. """ return tuple(__default_config_files)
[docs] def clear_cfg_files(unittest_mode: bool) -> None: """ Clears any previous set configurations and configuration files. After this method :py:func:`add_default_cfg` and :py:func:`set_cfg_files` need to be called. :param unittest_mode: Flag to put the holder into unit testing mode """ global __config, __template, __unittest_mode __config = None __default_config_files.clear() __template = None __unittest_mode = unittest_mode
def _pre_load_config() -> CamelCaseConfigParser: """ Loads configurations due to early access to a configuration value. :raises ConfigException: Raise if called before setup """ # If you get this error during a unit test, unittest_step was not called if not __unittest_mode: raise ConfigException( "Accessing config values before setup is not supported") global __config if not __default_config_files: raise ConfigException("No default configs set") __config = conf_loader.load_defaults(__default_config_files) return __config
[docs] def logging_parser(config: CamelCaseConfigParser) -> None: """ Create the root logger with the given level. Create filters based on logging levels """ try: if (has_config_option("Logging", "instantiate") and get_config_bool("Logging", "instantiate")): level = "INFO" if has_config_option("Logging", "default"): level = get_config_str("Logging", "default").upper() logging.basicConfig(level=level) for handler in logging.root.handlers: handler.addFilter( ConfiguredFilter(config)) # type: ignore[arg-type] handler.setFormatter(ConfiguredFormatter(config)) except NoOptionError: pass
def _find_user_cfg(config_file: str) -> None: """ Defines the list of places we can get configuration files from. :return: existing fully-qualified filename """ global __user_cfg __user_cfg = None dotname = "." + config_file for check in [os.path.join(appdirs.site_config_dir(), dotname), os.path.join(appdirs.user_config_dir(), dotname), os.path.join(os.path.expanduser("~"), dotname)]: if os.path.isfile(check): if __user_cfg: raise TwoUserConfigsException( f"Two user cfg files found {check} and {__user_cfg}") else: __user_cfg = check
[docs] def check_user_cfg() -> None: """ Checks for a user cfg and if not create one and errors Expected to be called when options to create a machine are missing Installs a local configuration file based on the templates. Then it prints a helpful message and throws an error with the same message. """ if __missing_config_file is None: # Creating a template and raising an error is incorrect here return assert __template is not None home_cfg = os.path.join( os.path.expanduser("~"), f".{__missing_config_file}") with open(home_cfg, "w", encoding="utf-8") as dst: with open(__template, "r", encoding="utf-8") as src: dst.write(src.read()) dst.write("\n") dst.write("\n# Additional config options can be found in:\n") for source in __default_config_files: dst.write(f"# {source}\n") dst.write("\n# Copy any additional settings you want to change" " here including section headings\n") msg = ('Unable to find config file in your home directory\n' '**********************************************************\n' f'{home_cfg} has been created. \n' 'Please edit this file and change "machineName" ' 'to the IP address of your SpiNNaker board ' "or change spalloc_server to the server urls " 'and change "version" to the version of ' 'SpiNNaker hardware you are running on:\n' '***********************************************************\n') print(msg) raise NoConfigFoundException(msg)
[docs] def load_config(config_file: str) -> CamelCaseConfigParser: """ Reads in all the configuration files, resetting all values. :raises ConfigException: If called before setting defaults :returns: A fully loaded parser object """ global __config, __missing_config_file if not __default_config_files: raise ConfigException("No default configs set") if not __template: raise ConfigException("No template set") _find_user_cfg(config_file) if __user_cfg is None: __missing_config_file = config_file logger.info(f".{config_file} not found in the home directory") __config = conf_loader.load_config( config_file, __user_cfg, __default_config_files) logging_parser(__config) logger.info("config files read = {}", __config.read_files) return __config
[docs] def is_config_none(section: str, option: str) -> bool: """ Check if the value of a configuration option would be considered None :param section: What section to get the option from. :param option: What option to read. :return: True if and only if the value would be considered None """ value = get_config_str_or_none(section, option) return value is None
[docs] def get_config_str(section: str, option: str) -> str: """ Get the string value of a configuration option. :param section: What section to get the option from. :param option: What option to read. :return: The option value :raises ConfigException: if the Value would be None """ value = get_config_str_or_none(section, option) if value is None: raise ConfigException(f"Unexpected None for {section=} {option=}") return value
[docs] def get_config_str_or_none(section: str, option: str) -> Optional[str]: """ Get the string value of a configuration option. :param section: What section to get the option from. :param option: What option to read. :return: The option value :raises ConfigException: if the Value would be None """ if __config is None: return _pre_load_config().get_str(section, option) else: return __config.get_str(section, option)
[docs] def get_config_str_list( section: str, option: str, token: str = ",") -> List[str]: """ Get the string value of a configuration option split into a list. :param section: What section to get the option from. :param option: What option to read. :param token: The token to split the string into a list :return: The list (possibly empty) of the option values """ if __config is None: return _pre_load_config().get_str_list(section, option, token) else: return __config.get_str_list(section, option, token)
[docs] def get_config_int(section: str, option: str) -> int: """ Get the integer value of a configuration option. :param section: What section to get the option from. :param option: What option to read. :return: The option value :raises ConfigException: if the Value would be None """ value = get_config_int_or_none(section, option) if value is None: raise ConfigException(f"Unexpected None for {section=} {option=}") return value
[docs] def get_config_int_or_none(section: str, option: str) -> Optional[int]: """ Get the integer value of a configuration option. :param section: What section to get the option from. :param option: What option to read. :return: The option value :raises ConfigException: if the Value would be None """ if __config is None: return _pre_load_config().get_int(section, option) else: return __config.get_int(section, option)
[docs] def get_config_float(section: str, option: str) -> float: """ Get the float value of a configuration option. :param section: What section to get the option from. :param option: What option to read. :return: The option value. :raises ConfigException: if the Value would be None """ value = get_config_float_or_none(section, option) if value is None: raise ConfigException(f"Unexpected None for {section=} {option=}") return value
[docs] def get_config_float_or_none(section: str, option: str) -> Optional[float]: """ Get the float value of a configuration option. :param section: What section to get the option from. :param option: What option to read. :return: The option value. """ if __config is None: return _pre_load_config().get_float(section, option) else: return __config.get_float(section, option)
[docs] def get_config_bool(section: str, option: str) -> bool: """ Get the Boolean value of a configuration option. :param section: What section to get the option from. :param option: What option to read. :return: The option value. :raises ConfigException: if the Value would be None """ value = get_config_bool_or_none(section, option) if value is None: raise ConfigException(f"Unexpected None for {section=} {option=}") return value
[docs] def get_config_bool_or_none(section: str, option: str, special_nones: Optional[List[str]] = None ) -> Optional[bool]: """ Get the Boolean value of a configuration option. :param section: What section to get the option from. :param option: What option to read. :param special_nones: What special values to except as None :return: The option value. :raises ConfigException: if the Value would be None """ if __config is None: return _pre_load_config().get_bool(section, option, special_nones) else: return __config.get_bool(section, option, special_nones)
[docs] def set_config(section: str, option: str, value: Optional[str]) -> None: """ Sets the value of a configuration option. This method should only be called by the simulator or by unit tests. :param section: What section to set the option in. :param option: What option to set. :param value: Value to set option to :raises ConfigException: If called unexpectedly """ if __config is None: _pre_load_config().set(section, option, value) else: __config.set(section, option, value)
[docs] def config_sections() -> List[str]: """ :returns: A list of section names """ if __config is None: raise ConfigException("configuration not loaded") return __config.sections()
[docs] def configs_loaded() -> bool: """ :returns: True if and only if the configuration was loaded """ if __config is None: return False else: return True
[docs] def has_config_option(section: str, option: str) -> bool: """ Check if the section has this configuration option. :param section: What section to check :param option: What option to check. :return: True if and only if the option is defined. It may be `None` """ if __config is None: raise ConfigException("configuration not loaded") else: return __config.has_option(section, option)
[docs] def config_options(section: str) -> List[str]: """ :param section: What section to list options for. :returns: a list of option names for the given section name. """ if __config is None: raise ConfigException("configuration not loaded") return __config.options(section)
[docs] def get_report_path( option: str, section: str = "Reports", n_run: Optional[int] = None, is_dir: bool = False) -> str: """ Gets and fixes the path for this option If the cfg path is relative it will be joined with the run_dir_path. Creates the path's directory if it does not exist. (n_run) and (reset_str) will be replaced Later updates may replace other bracketed expressions as needed so avoid using brackets in file names :param option: cfg option name :param section: cfg section. Needed if not Reports :param n_run: If provided will be used instead of the current run number :param is_dir: When true will make sure this path exists as a directory. When False will make sure the parent directory is exists. :return: An unchecked absolute path to the file or directory """ path = get_config_str(section, option) if path.startswith("(global)"): path = os.path.join(UtilsDataView.get_global_reports_dir(), path[8:]) if n_run is not None and n_run > 1: if "(n_run)" not in path: logger.warning( f"cfg option {option} does not have a (n_run) so " f"files from different runs may be overwritten") if "(n_run)" in path: if n_run is None: n_run = UtilsDataView.get_run_number() path = path.replace("(n_run)", str(n_run)) if "(reset_str)" in path: reset_str = UtilsDataView.get_reset_str() path = path.replace("(reset_str)", str(reset_str)) if "\\" in path: path = path.replace("\\", os.sep) if not os.path.isabs(path): path = os.path.join(UtilsDataView.get_run_dir_path(), path) if is_dir: os.makedirs(path, exist_ok=True) else: folder, _ = os.path.split(path) os.makedirs(folder, exist_ok=True) return path
[docs] def get_timestamp_path(option: str, section: str = "Reports") -> str: """ Gets and fixes the path for this option If the cfg path is relative it will be joined with the timestamp_path. Creates the path's directory if it does not exist. Later updates may replace bracketed expressions as needed so avoid using brackets in file names :param option: cfg option name :param section: cfg section. Needed if not Reports :return: An unchecked absolute path to the file or directory """ path = get_config_str(section, option) if path.startswith("(global)"): path = os.path.join(UtilsDataView.get_global_reports_dir(), path[8:]) elif not os.path.isabs(path): path = os.path.join(UtilsDataView.get_timestamp_dir_path(), path) folder, _ = os.path.split(path) os.makedirs(folder, exist_ok=True) return path