owners are *real* objects now
[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             print "ssdfsdfsdfsdfsdf", type(child)
333             child.setoption(self, value)
334             child.setowner(self, who)
335         else:
336             homeconfig = self._cfgimpl_get_toplevel()
337             child.setoption(homeconfig, value)
338
339     def set(self, **kwargs):
340         """
341         do what I mean"-interface to option setting. Searches all paths
342         starting from that config for matches of the optional arguments
343         and sets the found option if the match is not ambiguous.
344         :param kwargs: dict of name strings to values.
345         """
346         all_paths = [p.split(".") for p in self.getpaths(allpaths=True)]
347         for key, value in kwargs.iteritems():
348             key_p = key.split('.')
349             candidates = [p for p in all_paths if p[-len(key_p):] == key_p]
350             if len(candidates) == 1:
351                 name = '.'.join(candidates[0])
352                 homeconfig, name = self._cfgimpl_get_home_by_path(name)
353                 try:
354                     getattr(homeconfig, name)
355                 except MandatoryError:
356                     pass
357                 except Exception, e:
358                     raise e # HiddenOptionError or DisabledOptionError
359                 homeconfig.setoption(name, value, owners.user)
360             elif len(candidates) > 1:
361                 raise AmbigousOptionError(
362                     'more than one option that ends with %s' % (key, ))
363             else:
364                 raise NoMatchingOptionFound(
365                     'there is no option that matches %s'
366                     ' or the option is hidden or disabled'% (key, ))
367
368     def get(self, name):
369         """
370         same as a `find_first()` method in a config that has identical names:
371         it returns the first item of an option named `name`
372
373         much like the attribute access way, except that
374         the search for the option is performed recursively in the whole
375         configuration tree.
376         **carefull**: very slow !
377
378         :returns: option value.
379         """
380         paths = self.getpaths(allpaths=True)
381         pathsvalues = []
382         for path in paths:
383             pathname = path.split('.')[-1]
384             if pathname == name:
385                 try:
386                     value = getattr(self, path)
387                     return value
388                 except Exception, e:
389                     raise e
390         raise NotFoundError("option {0} not found in config".format(name))
391
392     def _cfgimpl_get_home_by_path(self, path):
393         """:returns: tuple (config, name)"""
394         path = path.split('.')
395         for step in path[:-1]:
396             self = getattr(self, step)
397         return self, path[-1]
398
399     def _cfgimpl_get_toplevel(self):
400         ":returns: root config"
401         while self._cfgimpl_parent is not None:
402             self = self._cfgimpl_parent
403         return self
404
405     def _cfgimpl_get_path(self):
406         "the path in the attribute access meaning."
407         subpath = []
408         obj = self
409         while obj._cfgimpl_parent is not None:
410             subpath.insert(0, obj._cfgimpl_descr._name)
411             obj = obj._cfgimpl_parent
412         return ".".join(subpath)
413     # ______________________________________________________________________
414     def cfgimpl_previous_value(self, path):
415         "stores the previous value"
416         home, name = self._cfgimpl_get_home_by_path(path)
417         return home._cfgimpl_previous_values[name]
418
419     def get_previous_value(self, name):
420         "for the time being, only the previous Option's value is accessible"
421         return self._cfgimpl_previous_values[name]
422     # ______________________________________________________________________
423     def add_warning(self, warning):
424         "Config implements its own warning pile. Could be useful"
425         self._cfgimpl_get_toplevel()._cfgimpl_warnings.append(warning)
426
427     def get_warnings(self):
428         "Config implements its own warning pile"
429         return self._cfgimpl_get_toplevel()._cfgimpl_warnings
430     # ____________________________________________________________
431     def getkey(self):
432         return self._cfgimpl_descr.getkey(self)
433
434     def __hash__(self):
435         return hash(self.getkey())
436
437     def __eq__(self, other):
438         "Config comparison"
439         if not isinstance(other, Config):
440             return False
441         return self.getkey() == other.getkey()
442
443     def __ne__(self, other):
444         "Config comparison"
445         return not self == other
446     # ______________________________________________________________________
447     def __iter__(self):
448         """Pythonesque way of parsing group's ordered options.
449         iteration only on Options (not OptionDescriptions)"""
450         for child in self._cfgimpl_descr._children:
451             if not isinstance(child, OptionDescription):
452                 try:
453                     yield child._name, getattr(self, child._name)
454                 except:
455                     pass # option with properties
456
457     def iter_groups(self, group_type=None):
458         """iteration on groups objects only.
459         All groups are returned if `group_type` is `None`, otherwise the groups
460         can be filtered by categories (families, or whatever).
461         :param group_type: if defined, is an instance of `groups.GroupName`
462                            or `groups.MasterGroupName` that lives in
463                            `settings.groups`
464
465         """
466         if group_type is not None:
467             if not isinstance(group_type, groups.GroupName):
468                 raise TypeError("Unknown group_type: {0}".format(group_type))
469         for child in self._cfgimpl_descr._children:
470             if isinstance(child, OptionDescription):
471                 try:
472                     if group_type is not None:
473                         if child.get_group_type() == group_type:
474                             yield child._name, getattr(self, child._name)
475                     else:
476                         yield child._name, getattr(self, child._name)
477                 except:
478                     pass
479     # ______________________________________________________________________
480     def __str__(self):
481         "Config's string representation"
482         lines = []
483         for name, grp in self.iter_groups():
484             lines.append("[%s]" % name)
485         for name, value in self:
486             try:
487                 lines.append("%s = %s" % (name, value))
488             except:
489                 pass
490         return '\n'.join(lines)
491
492     __repr__ = __str__
493
494
495     def getpaths(self, include_groups=False, allpaths=False, mandatory=False):
496         """returns a list of all paths in self, recursively, taking care of
497         the context of properties (hidden/disabled)
498
499         :param include_groups: if true, OptionDescription are included
500         :param allpaths: all the options (event the properties protected ones)
501         :param mandatory: includes the mandatory options
502         :returns: list of all paths
503         """
504         paths = []
505         for path in self._cfgimpl_descr.getpaths(include_groups=include_groups):
506             try:
507                 value = getattr(self, path)
508
509             except MandatoryError:
510                 if mandatory or allpaths:
511                     paths.append(path)
512             except PropertiesOptionError:
513                 if allpaths:
514                     paths.append(path) # option which have properties added
515             else:
516                  paths.append(path)
517         return paths
518
519     def _find(self, bytype, byname, byvalue, byattrs, first):
520         """
521             :param first: return only one option if True, a list otherwise
522         """
523         def _filter_by_attrs():
524             if byattrs is None:
525                 return True
526             for key, value in byattrs.items():
527                 if not hasattr(option, key):
528                     return False
529                 else:
530                     if getattr(option, key) != value:
531                         return False
532                     else:
533                         continue
534             return True
535         def _filter_by_name():
536             if byname is None:
537                 return True
538             pathname = path.split('.')[-1]
539             if pathname == byname:
540                 return True
541             else:
542                 return False
543         def _filter_by_value():
544             if byvalue is None:
545                 return True
546             try:
547                 value = getattr(self, path)
548                 if value == byvalue:
549                     return True
550             except Exception, e: # a property restricts the acces to value
551                 pass
552             return False
553         def _filter_by_type():
554             if bytype is None:
555                 return True
556             if isinstance(option, bytype):
557                 return True
558             return False
559
560         find_results = []
561         paths = self.getpaths(allpaths=True)
562         for path in paths:
563             option = self.unwrap_from_path(path)
564             if not _filter_by_name():
565                 continue
566             if not _filter_by_value():
567                 continue
568             if not _filter_by_type():
569                 continue
570             if not _filter_by_attrs():
571                 continue
572             if first:
573                 return option
574             else:
575                 find_results.append(option)
576         if first:
577             return None
578         else:
579             return find_results
580
581     def find(self, bytype=None, byname=None, byvalue=None, byattrs=None):
582         """
583             finds a list of options recursively in the config
584
585             :param bytype: Option class (BoolOption, StrOption, ...)
586             :param byname: filter by Option._name
587             :param byvalue: filter by the option's value
588             :param byattrs: dict of option attributes (default, callback...)
589             :returns: list of matching Option objects
590         """
591         return self._find(bytype, byname, byvalue, byattrs, first=False)
592
593     def find_first(self, bytype=None, byname=None, byvalue=None, byattrs=None):
594         """
595             finds an option recursively in the config
596
597             :param bytype: Option class (BoolOption, StrOption, ...)
598             :param byname: filter by Option._name
599             :param byvalue: filter by the option's value
600             :param byattrs: dict of option attributes (default, callback...)
601             :returns: list of matching Option objects
602         """
603         return self._find(bytype, byname, byvalue, byattrs, first=True)
604
605 def make_dict(config, flatten=False):
606     """export the whole config into a `dict`
607     :returns: dict of Option's name (or path) and values"""
608     paths = config.getpaths()
609     pathsvalues = []
610     for path in paths:
611         if flatten:
612             pathname = path.split('.')[-1]
613         else:
614             pathname = path
615         try:
616             value = getattr(config, path)
617             pathsvalues.append((pathname, value))
618         except:
619             pass # this just a hidden or disabled option
620     options = dict(pathsvalues)
621     return options
622
623 def mandatory_warnings(config):
624     """convenience function to trace Options that are mandatory and
625     where no value has been set
626
627     :returns: generator of mandatory Option's path
628     """
629     mandatory = settings.mandatory
630     settings.mandatory = True
631     for path in config._cfgimpl_descr.getpaths(include_groups=True):
632         try:
633             value = config._getattr(path, permissive=True)
634         except MandatoryError:
635             yield path
636         except PropertiesOptionError:
637             pass
638     settings.mandatory = mandatory