# -*- coding: utf-8 -*-
"""
Copyright (C) 2011 Oliver Tengler
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
import re
import types
import unittest
from typing import TypeVar, Any, Dict, Callable, Tuple, Iterator, cast
from tools.FileTools import fopen
T = TypeVar('T')
def identity (a: T) -> T:
return a
def boolParse (value: Any) -> bool:
if type(value) == bool:
return cast(bool, value)
if type(value) == str:
value = value.lower()
if value == "true" or value == "1" or value == "yes":
return True
if value == "false" or value == "0" or value == "no":
return False
if type(value) == int:
return bool(value)
raise RuntimeError("Unknown boolean value '" + value + "'")
def boolPersist (value: Any) -> str:
if type(value) == bool:
return str(value)
if type(value) == str:
value = value.lower()
if value == "true" or value == "1" or value == "yes":
return "True"
if value == "false" or value == "0" or value == "no":
return "False"
if type (value) == int:
if value:
return "True"
return "False"
raise RuntimeError("Cannot interpret '" + str(value) + "' as bool")
def plainTypeMapper(t: Any) -> Tuple[Callable, Callable, Callable]:
if bool == t:
return (boolParse, lambda: None, boolPersist)
if int == t:
return (int, lambda: None, str)
return (identity, lambda: None, str)
# A object which holds configuration data. The data is accessed like a property. It can contain objects of itself
# which allows to build deeper stacked configs.
# E.g.:
# c = Config()
# c.sub = Config()
# c.sub.a = "Text"
class Config:
def __init__ (self, name: str="", dataMap: Dict[str,Any]=None, configLines:Iterator[str]=None, typeInfoFunc: Callable=None) -> None:
self.__data = dataMap or {}
self.__typeMapper: Dict[str,Tuple[Callable,Callable,Callable]] = {}
if typeInfoFunc:
typeInfoFunc(self)
if name:
self.loadFile(name)
elif configLines:
self.parseLines(configLines)
def setPlainType(self, attr: str, attrType: Any) -> None:
self.__typeMapper[attr.lower()] = plainTypeMapper(attrType)
def setType (self, attr: str, typeFuncs: Tuple[Callable, Callable, Callable]) -> None:
self.__typeMapper[attr.lower()] = typeFuncs
def __typeParse (self, attr: str) -> Callable:
if attr in self.__typeMapper:
return self.__typeMapper[attr][0]
return lambda a: a
def __typeNotFound (self, attr: str) -> Callable:
if attr in self.__typeMapper:
return self.__typeMapper[attr][1]
return lambda:None
def __typePersist (self, attr: str) -> Callable:
if attr in self.__typeMapper:
return self.__typeMapper[attr][2]
return str
def __getattr__ (self, attr: str) -> Any:
attr = attr.lower()
if attr in self.__data:
return self.__typeParse(attr)(self.__data[attr])
result = self.__typeNotFound(attr)()
if result is not None:
if isinstance(result, Config):
self.__data[attr] = result
return result
raise AttributeError(attr + " does not exist in the configuration")
def __getitem__(self, attr: str) -> Any:
attr = attr.lower()
return self.__data[attr]
def __contains__(self, attr: str) -> bool:
return attr.lower() in self.__data
def __setattr__(self, attr: str, value: Any) -> None:
if attr.startswith("_Config__"):
super().__setattr__(attr, value)
return
attr = attr.lower()
if isinstance(value, Config):
self.__data[attr] = value
else:
v = self.__typeParse(attr)(value)
self.__data[attr] = v
def __iter__(self) -> Any:
return self.__data.__iter__()
def values(self) -> Any:
return self.__data.values()
def __repr__ (self) -> str:
return self.__dumpRec (self, 0)
def __dumpRec (self, config: 'Config', level: int) -> str:
s = ""
for k,v in config.__data.items(): # access of protected member pylint: disable=W0212
if s:
s += "\n"
s = s + " " *2*level + k
if type(v) is Config:
s += " {\n"
s = s + self.__dumpRec (v, level+1)
s = s + "\n" + " " *2*level + "}"
else:
s = s + " = " + config.__typePersist(k)(v) # access of protected member pylint: disable=W0212
return s
def remove (self, key: str) -> None:
key = key.lower()
if key in self.__data:
del self.__data[key]
def loadFile (self, name: str) -> None:
with fopen(name) as file:
self.parseLines ((line for line in file.readlines()))
def parseLines(self, lines: Iterator[str]) -> None:
if type(lines) is not types.GeneratorType:
raise TypeError("lines must be a generator type")
for line in lines:
line = line.strip()
try:
if not line or line.startswith("#"): # ignore empty lines and comments
continue
equalSign = line.find("=")
if equalSign != -1:
key = line[:equalSign]
value = line[equalSign+1:]
setattr(self, key.strip(), value.strip())
elif line.startswith("import"):
self.__handleImport(line)
elif line.startswith("}"):
return
elif line.endswith("{"):
groupname = line.split("{")[0].strip().lower()
if groupname in self.__data:
group = self.__data[groupname]
group.parseLines (lines)
else:
try:
group = getattr(self, groupname) # This allows to apply type information from __typeNotFound funcs
except AttributeError:
group = Config()
group.parseLines(lines)
setattr (self, groupname, group)
except:
print ("Do not understand line: " + line)
raise
def __handleImport (self, line: str) -> None:
# syntax: import file as groupname
# This imports the file into the group 'groupname'. If the group already exists it is merged
importTokens = re.match("import\\W+([\\w\\\\/\\.]+)\\W+as\\W+(\\w+)", line)
if importTokens:
groupname = importTokens.group(2).lower()
filename = importTokens.group(1)
try:
group = getattr(self, groupname) # This allows to apply type information from __typeNotFound funcs
except AttributeError:
group = Config()
try:
group.loadFile (filename)
setattr (self, groupname, group)
except IOError:
pass
else:
# syntax: import file
# This imports all properties of the file into the current config.
importTokens = re.match("import\\W+([\\w\\\\/\\.]+)", line)
if not importTokens:
raise RuntimeError("wrong import line")
try:
Config(importTokens.group(1), dataMap=self.__data)
except IOError:
pass
def typeDefaultBool (bDefault: bool) -> Tuple[Callable, Callable, Callable]:
return (boolParse, lambda: bDefault, boolPersist)
def typeDefaultInt (iDefault: int) -> Tuple[Callable, Callable, Callable]:
return (int, lambda: iDefault, str)
def typeDefaultString (strDefault: str) -> Tuple[Callable, Callable, Callable]:
return (identity, lambda: strDefault, str)
def typeDefaultConfig () -> Tuple[Callable, Callable, Callable]:
return (identity, lambda: Config(), identity) # Lambda may not be neccessary pylint: disable=W0108
class TestConfig(unittest.TestCase):
def test(self) -> None:
c = Config()
c.setPlainType ("b1", bool)
c.setType("b2", typeDefaultBool(True))
with self.assertRaises(AttributeError):
c.b1 # pylint: disable=W0104
self.assertEqual(c.b2, True)
c.b1 = False # pylint: disable=W0201
self.assertEqual(c.b1, False)
c.text = "Hallo" # pylint: disable=W0201
self.assertEqual(c.text, "Hallo")
c.setType("di", typeDefaultInt(42))
self.assertEqual(c.di, 42)
c.setType("ds", typeDefaultString("Spam"))
self.assertEqual(c.ds, "Spam")
hasB1 = "b1" in c
self.assertEqual(hasB1, True)
hasX1 = "x1" in c
self.assertEqual(hasX1, False)
if __name__ == "__main__":
unittest.main()