getowner need now an option
[tiramisu.git] / tiramisu / config.py
1 # -*- coding: utf-8 -*-
2 "options handler global entry point"
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 time import time
24 from tiramisu.error import PropertiesOptionError, ConfigError
25 from tiramisu.option import OptionDescription, Option, SymLinkOption, \
26     BaseInformation
27 from tiramisu.setting import groups, Settings, default_encoding, storage_type
28 from tiramisu.value import Values
29 from tiramisu.i18n import _
30
31
32 def gen_id(config):
33     return str(id(config)) + str(time())
34
35
36 class SubConfig(BaseInformation):
37     "sub configuration management entry"
38     __slots__ = ('_impl_context', '_impl_descr', '_impl_path')
39
40     def __init__(self, descr, context, subpath=None):
41         """ Configuration option management master class
42
43         :param descr: describes the configuration schema
44         :type descr: an instance of ``option.OptionDescription``
45         :param context: the current root config
46         :type context: `Config`
47         :type subpath: `str` with the path name
48         """
49         # main option description
50         if not isinstance(descr, OptionDescription):
51             raise TypeError(_('descr must be an optiondescription, not {0}'
52                               ).format(type(descr)))
53         self._impl_descr = descr
54         # sub option descriptions
55         if not isinstance(context, SubConfig):
56             raise ValueError('context must be a SubConfig')
57         self._impl_context = context
58         self._impl_path = subpath
59
60     def cfgimpl_reset_cache(self, only_expired=False, only=('values',
61                                                             'settings')):
62         self.cfgimpl_get_context().cfgimpl_reset_cache(only_expired, only)
63
64     def cfgimpl_get_home_by_path(self, path, force_permissive=False,
65                                  force_properties=None):
66         """:returns: tuple (config, name)"""
67         path = path.split('.')
68         for step in path[:-1]:
69             self = self._getattr(step,
70                                  force_permissive=force_permissive,
71                                  force_properties=force_properties)
72         return self, path[-1]
73
74     def __hash__(self):
75         return hash(self.cfgimpl_get_description().impl_getkey(self))
76
77     def __eq__(self, other):
78         "Config's comparison"
79         if not isinstance(other, Config):
80             return False
81         return self.cfgimpl_get_description().impl_getkey(self) == \
82             other.cfgimpl_get_description().impl_getkey(other)
83
84     def __ne__(self, other):
85         "Config's comparison"
86         if not isinstance(other, Config):
87             return True
88         return not self == other
89
90     # ______________________________________________________________________
91     def __iter__(self):
92         """Pythonesque way of parsing group's ordered options.
93         iteration only on Options (not OptionDescriptions)"""
94         for child in self.cfgimpl_get_description().impl_getchildren():
95             if not isinstance(child, OptionDescription):
96                 try:
97                     yield child._name, getattr(self, child._name)
98                 except GeneratorExit:
99                     raise StopIteration
100                 except PropertiesOptionError:
101                     pass  # option with properties
102
103     def iter_all(self):
104         """A way of parsing options **and** groups.
105         iteration on Options and OptionDescriptions."""
106         for child in self.cfgimpl_get_description().impl_getchildren():
107             try:
108                 yield child._name, getattr(self, child._name)
109             except GeneratorExit:
110                 raise StopIteration
111             except PropertiesOptionError:
112                 pass  # option with properties
113
114     def iter_groups(self, group_type=None):
115         """iteration on groups objects only.
116         All groups are returned if `group_type` is `None`, otherwise the groups
117         can be filtered by categories (families, or whatever).
118
119         :param group_type: if defined, is an instance of `groups.GroupType`
120                            or `groups.MasterGroupType` that lives in
121                            `setting.groups`
122         """
123         if group_type is not None and not isinstance(group_type,
124                                                      groups.GroupType):
125             raise TypeError(_("unknown group_type: {0}").format(group_type))
126         for child in self.cfgimpl_get_description().impl_getchildren():
127             if isinstance(child, OptionDescription):
128                 try:
129                     if group_type is None or (group_type is not None and
130                                               child.impl_get_group_type()
131                                               == group_type):
132                         yield child._name, getattr(self, child._name)
133                 except GeneratorExit:
134                     raise StopIteration
135                 except PropertiesOptionError:
136                     pass
137     # ______________________________________________________________________
138
139     def __str__(self):
140         "Config's string representation"
141         lines = []
142         for name, grp in self.iter_groups():
143             lines.append("[{0}]".format(name))
144         for name, value in self:
145             try:
146                 lines.append("{0} = {1}".format(name, value))
147             except UnicodeEncodeError:
148                 lines.append("{0} = {1}".format(name,
149                                                 value.encode(default_encoding)))
150             except PropertiesOptionError:
151                 pass
152         return '\n'.join(lines)
153
154     __repr__ = __str__
155
156     def cfgimpl_get_context(self):
157         return self._impl_context
158
159     def cfgimpl_get_description(self):
160         if self._impl_descr is None:
161             raise ConfigError(_('no option description found for this config'
162                                 ' (may be metaconfig without meta)'))
163         else:
164             return self._impl_descr
165
166     def cfgimpl_get_settings(self):
167         return self.cfgimpl_get_context()._impl_settings
168
169     def cfgimpl_get_values(self):
170         return self.cfgimpl_get_context()._impl_values
171
172     # ____________________________________________________________
173     # attribute methods
174     def __setattr__(self, name, value):
175         "attribute notation mechanism for the setting of the value of an option"
176         if name.startswith('_impl_'):
177             object.__setattr__(self, name, value)
178             return
179         self._setattr(name, value)
180
181     def _setattr(self, name, value, force_permissive=False):
182         if '.' in name:
183             homeconfig, name = self.cfgimpl_get_home_by_path(name)
184             return homeconfig.__setattr__(name, value)
185         child = getattr(self.cfgimpl_get_description(), name)
186         if not isinstance(child, SymLinkOption):
187             if self._impl_path is None:
188                 path = name
189             else:
190                 path = self._impl_path + '.' + name
191             self.cfgimpl_get_values().setitem(child, value, path,
192                                               force_permissive=force_permissive)
193         else:
194             context = self.cfgimpl_get_context()
195             path = context.cfgimpl_get_description().impl_get_path_by_opt(
196                 child._opt)
197             context._setattr(path, value, force_permissive=force_permissive)
198
199     def __delattr__(self, name):
200         child = getattr(self.cfgimpl_get_description(), name)
201         self.cfgimpl_get_values().__delitem__(child)
202
203     def __getattr__(self, name):
204         return self._getattr(name)
205
206     def _getattr(self, name, force_permissive=False, force_properties=None,
207                  validate=True):
208         """
209         attribute notation mechanism for accessing the value of an option
210         :param name: attribute name
211         :return: option's value if name is an option name, OptionDescription
212                  otherwise
213         """
214         # attribute access by passing a path,
215         # for instance getattr(self, "creole.general.family.adresse_ip_eth0")
216         if '.' in name:
217             homeconfig, name = self.cfgimpl_get_home_by_path(
218                 name, force_permissive=force_permissive,
219                 force_properties=force_properties)
220             return homeconfig._getattr(name, force_permissive=force_permissive,
221                                        force_properties=force_properties,
222                                        validate=validate)
223         opt_or_descr = getattr(self.cfgimpl_get_description(), name)
224         if self._impl_path is None:
225             subpath = name
226         else:
227             subpath = self._impl_path + '.' + name
228         # symlink options
229         if isinstance(opt_or_descr, SymLinkOption):
230             context = self.cfgimpl_get_context()
231             path = context.cfgimpl_get_description().impl_get_path_by_opt(
232                 opt_or_descr._opt)
233             return context._getattr(path, validate=validate,
234                                     force_properties=force_properties,
235                                     force_permissive=force_permissive)
236         elif isinstance(opt_or_descr, OptionDescription):
237             self.cfgimpl_get_settings().validate_properties(
238                 opt_or_descr, True, False, path=subpath,
239                 force_permissive=force_permissive,
240                 force_properties=force_properties)
241             return SubConfig(opt_or_descr, self.cfgimpl_get_context(), subpath)
242         else:
243             return self.cfgimpl_get_values().getitem(
244                 opt_or_descr, path=subpath,
245                 validate=validate,
246                 force_properties=force_properties,
247                 force_permissive=force_permissive)
248
249     def find(self, bytype=None, byname=None, byvalue=None, type_='option'):
250         """
251             finds a list of options recursively in the config
252
253             :param bytype: Option class (BoolOption, StrOption, ...)
254             :param byname: filter by Option._name
255             :param byvalue: filter by the option's value
256             :returns: list of matching Option objects
257         """
258         return self.cfgimpl_get_context()._find(bytype, byname, byvalue,
259                                                 first=False,
260                                                 type_=type_,
261                                                 _subpath=self.cfgimpl_get_path()
262                                                 )
263
264     def find_first(self, bytype=None, byname=None, byvalue=None,
265                    type_='option', display_error=True):
266         """
267             finds an option recursively in the config
268
269             :param bytype: Option class (BoolOption, StrOption, ...)
270             :param byname: filter by Option._name
271             :param byvalue: filter by the option's value
272             :returns: list of matching Option objects
273         """
274         return self.cfgimpl_get_context()._find(
275             bytype, byname, byvalue, first=True, type_=type_,
276             _subpath=self.cfgimpl_get_path(), display_error=display_error)
277
278     def _find(self, bytype, byname, byvalue, first, type_='option',
279               _subpath=None, check_properties=True, display_error=True):
280         """
281         convenience method for finding an option that lives only in the subtree
282
283         :param first: return only one option if True, a list otherwise
284         :return: find list or an exception if nothing has been found
285         """
286         def _filter_by_name():
287             try:
288                 if byname is None or path == byname or \
289                         path.endswith('.' + byname):
290                     return True
291             except IndexError:
292                 pass
293             return False
294
295         def _filter_by_value():
296             if byvalue is None:
297                 return True
298             try:
299                 value = getattr(self, path)
300                 if value == byvalue:
301                     return True
302             except PropertiesOptionError:  # a property is a restriction
303                                            # upon the access of the value
304                 pass
305             return False
306
307         def _filter_by_type():
308             if bytype is None:
309                 return True
310             if isinstance(option, bytype):
311                 return True
312             return False
313
314         if type_ not in ('option', 'path', 'value'):
315             raise ValueError(_('unknown type_ type {0}'
316                                'for _find').format(type_))
317         find_results = []
318         opts, paths = self.cfgimpl_get_description()._cache_paths
319         for index in range(0, len(paths)):
320             option = opts[index]
321             if isinstance(option, OptionDescription):
322                 continue
323             path = paths[index]
324             if _subpath is not None and not path.startswith(_subpath + '.'):
325                 continue
326             if not _filter_by_name():
327                 continue
328             if not _filter_by_value():
329                 continue
330             #remove option with propertyerror, ...
331             if check_properties:
332                 try:
333                     value = getattr(self, path)
334                 except PropertiesOptionError:
335                     # a property restricts the access of the value
336                     continue
337             if not _filter_by_type():
338                 continue
339             if type_ == 'value':
340                 retval = value
341             elif type_ == 'path':
342                 retval = path
343             elif type_ == 'option':
344                 retval = option
345             if first:
346                 return retval
347             else:
348                 find_results.append(retval)
349         return self._find_return_results(find_results, display_error)
350
351     def _find_return_results(self, find_results, display_error):
352         if find_results == []:
353             if display_error:
354                 raise AttributeError(_("no option found in config"
355                                        " with these criteria"))
356             else:
357                 # translation is slow
358                 raise AttributeError()
359         else:
360             return find_results
361
362     def make_dict(self, flatten=False, _currpath=None, withoption=None,
363                   withvalue=None):
364         """exports the whole config into a `dict`, for example:
365
366         >>> print cfg.make_dict()
367         {'od2.var4': None, 'od2.var5': None, 'od2.var6': None}
368
369
370
371         :param flatten: returns a dict(name=value) instead of a dict(path=value)
372                         ::
373
374                             >>> print cfg.make_dict(flatten=True)
375                             {'var5': None, 'var4': None, 'var6': None}
376
377         :param withoption: returns the options that are present in the very same
378                            `OptionDescription` than the `withoption` itself::
379
380                                 >>> print cfg.make_dict(withoption='var1')
381                                 {'od2.var4': None, 'od2.var5': None,
382                                 'od2.var6': None,
383                                 'od2.var1': u'value',
384                                 'od1.var1': None,
385                                 'od1.var3': None,
386                                 'od1.var2': None}
387
388         :param withvalue: returns the options that have the value `withvalue`
389                           ::
390
391                             >>> print c.make_dict(withoption='var1',
392                                                   withvalue=u'value')
393                             {'od2.var4': None,
394                             'od2.var5': None,
395                             'od2.var6': None,
396                             'od2.var1': u'value'}
397
398         :returns: dict of Option's name (or path) and values
399         """
400         pathsvalues = []
401         if _currpath is None:
402             _currpath = []
403         if withoption is None and withvalue is not None:
404             raise ValueError(_("make_dict can't filtering with value without "
405                                "option"))
406         if withoption is not None:
407             mypath = self.cfgimpl_get_path()
408             for path in self.cfgimpl_get_context()._find(bytype=Option,
409                                                          byname=withoption,
410                                                          byvalue=withvalue,
411                                                          first=False,
412                                                          type_='path',
413                                                          _subpath=mypath):
414                 path = '.'.join(path.split('.')[:-1])
415                 opt = self.cfgimpl_get_context().cfgimpl_get_description(
416                 ).impl_get_opt_by_path(path)
417                 if mypath is not None:
418                     if mypath == path:
419                         withoption = None
420                         withvalue = None
421                         break
422                     else:
423                         tmypath = mypath + '.'
424                         if not path.startswith(tmypath):
425                             raise AttributeError(_('unexpected path {0}, '
426                                                    'should start with {1}'
427                                                    '').format(path, mypath))
428                         path = path[len(tmypath):]
429                 self._make_sub_dict(opt, path, pathsvalues, _currpath, flatten)
430         #withoption can be set to None below !
431         if withoption is None:
432             for opt in self.cfgimpl_get_description().impl_getchildren():
433                 path = opt._name
434                 self._make_sub_dict(opt, path, pathsvalues, _currpath, flatten)
435         if _currpath == []:
436             options = dict(pathsvalues)
437             return options
438         return pathsvalues
439
440     def _make_sub_dict(self, opt, path, pathsvalues, _currpath, flatten):
441         if isinstance(opt, OptionDescription):
442             try:
443                 pathsvalues += getattr(self, path).make_dict(flatten,
444                                                              _currpath +
445                                                              path.split('.'))
446             except PropertiesOptionError:
447                 pass  # this just a hidden or disabled option
448         else:
449             try:
450                 value = self._getattr(opt._name)
451                 if flatten:
452                     name = opt._name
453                 else:
454                     name = '.'.join(_currpath + [opt._name])
455                 pathsvalues.append((name, value))
456             except PropertiesOptionError:
457                 pass  # this just a hidden or disabled option
458
459     def cfgimpl_get_path(self):
460         descr = self.cfgimpl_get_description()
461         context_descr = self.cfgimpl_get_context().cfgimpl_get_description()
462         return context_descr.impl_get_path_by_opt(descr)
463
464
465 class CommonConfig(SubConfig):
466     "abstract base class for the Config and the MetaConfig"
467     __slots__ = ('_impl_values', '_impl_settings', '_impl_meta')
468
469     def _init_storage(self, config_id, is_persistent):
470         if config_id is None:
471             config_id = gen_id(self)
472         import_lib = 'tiramisu.storage.{0}.storage'.format(storage_type)
473         return __import__(import_lib, globals(), locals(), ['Storage'],
474                           -1).Storage(config_id, is_persistent)
475
476     def _impl_build_all_paths(self):
477         self.cfgimpl_get_description().impl_build_cache()
478
479     def read_only(self):
480         "read only is a global config's setting, see `settings.py`"
481         self.cfgimpl_get_settings().read_only()
482
483     def read_write(self):
484         "read write is a global config's setting, see `settings.py`"
485         self.cfgimpl_get_settings().read_write()
486
487     def getowner(self, opt):
488         """convenience method to retrieve an option's owner
489         from the config itself
490         """
491         if not isinstance(opt, Option) and not isinstance(opt, SymLinkOption):
492             raise TypeError(_('opt in getowner must be an option not {0}'
493                               '').format(type(opt)))
494         return self.cfgimpl_get_values().getowner(opt)
495
496     def unwrap_from_path(self, path, force_permissive=False):
497         """convenience method to extract and Option() object from the Config()
498         and it is **fast**: finds the option directly in the appropriate
499         namespace
500
501         :returns: Option()
502         """
503         if '.' in path:
504             homeconfig, path = self.cfgimpl_get_home_by_path(
505                 path, force_permissive=force_permissive)
506             return getattr(homeconfig.cfgimpl_get_description(), path)
507         return getattr(self.cfgimpl_get_description(), path)
508
509     def cfgimpl_get_path(self):
510         return None
511
512     def cfgimpl_get_meta(self):
513         return self._impl_meta
514
515
516 # ____________________________________________________________
517 class Config(CommonConfig):
518     "main configuration management entry"
519     __slots__ = tuple()
520
521     def __init__(self, descr, config_id=None, is_persistent=False):
522         """ Configuration option management master class
523
524         :param descr: describes the configuration schema
525         :type descr: an instance of ``option.OptionDescription``
526         :param context: the current root config
527         :type context: `Config`
528         """
529         storage = self._init_storage(config_id, is_persistent)
530         self._impl_settings = Settings(self, storage)
531         self._impl_values = Values(self, storage)
532         super(Config, self).__init__(descr, self)
533         self._impl_build_all_paths()
534         self._impl_meta = None
535         self._impl_informations = {}
536
537     def cfgimpl_reset_cache(self,
538                             only_expired=False,
539                             only=('values', 'settings')):
540         if 'values' in only:
541             self.cfgimpl_get_values().reset_cache(only_expired=only_expired)
542         if 'settings' in only:
543             self.cfgimpl_get_settings().reset_cache(only_expired=only_expired)
544
545
546 class MetaConfig(CommonConfig):
547     __slots__ = ('_impl_children',)
548
549     def __init__(self, children, meta=True, config_id=None, is_persistent=False):
550         if not isinstance(children, list):
551             raise ValueError(_("metaconfig's children must be a list"))
552         self._impl_descr = None
553         self._impl_path = None
554         if meta:
555             for child in children:
556                 if not isinstance(child, CommonConfig):
557                     raise TypeError(_("metaconfig's children "
558                                       "must be config, not {0}"
559                                       ).format(type(child)))
560                 if self._impl_descr is None:
561                     self._impl_descr = child.cfgimpl_get_description()
562                 elif not self._impl_descr is child.cfgimpl_get_description():
563                     raise ValueError(_('all config in metaconfig must '
564                                        'have the same optiondescription'))
565                 if child.cfgimpl_get_meta() is not None:
566                     raise ValueError(_("child has already a metaconfig's"))
567                 child._impl_meta = self
568
569         if config_id is None:
570             config_id = gen_id(self)
571         self._impl_children = children
572         storage = self._init_storage(config_id, is_persistent)
573         self._impl_settings = Settings(self, storage)
574         self._impl_values = Values(self, storage)
575         self._impl_meta = None
576         self._impl_informations = {}
577
578     def cfgimpl_get_children(self):
579         return self._impl_children
580
581     def cfgimpl_get_context(self):
582         "a meta config is a config wich has a setting, that is itself"
583         return self
584
585     def cfgimpl_reset_cache(self,
586                             only_expired=False,
587                             only=('values', 'settings')):
588         if 'values' in only:
589             self.cfgimpl_get_values().reset_cache(only_expired=only_expired)
590         if 'settings' in only:
591             self.cfgimpl_get_settings().reset_cache(only_expired=only_expired)
592         for child in self._impl_children:
593             child.cfgimpl_reset_cache(only_expired=only_expired, only=only)
594
595     def set_contexts(self, path, value):
596         for child in self._impl_children:
597             try:
598                 if not isinstance(child, MetaConfig):
599                     setattr(child, path, value)
600                 else:
601                     child.set_contexts(path, value)
602             except PropertiesOptionError:
603                 pass
604
605     def find_first_contexts(self, byname=None, bypath=None, byvalue=None,
606                             type_='path', display_error=True):
607         ret = []
608         try:
609             if bypath is None and byname is not None and \
610                     self.cfgimpl_get_description() is not None:
611                 bypath = self._find(bytype=None, byvalue=None, byname=byname,
612                                     first=True, type_='path',
613                                     check_properties=False,
614                                     display_error=display_error)
615         except ConfigError:
616             pass
617         for child in self._impl_children:
618             try:
619                 if not isinstance(child, MetaConfig):
620                     if bypath is not None:
621                         if byvalue is not None:
622                             if getattr(child, bypath) == byvalue:
623                                 ret.append(child)
624                         else:
625                             #not raise
626                             getattr(child, bypath)
627                             ret.append(child)
628                     else:
629                         ret.append(child.find_first(byname=byname,
630                                                     byvalue=byvalue,
631                                                     type_=type_,
632                                                     display_error=False))
633                 else:
634                     ret.extend(child.find_first_contexts(byname=byname,
635                                                          bypath=bypath,
636                                                          byvalue=byvalue,
637                                                          type_=type_,
638                                                          display_error=False))
639             except AttributeError:
640                 pass
641         return self._find_return_results(ret, display_error)
642
643
644 def mandatory_warnings(config):
645     """convenience function to trace Options that are mandatory and
646     where no value has been set
647
648     :returns: generator of mandatory Option's path
649
650     """
651     #if value in cache, properties are not calculated
652     config.cfgimpl_reset_cache(only=('values',))
653     for path in config.cfgimpl_get_description().impl_getpaths(
654             include_groups=True):
655         try:
656             config._getattr(path, force_properties=frozenset(('mandatory',)))
657         except PropertiesOptionError, err:
658             if err.proptype == ['mandatory']:
659                 yield path
660     config.cfgimpl_reset_cache(only=('values',))