Create config system

This commit is contained in:
Joscha 2019-06-20 14:50:22 +00:00
parent 3c684070c5
commit a39dad7946
2 changed files with 132 additions and 118 deletions

View file

@ -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__

View file

@ -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: else:
self.fields[key] = value return self.parent.get(name)
@property return self._values.get(name)
def view(self) -> "ConfigView":
return ConfigView(self.tree)
@property def set(self, name: str, value: Any) -> None:
def v(self) -> "ConfigView": self._values[name] = value
return self.view
@property def items(self) -> List[Tuple[str, Any]]:
def tree(self) -> Any: return list(self._values.items())
both = dict(self.default_fields)
both.update(self.fields)
return self.to_tree(both)
# Special config reading and writing classes
class ConfigView: class Kind(Enum):
def __init__(self, INT = auto()
fields: Any, STR = auto()
prefix: str = "", FLOAT = auto()
) -> None: RAW = auto()
self._fields = fields def matches(self, value: Any) -> bool:
self._prefix = prefix 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
def __getattr__(self, name: str) -> Any: return False
return self._get(name)
def __getitem__(self, name: str) -> Any: Condition = Callable[[Any], bool]
return self._get(name)
def _get(self, name: str) -> Any: @dataclass
""" class Option:
This function assumes that _default_fields and _fields have the same kind: Kind
dict structure. default: Any
""" conditions: List[Tuple[Condition, str]] = field(default_factory=list)
field = self._fields.get(name) def check_valid(self, value: Any) -> None:
if not self.kind.matches(value):
raise ConfigValueException(f"value {value!r} does not match {self.kind}")
if isinstance(field, dict): self.apply_conditions(value)
return ConfigView(field, f"{self._prefix}{name}.")
elif field is None:
raise ConfigException(f"Field {self._prefix}{name} does not exist")
return field 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:
return config
@classmethod
def export(cls, config: TransparentConfig) -> Any:
tree: Any = {}
for key, value in config.items():
cls._insert_into_dict(tree, key, value)
return tree
@staticmethod
def _get_from_dict(d: Any, name: str) -> Optional[Any]:
path = name.split(".")
for segment in path:
if d is None or type(d) is not dict:
return None
d = d.get(segment)
return d
@staticmethod
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}")
for segment in path[:-1]:
if type(d) is not dict:
raise ConfigException(f"could not insert value for {name}")
new_d = d.get(segment, {})
d[segment] = new_d
d = new_d
if type(d) is not dict:
raise ConfigException(f"could not insert value for {name}")
d[path[-1]] = value