# 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.
import logging
import os
from typing import Callable, Dict, List, Sequence, Tuple, Union
import appdirs
from typing_extensions import TypeAlias
from spinn_utilities import log
from spinn_utilities.configs import (
CamelCaseConfigParser, ConfigTemplateException,
NoConfigFoundException, UnexpectedConfigException)
logger = log.FormatAdapter(logging.getLogger(__name__))
_SectionParser: TypeAlias = Callable[[CamelCaseConfigParser], None]
[docs]
def install_cfg_and_error(
filename: str, defaults: List[str],
config_locations: List[str]) -> NoConfigFoundException:
"""
Installs a local configuration file based on the templates and raises
an exception.
This method is called when no user configuration file is found.
It will create a file in the users home directory based on the defaults.
Then it prints a helpful message and throws an error with the same message.
:param filename:
Name under which to save the new configuration file
:param defaults:
List of full paths to the default configuration files.
Each of which *must* have an associated template file with exactly the
same path plus `.template`.
:param config_locations:
List of paths where the user configuration files
were looked for. Only used for the message
:raise spinn_utilities.configs.NoConfigFoundException:
Always raised
"""
home_cfg = os.path.join(os.path.expanduser("~"), f".{filename}")
found = False
with open(home_cfg, "w", encoding="utf-8") as dst:
for source in defaults:
template = source + ".template"
if os.path.isfile(template):
if found:
raise ConfigTemplateException(
f"Second template found at {template}")
with open(source + ".template", "r", encoding="utf-8") as src:
dst.write(src.read())
dst.write("\n")
found = True
if not found:
if defaults:
raise ConfigTemplateException(
f"No template file found for {defaults}")
else:
raise ConfigTemplateException(
f"No default cfg files found. "
f"New {home_cfg} will be empty")
dst.write("\n# Additional config options can be found in:\n")
for source in defaults:
dst.write(f"# {source}\n")
dst.write("\n# Copy any additional settings you want to change"
" here including section headings\n")
msg = (f'Unable to find config file in any of the following locations: \n'
f'{config_locations}\n'
f'**********************************************************\n'
f'{home_cfg} has been created. \n'
f'Please edit this file and change "None" after "machineName" '
f'to the hostname or IP address of your SpiNNaker board, '
f'and change "None" after "version" to the version of '
f'SpiNNaker hardware you are running on:\n'
f'[Machine]\n'
f'machineName = None\n'
f'version = None\n'
f'***********************************************************\n')
print(msg)
return NoConfigFoundException(msg)
def _check_config(cfg_file: str, default_configs: CamelCaseConfigParser,
strict: bool) -> None:
"""
Checks the configuration read up to this point to see if it is outdated.
Once one difference is found a full reports is generated and an error
raised.
Any section specifically listed as Dead will cause a error
Any section in the default_cfg should not have extra values.
It will never have less as the default_cfg are in the configuration.
Errors on any values listed as PreviousValues.
These are specific values in specific options no longer supported.
For example old algorithm names.
:param cfg_file: Path of last file read in
:param default_configs:
configuration with just the default files in
:param strict: Flag to say an exception should be raised
"""
if not default_configs.sections(): # empty
logger.warning("Can not validate cfg files as no default.")
return
configs = CamelCaseConfigParser()
configs.read(cfg_file)
msg = ""
for section in configs.sections():
if default_configs.has_section(section):
for option in configs.options(section):
if not default_configs.has_option(section, option):
msg += f"Unexpected Option: [{section}]{option}\n"
else:
msg += f"Unexpected Section: [{section}]\n"
if msg:
msg += f"found in {cfg_file}"
if strict:
raise UnexpectedConfigException(msg)
else:
logger.warning(msg)
def _read_a_config(
configuration: CamelCaseConfigParser, cfg_file: str,
default_configs: CamelCaseConfigParser, strict: bool) -> None:
"""
Reads in a configuration file and then directly its `machine_spec_file`.
:param configuration:
configuration to be updated by the reading of a file
:param cfg_file: path to file which should be read in
:param default_configs:
configuration with just the default files in
:param strict: Flag to say checker should raise an exception
"""
_check_config(cfg_file, default_configs, strict)
configuration.read(cfg_file)
if configuration.has_option("Machine", "machine_spec_file"):
machine_spec_file = configuration.get("Machine", "machine_spec_file")
_check_config(machine_spec_file, default_configs, strict)
configuration.read(machine_spec_file)
configuration.remove_option("Machine", "machine_spec_file")
def _config_locations(filename: str) -> List[str]:
"""
Defines the list of places we can get configuration files from.
:param filename:
The local name of the configuration file, e.g., 'spynnaker.cfg'
:return: list of fully-qualified filenames
"""
dotname = "." + filename
# locations to read as well as default later overrides earlier
system_config_cfg_file = os.path.join(appdirs.site_config_dir(), dotname)
user_config_cfg_file = os.path.join(appdirs.user_config_dir(), dotname)
user_home_cfg_file = os.path.join(os.path.expanduser("~"), dotname)
# locations to read as well as default later overrides earlier
return [system_config_cfg_file, user_config_cfg_file,
user_home_cfg_file]
[docs]
def load_config(
filename: str, defaults: List[str], config_parsers: Union[
Sequence[Tuple[str, _SectionParser]],
Dict[str, _SectionParser]] = ()) -> CamelCaseConfigParser:
"""
Load the configuration.
:param filename:
The base name of the configuration file(s).
Should not include any path components.
:param defaults:
The list of files to get default configurations from.
:param config_parsers:
The parsers to parse the sections of the configuration file with, as
a list of (section name, parser) or a dictionary from section name to
parser; a configuration section will only
be parsed if the section_name is found in the configuration files
already loaded. The standard logging parser is appended to (a copy
of) this.
:return: the fully-loaded and checked configuration
"""
configs = CamelCaseConfigParser()
# locations to read as well as default later overrides earlier
config_locations = _config_locations(filename)
if not any(os.path.isfile(f) for f in config_locations):
if defaults:
raise install_cfg_and_error(
filename, defaults, config_locations)
else:
logger.error("No default cfg files provided")
configs.read(defaults)
default_configs = CamelCaseConfigParser()
default_configs.read(defaults)
for cfg_file in config_locations:
_read_a_config(configs, cfg_file, default_configs, False)
cfg_file = os.path.join(os.curdir, filename)
_read_a_config(configs, cfg_file, default_configs, True)
parsers = dict(config_parsers)
for section in parsers:
if configs.has_section(section):
parsers[section](configs)
# Log which configs files we read
print(configs.read_files)
logger.info("Read configs files: {}", ", ".join(configs.read_files))
return configs