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