add new option's type: PortOption
[tiramisu.git] / tiramisu / option.py
1 # -*- coding: utf-8 -*-
2 "option types and option description for the configuration management"
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 import re
24 from copy import copy, deepcopy
25 from types import FunctionType
26 from IPy import IP
27
28 from tiramisu.error import ConflictError
29 from tiramisu.setting import groups, multitypes
30 from tiramisu.i18n import _
31 from tiramisu.autolib import carry_out_calculation
32
33 name_regexp = re.compile(r'^\d+')
34 forbidden_names = ('iter_all', 'iter_group', 'find', 'find_first',
35                    'make_dict', 'unwrap_from_path', 'read_only',
36                    'read_write', 'getowner', 'set_contexts')
37
38
39 def valid_name(name):
40     "an option's name is a str and does not start with 'impl' or 'cfgimpl'"
41     try:
42         name = str(name)
43     except:
44         return False
45     if re.match(name_regexp, name) is None and not name.startswith('_') \
46             and name not in forbidden_names \
47             and not name.startswith('impl_') \
48             and not name.startswith('cfgimpl_'):
49         return True
50     else:
51         return False
52 #____________________________________________________________
53 #
54
55
56 class BaseInformation(object):
57     "interface for an option's information attribute"
58     __slots__ = ('_impl_informations',)
59
60     def impl_set_information(self, key, value):
61         """updates the information's attribute
62         (wich is a dictionnary)
63
64         :param key: information's key (ex: "help", "doc"
65         :param value: information's value (ex: "the help string")
66         """
67         try:
68             self._impl_informations[key] = value
69         except AttributeError:
70             raise AttributeError(_('{0} instance has no attribute '
71                                    'impl_set_information').format(self.__class__.__name__))
72
73     def impl_get_information(self, key, default=None):
74         """retrieves one information's item
75
76         :param key: the item string (ex: "help")
77         """
78         try:
79             if key in self._impl_informations:
80                 return self._impl_informations[key]
81             elif default is not None:
82                 return default
83             else:
84                 raise ValueError(_("Information's item not found: {0}").format(key))
85         except AttributeError:
86             raise AttributeError(_('{0} instance has no attribute '
87                                    'impl_get_information').format(self.__class__.__name__))
88
89
90 class Option(BaseInformation):
91     """
92     Abstract base class for configuration option's.
93
94     Reminder: an Option object is **not** a container for the value
95     """
96     __slots__ = ('_name', '_requires', '_multi', '_validator', '_default_multi',
97                  '_default', '_properties', '_callback', '_multitype',
98                  '_master_slaves', '_consistencies', '_empty')
99     _empty = ''
100
101     def __init__(self, name, doc, default=None, default_multi=None,
102                  requires=None, multi=False, callback=None,
103                  callback_params=None, validator=None, validator_args=None,
104                  properties=None):
105         """
106         :param name: the option's name
107         :param doc: the option's description
108         :param default: specifies the default value of the option,
109                         for a multi : ['bla', 'bla', 'bla']
110         :param default_multi: 'bla' (used in case of a reset to default only at
111                         a given index)
112         :param requires: is a list of names of options located anywhere
113                          in the configuration.
114         :param multi: if true, the option's value is a list
115         :param callback: the name of a function. If set, the function's output
116                          is responsible of the option's value
117         :param callback_params: the callback's parameter
118         :param validator: the name of a function wich stands for a custom
119                           validation of the value
120         :param validator_args: the validator's parameters
121
122         """
123         if not valid_name(name):
124             raise ValueError(_("invalid name: {0} for option").format(name))
125         self._name = name
126         self._impl_informations = {}
127         self.impl_set_information('doc', doc)
128         requires = validate_requires_arg(requires, self._name)
129         self._requires = requires
130         self._multi = multi
131         self._consistencies = None
132         if validator is not None:
133             if type(validator) != FunctionType:
134                 raise TypeError(_("validator must be a function"))
135             if validator_args is None:
136                 validator_args = {}
137             self._validator = (validator, validator_args)
138         else:
139             self._validator = None
140         if not self._multi and default_multi is not None:
141             raise ValueError(_("a default_multi is set whereas multi is False"
142                              " in option: {0}").format(name))
143         if default_multi is not None and not self._validate(default_multi):
144             raise ValueError(_("invalid default_multi value {0} "
145                              "for option {1}").format(str(default_multi), name))
146         if callback is not None and (default is not None or default_multi is not None):
147             raise ValueError(_("defaut values not allowed if option: {0} "
148                              "is calculated").format(name))
149         if callback is None and callback_params is not None:
150             raise ValueError(_("params defined for a callback function but "
151                              "no callback defined yet for option {0}").format(name))
152         if callback is not None:
153             if type(callback) != FunctionType:
154                 raise ValueError('callback must be a function')
155             if callback_params is not None and \
156                     not isinstance(callback_params, dict):
157                 raise ValueError('callback_params must be a dict')
158             self._callback = (callback, callback_params)
159         else:
160             self._callback = None
161         if self._multi:
162             if default is None:
163                 default = []
164             self._multitype = multitypes.default
165             self._default_multi = default_multi
166         self.impl_validate(default)
167         self._default = default
168         if properties is None:
169             properties = tuple()
170         if not isinstance(properties, tuple):
171             raise TypeError(_('invalid properties type {0} for {1},'
172                             ' must be a tuple').format(type(properties), self._name))
173         self._properties = set(properties)  # 'hidden', 'disabled'...
174
175     def __eq__(self, other):
176         "Option comparison"
177         if not isinstance(other, Option):
178             return False
179         slots = list(self.__slots__ + Option.__slots__ + BaseInformation.__slots__)
180         for var in slots:
181             try:
182                 val1 = getattr(self, var)
183                 not_in1 = False
184             except:
185                 not_in1 = True
186             try:
187                 val2 = getattr(other, var)
188                 not_in2 = False
189             except:
190                 not_in2 = True
191             if True in (not_in1, not_in2):
192                 if not_in1 != not_in2:
193                     return False
194             elif val1 != val2:
195                 return False
196         return True
197
198     def __ne__(self, other):
199         if not isinstance(other, Option):
200             return False
201         return not self == other
202
203     def _launch_consistency(self, func, opt, vals, context, index, opt_):
204         if context is not None:
205             descr = context.cfgimpl_get_description()
206         if opt is self:
207             #values are for self, search opt_ values
208             values = vals
209             if context is not None:
210                 path = descr.impl_get_path_by_opt(opt_)
211                 values_ = context._getattr(path, validate=False)
212             else:
213                 values_ = opt_.impl_getdefault()
214             if index is not None:
215                 #value is not already set, could be higher
216                 try:
217                     values_ = values_[index]
218                 except IndexError:
219                     values_ = None
220         else:
221             #values are for opt_, search self values
222             values_ = vals
223             if context is not None:
224                 path = descr.impl_get_path_by_opt(self)
225                 values = context._getattr(path, validate=False)
226             else:
227                 values = self.impl_getdefault()
228             if index is not None:
229                 #value is not already set, could be higher
230                 try:
231                     values = values[index]
232                 except IndexError:
233                     values = None
234         if index is None and self.impl_is_multi():
235             for index in range(0, len(values)):
236                 try:
237                     value = values[index]
238                     value_ = values_[index]
239                 except IndexError:
240                     value = None
241                     value_ = None
242                 if None not in (value, value_):
243                     getattr(self, func)(opt_._name, value, value_)
244         else:
245             if None not in (values, values_):
246                 getattr(self, func)(opt_._name, values, values_)
247
248     def impl_validate(self, value, context=None, validate=True):
249         """
250         :param value: the option's value
251         :param validate: if true enables ``self._validator`` validation
252         """
253         if not validate:
254             return
255
256         def _val_validator(val):
257             callback_params = deepcopy(self._validator[1])
258             callback_params.setdefault('', []).insert(0, val)
259             return carry_out_calculation(self._name, config=context,
260                                          callback=self._validator[0],
261                                          callback_params=callback_params)
262
263         def val_validator():
264             #add current value has first argument
265             if self.impl_is_multi():
266                 for val in value:
267                     if not _val_validator(val):
268                         return False
269                 return True
270             else:
271                 return _val_validator(value)
272
273         # generic calculation
274         if context is not None:
275             descr = context.cfgimpl_get_description()
276         if not self._multi:
277             if value is not None and ((self._validator is not None and
278                                        not val_validator()) or
279                                       not self._validate(value)):
280                 raise ValueError(_("invalid value {0} for option {1} (type {2})"
281                                    "").format(value, self._name, self.__class__))
282             if context is not None:
283                 descr._valid_consistency(self, value, context, None)
284         else:
285             if not isinstance(value, list):
286                 raise ValueError(_("invalid value {0} for option {1} "
287                                    "which must be a list").format(value,
288                                                                   self._name))
289             for index in range(0, len(value)):
290                 val = value[index]
291                 if val is not None and ((self._validator is not None and
292                                          not val_validator()) or
293                                         not self._validate(val)):
294                         raise ValueError(_("invalid value {0} for option {1}"
295                                            "").format(value, self._name))
296                 if context is not None:
297                     descr._valid_consistency(self, val, context, index)
298
299     def impl_getdefault(self, default_multi=False):
300         "accessing the default value"
301         if not default_multi or not self.impl_is_multi():
302             return self._default
303         else:
304             return self.getdefault_multi()
305
306     def impl_getdefault_multi(self):
307         "accessing the default value for a multi"
308         return self._default_multi
309
310     def impl_get_multitype(self):
311         return self._multitype
312
313     def impl_get_master_slaves(self):
314         return self._master_slaves
315
316     def impl_is_empty_by_default(self):
317         "no default value has been set yet"
318         if ((not self.impl_is_multi() and self._default is None) or
319                 (self.impl_is_multi() and (self._default == [] or None in self._default))):
320             return True
321         return False
322
323     def impl_getdoc(self):
324         "accesses the Option's doc"
325         return self.impl_get_information('doc')
326
327     def impl_has_callback(self):
328         "to know if a callback has been defined or not"
329         if self._callback is None:
330             return False
331         else:
332             return True
333
334     def impl_getkey(self, value):
335         return value
336
337     def impl_is_multi(self):
338         return self._multi
339
340     def impl_add_consistency(self, func, opt):
341         if self._consistencies is None:
342             self._consistencies = []
343         if not isinstance(opt, Option):
344             raise ValueError('consistency must be set with an option')
345         if self is opt:
346             raise ValueError('cannot add consistency with itself')
347         if self.impl_is_multi() != opt.impl_is_multi():
348             raise ValueError('options in consistency should be multi in two sides')
349         func = '_cons_{0}'.format(func)
350         self._launch_consistency(func, self, self.impl_getdefault(), None, None, opt)
351         self._consistencies.append((func, opt))
352         self.impl_validate(self.impl_getdefault())
353
354     def _cons_not_equal(self, optname, value, value_):
355         if value == value_:
356             raise ValueError(_("invalid value {0} for option {1} "
357                                "must be different as {2} option"
358                                "").format(value, self._name, optname))
359
360
361 class ChoiceOption(Option):
362     """represents a choice out of several objects.
363
364     The option can also have the value ``None``
365     """
366
367     __slots__ = ('_values', '_open_values', '_opt_type')
368     _opt_type = 'string'
369
370     def __init__(self, name, doc, values, default=None, default_multi=None,
371                  requires=None, multi=False, callback=None,
372                  callback_params=None, open_values=False, validator=None,
373                  validator_args=None, properties=()):
374         """
375         :param values: is a list of values the option can possibly take
376         """
377         if not isinstance(values, tuple):
378             raise TypeError(_('values must be a tuple for {0}').format(name))
379         self._values = values
380         if open_values not in (True, False):
381             raise TypeError(_('open_values must be a boolean for '
382                             '{0}').format(name))
383         self._open_values = open_values
384         super(ChoiceOption, self).__init__(name, doc, default=default,
385                                            default_multi=default_multi,
386                                            callback=callback,
387                                            callback_params=callback_params,
388                                            requires=requires,
389                                            multi=multi,
390                                            validator=validator,
391                                            validator_args=validator_args,
392                                            properties=properties)
393
394     def impl_get_values(self):
395         return self._values
396
397     def impl_is_openvalues(self):
398         return self._open_values
399
400     def _validate(self, value):
401         if not self._open_values:
402             return value is None or value in self._values
403         else:
404             return True
405
406
407 class BoolOption(Option):
408     "represents a choice between ``True`` and ``False``"
409     __slots__ = ('_opt_type',)
410     _opt_type = 'bool'
411
412     def _validate(self, value):
413         return isinstance(value, bool)
414
415
416 class IntOption(Option):
417     "represents a choice of an integer"
418     __slots__ = ('_opt_type',)
419     _opt_type = 'int'
420
421     def _validate(self, value):
422         return isinstance(value, int)
423
424
425 class FloatOption(Option):
426     "represents a choice of a floating point number"
427     __slots__ = ('_opt_type',)
428     _opt_type = 'float'
429
430     def _validate(self, value):
431         return isinstance(value, float)
432
433
434 class StrOption(Option):
435     "represents the choice of a string"
436     __slots__ = ('_opt_type',)
437     _opt_type = 'string'
438
439     def _validate(self, value):
440         return isinstance(value, str)
441
442
443 class UnicodeOption(Option):
444     "represents the choice of a unicode string"
445     __slots__ = ('_opt_type',)
446     _opt_type = 'unicode'
447     _empty = u''
448
449     def _validate(self, value):
450         return isinstance(value, unicode)
451
452
453 class SymLinkOption(object):
454     __slots__ = ('_name', '_opt', '_consistencies')
455     _opt_type = 'symlink'
456     _consistencies = None
457
458     def __init__(self, name, opt):
459         self._name = name
460         if not isinstance(opt, Option):
461             raise ValueError(_('malformed symlinkoption '
462                                'must be an option for symlink {0}').format(name))
463         self._opt = opt
464
465     def __getattr__(self, name):
466         if name in ('_name', '_opt', '_consistencies'):
467             return object.__gettattr__(self, name)
468         else:
469             return getattr(self._opt, name)
470
471
472 class IPOption(Option):
473     "represents the choice of an ip"
474     __slots__ = ('_opt_type', '_only_private')
475     _opt_type = 'ip'
476
477     def __init__(self, name, doc, default=None, default_multi=None,
478                  requires=None, multi=False, callback=None,
479                  callback_params=None, validator=None, validator_args=None,
480                  properties=None, only_private=False):
481         self._only_private = only_private
482         super(IPOption, self).__init__(name, doc, default=default,
483                                        default_multi=default_multi,
484                                        callback=callback,
485                                        callback_params=callback_params,
486                                        requires=requires,
487                                        multi=multi,
488                                        validator=validator,
489                                        validator_args=validator_args,
490                                        properties=properties)
491
492     def _validate(self, value):
493         try:
494             ip = IP('{0}/32'.format(value))
495             if self._only_private:
496                 return ip.iptype() == 'PRIVATE'
497             return True
498         except ValueError:
499             return False
500
501
502 class PortOption(Option):
503     """represents the choice of a port
504     The port numbers are divided into three ranges:
505     the well-known ports,
506     the registered ports,
507     and the dynamic or private ports.
508     You can actived this three range.
509     Port number 0 is reserved and can't be used.
510     see: http://en.wikipedia.org/wiki/Port_numbers
511     """
512     __slots__ = ('_opt_type', '_allow_range', '_allow_zero', '_min_value',
513                  '_max_value')
514     _opt_type = 'port'
515
516     def __init__(self, name, doc, default=None, default_multi=None,
517                  requires=None, multi=False, callback=None,
518                  callback_params=None, validator=None, validator_args=None,
519                  properties=None, allow_range=False, allow_zero=False,
520                  allow_wellknown=True, allow_registred=True,
521                  allow_private=False):
522         self._allow_range = allow_range
523         self._min_value = None
524         self._max_value = None
525         ports_min = [0, 1, 1024, 49152]
526         ports_max = [0, 1023, 49151, 65535]
527         is_finally = False
528         for index, allowed in enumerate([allow_zero, allow_wellknown, allow_registred, allow_private]):
529             if self._min_value is None:
530                 if allowed:
531                     self._min_value = ports_min[index]
532             elif not allowed:
533                 is_finally = True
534             elif allowed and is_finally:
535                 raise ValueError(_('inconsistency in allowed range'))
536             if allowed:
537                 self._max_value = ports_max[index]
538
539         if self._max_value is None:
540             raise ValueError(_('max value is empty'))
541
542         super(PortOption, self).__init__(name, doc, default=default,
543                                          default_multi=default_multi,
544                                          callback=callback,
545                                          callback_params=callback_params,
546                                          requires=requires,
547                                          multi=multi,
548                                          validator=validator,
549                                          validator_args=validator_args,
550                                          properties=properties)
551
552     def _validate(self, value):
553         try:
554             if self._allow_range and ":" in str(value):
555                 value = str(value).split(':')
556                 if len(value) != 2:
557                     return False
558                 if not value[0] < value[1]:
559                     return False
560             else:
561                 value = [value]
562
563             for val in value:
564                 if not self._min_value <= int(val) <= self._max_value:
565                     return False
566
567             return True
568         except ValueError:
569             return False
570
571
572 class NetworkOption(Option):
573     "represents the choice of a network"
574     __slots__ = ('_opt_type',)
575     _opt_type = 'network'
576
577     def _validate(self, value):
578         try:
579             IP(value)
580             return True
581         except ValueError:
582             return False
583
584
585 class NetmaskOption(Option):
586     "represents the choice of a netmask"
587     __slots__ = ('_opt_type',)
588     _opt_type = 'netmask'
589
590     def _validate(self, value):
591         try:
592             IP('0.0.0.0/{0}'.format(value))
593             return True
594         except ValueError:
595             return False
596
597     def _cons_network_netmask(self, optname, value, value_):
598         #opts must be (netmask, network) options
599         self.__cons_netmask(optname, value, value_, False)
600
601     def _cons_ip_netmask(self, optname, value, value_):
602         #opts must be (netmask, ip) options
603         self.__cons_netmask(optname, value, value_, True)
604
605     #def __cons_netmask(self, opt, value, context, index, opts, make_net):
606     def __cons_netmask(self, optname, val_netmask, val_ipnetwork, make_net):
607         msg = None
608         try:
609             ip = IP('{0}/{1}'.format(val_ipnetwork, val_netmask),
610                     make_net=make_net)
611             #if cidr == 32, ip same has network
612             if ip.prefixlen() != 32:
613                 try:
614                     IP('{0}/{1}'.format(val_ipnetwork, val_netmask),
615                         make_net=not make_net)
616                 except ValueError:
617                     if not make_net:
618                         msg = _("invalid network {0} ({1}) with netmask {2} ({3}),"
619                                 " this network is an ip")
620                 else:
621                     if make_net:
622                         msg = _("invalid ip {0} ({1}) with netmask {2} ({3}),"
623                                 " this ip is a network")
624
625         except ValueError:
626             if make_net:
627                 msg = _("invalid ip {0} ({1}) with netmask {2} ({3})")
628             else:
629                 msg = _("invalid network {0} ({1}) with netmask {2} ({3})")
630         if msg is not None:
631             raise ValueError(msg.format(val_ipnetwork, optname,
632                                         val_netmask, self._name))
633
634
635 class DomainnameOption(Option):
636     "represents the choice of a domain name"
637     __slots__ = ('_opt_type', '_type', '_allow_ip')
638     _opt_type = 'domainname'
639
640     def __init__(self, name, doc, default=None, default_multi=None,
641                  requires=None, multi=False, callback=None,
642                  callback_params=None, validator=None, validator_args=None,
643                  properties=None, allow_ip=False, type_='domainname'):
644         #netbios: for MS domain
645         #hostname: to identify the device
646         #domainname:
647         #fqdn: with tld, not supported yet
648         if type_ not in ['netbios', 'hostname', 'domainname']:
649             raise ValueError(_('unknown type_ {0} for hostname').format(type_))
650         self._type = type_
651         if allow_ip not in [True, False]:
652             raise ValueError(_('allow_ip must be a boolean'))
653         self._allow_ip = allow_ip
654         super(DomainnameOption, self).__init__(name, doc, default=default,
655                                                default_multi=default_multi,
656                                                callback=callback,
657                                                callback_params=callback_params,
658                                                requires=requires,
659                                                multi=multi,
660                                                validator=validator,
661                                                validator_args=validator_args,
662                                                properties=properties)
663
664     def _validate(self, value):
665         if self._allow_ip is True:
666             try:
667                 IP('{0}/32'.format(value))
668                 return True
669             except ValueError:
670                 pass
671         if self._type == 'netbios':
672             length = 15
673             extrachar = ''
674         elif self._type == 'hostname':
675             length = 63
676             extrachar = ''
677         elif self._type == 'domainname':
678             length = 255
679             extrachar = '\.'
680             if '.' not in value:
681                 raise ValueError(_("invalid value for {0}, must have dot"
682                                    "").format(self._name))
683         if len(value) > length:
684             raise ValueError(_("invalid value's length for {0} (max {1})"
685                                "").format(self._name, length))
686         regexp = r'^[a-z]([a-z\d{0}-])*[a-z\d]$'.format(extrachar)
687         return re.match(regexp, value) is not None
688
689
690 class OptionDescription(BaseInformation):
691     """Config's schema (organisation, group) and container of Options
692     The `OptionsDescription` objects lives in the `tiramisu.config.Config`.
693     """
694     __slots__ = ('_name', '_requires', '_cache_paths', '_group_type',
695                  '_properties', '_children', '_consistencies')
696
697     def __init__(self, name, doc, children, requires=None, properties=None):
698         """
699         :param children: a list of options (including option descriptions)
700
701         """
702         if not valid_name(name):
703             raise ValueError(_("invalid name: {0} for option descr").format(name))
704         self._name = name
705         self._impl_informations = {}
706         self.impl_set_information('doc', doc)
707         child_names = [child._name for child in children]
708         #better performance like this
709         valid_child = copy(child_names)
710         valid_child.sort()
711         old = None
712         for child in valid_child:
713             if id(child) == id(old):
714                 raise ConflictError(_('duplicate option name: '
715                                       '{0}').format(child))
716             old = child
717         self._children = (tuple(child_names), tuple(children))
718         requires = validate_requires_arg(requires, self._name)
719         self._requires = requires
720         self._cache_paths = None
721         self._consistencies = None
722         if properties is None:
723             properties = tuple()
724         if not isinstance(properties, tuple):
725             raise TypeError(_('invalid properties type {0} for {1},'
726                               ' must be a tuple').format(type(properties), self._name))
727         self._properties = set(properties)  # 'hidden', 'disabled'...
728         # the group_type is useful for filtering OptionDescriptions in a config
729         self._group_type = groups.default
730
731     def impl_getdoc(self):
732         return self.impl_get_information('doc')
733
734     def __getattr__(self, name):
735         try:
736             return self._children[1][self._children[0].index(name)]
737         except ValueError:
738             raise AttributeError(_('unknown Option {0} in OptionDescription {1}'
739                                  '').format(name, self._name))
740
741     def impl_getkey(self, config):
742         return tuple([child.impl_getkey(getattr(config, child._name))
743                       for child in self.impl_getchildren()])
744
745     def impl_getpaths(self, include_groups=False, _currpath=None):
746         """returns a list of all paths in self, recursively
747            _currpath should not be provided (helps with recursion)
748         """
749         if _currpath is None:
750             _currpath = []
751         paths = []
752         for option in self.impl_getchildren():
753             attr = option._name
754             if isinstance(option, OptionDescription):
755                 if include_groups:
756                     paths.append('.'.join(_currpath + [attr]))
757                 paths += option.impl_getpaths(include_groups=include_groups,
758                                               _currpath=_currpath + [attr])
759             else:
760                 paths.append('.'.join(_currpath + [attr]))
761         return paths
762
763     def impl_getchildren(self):
764         return self._children[1]
765
766     def impl_build_cache(self, cache_path=None, cache_option=None, _currpath=None, _consistencies=None):
767         if _currpath is None and self._cache_paths is not None:
768             return
769         if _currpath is None:
770             save = True
771             _currpath = []
772             _consistencies = {}
773         else:
774             save = False
775         if cache_path is None:
776             cache_path = [self._name]
777             cache_option = [self]
778         for option in self.impl_getchildren():
779             attr = option._name
780             if attr.startswith('_cfgimpl'):
781                 continue
782             cache_option.append(option)
783             cache_path.append(str('.'.join(_currpath + [attr])))
784             if not isinstance(option, OptionDescription):
785                 if option._consistencies is not None:
786                     for consistency in option._consistencies:
787                         func, opt = consistency
788                         opts = (option, opt)
789                         _consistencies.setdefault(opt, []).append((func, opts))
790                         _consistencies.setdefault(option, []).append((func, opts))
791             else:
792                 _currpath.append(attr)
793                 option.impl_build_cache(cache_path, cache_option, _currpath, _consistencies)
794                 _currpath.pop()
795         if save:
796             #valid no duplicated option
797             valid_child = copy(cache_option)
798             valid_child.sort()
799             old = None
800             for child in valid_child:
801                 if id(child) == id(old):
802                     raise ConflictError(_('duplicate option: '
803                                           '{0}').format(child))
804                 old = child
805             self._cache_paths = (tuple(cache_option), tuple(cache_path))
806             self._consistencies = _consistencies
807
808     def impl_get_opt_by_path(self, path):
809         try:
810             return self._cache_paths[0][self._cache_paths[1].index(path)]
811         except ValueError:
812             raise AttributeError(_('no option for path {0}').format(path))
813
814     def impl_get_path_by_opt(self, opt):
815         try:
816             return self._cache_paths[1][self._cache_paths[0].index(opt)]
817         except ValueError:
818             raise AttributeError(_('no option {0} found').format(opt))
819
820     # ____________________________________________________________
821     def impl_set_group_type(self, group_type):
822         """sets a given group object to an OptionDescription
823
824         :param group_type: an instance of `GroupType` or `MasterGroupType`
825                               that lives in `setting.groups`
826         """
827         if self._group_type != groups.default:
828             raise TypeError(_('cannot change group_type if already set '
829                             '(old {0}, new {1})').format(self._group_type, group_type))
830         if isinstance(group_type, groups.GroupType):
831             self._group_type = group_type
832             if isinstance(group_type, groups.MasterGroupType):
833                 #if master (same name has group) is set
834                 identical_master_child_name = False
835                 #for collect all slaves
836                 slaves = []
837                 master = None
838                 for child in self.impl_getchildren():
839                     if isinstance(child, OptionDescription):
840                         raise ValueError(_("master group {0} shall not have "
841                                          "a subgroup").format(self._name))
842                     if isinstance(child, SymLinkOption):
843                         raise ValueError(_("master group {0} shall not have "
844                                          "a symlinkoption").format(self._name))
845                     if not child.impl_is_multi():
846                         raise ValueError(_("not allowed option {0} in group {1}"
847                                          ": this option is not a multi"
848                                          "").format(child._name, self._name))
849                     if child._name == self._name:
850                         identical_master_child_name = True
851                         child._multitype = multitypes.master
852                         master = child
853                     else:
854                         slaves.append(child)
855                 if master is None:
856                     raise ValueError(_('master group with wrong master name for {0}'
857                                      '').format(self._name))
858                 master._master_slaves = tuple(slaves)
859                 for child in self.impl_getchildren():
860                     if child != master:
861                         child._master_slaves = master
862                         child._multitype = multitypes.slave
863                 if not identical_master_child_name:
864                     raise ValueError(_("the master group: {0} has not any "
865                                      "master child").format(self._name))
866         else:
867             raise ValueError(_('not allowed group_type : {0}').format(group_type))
868
869     def impl_get_group_type(self):
870         return self._group_type
871
872     def _valid_consistency(self, opt, value, context=None, index=None):
873         consistencies = self._consistencies.get(opt)
874         if consistencies is not None:
875             for consistency in consistencies:
876                 opt_ = consistency[1]
877                 ret = opt_[0]._launch_consistency(consistency[0], opt, value, context,
878                                                   index, opt_[1])
879                 if ret is False:
880                     return False
881         return True
882
883
884 def validate_requires_arg(requires, name):
885     """check malformed requirements
886     and tranform dict to internal tuple
887     """
888     if requires is None:
889         return None
890     ret_requires = {}
891     config_action = {}
892
893     for require in requires:
894         if not type(require) == dict:
895             raise ValueError(_("malformed requirements type for option:"
896                                " {0}, must be a dict").format(name))
897         valid_keys = ('option', 'expected', 'action', 'inverse', 'transitive',
898                       'same_action')
899         unknown_keys = frozenset(require.keys()) - frozenset(valid_keys)
900         if unknown_keys != frozenset():
901             raise ValueError('malformed requirements for option: {0}'
902                              ' unknown keys {1}, must only '
903                              '{2}'.format(name,
904                                           unknown_keys,
905                                           valid_keys))
906         try:
907             option = require['option']
908             expected = require['expected']
909             action = require['action']
910         except KeyError:
911             raise ValueError(_("malformed requirements for option: {0}"
912                                " require must have option, expected and"
913                                " action keys").format(name))
914         inverse = require.get('inverse', False)
915         if inverse not in [True, False]:
916             raise ValueError(_('malformed requirements for option: {0}'
917                                ' inverse must be boolean'))
918         transitive = require.get('transitive', True)
919         if transitive not in [True, False]:
920             raise ValueError(_('malformed requirements for option: {0}'
921                                ' transitive must be boolean'))
922         same_action = require.get('same_action', True)
923         if same_action not in [True, False]:
924             raise ValueError(_('malformed requirements for option: {0}'
925                                ' same_action must be boolean'))
926
927         if not isinstance(option, Option):
928             raise ValueError(_('malformed requirements first argument '
929                                'must be an option in option {0}').format(name))
930         if option.impl_is_multi():
931             raise ValueError(_('malformed requirements option {0} '
932                                'should not be a multi').format(name))
933         if expected is not None and not option._validate(expected):
934             raise ValueError(_('malformed requirements second argument '
935                                'must be valid for option {0}').format(name))
936         if action in config_action:
937             if inverse != config_action[action]:
938                 raise ValueError(_("inconsistency in action types for option: {0}"
939                                    " action: {1}").format(name, action))
940         else:
941             config_action[action] = inverse
942         if action not in ret_requires:
943             ret_requires[action] = {}
944         if option not in ret_requires[action]:
945             ret_requires[action][option] = (option, [expected], action,
946                                             inverse, transitive, same_action)
947         else:
948             ret_requires[action][option][1].append(expected)
949
950     ret = []
951     for opt_requires in ret_requires.values():
952         ret_action = []
953         for require in opt_requires.values():
954             req = (require[0], tuple(require[1]), require[2], require[3], require[4], require[5])
955             ret_action.append(req)
956         ret.append(tuple(ret_action))
957     return tuple(ret)