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