1 # -*- coding: utf-8 -*-
2 "pretty small and local configuration management tool"
3 # Copyright (C) 2012-2013 Team tiramisu (see AUTHORS for all contributors)
5 # This program is free software; you can redistribute it and/or modify
6 # it under the terms of the GNU General Public License as published by
7 # the Free Software Foundation; either version 2 of the License, or
8 # (at your option) any later version.
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU General Public License for more details.
15 # You should have received a copy of the GNU General Public License
16 # along with this program; if not, write to the Free Software
17 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
19 # The original `Config` design model is unproudly borrowed from
20 # the rough pypy's guys: http://codespeak.net/svn/pypy/dist/pypy/config/
21 # the whole pypy projet is under MIT licence
22 # ____________________________________________________________
24 from inspect import getmembers, ismethod
25 from tiramisu.error import (PropertiesOptionError, ConfigError, NotFoundError,
26 AmbigousOptionError, ConflictConfigError, NoMatchingOptionFound,
27 MandatoryError, MethodCallError, NoValueReturned)
28 from tiramisu.option import (OptionDescription, Option, SymLinkOption,
30 from tiramisu.setting import groups, owners, Setting
31 from tiramisu.value import Values
33 # ____________________________________________________________
35 "main configuration management entry"
36 _cfgimpl_toplevel = None
38 def __init__(self, descr, parent=None, context=None):
39 """ Configuration option management master class
41 :param descr: describes the configuration schema
42 :type descr: an instance of ``option.OptionDescription``
43 :param parent: is None if the ``Config`` is root parent Config otherwise
44 :type parent: ``Config``
45 :param context: the current root config
46 :type context: `Config`
48 # main option description
49 self._cfgimpl_descr = descr
50 # sub option descriptions
51 self._cfgimpl_subconfigs = {}
52 self._cfgimpl_parent = parent
54 self._cfgimpl_context = self
56 self._cfgimpl_context = context
58 self._cfgimpl_settings = Setting()
59 self._cfgimpl_values = Values(self._cfgimpl_context)
60 self._cfgimpl_all_paths = {}
63 raise ConfigError("cannot find a value for this config")
64 self._cfgimpl_settings = None
65 self._cfgimpl_values = None
66 self._cfgimpl_all_paths = None
67 "warnings are a great idea, let's make up a better use of it"
68 self._cfgimpl_warnings = []
69 self._cfgimpl_toplevel = self._cfgimpl_get_toplevel()
70 # some api members shall not be used as option's names !
71 methods = getmembers(self, ismethod)
72 self._cfgimpl_slots = [key for key, value in methods
73 if not key.startswith("_")]
76 self._cfgimpl_build_all_paths()
78 def _cfgimpl_build_all_paths(self):
79 if self._cfgimpl_all_paths == None:
80 raise ConfigError('cache paths must not be None')
81 self._cfgimpl_descr.build_cache(self._cfgimpl_all_paths)
83 def cfgimpl_get_settings(self):
84 return self._cfgimpl_context._cfgimpl_settings
86 def cfgimpl_set_settings(self, settings):
87 if not isinstance(settings, Setting):
88 raise ConfigError("setting not allowed")
89 self._cfgimpl_context._cfgimpl_settings = settings
91 def cfgimpl_get_description(self):
92 return self._cfgimpl_descr
94 def cfgimpl_get_value(self, path):
95 """same as multiple getattrs
97 :param path: list or tuple of path
99 example : ``path = (sub_conf, opt)`` makes a getattr of sub_conf, then
100 a getattr of opt, and then returns the opt's value.
102 :returns: subconf or option's value
104 subpaths = list(path)
105 subconf_or_opt = self
106 for subpath in subpaths:
107 subconf_or_opt = getattr(subconf_or_opt, subpath)
108 return subconf_or_opt
110 def _validate_duplicates(self, children):
111 """duplicates Option names in the schema
112 :type children: list of `Option` or `OptionDescription`
116 if dup._name not in duplicates:
117 duplicates.append(dup._name)
119 raise ConflictConfigError('duplicate option name: '
120 '{0}'.format(dup._name))
122 def _cfgimpl_build(self):
124 - builds the config object from the schema
125 - settles various default values for options
127 self._validate_duplicates(self._cfgimpl_descr._children)
128 if self._cfgimpl_descr.group_type == groups.master:
129 mastername = self._cfgimpl_descr._name
130 masteropt = getattr(self._cfgimpl_descr, mastername)
131 self._cfgimpl_context._cfgimpl_values.masters[masteropt] = []
133 for child in self._cfgimpl_descr._children:
134 if isinstance(child, OptionDescription):
135 self._validate_duplicates(child._children)
136 self._cfgimpl_subconfigs[child] = Config(child, parent=self,
137 context=self._cfgimpl_context)
139 if child._name in self._cfgimpl_slots:
140 raise NameError("invalid name for the option:"
141 " {0}".format(child._name))
143 if (self._cfgimpl_descr.group_type == groups.master and
145 self._cfgimpl_context._cfgimpl_values.slaves[child] = masteropt
146 self._cfgimpl_context._cfgimpl_values.masters[masteropt].append(child)
148 # ____________________________________________________________
150 def __setattr__(self, name, value):
151 "attribute notation mechanism for the setting of the value of an option"
152 if name.startswith('_cfgimpl_'):
153 self.__dict__[name] = value
156 homeconfig, name = self._cfgimpl_get_home_by_path(name)
157 return setattr(homeconfig, name, value)
158 if type(getattr(self._cfgimpl_descr, name)) != SymLinkOption:
159 self._validate(name, getattr(self._cfgimpl_descr, name))
160 if name in self._cfgimpl_slots:
161 raise NameError("invalid name for the option:"
163 self.setoption(name, value)
165 def _validate(self, name, opt_or_descr, permissive=False):
166 "validation for the setattr and the getattr"
167 apply_requires(opt_or_descr, self, permissive=permissive)
168 if not isinstance(opt_or_descr, Option) and \
169 not isinstance(opt_or_descr, OptionDescription):
170 raise TypeError('Unexpected object: {0}'.format(repr(opt_or_descr)))
171 properties = copy(opt_or_descr.properties)
172 for proper in copy(properties):
173 if not self._cfgimpl_context._cfgimpl_settings.has_property(proper):
174 properties.remove(proper)
176 for perm in self._cfgimpl_context._cfgimpl_settings.permissive:
177 if perm in properties:
178 properties.remove(perm)
180 raise PropertiesOptionError("trying to access"
181 " to an option named: {0} with properties"
182 " {1}".format(name, str(properties)),
185 def __getattr__(self, name):
186 return self._getattr(name)
188 # def fill_multi(self, opt, result, use_default_multi=False, default_multi=None):
189 # """fills a multi option with default and calculated values
191 # # FIXME C'EST ENCORE DU N'IMPORTE QUOI
192 # if not isinstance(result, list):
196 # return Multi(_result, self._cfgimpl_context, opt)
198 def _getattr(self, name, permissive=False):
200 attribute notation mechanism for accessing the value of an option
201 :param name: attribute name
202 :param permissive: permissive doesn't raise some property error
204 :return: option's value if name is an option name, OptionDescription
207 # attribute access by passing a path,
208 # for instance getattr(self, "creole.general.family.adresse_ip_eth0")
210 homeconfig, name = self._cfgimpl_get_home_by_path(name)
211 return homeconfig._getattr(name, permissive)
212 opt_or_descr = getattr(self._cfgimpl_descr, name)
214 if type(opt_or_descr) == SymLinkOption:
215 rootconfig = self._cfgimpl_get_toplevel()
216 return getattr(rootconfig, opt_or_descr.path)
218 self._validate(name, opt_or_descr, permissive)
219 if isinstance(opt_or_descr, OptionDescription):
220 if opt_or_descr not in self._cfgimpl_subconfigs:
221 raise AttributeError("%s with name %s object has no attribute %s" %
222 (self.__class__, opt_or_descr._name, name))
223 return self._cfgimpl_subconfigs[opt_or_descr]
225 if name.startswith('_cfgimpl_'):
226 # if it were in __dict__ it would have been found already
227 return self.__dict__[name]
228 return self._cfgimpl_context._cfgimpl_values[opt_or_descr]
230 def unwrap_from_name(self, name):
231 """convenience method to extract and Option() object from the Config()
232 **and it is slow**: it recursively searches into the namespaces
236 paths = self.getpaths(allpaths=True)
237 opts = dict([(path, self.unwrap_from_path(path)) for path in paths])
238 all_paths = [p.split(".") for p in self.getpaths()]
239 for pth in all_paths:
241 return opts[".".join(pth)]
242 raise NotFoundError("name: {0} not found".format(name))
244 def unwrap_from_path(self, path):
245 """convenience method to extract and Option() object from the Config()
246 and it is **fast**: finds the option directly in the appropriate
252 homeconfig, path = self._cfgimpl_get_home_by_path(path)
253 return getattr(homeconfig._cfgimpl_descr, path)
254 return getattr(self._cfgimpl_descr, path)
256 def setoption(self, name, value, who=None):
257 """effectively modifies the value of an Option()
258 (typically called by the __setattr__)
260 child = getattr(self._cfgimpl_descr, name)
261 child.setoption(self, value)
263 def set(self, **kwargs):
265 do what I mean"-interface to option setting. Searches all paths
266 starting from that config for matches of the optional arguments
267 and sets the found option if the match is not ambiguous.
269 :param kwargs: dict of name strings to values.
271 all_paths = [p.split(".") for p in self.getpaths(allpaths=True)]
272 for key, value in kwargs.iteritems():
273 key_p = key.split('.')
274 candidates = [p for p in all_paths if p[-len(key_p):] == key_p]
275 if len(candidates) == 1:
276 name = '.'.join(candidates[0])
277 homeconfig, name = self._cfgimpl_get_home_by_path(name)
279 getattr(homeconfig, name)
280 except MandatoryError:
283 raise e # HiddenOptionError or DisabledOptionError
284 homeconfig.setoption(name, value)
285 elif len(candidates) > 1:
286 raise AmbigousOptionError(
287 'more than one option that ends with %s' % (key, ))
289 raise NoMatchingOptionFound(
290 'there is no option that matches %s'
291 ' or the option is hidden or disabled' % (key, ))
295 same as a `find_first()` method in a config that has identical names:
296 it returns the first item of an option named `name`
298 much like the attribute access way, except that
299 the search for the option is performed recursively in the whole
301 **carefull**: very slow !
303 :returns: option value.
305 paths = self.getpaths(allpaths=True)
308 pathname = path.split('.')[-1]
311 value = getattr(self, path)
315 raise NotFoundError("option {0} not found in config".format(name))
317 def _cfgimpl_get_home_by_path(self, path):
318 """:returns: tuple (config, name)"""
319 path = path.split('.')
320 for step in path[:-1]:
321 self = getattr(self, step)
322 return self, path[-1]
324 def _cfgimpl_get_toplevel(self):
325 ":returns: root config"
326 while self._cfgimpl_parent is not None:
327 self = self._cfgimpl_parent
330 def _cfgimpl_get_path(self):
331 "the path in the attribute access meaning."
334 while obj._cfgimpl_parent is not None:
335 subpath.insert(0, obj._cfgimpl_descr._name)
336 obj = obj._cfgimpl_parent
337 return ".".join(subpath)
338 # ______________________________________________________________________
339 # def cfgimpl_previous_value(self, path):
340 # "stores the previous value"
341 # home, name = self._cfgimpl_get_home_by_path(path)
342 # # FIXME fucking name
343 # return home._cfgimpl_context._cfgimpl_values.previous_values[name]
345 # def get_previous_value(self, name):
346 # "for the time being, only the previous Option's value is accessible"
347 # return self._cfgimpl_context._cfgimpl_values.previous_values[name]
348 # ______________________________________________________________________
349 def add_warning(self, warning):
350 "Config implements its own warning pile. Could be useful"
351 self._cfgimpl_get_toplevel()._cfgimpl_warnings.append(warning)
353 def get_warnings(self):
354 "Config implements its own warning pile"
355 return self._cfgimpl_get_toplevel()._cfgimpl_warnings
357 # ____________________________________________________________
359 return self._cfgimpl_descr.getkey(self)
362 return hash(self.getkey())
364 def __eq__(self, other):
366 if not isinstance(other, Config):
368 return self.getkey() == other.getkey()
370 def __ne__(self, other):
372 return not self == other
374 # ______________________________________________________________________
376 """Pythonesque way of parsing group's ordered options.
377 iteration only on Options (not OptionDescriptions)"""
378 for child in self._cfgimpl_descr._children:
379 if not isinstance(child, OptionDescription):
381 yield child._name, getattr(self, child._name)
383 pass # option with properties
385 def iter_groups(self, group_type=None):
386 """iteration on groups objects only.
387 All groups are returned if `group_type` is `None`, otherwise the groups
388 can be filtered by categories (families, or whatever).
390 :param group_type: if defined, is an instance of `groups.GroupType`
391 or `groups.MasterGroupType` that lives in
395 if group_type is not None:
396 if not isinstance(group_type, groups.GroupType):
397 raise TypeError("Unknown group_type: {0}".format(group_type))
398 for child in self._cfgimpl_descr._children:
399 if isinstance(child, OptionDescription):
401 if group_type is not None:
402 if child.get_group_type() == group_type:
403 yield child._name, getattr(self, child._name)
405 yield child._name, getattr(self, child._name)
408 # ______________________________________________________________________
410 "Config's string representation"
412 for name, grp in self.iter_groups():
413 lines.append("[%s]" % name)
414 for name, value in self:
416 lines.append("%s = %s" % (name, value))
419 return '\n'.join(lines)
423 def getpaths(self, include_groups=False, allpaths=False, mandatory=False):
424 """returns a list of all paths in self, recursively, taking care of
425 the context of properties (hidden/disabled)
427 :param include_groups: if true, OptionDescription are included
428 :param allpaths: all the options (event the properties protected ones)
429 :param mandatory: includes the mandatory options
430 :returns: list of all paths
433 for path in self._cfgimpl_descr.getpaths(include_groups=include_groups):
438 value = getattr(self, path)
440 except MandatoryError:
443 except PropertiesOptionError:
449 def _find(self, bytype, byname, byvalue, byattrs, first):
451 convenience method for finding an option that lives only in the subtree
453 :param first: return only one option if True, a list otherwise
454 :return: find list or an exception if nothing has been found
456 def _filter_by_attrs():
459 for key, value in byattrs.items():
460 if not hasattr(option, key):
463 if getattr(option, key) != value:
468 def _filter_by_name():
471 pathname = path.split('.')[-1]
472 if pathname == byname:
476 def _filter_by_value():
480 value = getattr(self, path)
483 except: # a property restricts the access of the value
486 def _filter_by_type():
489 if isinstance(option, bytype):
494 paths = self.getpaths(allpaths=True)
497 option = self.unwrap_from_path(path)
498 except PropertiesOptionError, err:
500 if not _filter_by_name():
502 if not _filter_by_value():
504 if not _filter_by_type():
506 if not _filter_by_attrs():
511 find_results.append(option)
513 if find_results == []:
514 raise NotFoundError("no option found in config with these criteria")
518 def find(self, bytype=None, byname=None, byvalue=None, byattrs=None):
520 finds a list of options recursively in the config
522 :param bytype: Option class (BoolOption, StrOption, ...)
523 :param byname: filter by Option._name
524 :param byvalue: filter by the option's value
525 :param byattrs: dict of option attributes (default, callback...)
526 :returns: list of matching Option objects
528 return self._find(bytype, byname, byvalue, byattrs, first=False)
530 def find_first(self, bytype=None, byname=None, byvalue=None, byattrs=None):
532 finds an option recursively in the config
534 :param bytype: Option class (BoolOption, StrOption, ...)
535 :param byname: filter by Option._name
536 :param byvalue: filter by the option's value
537 :param byattrs: dict of option attributes (default, callback...)
538 :returns: list of matching Option objects
540 return self._find(bytype, byname, byvalue, byattrs, first=True)
543 def make_dict(config, flatten=False):
544 """export the whole config into a `dict`
545 :returns: dict of Option's name (or path) and values"""
546 paths = config.getpaths()
550 pathname = path.split('.')[-1]
554 value = getattr(config, path)
555 pathsvalues.append((pathname, value))
557 pass # this just a hidden or disabled option
558 options = dict(pathsvalues)
562 def mandatory_warnings(config):
563 """convenience function to trace Options that are mandatory and
564 where no value has been set
566 :returns: generator of mandatory Option's path
567 FIXME : CAREFULL : not multi-user
569 mandatory = config._cfgimpl_context._cfgimpl_settings.mandatory
570 config._cfgimpl_context._cfgimpl_settings.mandatory = True
571 for path in config._cfgimpl_descr.getpaths(include_groups=True):
573 value = config._getattr(path, permissive=True)
574 except MandatoryError:
576 except PropertiesOptionError:
578 config._cfgimpl_context._cfgimpl_settings.mandatory = mandatory