Create config system
This commit is contained in:
parent
3c684070c5
commit
a39dad7946
2 changed files with 132 additions and 118 deletions
|
|
@ -3,6 +3,7 @@ from typing import List
|
||||||
from .attributed_lines import *
|
from .attributed_lines import *
|
||||||
from .attributed_lines_widget import *
|
from .attributed_lines_widget import *
|
||||||
from .attributed_text_widget import *
|
from .attributed_text_widget import *
|
||||||
|
from .config import *
|
||||||
from .cursor_rendering import *
|
from .cursor_rendering import *
|
||||||
from .cursor_tree_widget import *
|
from .cursor_tree_widget import *
|
||||||
from .element import *
|
from .element import *
|
||||||
|
|
@ -14,9 +15,11 @@ from .utils import *
|
||||||
|
|
||||||
__all__: List[str] = []
|
__all__: List[str] = []
|
||||||
|
|
||||||
|
|
||||||
__all__ += attributed_lines.__all__
|
__all__ += attributed_lines.__all__
|
||||||
__all__ += attributed_lines_widget.__all__
|
__all__ += attributed_lines_widget.__all__
|
||||||
__all__ += attributed_text_widget.__all__
|
__all__ += attributed_text_widget.__all__
|
||||||
|
__all__ += config.__all__
|
||||||
__all__ += cursor_rendering.__all__
|
__all__ += cursor_rendering.__all__
|
||||||
__all__ += cursor_tree_widget.__all__
|
__all__ += cursor_tree_widget.__all__
|
||||||
__all__ += element.__all__
|
__all__ += element.__all__
|
||||||
|
|
|
||||||
247
cheuph/config.py
247
cheuph/config.py
|
|
@ -1,144 +1,155 @@
|
||||||
# TODO define a config structure including config element descriptions and
|
from dataclasses import dataclass, field
|
||||||
# default values
|
from enum import Enum, auto
|
||||||
#
|
from typing import Any, Callable, Dict, List, Optional, Tuple
|
||||||
# TODO improve interface for accessing config values
|
|
||||||
#
|
|
||||||
# TODO load from and save to yaml file (only the values which differ from the
|
|
||||||
# defaults or which were explicitly set)
|
|
||||||
|
|
||||||
from typing import Any, Dict
|
|
||||||
|
|
||||||
__all__ = ["Fields", "Config", "ConfigView"]
|
|
||||||
|
|
||||||
|
|
||||||
Fields = Dict[str, Any]
|
|
||||||
|
|
||||||
|
__all__ = ["ConfigException", "ConfigValueException", "TransparentConfig",
|
||||||
|
"Kind", "Condition", "Option", "TreeLoader"]
|
||||||
|
|
||||||
class ConfigException(Exception):
|
class ConfigException(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
class ConfigValueException(ConfigException):
|
||||||
|
pass
|
||||||
|
|
||||||
class Config:
|
class TransparentConfig:
|
||||||
@staticmethod
|
|
||||||
def from_tree(tree: Any, prefix: str = "") -> Fields:
|
|
||||||
"""
|
|
||||||
This function takes a nested dict using str-s as keys, and converts it
|
|
||||||
to a flat Fields dict. This means that an option's value can't be a
|
|
||||||
dict, and all dicts are expected to only use str-s as keys.
|
|
||||||
|
|
||||||
It uses the '.' character as separator, so {"a":{"b": 1, "c": 2}}
|
def __init__(self, parent: Optional["TransparentConfig"] = None) -> None:
|
||||||
becomes {"a.b": 1, "a.c": 2}.
|
self.parent = parent
|
||||||
"""
|
|
||||||
|
|
||||||
result: Fields = {}
|
self._values: Dict[str, Any] = {}
|
||||||
|
|
||||||
for k, v in tree.items():
|
|
||||||
if not isinstance(k, str):
|
|
||||||
raise ConfigException("Keys must be strings")
|
|
||||||
|
|
||||||
if "." in k:
|
|
||||||
raise ConfigException("Keys may not contain the '.' character")
|
|
||||||
|
|
||||||
name = prefix + k
|
|
||||||
|
|
||||||
if isinstance(v, dict):
|
|
||||||
result.update(Config.from_tree(v, prefix=name+"."))
|
|
||||||
else:
|
|
||||||
result[name] = v
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def to_tree(fields: Fields) -> Fields:
|
|
||||||
"""
|
|
||||||
This function does the opposite of from_tree().
|
|
||||||
|
|
||||||
It uses the '.' character as separator, so {"a.b": 1, "a.c": 2}
|
|
||||||
becomes {"a":{"b": 1, "c": 2}}.
|
|
||||||
"""
|
|
||||||
|
|
||||||
result: Any = {}
|
|
||||||
|
|
||||||
for k, v in fields.items():
|
|
||||||
steps = k.split(".")
|
|
||||||
|
|
||||||
subdict = result
|
|
||||||
for step in steps[:-1]:
|
|
||||||
new_subdict = subdict.get(step, {})
|
|
||||||
subdict[step] = new_subdict
|
|
||||||
subdict = new_subdict
|
|
||||||
|
|
||||||
subdict[steps[-1]] = v
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
def __init__(self, default_fields: Fields = {}) -> None:
|
|
||||||
self.default_fields = default_fields
|
|
||||||
self.fields: Fields = {}
|
|
||||||
|
|
||||||
def __getitem__(self, key: str) -> Any:
|
def __getitem__(self, key: str) -> Any:
|
||||||
value = self.fields.get(key)
|
return self.get(key)
|
||||||
|
|
||||||
if value is None:
|
|
||||||
value = self.default_fields.get(key)
|
|
||||||
|
|
||||||
if value is None:
|
|
||||||
raise ConfigException(f"No value for {key} found")
|
|
||||||
|
|
||||||
return value
|
|
||||||
|
|
||||||
def __setitem__(self, key: str, value: Any) -> None:
|
def __setitem__(self, key: str, value: Any) -> None:
|
||||||
if isinstance(value, dict):
|
self.set(key, value)
|
||||||
raise ConfigException("No dicts allowed")
|
|
||||||
default = self.default_fields.get(key)
|
|
||||||
|
|
||||||
if value == default:
|
def get(self, name: str) -> Any:
|
||||||
if self.fields.get(key)is not None:
|
if name not in self._values:
|
||||||
self.fields.pop(key)
|
if self.parent is None:
|
||||||
|
raise ConfigValueException(f"No such value: {name!r}")
|
||||||
|
else:
|
||||||
|
return self.parent.get(name)
|
||||||
|
|
||||||
|
return self._values.get(name)
|
||||||
|
|
||||||
|
def set(self, name: str, value: Any) -> None:
|
||||||
|
self._values[name] = value
|
||||||
|
|
||||||
|
def items(self) -> List[Tuple[str, Any]]:
|
||||||
|
return list(self._values.items())
|
||||||
|
|
||||||
|
# Special config reading and writing classes
|
||||||
|
|
||||||
|
class Kind(Enum):
|
||||||
|
INT = auto()
|
||||||
|
STR = auto()
|
||||||
|
FLOAT = auto()
|
||||||
|
RAW = auto()
|
||||||
|
|
||||||
|
def matches(self, value: Any) -> bool:
|
||||||
|
if self == self.INT:
|
||||||
|
return type(value) is int
|
||||||
|
elif self == self.STR:
|
||||||
|
return type(value) is str
|
||||||
|
elif self == self.FLOAT:
|
||||||
|
return type(value) is float
|
||||||
|
elif self == self.RAW:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
Condition = Callable[[Any], bool]
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Option:
|
||||||
|
kind: Kind
|
||||||
|
default: Any
|
||||||
|
conditions: List[Tuple[Condition, str]] = field(default_factory=list)
|
||||||
|
|
||||||
|
def check_valid(self, value: Any) -> None:
|
||||||
|
if not self.kind.matches(value):
|
||||||
|
raise ConfigValueException(f"value {value!r} does not match {self.kind}")
|
||||||
|
|
||||||
|
self.apply_conditions(value)
|
||||||
|
|
||||||
|
def apply_conditions(self, value: Any) -> None:
|
||||||
|
for condition, error_message in self.conditions:
|
||||||
|
if not condition(value):
|
||||||
|
raise ConfigValueException(error_message)
|
||||||
|
|
||||||
|
class TreeLoader:
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self._options: Dict[str, Any] = {}
|
||||||
|
|
||||||
|
def add_option(self, name: str, option: Option) -> None:
|
||||||
|
self._options[name] = option
|
||||||
|
|
||||||
|
def defaults(self) -> TransparentConfig:
|
||||||
|
config = TransparentConfig()
|
||||||
|
|
||||||
|
for name, option in self._options.items():
|
||||||
|
config[name] = option.default
|
||||||
|
|
||||||
|
return config
|
||||||
|
|
||||||
|
def load(self, data: Any) -> TransparentConfig:
|
||||||
|
config = TransparentConfig()
|
||||||
|
errors = []
|
||||||
|
|
||||||
|
for name, option in self._options.items():
|
||||||
|
value = self._get_from_dict(data, name)
|
||||||
|
if value is None: continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
option.check_valid(value)
|
||||||
|
except ConfigValueException as e:
|
||||||
|
errors.append(f"{name}: {e}")
|
||||||
|
else:
|
||||||
|
config[name] = value
|
||||||
|
|
||||||
|
if errors:
|
||||||
|
raise ConfigValueException(errors)
|
||||||
else:
|
else:
|
||||||
self.fields[key] = value
|
return config
|
||||||
|
|
||||||
@property
|
@classmethod
|
||||||
def view(self) -> "ConfigView":
|
def export(cls, config: TransparentConfig) -> Any:
|
||||||
return ConfigView(self.tree)
|
tree: Any = {}
|
||||||
|
|
||||||
@property
|
for key, value in config.items():
|
||||||
def v(self) -> "ConfigView":
|
cls._insert_into_dict(tree, key, value)
|
||||||
return self.view
|
|
||||||
|
|
||||||
@property
|
return tree
|
||||||
def tree(self) -> Any:
|
|
||||||
both = dict(self.default_fields)
|
|
||||||
both.update(self.fields)
|
|
||||||
return self.to_tree(both)
|
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _get_from_dict(d: Any, name: str) -> Optional[Any]:
|
||||||
|
path = name.split(".")
|
||||||
|
|
||||||
class ConfigView:
|
for segment in path:
|
||||||
def __init__(self,
|
if d is None or type(d) is not dict:
|
||||||
fields: Any,
|
return None
|
||||||
prefix: str = "",
|
|
||||||
) -> None:
|
|
||||||
|
|
||||||
self._fields = fields
|
d = d.get(segment)
|
||||||
self._prefix = prefix
|
|
||||||
|
|
||||||
def __getattr__(self, name: str) -> Any:
|
return d
|
||||||
return self._get(name)
|
|
||||||
|
|
||||||
def __getitem__(self, name: str) -> Any:
|
@staticmethod
|
||||||
return self._get(name)
|
def _insert_into_dict(d: Any, name: str, value: Any) -> None:
|
||||||
|
path = name.split(".")
|
||||||
|
if not path:
|
||||||
|
raise ConfigException(f"could not insert value for {name}")
|
||||||
|
|
||||||
def _get(self, name: str) -> Any:
|
for segment in path[:-1]:
|
||||||
"""
|
if type(d) is not dict:
|
||||||
This function assumes that _default_fields and _fields have the same
|
raise ConfigException(f"could not insert value for {name}")
|
||||||
dict structure.
|
|
||||||
"""
|
|
||||||
|
|
||||||
field = self._fields.get(name)
|
new_d = d.get(segment, {})
|
||||||
|
d[segment] = new_d
|
||||||
|
d = new_d
|
||||||
|
|
||||||
if isinstance(field, dict):
|
if type(d) is not dict:
|
||||||
return ConfigView(field, f"{self._prefix}{name}.")
|
raise ConfigException(f"could not insert value for {name}")
|
||||||
elif field is None:
|
|
||||||
raise ConfigException(f"Field {self._prefix}{name} does not exist")
|
|
||||||
|
|
||||||
return field
|
d[path[-1]] = value
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue