Deal with default config separately
This commit is contained in:
parent
a6c2e7897a
commit
f5e8ff6b5d
1 changed files with 155 additions and 104 deletions
|
|
@ -6,16 +6,19 @@ The result of loading a config file are the "local" variables,
|
||||||
including the modules loaded via "import".
|
including the modules loaded via "import".
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import logging
|
from dataclasses import dataclass
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Dict, List, Optional, Tuple, Union
|
from typing import Any, Dict, List, Optional, Tuple, Union
|
||||||
|
import logging
|
||||||
|
|
||||||
from .colors import *
|
from .colors import *
|
||||||
from .util import *
|
from .util import *
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"DEFAULT_LOCATIONS", "DEFAULT_CONFIG_FILE",
|
"DEFAULT_LOCATIONS",
|
||||||
"ConfigurationException", "Config",
|
"ConfigurationException",
|
||||||
|
"DefaultConfigValue", "DefaultConfig", "DEFAULT_CONFIG",
|
||||||
|
"Config",
|
||||||
]
|
]
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
@ -25,71 +28,178 @@ DEFAULT_LOCATIONS = [
|
||||||
Path("~/.evering.py"),
|
Path("~/.evering.py"),
|
||||||
]
|
]
|
||||||
|
|
||||||
DEFAULT_CONFIG_FILE = """
|
|
||||||
known_files = "known_files"
|
|
||||||
config_dir = "config/"
|
|
||||||
binary = True
|
|
||||||
statement_prefix = "#"
|
|
||||||
expression_delimiters = ("{{", "}}")
|
|
||||||
"""
|
|
||||||
|
|
||||||
class ConfigurationException(Exception):
|
class ConfigurationException(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class DefaultConfigValue:
|
||||||
|
# A short textual description of the value's function
|
||||||
|
description: str
|
||||||
|
# The actual default value
|
||||||
|
value: Any
|
||||||
|
# Whether this variable even has a default value or the value is set at runtime
|
||||||
|
has_constant_value: bool
|
||||||
|
|
||||||
|
class DefaultConfig:
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self._values: Dict[str, DefaultConfigValue] = {}
|
||||||
|
|
||||||
|
def add(self,
|
||||||
|
name: str,
|
||||||
|
description: str,
|
||||||
|
value: Any = None,
|
||||||
|
has_constant_value: bool = True
|
||||||
|
) -> None:
|
||||||
|
if name in self._values:
|
||||||
|
raise ConfigurationException(f"Value {name!r} already exists")
|
||||||
|
|
||||||
|
self._values[name] = DefaultConfigValue(
|
||||||
|
description, value=value, has_constant_value=has_constant_value)
|
||||||
|
|
||||||
|
def get(self, name: str) -> Optional[DefaultConfigValue]:
|
||||||
|
return self._values.get(name)
|
||||||
|
|
||||||
|
def to_local_vars(self) -> Dict[str, Any]:
|
||||||
|
return {name: d.value for name, d in self._values.items() if d.has_constant_value}
|
||||||
|
|
||||||
|
def to_config(self) -> "Config":
|
||||||
|
return Config(self.to_local_vars())
|
||||||
|
|
||||||
|
def to_config_file(self) -> str:
|
||||||
|
"""
|
||||||
|
Attempt to convert the DefaultConfig into a format that can be read by a
|
||||||
|
python interpreter. This assumes that all names are valid variable names
|
||||||
|
and that the repr() representations of each object can be read by the
|
||||||
|
interpreter.
|
||||||
|
|
||||||
|
This solution is quite hacky, so use at your own risk :P (At least make
|
||||||
|
sure that this works with all your default values before you use it).
|
||||||
|
"""
|
||||||
|
|
||||||
|
lines = ["from pathlib import *", ""]
|
||||||
|
|
||||||
|
for name in sorted(self._values):
|
||||||
|
value = self._values[name]
|
||||||
|
|
||||||
|
if value.has_constant_value:
|
||||||
|
lines.append(f"{name} = {value.value!r} # {value.description}\n")
|
||||||
|
else:
|
||||||
|
lines.append(f"# {name} : {value.description}\n")
|
||||||
|
|
||||||
|
return "".join(lines)
|
||||||
|
|
||||||
|
DEFAULT_CONFIG = DefaultConfig()
|
||||||
|
|
||||||
|
DEFAULT_CONFIG.add(
|
||||||
|
"base_dir",
|
||||||
|
"All relative paths are interpreted as relative to this directory."
|
||||||
|
" Default: The directory the config file was loaded from",
|
||||||
|
has_constant_value=False)
|
||||||
|
|
||||||
|
DEFAULT_CONFIG.add(
|
||||||
|
"known_files",
|
||||||
|
"The file where evering stores which files it is currently managing",
|
||||||
|
value="known_files")
|
||||||
|
|
||||||
|
DEFAULT_CONFIG.add(
|
||||||
|
"config_dir",
|
||||||
|
"The directory containing the config files",
|
||||||
|
value="config")
|
||||||
|
|
||||||
|
DEFAULT_CONFIG.add(
|
||||||
|
"action_dir",
|
||||||
|
"The directory containing the action scripts",
|
||||||
|
value="actions")
|
||||||
|
|
||||||
|
DEFAULT_CONFIG.add(
|
||||||
|
"binary",
|
||||||
|
"When interpreting a header file: When True, the corresponding file is copied directly to the target instead of compiled. Has no effect if a file has no header file",
|
||||||
|
value=True)
|
||||||
|
|
||||||
|
DEFAULT_CONFIG.add(
|
||||||
|
"targets",
|
||||||
|
"The locations a config file should be placed in. Must be set for all files. Either a path or a list of paths",
|
||||||
|
has_constant_value=False)
|
||||||
|
|
||||||
|
DEFAULT_CONFIG.add(
|
||||||
|
"action",
|
||||||
|
"Whether a file should be treated as an action with a certain name. If set, must be a string",
|
||||||
|
has_constant_value=False)
|
||||||
|
|
||||||
|
DEFAULT_CONFIG.add(
|
||||||
|
"statement_prefix",
|
||||||
|
"Determines the prefix for statements like \"if\"",
|
||||||
|
value="#")
|
||||||
|
|
||||||
|
DEFAULT_CONFIG.add(
|
||||||
|
"expression_delimiters",
|
||||||
|
"Determines the delimiters for in-line expressions",
|
||||||
|
value=("{{", "}}"))
|
||||||
|
|
||||||
|
DEFAULT_CONFIG.add(
|
||||||
|
"filename",
|
||||||
|
"Name of the file currently being compiled, as a string. Set during compilation",
|
||||||
|
has_constant_value=False)
|
||||||
|
|
||||||
|
DEFAULT_CONFIG.add(
|
||||||
|
"target",
|
||||||
|
"Location the file is currently being compiled for, as a Path. Set during compilation",
|
||||||
|
has_constant_value=False)
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
@classmethod
|
@staticmethod
|
||||||
def load_config_file(cls, path: Optional[Path]) -> "Config":
|
def load_config_file(path: Optional[Path]) -> "Config":
|
||||||
"""
|
"""
|
||||||
May raise: ConfigurationException
|
May raise: ConfigurationException
|
||||||
"""
|
"""
|
||||||
|
|
||||||
local_vars: Dict[str, Any]
|
conf = DEFAULT_CONFIG.to_config()
|
||||||
|
|
||||||
if path is None:
|
if path is None:
|
||||||
# Try out all default config file locations
|
# Try out all default config file locations
|
||||||
for path in DEFAULT_LOCATIONS:
|
for path in DEFAULT_LOCATIONS:
|
||||||
try:
|
try:
|
||||||
local_vars = cls._load_config_file(path)
|
copy = conf.copy()
|
||||||
|
copy.apply_config_file(path)
|
||||||
|
conf = copy
|
||||||
break
|
break
|
||||||
except (ReadFileException, ExecuteException) as e:
|
except ConfigurationException as e:
|
||||||
logger.debug(f"Could not load config from {style_path(path)}: {e}")
|
logger.debug(f"Tried default config file at {style_path(path)} and it didn't work")
|
||||||
else:
|
else:
|
||||||
raise ConfigurationException(style_error(
|
raise ConfigurationException(style_error(
|
||||||
"No valid config file found in any of the default locations"))
|
"No valid config file found in any of the default locations"))
|
||||||
else:
|
else:
|
||||||
# Use the path
|
# Use the path
|
||||||
try:
|
try:
|
||||||
local_vars = cls._load_config_file(path)
|
copy = conf.copy()
|
||||||
|
copy.apply_config_file(path)
|
||||||
|
conf = copy
|
||||||
except (ReadFileException, ExecuteException) as e:
|
except (ReadFileException, ExecuteException) as e:
|
||||||
raise ConfigurationException(
|
raise ConfigurationException(
|
||||||
style_error("Could not load config file from ") +
|
style_error("Could not load config file from ") +
|
||||||
style_path(path) + f": {e}")
|
style_path(path) + f": {e}")
|
||||||
|
|
||||||
return cls(local_vars)
|
return conf
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _load_config_file(path: Path) -> Dict[str, Any]:
|
|
||||||
"""
|
|
||||||
May raise: ReadFileException, ExecuteException
|
|
||||||
"""
|
|
||||||
|
|
||||||
local_vars: Dict[str, Any] = {}
|
|
||||||
safer_exec(DEFAULT_CONFIG_FILE, local_vars)
|
|
||||||
|
|
||||||
safer_exec(read_file(path), local_vars)
|
|
||||||
if not "base_dir" in local_vars:
|
|
||||||
local_vars["base_dir"] = path.parent
|
|
||||||
|
|
||||||
logger.info(f"Loaded config from {style_path(str(path))}")
|
|
||||||
|
|
||||||
return local_vars
|
|
||||||
|
|
||||||
def __init__(self, local_vars: Dict[str, Any]) -> None:
|
def __init__(self, local_vars: Dict[str, Any]) -> None:
|
||||||
|
self.local_vars = local_vars
|
||||||
|
|
||||||
|
def apply_config_file(self, path: Path) -> None:
|
||||||
"""
|
"""
|
||||||
May raise: ConfigurationException
|
May raise: ConfigurationException
|
||||||
"""
|
"""
|
||||||
|
|
||||||
self.local_vars = local_vars
|
try:
|
||||||
|
safer_exec(read_file(path), self.local_vars)
|
||||||
|
except (ReadFileException, ExecuteException) as e:
|
||||||
|
error_msg = f"Could not load config from {style_path(path)}: {e}"
|
||||||
|
logger.debug(error_msg)
|
||||||
|
raise ConfigurationException(error_msg)
|
||||||
|
else:
|
||||||
|
if not "base_dir" in self.local_vars:
|
||||||
|
self.local_vars["base_dir"] = path.parent
|
||||||
|
|
||||||
|
logger.info(f"Loaded config from {style_path(path)}")
|
||||||
|
|
||||||
def copy(self) -> "Config":
|
def copy(self) -> "Config":
|
||||||
return Config(copy_local_variables(self.local_vars))
|
return Config(copy_local_variables(self.local_vars))
|
||||||
|
|
@ -135,12 +245,6 @@ class Config:
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def base_dir(self) -> Path:
|
def base_dir(self) -> Path:
|
||||||
"""
|
|
||||||
The path that is the base of all other relative paths.
|
|
||||||
|
|
||||||
Default: The directory the config file was loaded from.
|
|
||||||
"""
|
|
||||||
|
|
||||||
return Path(self._get("base_dir", str, Path)).expanduser()
|
return Path(self._get("base_dir", str, Path)).expanduser()
|
||||||
|
|
||||||
@base_dir.setter
|
@base_dir.setter
|
||||||
|
|
@ -158,50 +262,24 @@ class Config:
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def known_files(self) -> Path:
|
def known_files(self) -> Path:
|
||||||
"""
|
|
||||||
The path where evering stores which files it is currently
|
|
||||||
managing.
|
|
||||||
|
|
||||||
Default: "known_files"
|
|
||||||
"""
|
|
||||||
|
|
||||||
return self._interpret_path(self._get("known_files", str, Path))
|
return self._interpret_path(self._get("known_files", str, Path))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def config_dir(self) -> Path:
|
def config_dir(self) -> Path:
|
||||||
"""
|
|
||||||
The directory containing the config files.
|
|
||||||
|
|
||||||
Default: "config/"
|
|
||||||
"""
|
|
||||||
|
|
||||||
return self._interpret_path(self._get("config_dir", str, Path))
|
return self._interpret_path(self._get("config_dir", str, Path))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def action_dir(self) -> Path:
|
||||||
|
return self._interpret_path(self._get("action_dir", str, Path))
|
||||||
|
|
||||||
# Parsing and compiling behavior
|
# Parsing and compiling behavior
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def binary(self) -> bool:
|
def binary(self) -> bool:
|
||||||
"""
|
|
||||||
When interpreting a separate header file: Whether the
|
|
||||||
corresponding file should not be parsed and compiled, but
|
|
||||||
instead just copied to the targets.
|
|
||||||
|
|
||||||
Has no effect if the file has no header files.
|
|
||||||
|
|
||||||
Default: True
|
|
||||||
"""
|
|
||||||
|
|
||||||
return self._get("binary", bool)
|
return self._get("binary", bool)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def targets(self) -> List[Path]:
|
def targets(self) -> List[Path]:
|
||||||
"""
|
|
||||||
The locations the (compiled) config file should be put
|
|
||||||
in. Must be set for all files.
|
|
||||||
|
|
||||||
Default: not set
|
|
||||||
"""
|
|
||||||
|
|
||||||
name = "targets"
|
name = "targets"
|
||||||
target = self._get(name)
|
target = self._get(name)
|
||||||
is_path = self._is_pathy(target)
|
is_path = self._is_pathy(target)
|
||||||
|
|
@ -218,16 +296,12 @@ class Config:
|
||||||
else:
|
else:
|
||||||
return [self._interpret_path(elem) for elem in target]
|
return [self._interpret_path(elem) for elem in target]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def action(self) -> Optional[str]:
|
||||||
|
return self._get_optional("action", str)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def statement_prefix(self) -> str:
|
def statement_prefix(self) -> str:
|
||||||
"""
|
|
||||||
This determines the prefix for statements like "# if",
|
|
||||||
"# elif", "# else" or "# endif". The prefix always has at
|
|
||||||
least length 1.
|
|
||||||
|
|
||||||
Default: "#"
|
|
||||||
"""
|
|
||||||
|
|
||||||
name = "statement_prefix"
|
name = "statement_prefix"
|
||||||
prefix = self._get(name, str)
|
prefix = self._get(name, str)
|
||||||
|
|
||||||
|
|
@ -240,16 +314,6 @@ class Config:
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def expression_delimiters(self) -> Tuple[str, str]:
|
def expression_delimiters(self) -> Tuple[str, str]:
|
||||||
"""
|
|
||||||
This determines the delimiters for expressions like
|
|
||||||
"{{ 1 + 1 }}".
|
|
||||||
|
|
||||||
It is a tuple of the form: (<prefix>, <suffix>), where both
|
|
||||||
the prefix and suffix are strings of at least length 1.
|
|
||||||
|
|
||||||
Default: ("{{", "}}")
|
|
||||||
"""
|
|
||||||
|
|
||||||
name = "expression_delimiters"
|
name = "expression_delimiters"
|
||||||
delimiters = self._get(name, tuple)
|
delimiters = self._get(name, tuple)
|
||||||
|
|
||||||
|
|
@ -269,12 +333,6 @@ class Config:
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def filename(self) -> str:
|
def filename(self) -> str:
|
||||||
"""
|
|
||||||
The name of the file currently being compiled, as a string.
|
|
||||||
|
|
||||||
Only set during compilation.
|
|
||||||
"""
|
|
||||||
|
|
||||||
return self._get("filename", str)
|
return self._get("filename", str)
|
||||||
|
|
||||||
@filename.setter
|
@filename.setter
|
||||||
|
|
@ -283,13 +341,6 @@ class Config:
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def target(self) -> Path:
|
def target(self) -> Path:
|
||||||
"""
|
|
||||||
The location the file is currently being compiled for, as a
|
|
||||||
Path.
|
|
||||||
|
|
||||||
Only set during compilation.
|
|
||||||
"""
|
|
||||||
|
|
||||||
return self._interpret_path(self._get("target", str, Path))
|
return self._interpret_path(self._get("target", str, Path))
|
||||||
|
|
||||||
@target.setter
|
@target.setter
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue