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

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