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