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

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