Source code for hop.configure
import dataclasses
import logging
import os
import toml
from typing import Optional
import warnings
from . import cli
logger = logging.getLogger("hop")
[docs]
def get_config_path(type: str = "general"):
"""Determines the default location for auth configuration.
Args:
type: The type of configuration data for which the path should be looked up.
Recognized types are 'general' and 'auth'.
Returns:
The path to the requested configuration file.
Raises:
ValueError: Unrecognized config type requested.
"""
file_for_type = {
"general": "config.toml",
"auth": "auth.toml",
}
if type not in file_for_type:
raise ValueError(f"Unknown configuration type: '{type}'")
auth_filepath = os.path.join("hop", file_for_type[type])
if "XDG_CONFIG_HOME" in os.environ:
return os.path.join(os.getenv("XDG_CONFIG_HOME"), auth_filepath)
else:
return os.path.join(os.getenv("HOME"), ".config", auth_filepath)
[docs]
@dataclasses.dataclass
class Config:
"""A container class for general configuration parameters
"""
fetch_external: bool = True
automatic_offload: bool = True
def asdict(self):
return dataclasses.asdict(self)
@classmethod
def load(cls, input):
if hasattr(input, "read"):
input = input.read()
try:
return cls(**toml.loads(input)["config"])
except KeyError:
raise RuntimeError("configuration data has no config section") from None
except Exception as ex:
raise RuntimeError(f"configuration data is malformed: {ex}")
def save(self, output):
toml.dump({"config": self.asdict()}, output)
def _parse_bool(s: str):
"""Interpret a range of possible truthy string values as booleans
"""
s = s.lower()
if s == "true" or s == "yes" or s == "on" or s == "1":
return True
if s == "false" or s == "no" or s == "off" or s == "0":
return False
raise ValueError(f"Invalid boolean value '{s}'")
# The mapping of types to functions used for parsing strings provided by the user into configuration
# values. When no entry for a given type exists in this object, the type's normal constructor should
# be used.
_conversion_functions = {
bool: _parse_bool
}
[docs]
def load_config(config_path: Optional[str] = None):
"""
Load the configuration settings for the library combining, in order of increasing precedence:
1. The built-in defaults
2. Defaults stored in the configuration file
3. Values currently set in the environment as environment variables
Args:
config_path: The path to the configuration file to load. If unspecified, the default
location is used.
Return: A Config object with all current settings
"""
# First, load the specified file, or the default file if not specified
# If the file does not exist, use built-in defaults
if config_path is None:
config_path = get_config_path("general")
if os.path.exists(config_path):
try:
file = open(config_path, 'r')
except Exception:
warnings.warn(f"Unable to open {config_path} for reading; using default configuration")
config = Config()
else:
try:
with file:
config = Config.load(file)
except Exception as ex:
raise RuntimeError(f"Error loading {config_path}") from ex
else:
config = Config()
# Second, check for any environment variables and apply them if found
env_var_prefix = "HOP_"
for field in dataclasses.fields(config):
var_name = (env_var_prefix + field.name).upper()
value = os.getenv(var_name)
if value is None:
continue
try:
if field.type in _conversion_functions:
value = _conversion_functions[field.type](value)
else:
value = field.type(value)
except Exception as ex:
raise RuntimeError(f"Error parsing configuration variable {var_name}") from ex
setattr(config, field.name, value)
return config
[docs]
def write_config_value(name: str, value):
"""
Store a configuration value to the configuation file.
Args:
name: The name of the parameter to store. Must be a valid parameter name.
value: The parameter value to store. Must be the correct type for the specified parameter.
"""
config_path = get_config_path("general")
# Load any existing configuration
# We do not use load_config because we want to avoid baking in any temporary settings
# provided in environment variables, or any built-in defaults not explicitly chosen by the
# user.
if os.path.exists(config_path):
try:
config = toml.load(config_path)["config"]
except KeyError:
raise RuntimeError("{config_path} has no config section") from None
except Exception as ex:
raise RuntimeError(f"{config_path} is malformed: {ex}")
else:
config = {}
# Set/overwrite the parameter specified by the user
config[name] = value
# Write the resulting configuration back out
os.makedirs(os.path.dirname(config_path), exist_ok=True)
with open(config_path, 'w') as output:
toml.dump({"config": config}, output)
logger.info(f"Wrote {name} = {value} to {config_path}")
def _add_parser_args(parser):
cli.add_logging_opts(parser)
subparser = parser.add_subparsers(title="commands", metavar="<command>", dest="command")
subparser.required = True
locate_parser = subparser.add_parser("locate", help="display configuration path")
locate_parser.add_argument("-t", "--type", dest="type", default="general", required=False,
choices=["general", "auth"],
help="The type of configuration file to locate. "
"Defaults to general configuration.")
subparser.add_parser("show", help="display current configuration")
set_parser = subparser.add_parser("set", help="set default value for a configuration parameter")
set_parser.add_argument("name",
choices=[field.name for field in dataclasses.fields(Config)],
help="The configuration parameter to set")
set_parser.add_argument("value",
help="The parameter value to set")
def _main(args):
"""Configuration utilities.
"""
cli.set_up_logger(args)
if args.command == "locate":
print(get_config_path(args.type))
elif args.command == "show":
config = load_config()
for field in dataclasses.fields(config):
print(f"{field.name}: {getattr(config, field.name)}")
elif args.command == "set":
# Collect data supplied by the user
name = args.name
# the argument parser should enforce that name is a value configuration parameter
for field in dataclasses.fields(Config):
if field.name == name:
break
value = args.value
# sanitize/normalize the provided value
if field.type in _conversion_functions:
value = _conversion_functions[field.type](value)
else:
value = field.type(value)
write_config_value(name, value)