add owner
[tiramisu.git] / tiramisu / config.py
1 # -*- coding: utf-8 -*-
2 "pretty small and local configuration management tool"
3 # Copyright (C) 2012 Team tiramisu (see AUTHORS for all contributors)
4 #
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.
9 #
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.
14 #
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
18 #
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 # ____________________________________________________________
23 from copy import copy
24 from tiramisu.error import (PropertiesOptionError, ConfigError, NotFoundError,
25     AmbigousOptionError, ConflictConfigError, NoMatchingOptionFound,
26     MandatoryError, MethodCallError, NoValueReturned)
27 from tiramisu.option import (OptionDescription, Option, SymLinkOption,
28     Multi, apply_requires)
29 from tiramisu.setting import settings, groups, owners
30
31 # ____________________________________________________________
32 class Config(object):
33     "main configuration management entry"
34     _cfgimpl_toplevel = None
35
36     def __init__(self, descr, parent=None):
37         """ Configuration option management master class
38         :param descr: describes the configuration schema
39         :type descr: an instance of ``option.OptionDescription``
40         :param parent: is None if the ``Config`` is root parent Config otherwise
41         :type parent: ``Config``
42         """
43         self._cfgimpl_descr = descr
44         self._cfgimpl_value_owners = {}
45         self._cfgimpl_parent = parent
46         "`Config()` indeed is in charge of the `Option()`'s values"
47         self._cfgimpl_values = {}
48         self._cfgimpl_previous_values = {}
49         "warnings are a great idea, let's make up a better use of it"
50         self._cfgimpl_warnings = []
51         self._cfgimpl_toplevel = self._cfgimpl_get_toplevel()
52         '`freeze()` allows us to carry out this calculation again if necessary'
53         self._cfgimpl_build()
54
55     def _validate_duplicates(self, children):
56         """duplicates Option names in the schema
57         :type children: list of `Option` or `OptionDescription`
58         """
59         duplicates = []
60         for dup in children:
61             if dup._name not in duplicates:
62                 duplicates.append(dup._name)
63             else:
64                 raise ConflictConfigError('duplicate option name: '
65                     '{0}'.format(dup._name))
66
67     def _cfgimpl_build(self):
68         """
69         - builds the config object from the schema
70         - settles various default values for options
71         """
72         self._validate_duplicates(self._cfgimpl_descr._children)
73         for child in self._cfgimpl_descr._children:
74             if isinstance(child, Option):
75                 if child.is_multi():
76                     childdef = Multi(copy(child.getdefault()), config=self,
77                                      opt=child)
78                     self._cfgimpl_values[child._name] = childdef
79                     self._cfgimpl_previous_values[child._name] = list(childdef)
80                 else:
81                     childdef = child.getdefault()
82                     self._cfgimpl_values[child._name] = childdef
83                     self._cfgimpl_previous_values[child._name] = childdef
84                 child.setowner(self, owners.default)
85             elif isinstance(child, OptionDescription):
86                 self._validate_duplicates(child._children)
87                 self._cfgimpl_values[child._name] = Config(child, parent=self)
88 #        self.override(overrides)
89
90     def cfgimpl_update(self):
91         """dynamically adds `Option()` or `OptionDescription()`
92         """
93         # FIXME this is an update for new options in the schema only
94         # see the update_child() method of the descr object
95         for child in self._cfgimpl_descr._children:
96             if isinstance(child, Option):
97                 if child._name not in self._cfgimpl_values:
98                     if child.is_multi():
99                         self._cfgimpl_values[child._name] = Multi(
100                                 copy(child.getdefault()), config=self, opt=child)
101                     else:
102                         self._cfgimpl_values[child._name] = copy(child.getdefault())
103                     child.setowner(self, owners.default)
104             elif isinstance(child, OptionDescription):
105                 if child._name not in self._cfgimpl_values:
106                     self._cfgimpl_values[child._name] = Config(child, parent=self)
107
108     # ____________________________________________________________
109     # attribute methods
110     def __setattr__(self, name, value):
111         "attribute notation mechanism for the setting of the value of an option"
112         if name.startswith('_cfgimpl_'):
113             self.__dict__[name] = value
114             return
115         if '.' in name:
116             homeconfig, name = self._cfgimpl_get_home_by_path(name)
117             return setattr(homeconfig, name, value)
118         if type(getattr(self._cfgimpl_descr, name)) != SymLinkOption:
119             self._validate(name, getattr(self._cfgimpl_descr, name))
120         self.setoption(name, value, owners.user)
121
122     def _validate(self, name, opt_or_descr, permissive=False):
123         "validation for the setattr and the getattr"
124         apply_requires(opt_or_descr, self, permissive=permissive)
125         if not isinstance(opt_or_descr, Option) and \
126                 not isinstance(opt_or_descr, OptionDescription):
127             raise TypeError('Unexpected object: {0}'.format(repr(opt_or_descr)))
128         properties = copy(opt_or_descr.properties)
129         for proper in copy(properties):
130             if not settings.has_property(proper):
131                 properties.remove(proper)
132         if permissive:
133             for perm in settings.permissive:
134                 if perm in properties:
135                     properties.remove(perm)
136         if properties != []:
137             raise PropertiesOptionError("trying to access"
138                     " to an option named: {0} with properties"
139                     " {1}".format(name, str(properties)),
140                     properties)
141
142     def _is_empty(self, opt):
143         "convenience method to know if an option is empty"
144         if (not opt.is_multi() and self._cfgimpl_values[opt._name] == None) or \
145             (opt.is_multi() and (self._cfgimpl_values[opt._name] == [] or \
146                 None in self._cfgimpl_values[opt._name])):
147             return True
148         return False
149
150     def _test_mandatory(self, path, opt):
151         # mandatory options
152         mandatory = settings.mandatory
153         if opt.is_mandatory() and mandatory:
154             if self._is_empty(opt) and \
155                     opt.is_empty_by_default():
156                 raise MandatoryError("option: {0} is mandatory "
157                                       "and shall have a value".format(path))
158
159     def __getattr__(self, name):
160         return self._getattr(name)
161
162     def _get_master_len(self, slave_name):
163         try:
164             master_name = self._cfgimpl_descr.get_master_name()
165             if master_name == slave_name:
166                 return None
167             master_value = self._cfgimpl_values[master_name]
168             return len(master_value)
169         except TypeError, err:
170             # in this case we just don't care about the len
171             return None
172
173     def _valid_len(self, slave_name, slave_value):
174         master_len = self._get_master_len(slave_name)
175         if master_len == None:
176             return True
177         if master_len != len(slave_value):
178             master_name = self._cfgimpl_descr.get_master_name()
179             raise ValueError("invalid len for the group of"
180                     " the option {0}".format(slave_name))
181
182     def fill_multi(self, name, result, use_default_multi=False, default_multi=None):
183         """fills a multi option with default and calculated values
184         """
185         value = self._cfgimpl_values[name]
186         master_len = self._get_master_len(name)
187         if not isinstance(result, list):
188             if master_len is None:
189                 master_len = 1
190             # a list is built with the same len as the master
191             _result = []
192             for i in range(master_len):
193                 _result.append(result)
194         elif use_default_multi != False:
195             _result = result
196             if master_len != None:
197                 slave_len = len(result)
198                 if slave_len > master_len:
199                     raise ValueError("invalid value's len for"
200                             "the option: {1}".format(name))
201                 if slave_len != master_len:
202                     delta_len = master_len - len(result)
203                     for i in range(delta_len):
204                         _result.append(default_multi)
205         else:
206             _result = result
207         return Multi(_result, value.config, opt=value.opt)
208
209     def _getattr(self, name, permissive=False):
210         """
211         attribute notation mechanism for accessing the value of an option
212         :param name: attribute name
213         :param permissive: permissive doesn't raise some property error
214                           (see ``settings.permissive``)
215         :return: option's value if name is an option name, OptionDescription
216                  otherwise
217         """
218         # attribute access by passing a path,
219         # for instance getattr(self, "creole.general.family.adresse_ip_eth0")
220         if '.' in name:
221             homeconfig, name = self._cfgimpl_get_home_by_path(name)
222             return homeconfig._getattr(name, permissive)
223         opt_or_descr = getattr(self._cfgimpl_descr, name)
224         # symlink options
225         if type(opt_or_descr) == SymLinkOption:
226             rootconfig = self._cfgimpl_get_toplevel()
227             return getattr(rootconfig, opt_or_descr.path)
228         if name not in self._cfgimpl_values:
229             raise AttributeError("%s object has no attribute %s" %
230                                  (self.__class__, name))
231         self._validate(name, opt_or_descr, permissive)
232         # special attributes
233         if name.startswith('_cfgimpl_'):
234             # if it were in __dict__ it would have been found already
235             return self.__dict__[name]
236             raise AttributeError("%s object has no attribute %s" %
237                                  (self.__class__, name))
238         if not isinstance(opt_or_descr, OptionDescription):
239             # options with callbacks
240             if opt_or_descr.has_callback():
241                 value = self._cfgimpl_values[name]
242                 if (not opt_or_descr.is_frozen() or \
243                         not opt_or_descr.is_forced_on_freeze()) and \
244                         not opt_or_descr.is_default_owner(self):
245                     self._valid_len(name, value)
246                     return value
247                 try:
248                     result = opt_or_descr.getcallback_value(
249                             self._cfgimpl_get_toplevel())
250                 except NoValueReturned, err:
251                     pass
252                 else:
253                     if opt_or_descr.is_multi():
254                         _result = self.fill_multi(name, result)
255                     else:
256                         # this result **shall not** be a list
257                         if isinstance(result, list):
258                             raise ConfigError('invalid calculated value returned'
259                                 ' for option {0} : shall not be a list'.format(name))
260                         _result = result
261                     if _result != None and not opt_or_descr.validate(_result,
262                                 settings.validator):
263                         raise ConfigError('invalid calculated value returned'
264                             ' for option {0}'.format(name))
265                     self._cfgimpl_values[name] = _result
266                     opt_or_descr.setowner(self, owners.default)
267             # frozen and force default
268             if not opt_or_descr.has_callback() and opt_or_descr.is_forced_on_freeze():
269                 value = opt_or_descr.getdefault()
270                 if opt_or_descr.is_multi():
271                     value = self.fill_multi(name, value,
272                                 use_default_multi=True,
273                                 default_multi=opt_or_descr.getdefault_multi())
274                 self._cfgimpl_values[name] = value
275                 opt_or_descr.setowner(self, owners.default)
276             self._test_mandatory(name, opt_or_descr)
277         value = self._cfgimpl_values[name]
278         self._valid_len(name, value)
279         return value
280
281     def unwrap_from_name(self, name):
282         """convenience method to extract and Option() object from the Config()
283         **and it is slow**: it recursively searches into the namespaces
284
285         :returns: Option()
286         """
287         paths = self.getpaths(allpaths=True)
288         opts = dict([(path, self.unwrap_from_path(path)) for path in paths])
289         all_paths = [p.split(".") for p in self.getpaths()]
290         for pth in all_paths:
291             if name in pth:
292                 return opts[".".join(pth)]
293         raise NotFoundError("name: {0} not found".format(name))
294
295     def unwrap_from_path(self, path):
296         """convenience method to extract and Option() object from the Config()
297         and it is **fast**: finds the option directly in the appropriate
298         namespace
299
300         :returns: Option()
301         """
302         if '.' in path:
303             homeconfig, path = self._cfgimpl_get_home_by_path(path)
304             return getattr(homeconfig._cfgimpl_descr, path)
305         return getattr(self._cfgimpl_descr, path)
306
307     def setoption(self, name, value, who=None):
308         """effectively modifies the value of an Option()
309         (typically called by the __setattr__)
310         :param who : an object that lives in `setting.owners`
311         """
312         child = getattr(self._cfgimpl_descr, name)
313         if type(child) != SymLinkOption:
314             if who == None:
315                 who = settings.owner
316             if child.is_multi():
317                 if not isinstance(who, owners.DefaultOwner):
318                     if type(value) != Multi:
319                         if type(value) == list:
320                             value = Multi(value, self, child)
321                         else:
322                             raise ConfigError("invalid value for option:"
323                                        " {0} that is set to multi".format(name))
324                 else:
325                     value = self.fill_multi(name, child.getdefault(),
326                                         use_default_multi=True,
327                                         default_multi=child.getdefault_multi())
328                 self._valid_len(name, value)
329             if not isinstance(who, owners.Owner):
330                 raise TypeError("invalid owner [{0}] for option: {1}".format(
331                                 str(who), name))
332             child.setoption(self, value)
333             child.setowner(self, who)
334         else:
335             homeconfig = self._cfgimpl_get_toplevel()
336             child.setoption(homeconfig, value)
337
338     def set(self, **kwargs):
339         """
340         do what I mean"-interface to option setting. Searches all paths
341         starting from that config for matches of the optional arguments
342         and sets the found option if the match is not ambiguous.
343         :param kwargs: dict of name strings to values.
344         """
345         all_paths = [p.split(".") for p in self.getpaths(allpaths=True)]
346         for key, value in kwargs.iteritems():
347             key_p = key.split('.')
348             candidates = [p for p in all_paths if p[-len(key_p):] == key_p]
349             if len(candidates) == 1:
350                 name = '.'.join(candidates[0])
351                 homeconfig, name = self._cfgimpl_get_home_by_path(name)
352                 try:
353                     getattr(homeconfig, name)
354                 except MandatoryError:
355                     pass
356                 except Exception, e:
357                     raise e # HiddenOptionError or DisabledOptionError
358                 homeconfig.setoption(name, value, owners.user)
359             elif len(candidates) > 1:
360                 raise AmbigousOptionError(
361                     'more than one option that ends with %s' % (key, ))
362             else:
363                 raise NoMatchingOptionFound(
364                     'there is no option that matches %s'
365                     ' or the option is hidden or disabled'% (key, ))
366
367     def get(self, name):
368         """
369         same as a `find_first()` method in a config that has identical names:
370         it returns the first item of an option named `name`
371
372         much like the attribute access way, except that
373         the search for the option is performed recursively in the whole
374         configuration tree.
375         **carefull**: very slow !
376
377         :returns: option value.
378         """
379         paths = self.getpaths(allpaths=True)
380         pathsvalues = []
381         for path in paths:
382             pathname = path.split('.')[-1]
383             if pathname == name:
384                 try:
385                     value = getattr(self, path)
386                     return value
387                 except Exception, e:
388                     raise e
389         raise NotFoundError("option {0} not found in config".format(name))
390
391     def _cfgimpl_get_home_by_path(self, path):
392         """:returns: tuple (config, name)"""
393         path = path.split('.')
394         for step in path[:-1]:
395             self = getattr(self, step)
396         return self, path[-1]
397
398     def _cfgimpl_get_toplevel(self):
399         ":returns: root config"
400         while self._cfgimpl_parent is not None:
401             self = self._cfgimpl_parent
402         return self
403
404     def _cfgimpl_get_path(self):
405         "the path in the attribute access meaning."
406         subpath = []
407         obj = self
408         while obj._cfgimpl_parent is not None:
409             subpath.insert(0, obj._cfgimpl_descr._name)
410             obj = obj._cfgimpl_parent
411         return ".".join(subpath)
412     # ______________________________________________________________________
413     def cfgimpl_previous_value(self, path):
414         "stores the previous value"
415         home, name = self._cfgimpl_get_home_by_path(path)
416         return home._cfgimpl_previous_values[name]
417
418     def get_previous_value(self, name):
419         "for the time being, only the previous Option's value is accessible"
420         return self._cfgimpl_previous_values[name]
421     # ______________________________________________________________________
422     def add_warning(self, warning):
423         "Config implements its own warning pile. Could be useful"
424         self._cfgimpl_get_toplevel()._cfgimpl_warnings.append(warning)
425
426     def get_warnings(self):
427         "Config implements its own warning pile"
428         return self._cfgimpl_get_toplevel()._cfgimpl_warnings
429     # ____________________________________________________________
430     def getkey(self):
431         return self._cfgimpl_descr.getkey(self)
432
433     def __hash__(self):
434         return hash(self.getkey())
435
436     def __eq__(self, other):
437         "Config comparison"
438         if not isinstance(other, Config):
439             return False
440         return self.getkey() == other.getkey()
441
442     def __ne__(self, other):
443         "Config comparison"
444         return not self == other
445     # ______________________________________________________________________
446     def __iter__(self):
447         """Pythonesque way of parsing group's ordered options.
448         iteration only on Options (not OptionDescriptions)"""
449         for child in self._cfgimpl_descr._children:
450             if not isinstance(child, OptionDescription):
451                 try:
452                     yield child._name, getattr(self, child._name)
453                 except:
454                     pass # option with properties
455
456     def iter_groups(self, group_type=None):
457         """iteration on groups objects only.
458         All groups are returned if `group_type` is `None`, otherwise the groups
459         can be filtered by categories (families, or whatever).
460         :param group_type: if defined, is an instance of `groups.GroupType`
461                            or `groups.MasterGroupType` that lives in
462                            `settings.groups`
463
464         """
465         if group_type is not None:
466             if not isinstance(group_type, groups.GroupType):
467                 raise TypeError("Unknown group_type: {0}".format(group_type))
468         for child in self._cfgimpl_descr._children:
469             if isinstance(child, OptionDescription):
470                 try:
471                     if group_type is not None:
472                         if child.get_group_type() == group_type:
473                             yield child._name, getattr(self, child._name)
474                     else:
475                         yield child._name, getattr(self, child._name)
476                 except:
477                     pass
478     # ______________________________________________________________________
479     def __str__(self):
480         "Config's string representation"
481         lines = []
482         for name, grp in self.iter_groups():
483             lines.append("[%s]" % name)
484         for name, value in self:
485             try:
486                 lines.append("%s = %s" % (name, value))
487             except:
488                 pass
489         return '\n'.join(lines)
490
491     __repr__ = __str__
492
493
494     def getpaths(self, include_groups=False, allpaths=False, mandatory=False):
495         """returns a list of all paths in self, recursively, taking care of
496         the context of properties (hidden/disabled)
497
498         :param include_groups: if true, OptionDescription are included
499         :param allpaths: all the options (event the properties protected ones)
500         :param mandatory: includes the mandatory options
501         :returns: list of all paths
502         """
503         paths = []
504         for path in self._cfgimpl_descr.getpaths(include_groups=include_groups):
505             try:
506                 value = getattr(self, path)
507
508             except MandatoryError:
509                 if mandatory or allpaths:
510                     paths.append(path)
511             except PropertiesOptionError:
512                 if allpaths:
513                     paths.append(path) # option which have properties added
514             else:
515                  paths.append(path)
516         return paths
517
518     def _find(self, bytype, byname, byvalue, byattrs, first):
519         """
520             :param first: return only one option if True, a list otherwise
521         """
522         def _filter_by_attrs():
523             if byattrs is None:
524                 return True
525             for key, value in byattrs.items():
526                 if not hasattr(option, key):
527                     return False
528                 else:
529                     if getattr(option, key) != value:
530                         return False
531                     else:
532                         continue
533             return True
534         def _filter_by_name():
535             if byname is None:
536                 return True
537             pathname = path.split('.')[-1]
538             if pathname == byname:
539                 return True
540             else:
541                 return False
542         def _filter_by_value():
543             if byvalue is None:
544                 return True
545             try:
546                 value = getattr(self, path)
547                 if value == byvalue:
548                     return True
549             except Exception, e: # a property restricts the acces to value
550                 pass
551             return False
552         def _filter_by_type():
553             if bytype is None:
554                 return True
555             if isinstance(option, bytype):
556                 return True
557             return False
558
559         find_results = []
560         paths = self.getpaths(allpaths=True)
561         for path in paths:
562             option = self.unwrap_from_path(path)
563             if not _filter_by_name():
564                 continue
565             if not _filter_by_value():
566                 continue
567             if not _filter_by_type():
568                 continue
569             if not _filter_by_attrs():
570                 continue
571             if first:
572                 return option
573             else:
574                 find_results.append(option)
575         if first:
576             return None
577         else:
578             return find_results
579
580     def find(self, bytype=None, byname=None, byvalue=None, byattrs=None):
581         """
582             finds a list of options recursively in the config
583
584             :param bytype: Option class (BoolOption, StrOption, ...)
585             :param byname: filter by Option._name
586             :param byvalue: filter by the option's value
587             :param byattrs: dict of option attributes (default, callback...)
588             :returns: list of matching Option objects
589         """
590         return self._find(bytype, byname, byvalue, byattrs, first=False)
591
592     def find_first(self, bytype=None, byname=None, byvalue=None, byattrs=None):
593         """
594             finds an option recursively in the config
595
596             :param bytype: Option class (BoolOption, StrOption, ...)
597             :param byname: filter by Option._name
598             :param byvalue: filter by the option's value
599             :param byattrs: dict of option attributes (default, callback...)
600             :returns: list of matching Option objects
601         """
602         return self._find(bytype, byname, byvalue, byattrs, first=True)
603
604 def make_dict(config, flatten=False):
605     """export the whole config into a `dict`
606     :returns: dict of Option's name (or path) and values"""
607     paths = config.getpaths()
608     pathsvalues = []
609     for path in paths:
610         if flatten:
611             pathname = path.split('.')[-1]
612         else:
613             pathname = path
614         try:
615             value = getattr(config, path)
616             pathsvalues.append((pathname, value))
617         except:
618             pass # this just a hidden or disabled option
619     options = dict(pathsvalues)
620     return options
621
622 def mandatory_warnings(config):
623     """convenience function to trace Options that are mandatory and
624     where no value has been set
625
626     :returns: generator of mandatory Option's path
627     """
628     mandatory = settings.mandatory
629     settings.mandatory = True
630     for path in config._cfgimpl_descr.getpaths(include_groups=True):
631         try:
632             value = config._getattr(path, permissive=True)
633         except MandatoryError:
634             yield path
635         except PropertiesOptionError:
636             pass
637     settings.mandatory = mandatory