No Description
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

settings.py 12KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315
  1. #-*- coding: utf-8 -*-
  2. import sys
  3. import os
  4. import configparser
  5. import copy
  6. import warnings
  7. import types # for dynamic bindings
  8. from collections import namedtuple
  9. from lodel.context import LodelContext
  10. LodelContext.expose_modules(globals(),{
  11. 'lodel.logger': 'logger',
  12. 'lodel.settings.utils': ['SettingsError', 'SettingsErrors'],
  13. 'lodel.settings.validator': ['SettingValidator', 'LODEL2_CONF_SPECS',
  14. 'confspec_append'],
  15. 'lodel.settings.settings_loader':['SettingsLoader']})
  16. ## @package lodel.settings.settings Lodel2 settings module
  17. #
  18. # Contains the class that handles the namedtuple tree of settings
  19. ##@brief A default python system lib path
  20. PYTHON_SYS_LIB_PATH = '/usr/local/lib/python{major}.{minor}/'.format(
  21. major = sys.version_info.major,
  22. minor = sys.version_info.minor)
  23. class MetaSettings(type):
  24. @property
  25. def s(self):
  26. self.singleton_assert(True)
  27. return self.instance.settings
  28. ##@brief Handles configuration load etc.
  29. #
  30. # To see howto bootstrap Settings and use it in lodel instance see
  31. # @ref lodel.settings
  32. #
  33. # @par Basic instance usage
  34. # For example if a file defines confs like :
  35. # <pre>
  36. # [super_section]
  37. # super_conf = super_value
  38. # </pre>
  39. # You can access it with :
  40. # <pre> settings_instance.confs.super_section.super_conf </pre>
  41. #
  42. # @par Init sequence
  43. # The initialization sequence is a bit tricky. In fact, plugins adds allowed
  44. # configuration sections/values, but the list of plugins to load are in... the
  45. # settings.
  46. # Here is the conceptual presentation of Settings class initialization stages :
  47. # -# Preloading (sets values like lodel2 library path or the plugins path)
  48. # -# Ask a @ref lodel.settings.settings_loader.SettingsLoader to load all
  49. #configurations files
  50. # -# Fetch the list of plugins in the loaded settings
  51. # -# Merge plugins settings specification with the global lodel settings
  52. #specs ( see @ref lodel.plugin )
  53. # -# Fetch all settings from the merged settings specs
  54. #
  55. # @par Init sequence in practical
  56. # In practice those steps are done by calling a succession of private methods :
  57. # -# @ref Settings.__bootstrap() ( steps 1 to 3 )
  58. # -# @ref Settings.__merge_specs() ( step 4 )
  59. # -# @ref Settings.__populate_from_specs() (step 5)
  60. # -# And finally @ref Settings.__confs_to_namedtuple()
  61. #
  62. # @todo handles default sections for variable sections (sections ending with
  63. # '.*')
  64. # @todo delete the first stage, the lib path HAVE TO BE HARDCODED. In fact
  65. #when we will run lodel in production the lodel2 lib will be in the python path
  66. #@todo add log messages (now we can)
  67. class Settings(object, metaclass=MetaSettings):
  68. ## @brief Stores the singleton instance
  69. instance = None
  70. ## @brief Instanciate the Settings singleton
  71. # @param conf_dir str : The configuration directory
  72. #@param custom_confspecs None | dict : if given overwrite default lodel2
  73. #confspecs
  74. def __init__(self, conf_dir, custom_confspecs = None):
  75. self.singleton_assert() # check that it is the only instance
  76. Settings.instance = self
  77. ## @brief Configuration specification
  78. #
  79. # Initialized by Settings.__bootstrap() method
  80. self.__conf_specs = custom_confspecs
  81. ## @brief Stores the configurations in namedtuple tree
  82. self.__confs = None
  83. self.__conf_dir = conf_dir
  84. self.__started = False
  85. self.__bootstrap()
  86. ## @brief Get the named tuple representing configuration
  87. @property
  88. def settings(self):
  89. return self.__confs.lodel2
  90. ## @brief Delete the singleton instance
  91. @classmethod
  92. def stop(cls):
  93. del(cls.instance)
  94. cls.instance = None
  95. @classmethod
  96. def started(cls):
  97. return cls.instance is not None and cls.instance.__started
  98. ##@brief An utility method that raises if the singleton is not in a good
  99. # state
  100. #@param cls
  101. #@param expect_instanciated bool : if True we expect that the class is
  102. # allready instanciated, else not
  103. # @throw RuntimeError
  104. @classmethod
  105. def singleton_assert(cls, expect_instanciated=False):
  106. if expect_instanciated:
  107. if not cls.started():
  108. raise RuntimeError("The Settings class is not started yet")
  109. else:
  110. if cls.started():
  111. raise RuntimeError("The Settings class is already started")
  112. ##@brief Saves a new configuration for section confname
  113. #@param cls
  114. #@param confname is the name of the modified section
  115. #@param confvalue is a dict with variables to save
  116. #@param validator is a dict with adapted validator
  117. @classmethod
  118. def set(cls, confname, confvalue,validator):
  119. loader = SettingsLoader(cls.instance.__conf_dir)
  120. confkey=confname.rpartition('.')
  121. loader.setoption(confkey[0], confkey[2], confvalue, validator)
  122. ##@brief This method handles Settings instance bootstraping
  123. def __bootstrap(self):
  124. LodelContext.expose_modules(globals(), {
  125. 'lodel.plugin.plugins': ['Plugin', 'PluginError']})
  126. logger.debug("Settings bootstraping")
  127. if self.__conf_specs is None:
  128. lodel2_specs = LODEL2_CONF_SPECS
  129. else:
  130. lodel2_specs = self.__conf_specs
  131. self.__conf_specs = None
  132. loader = SettingsLoader(self.__conf_dir)
  133. plugin_list = []
  134. for ptype_name,ptype in Plugin.plugin_types().items():
  135. pls = ptype.plist_confspecs()
  136. lodel2_specs = confspec_append(lodel2_specs, **pls)
  137. cur_list = loader.getoption(
  138. pls['section'],
  139. pls['key'],
  140. pls['validator'],
  141. pls['default'])
  142. if cur_list is None:
  143. continue
  144. try:
  145. if isinstance(cur_list, str):
  146. cur_list = [cur_list]
  147. plugin_list += cur_list
  148. except TypeError:
  149. plugin_list += [cur_list]
  150. #Checking confspecs
  151. for section in lodel2_specs:
  152. if section.lower() != section:
  153. raise SettingsError("Only lower case are allowed in section name (thank's ConfigParser...)")
  154. for kname in lodel2_specs[section]:
  155. if kname.lower() != kname:
  156. raise SettingsError("Only lower case are allowed in section name (thank's ConfigParser...)")
  157. # Starting the Plugins class
  158. logger.debug("Starting lodel.plugin.Plugin class")
  159. Plugin.start(plugin_list)
  160. # Fetching conf specs from plugins
  161. specs = [lodel2_specs]
  162. errors = list()
  163. for plugin_name in plugin_list:
  164. try:
  165. specs.append(Plugin.get(plugin_name).confspecs)
  166. except PluginError as e:
  167. errors.append(SettingsError(msg=str(e)))
  168. if len(errors) > 0: #Raise all plugins import errors
  169. raise SettingsErrors(errors)
  170. self.__conf_specs = self.__merge_specs(specs)
  171. self.__populate_from_specs(self.__conf_specs, loader)
  172. self.__started = True
  173. ##@brief Produce a configuration specification dict by merging all specifications
  174. #
  175. # Merges global lodel2 conf spec from @ref lodel.settings.validator.LODEL2_CONF_SPECS
  176. # and configuration specifications from loaded plugins
  177. # @param specs list : list of specifications dict
  178. # @return a specification dict
  179. def __merge_specs(self, specs):
  180. res = copy.copy(specs.pop())
  181. for spec in specs:
  182. for section in spec:
  183. if section.lower() != section:
  184. raise SettingsError("Only lower case are allowed in section name (thank's ConfigParser...)")
  185. if section not in res:
  186. res[section] = dict()
  187. for kname in spec[section]:
  188. if kname.lower() != kname:
  189. raise SettingsError("Only lower case are allowed in section name (thank's ConfigParser...)")
  190. if kname in res[section]:
  191. raise SettingsError("Duplicated key '%s' in section '%s'" % (kname, section))
  192. res[section.lower()][kname] = copy.copy(spec[section][kname])
  193. return res
  194. ##@brief Populate the Settings instance with options values fetched with the loader from merged specs
  195. #
  196. # Populate the __confs attribute
  197. # @param specs dict : Settings specification dictionnary as returned by __merge_specs
  198. # @param loader SettingsLoader : A SettingsLoader instance
  199. def __populate_from_specs(self, specs, loader):
  200. self.__confs = dict()
  201. specs = copy.copy(specs) #Avoid destroying original specs dict (may be useless)
  202. # Construct final specs dict replacing variable sections
  203. # by the actual existing sections
  204. variable_sections = [ section for section in specs if section.endswith('.*') ]
  205. for vsec in variable_sections:
  206. preffix = vsec[:-2]
  207. for section in loader.getsection(preffix, 'default'): #WARNING : hardcoded default section
  208. specs[section] = copy.copy(specs[vsec])
  209. del(specs[vsec])
  210. # Fetching values for sections
  211. for section in specs:
  212. for kname in specs[section]:
  213. validator = specs[section][kname][1]
  214. default = specs[section][kname][0]
  215. if section not in self.__confs:
  216. self.__confs[section] = dict()
  217. self.__confs[section][kname] = loader.getoption(section, kname, validator, default)
  218. # Checking unfectched values
  219. loader.raise_errors()
  220. self.__confs_to_namedtuple()
  221. pass
  222. ##@brief Transform the __confs attribute into imbricated namedtuple
  223. #
  224. # For example an option named "foo" in a section named "hello.world" will
  225. # be acessible with self.__confs.hello.world.foo
  226. def __confs_to_namedtuple(self):
  227. res = None
  228. end = False
  229. splits = list()
  230. for section in self.__confs:
  231. splits.append(section.split('.'))
  232. max_len = max([len(spl) for spl in splits])
  233. # building a tree from sections splits
  234. section_tree = dict()
  235. for spl in splits:
  236. section_name = ""
  237. cur = section_tree
  238. for sec_part in spl:
  239. section_name += sec_part+'.'
  240. if sec_part not in cur:
  241. cur[sec_part] = dict()
  242. cur = cur[sec_part]
  243. section_name = section_name[:-1]
  244. for kname, kval in self.__confs[section_name].items():
  245. if kname in cur:
  246. raise SettingsError("Duplicated key for '%s.%s'" % (section_name, kname))
  247. cur[kname] = kval
  248. path = [ ('root', section_tree) ]
  249. visited = set()
  250. curname = 'root'
  251. nodename = 'Lodel2Settings'
  252. cur = section_tree
  253. while True:
  254. visited.add(nodename)
  255. left = [ (kname, cur[kname])
  256. for kname in cur
  257. if nodename+'.'+kname.title() not in visited and isinstance(cur[kname], dict)
  258. ]
  259. if len(left) == 0:
  260. name, leaf = path.pop()
  261. typename = nodename.replace('.', '')
  262. if len(path) == 0:
  263. # END
  264. self.__confs = self.__tree2namedtuple(leaf,typename)
  265. break
  266. else:
  267. path[-1][1][name] = self.__tree2namedtuple(leaf,typename)
  268. nodename = '.'.join(nodename.split('.')[:-1])
  269. cur = path[-1][1]
  270. else:
  271. curname, cur = left[0]
  272. path.append( (curname, cur) )
  273. nodename += '.' + curname.title()
  274. ##@brief Forge a named tuple given a conftree node
  275. # @param conftree dict : A conftree node
  276. # @param name str
  277. # @return a named tuple with fieldnames corresponding to conftree keys
  278. def __tree2namedtuple(self, conftree, name):
  279. ResNamedTuple = namedtuple(name, conftree.keys())
  280. return ResNamedTuple(**conftree)
  281. class MetaSettingsRO(type):
  282. def __getattr__(self, name):
  283. return getattr(Settings.s, name)
  284. ## @brief A class that provide . notation read only access to configurations
  285. class SettingsRO(object, metaclass=MetaSettingsRO):
  286. pass