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