merge
[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         self._properties = properties  # 'hidden', 'disabled'...
296
297     def _launch_consistency(self, func, opt, vals, context, index, opt_):
298         if context is not None:
299             descr = context.cfgimpl_get_description()
300         if opt is self:
301             #values are for self, search opt_ values
302             values = vals
303             if context is not None:
304                 path = descr.impl_get_path_by_opt(opt_)
305                 values_ = context._getattr(path, validate=False)
306             else:
307                 values_ = opt_.impl_getdefault()
308             if index is not None:
309                 #value is not already set, could be higher
310                 try:
311                     values_ = values_[index]
312                 except IndexError:
313                     values_ = None
314         else:
315             #values are for opt_, search self values
316             values_ = vals
317             if context is not None:
318                 path = descr.impl_get_path_by_opt(self)
319                 values = context._getattr(path, validate=False)
320             else:
321                 values = self.impl_getdefault()
322             if index is not None:
323                 #value is not already set, could be higher
324                 try:
325                     values = values[index]
326                 except IndexError:
327                     values = None
328         if index is None and self.impl_is_multi():
329             for index in range(0, len(values)):
330                 try:
331                     value = values[index]
332                     value_ = values_[index]
333                 except IndexError:
334                     value = None
335                     value_ = None
336                 if None not in (value, value_):
337                     getattr(self, func)(opt_._name, value, value_)
338         else:
339             if None not in (values, values_):
340                 getattr(self, func)(opt_._name, values, values_)
341
342     def impl_validate(self, value, context=None, validate=True):
343         """
344         :param value: the option's value
345         :param validate: if true enables ``self._validator`` validation
346         """
347         if not validate:
348             return
349
350         def val_validator(val):
351             if self._validator is not None:
352                 callback_params = deepcopy(self._validator[1])
353                 callback_params.setdefault('', []).insert(0, val)
354                 return carry_out_calculation(self._name, config=context,
355                                              callback=self._validator[0],
356                                              callback_params=callback_params)
357             else:
358                 return True
359
360         def do_validation(_value, _index=None):
361             if _value is None:
362                 return True
363             if not val_validator(_value):
364                 raise ValueError(_("invalid value {0} "
365                                    "for option {1} for object {2}"
366                                    ).format(_value,
367                                             self._name,
368                                             self.__class__.__name__))
369             try:
370                 self._validate(_value)
371             except ValueError as err:
372                 raise ValueError(_("invalid value {0} for option {1}: {2}"
373                                    "").format(_value, self._name, err))
374             if context is not None:
375                 descr._valid_consistency(self, _value, context, _index)
376
377         # generic calculation
378         if context is not None:
379             descr = context.cfgimpl_get_description()
380         if not self._multi:
381             do_validation(value)
382         else:
383             if not isinstance(value, list):
384                 raise ValueError(_("invalid value {0} for option {1} "
385                                    "which must be a list").format(value,
386                                                                   self._name))
387             for index in range(0, len(value)):
388                 val = value[index]
389                 do_validation(val, index)
390
391     def impl_getdefault(self, default_multi=False):
392         "accessing the default value"
393         if not default_multi or not self.impl_is_multi():
394             return self._default
395         else:
396             return self.getdefault_multi()
397
398     def impl_getdefault_multi(self):
399         "accessing the default value for a multi"
400         return self._default_multi
401
402     def impl_get_multitype(self):
403         return self._multitype
404
405     def impl_get_master_slaves(self):
406         return self._master_slaves
407
408     def impl_is_empty_by_default(self):
409         "no default value has been set yet"
410         if ((not self.impl_is_multi() and self._default is None) or
411                 (self.impl_is_multi() and (self._default == []
412                                            or None in self._default))):
413             return True
414         return False
415
416     def impl_getdoc(self):
417         "accesses the Option's doc"
418         return self.impl_get_information('doc')
419
420     def impl_has_callback(self):
421         "to know if a callback has been defined or not"
422         if self._callback is None:
423             return False
424         else:
425             return True
426
427     def impl_getkey(self, value):
428         return value
429
430     def impl_is_multi(self):
431         return self._multi
432
433     def impl_add_consistency(self, func, opt):
434         if self._consistencies is None:
435             self._consistencies = []
436         if not isinstance(opt, Option):
437             raise ValueError('consistency must be set with an option')
438         if self is opt:
439             raise ValueError('cannot add consistency with itself')
440         if self.impl_is_multi() != opt.impl_is_multi():
441             raise ValueError('options in consistency'
442                              ' should be multi in two sides')
443         func = '_cons_{0}'.format(func)
444         self._launch_consistency(func,
445                                  self,
446                                  self.impl_getdefault(),
447                                  None, None, opt)
448         self._consistencies.append((func, opt))
449         self.impl_validate(self.impl_getdefault())
450
451     def _cons_not_equal(self, optname, value, value_):
452         if value == value_:
453             raise ValueError(_("invalid value {0} for option {1} "
454                                "must be different as {2} option"
455                                "").format(value, self._name, optname))
456
457
458 class ChoiceOption(Option):
459     """represents a choice out of several objects.
460
461     The option can also have the value ``None``
462     """
463
464     __slots__ = ('_values', '_open_values')
465     _opt_type = 'string'
466
467     def __init__(self, name, doc, values, default=None, default_multi=None,
468                  requires=None, multi=False, callback=None,
469                  callback_params=None, open_values=False, validator=None,
470                  validator_args=None, properties=()):
471         """
472         :param values: is a list of values the option can possibly take
473         """
474         if not isinstance(values, tuple):
475             raise TypeError(_('values must be a tuple for {0}').format(name))
476         self._values = values
477         if open_values not in (True, False):
478             raise TypeError(_('open_values must be a boolean for '
479                             '{0}').format(name))
480         self._open_values = open_values
481         super(ChoiceOption, self).__init__(name, doc, default=default,
482                                            default_multi=default_multi,
483                                            callback=callback,
484                                            callback_params=callback_params,
485                                            requires=requires,
486                                            multi=multi,
487                                            validator=validator,
488                                            validator_args=validator_args,
489                                            properties=properties)
490
491     def impl_get_values(self):
492         return self._values
493
494     def impl_is_openvalues(self):
495         return self._open_values
496
497     def _validate(self, value):
498         if not self._open_values and not value in self._values:
499             raise ValueError(_('value {0} is not permitted, '
500                                'only {1} is allowed'
501                                '').format(value, self._values))
502
503
504 class BoolOption(Option):
505     "represents a choice between ``True`` and ``False``"
506     __slots__ = tuple()
507     _opt_type = 'bool'
508
509     def _validate(self, value):
510         if not isinstance(value, bool):
511             raise ValueError(_('value must be a boolean'))
512
513
514 class IntOption(Option):
515     "represents a choice of an integer"
516     __slots__ = tuple()
517     _opt_type = 'int'
518
519     def _validate(self, value):
520         if not isinstance(value, int):
521             raise ValueError(_('value must be an integer'))
522
523
524 class FloatOption(Option):
525     "represents a choice of a floating point number"
526     __slots__ = tuple()
527     _opt_type = 'float'
528
529     def _validate(self, value):
530         if not isinstance(value, float):
531             raise ValueError(_('value must be a float'))
532
533
534 class StrOption(Option):
535     "represents the choice of a string"
536     __slots__ = tuple()
537     _opt_type = 'string'
538
539     def _validate(self, value):
540         if not isinstance(value, str):
541             raise ValueError(_('value must be a string, not '
542                                '{0}').format(type(value)))
543
544
545 if sys.version_info[0] >= 3:
546     #UnicodeOption is same has StrOption in python 3+
547     class UnicodeOption(StrOption):
548         __slots__ = tuple()
549         pass
550 else:
551     class UnicodeOption(Option):
552         "represents the choice of a unicode string"
553         __slots__ = tuple()
554         _opt_type = 'unicode'
555         _empty = u''
556
557         def _validate(self, value):
558             if not isinstance(value, unicode):
559                 raise ValueError(_('value must be an unicode'))
560
561
562 class SymLinkOption(BaseOption):
563     __slots__ = ('_name', '_opt', '_state_opt')
564     _opt_type = 'symlink'
565
566     def __init__(self, name, opt):
567         self._name = name
568         if not isinstance(opt, Option):
569             raise ValueError(_('malformed symlinkoption '
570                                'must be an option '
571                                'for symlink {0}').format(name))
572         self._opt = opt
573         self._readonly = True
574
575     def __getattr__(self, name):
576         if name in ('_name', '_opt', '_opt_type', '_readonly'):
577             return object.__getattr__(self, name)
578         else:
579             return getattr(self._opt, name)
580
581     def _impl_getstate(self, descr):
582         super(SymLinkOption, self)._impl_getstate(descr)
583         self._state_opt = descr.impl_get_path_by_opt(self._opt)
584
585
586 class IPOption(Option):
587     "represents the choice of an ip"
588     __slots__ = ('_only_private',)
589     _opt_type = 'ip'
590
591     def __init__(self, name, doc, default=None, default_multi=None,
592                  requires=None, multi=False, callback=None,
593                  callback_params=None, validator=None, validator_args=None,
594                  properties=None, only_private=False):
595         self._only_private = only_private
596         super(IPOption, self).__init__(name, doc, default=default,
597                                        default_multi=default_multi,
598                                        callback=callback,
599                                        callback_params=callback_params,
600                                        requires=requires,
601                                        multi=multi,
602                                        validator=validator,
603                                        validator_args=validator_args,
604                                        properties=properties)
605
606     def _validate(self, value):
607         ip = IP('{0}/32'.format(value))
608         if ip.iptype() == 'RESERVED':
609             raise ValueError(_("IP mustn't not be in reserved class"))
610         if self._only_private and not ip.iptype() == 'PRIVATE':
611             raise ValueError(_("IP must be in private class"))
612
613
614 class PortOption(Option):
615     """represents the choice of a port
616     The port numbers are divided into three ranges:
617     the well-known ports,
618     the registered ports,
619     and the dynamic or private ports.
620     You can actived this three range.
621     Port number 0 is reserved and can't be used.
622     see: http://en.wikipedia.org/wiki/Port_numbers
623     """
624     __slots__ = ('_allow_range', '_allow_zero', '_min_value', '_max_value')
625     _opt_type = 'port'
626
627     def __init__(self, name, doc, default=None, default_multi=None,
628                  requires=None, multi=False, callback=None,
629                  callback_params=None, validator=None, validator_args=None,
630                  properties=None, allow_range=False, allow_zero=False,
631                  allow_wellknown=True, allow_registred=True,
632                  allow_private=False):
633         self._allow_range = allow_range
634         self._min_value = None
635         self._max_value = None
636         ports_min = [0, 1, 1024, 49152]
637         ports_max = [0, 1023, 49151, 65535]
638         is_finally = False
639         for index, allowed in enumerate([allow_zero,
640                                          allow_wellknown,
641                                          allow_registred,
642                                          allow_private]):
643             if self._min_value is None:
644                 if allowed:
645                     self._min_value = ports_min[index]
646             elif not allowed:
647                 is_finally = True
648             elif allowed and is_finally:
649                 raise ValueError(_('inconsistency in allowed range'))
650             if allowed:
651                 self._max_value = ports_max[index]
652
653         if self._max_value is None:
654             raise ValueError(_('max value is empty'))
655
656         super(PortOption, self).__init__(name, doc, default=default,
657                                          default_multi=default_multi,
658                                          callback=callback,
659                                          callback_params=callback_params,
660                                          requires=requires,
661                                          multi=multi,
662                                          validator=validator,
663                                          validator_args=validator_args,
664                                          properties=properties)
665
666     def _validate(self, value):
667         if self._allow_range and ":" in str(value):
668             value = str(value).split(':')
669             if len(value) != 2:
670                 raise ValueError('range must have two values only')
671             if not value[0] < value[1]:
672                 raise ValueError('first port in range must be'
673                                  ' smaller than the second one')
674         else:
675             value = [value]
676
677         for val in value:
678             if not self._min_value <= int(val) <= self._max_value:
679                 raise ValueError('port must be an between {0} and {1}'
680                                  ''.format(self._min_value, self._max_value))
681
682
683 class NetworkOption(Option):
684     "represents the choice of a network"
685     __slots__ = tuple()
686     _opt_type = 'network'
687
688     def _validate(self, value):
689         ip = IP(value)
690         if ip.iptype() == 'RESERVED':
691             raise ValueError(_("network mustn't not be in reserved class"))
692
693
694 class NetmaskOption(Option):
695     "represents the choice of a netmask"
696     __slots__ = tuple()
697     _opt_type = 'netmask'
698
699     def _validate(self, value):
700         IP('0.0.0.0/{0}'.format(value))
701
702     def _cons_network_netmask(self, optname, value, value_):
703         #opts must be (netmask, network) options
704         self.__cons_netmask(optname, value, value_, False)
705
706     def _cons_ip_netmask(self, optname, value, value_):
707         #opts must be (netmask, ip) options
708         self.__cons_netmask(optname, value, value_, True)
709
710     #def __cons_netmask(self, opt, value, context, index, opts, make_net):
711     def __cons_netmask(self, optname, val_netmask, val_ipnetwork, make_net):
712         msg = None
713         try:
714             ip = IP('{0}/{1}'.format(val_ipnetwork, val_netmask),
715                     make_net=make_net)
716             #if cidr == 32, ip same has network
717             if ip.prefixlen() != 32:
718                 try:
719                     IP('{0}/{1}'.format(val_ipnetwork, val_netmask),
720                         make_net=not make_net)
721                 except ValueError:
722                     if not make_net:
723                         msg = _("invalid network {0} ({1}) "
724                                 "with netmask {2} ({3}),"
725                                 " this network is an IP")
726                 else:
727                     if make_net:
728                         msg = _("invalid IP {0} ({1}) with netmask {2} ({3}),"
729                                 " this IP is a network")
730
731         except ValueError:
732             if make_net:
733                 msg = _("invalid IP {0} ({1}) with netmask {2} ({3})")
734             else:
735                 msg = _("invalid network {0} ({1}) with netmask {2} ({3})")
736         if msg is not None:
737             raise ValueError(msg.format(val_ipnetwork, optname,
738                                         val_netmask, self._name))
739
740
741 class DomainnameOption(Option):
742     "represents the choice of a domain name"
743     __slots__ = ('_type', '_allow_ip')
744     _opt_type = 'domainname'
745
746     def __init__(self, name, doc, default=None, default_multi=None,
747                  requires=None, multi=False, callback=None,
748                  callback_params=None, validator=None, validator_args=None,
749                  properties=None, allow_ip=False, type_='domainname'):
750         #netbios: for MS domain
751         #hostname: to identify the device
752         #domainname:
753         #fqdn: with tld, not supported yet
754         if type_ not in ['netbios', 'hostname', 'domainname']:
755             raise ValueError(_('unknown type_ {0} for hostname').format(type_))
756         self._type = type_
757         if allow_ip not in [True, False]:
758             raise ValueError(_('allow_ip must be a boolean'))
759         self._allow_ip = allow_ip
760         super(DomainnameOption, self).__init__(name, doc, default=default,
761                                                default_multi=default_multi,
762                                                callback=callback,
763                                                callback_params=callback_params,
764                                                requires=requires,
765                                                multi=multi,
766                                                validator=validator,
767                                                validator_args=validator_args,
768                                                properties=properties)
769
770     def _validate(self, value):
771         if self._allow_ip is True:
772             try:
773                 IP('{0}/32'.format(value))
774                 return
775             except ValueError:
776                 pass
777         if self._type == 'netbios':
778             length = 15
779             extrachar = ''
780         elif self._type == 'hostname':
781             length = 63
782             extrachar = ''
783         elif self._type == 'domainname':
784             length = 255
785             extrachar = '\.'
786             if '.' not in value:
787                 raise ValueError(_("invalid value for {0}, must have dot"
788                                    "").format(self._name))
789         if len(value) > length:
790             raise ValueError(_("invalid domainname's length for"
791                                " {0} (max {1})").format(self._name, length))
792         if len(value) == 1:
793             raise ValueError(_("invalid domainname's length for {0} (min 2)"
794                                "").format(self._name))
795         regexp = r'^[a-z]([a-z\d{0}-])*[a-z\d]$'.format(extrachar)
796         if re.match(regexp, value) is None:
797             raise ValueError(_('invalid domainname'))
798
799
800 class OptionDescription(BaseOption):
801     """Config's schema (organisation, group) and container of Options
802     The `OptionsDescription` objects lives in the `tiramisu.config.Config`.
803     """
804     __slots__ = ('_name', '_requires', '_cache_paths', '_group_type',
805                  '_state_group_type', '_properties', '_children',
806                  '_consistencies', '_calc_properties', '__weakref__',
807                  '_readonly', '_impl_informations')
808     _opt_type = 'optiondescription'
809
810     def __init__(self, name, doc, children, requires=None, properties=None):
811         """
812         :param children: a list of options (including optiondescriptions)
813
814         """
815         if not valid_name(name):
816             raise ValueError(_("invalid name:"
817                                " {0} for optiondescription").format(name))
818         self._name = name
819         self._impl_informations = {}
820         self.impl_set_information('doc', doc)
821         child_names = [child._name for child in children]
822         #better performance like this
823         valid_child = copy(child_names)
824         valid_child.sort()
825         old = None
826         for child in valid_child:
827             if child == old:
828                 raise ConflictError(_('duplicate option name: '
829                                       '{0}').format(child))
830             old = child
831         self._children = (tuple(child_names), tuple(children))
832         self._calc_properties, self._requires = validate_requires_arg(requires, self._name)
833         self._cache_paths = None
834         self._consistencies = None
835         if properties is None:
836             properties = tuple()
837         if not isinstance(properties, tuple):
838             raise TypeError(_('invalid properties type {0} for {1},'
839                               ' must be a tuple').format(type(properties),
840                                                          self._name))
841         self._properties = properties  # 'hidden', 'disabled'...
842         # the group_type is useful for filtering OptionDescriptions in a config
843         self._group_type = groups.default
844
845     def impl_getdoc(self):
846         return self.impl_get_information('doc')
847
848     def __getattr__(self, name):
849         if name in self.__slots__:
850             return object.__getattribute__(self, name)
851         try:
852             return self._children[1][self._children[0].index(name)]
853         except ValueError:
854             raise AttributeError(_('unknown Option {0} '
855                                    'in OptionDescription {1}'
856                                    '').format(name, self._name))
857
858     def impl_getkey(self, config):
859         return tuple([child.impl_getkey(getattr(config, child._name))
860                       for child in self.impl_getchildren()])
861
862     def impl_getpaths(self, include_groups=False, _currpath=None):
863         """returns a list of all paths in self, recursively
864            _currpath should not be provided (helps with recursion)
865         """
866         if _currpath is None:
867             _currpath = []
868         paths = []
869         for option in self.impl_getchildren():
870             attr = option._name
871             if isinstance(option, OptionDescription):
872                 if include_groups:
873                     paths.append('.'.join(_currpath + [attr]))
874                 paths += option.impl_getpaths(include_groups=include_groups,
875                                               _currpath=_currpath + [attr])
876             else:
877                 paths.append('.'.join(_currpath + [attr]))
878         return paths
879
880     def impl_getchildren(self):
881         return self._children[1]
882
883     def impl_build_cache(self,
884                          cache_path=None,
885                          cache_option=None,
886                          _currpath=None,
887                          _consistencies=None):
888         if _currpath is None and self._cache_paths is not None:
889             # cache already set
890             return
891         if _currpath is None:
892             save = True
893             _currpath = []
894             _consistencies = {}
895         else:
896             save = False
897         if cache_path is None:
898             cache_path = []
899             cache_option = []
900         for option in self.impl_getchildren():
901             attr = option._name
902             if option in cache_option:
903                 raise ConflictError(_('duplicate option: {0}').format(option))
904
905             cache_option.append(option)
906             option._readonly = True
907             cache_path.append(str('.'.join(_currpath + [attr])))
908             if not isinstance(option, OptionDescription):
909                 if option._consistencies is not None:
910                     for consistency in option._consistencies:
911                         func, opt = consistency
912                         opts = (option, opt)
913                         _consistencies.setdefault(opt,
914                                                   []).append((func, opts))
915                         _consistencies.setdefault(option,
916                                                   []).append((func, opts))
917             else:
918                 _currpath.append(attr)
919                 option.impl_build_cache(cache_path,
920                                         cache_option,
921                                         _currpath,
922                                         _consistencies)
923                 _currpath.pop()
924         if save:
925             self._cache_paths = (tuple(cache_option), tuple(cache_path))
926             self._consistencies = _consistencies
927             self._readonly = True
928
929     def impl_get_opt_by_path(self, path):
930         try:
931             return self._cache_paths[0][self._cache_paths[1].index(path)]
932         except ValueError:
933             raise AttributeError(_('no option for path {0}').format(path))
934
935     def impl_get_path_by_opt(self, opt):
936         try:
937             return self._cache_paths[1][self._cache_paths[0].index(opt)]
938         except ValueError:
939             raise AttributeError(_('no option {0} found').format(opt))
940
941     # ____________________________________________________________
942     def impl_set_group_type(self, group_type):
943         """sets a given group object to an OptionDescription
944
945         :param group_type: an instance of `GroupType` or `MasterGroupType`
946                               that lives in `setting.groups`
947         """
948         if self._group_type != groups.default:
949             raise TypeError(_('cannot change group_type if already set '
950                             '(old {0}, new {1})').format(self._group_type,
951                                                          group_type))
952         if isinstance(group_type, groups.GroupType):
953             self._group_type = group_type
954             if isinstance(group_type, groups.MasterGroupType):
955                 #if master (same name has group) is set
956                 identical_master_child_name = False
957                 #for collect all slaves
958                 slaves = []
959                 master = None
960                 for child in self.impl_getchildren():
961                     if isinstance(child, OptionDescription):
962                         raise ValueError(_("master group {0} shall not have "
963                                          "a subgroup").format(self._name))
964                     if isinstance(child, SymLinkOption):
965                         raise ValueError(_("master group {0} shall not have "
966                                          "a symlinkoption").format(self._name))
967                     if not child.impl_is_multi():
968                         raise ValueError(_("not allowed option {0} "
969                                          "in group {1}"
970                                          ": this option is not a multi"
971                                          "").format(child._name, self._name))
972                     if child._name == self._name:
973                         identical_master_child_name = True
974                         child._multitype = multitypes.master
975                         master = child
976                     else:
977                         slaves.append(child)
978                 if master is None:
979                     raise ValueError(_('master group with wrong'
980                                        ' master name for {0}'
981                                        ).format(self._name))
982                 master._master_slaves = tuple(slaves)
983                 for child in self.impl_getchildren():
984                     if child != master:
985                         child._master_slaves = master
986                         child._multitype = multitypes.slave
987                 if not identical_master_child_name:
988                     raise ValueError(_("no child has same nom has master group"
989                                        " for: {0}").format(self._name))
990         else:
991             raise ValueError(_('group_type: {0}'
992                                ' not allowed').format(group_type))
993
994     def impl_get_group_type(self):
995         return self._group_type
996
997     def _valid_consistency(self, opt, value, context=None, index=None):
998         consistencies = self._consistencies.get(opt)
999         if consistencies is not None:
1000             for consistency in consistencies:
1001                 opt_ = consistency[1]
1002                 ret = opt_[0]._launch_consistency(consistency[0],
1003                                                   opt,
1004                                                   value,
1005                                                   context,
1006                                                   index,
1007                                                   opt_[1])
1008                 if ret is False:
1009                     return False
1010         return True
1011
1012     def _impl_getstate(self, descr=None):
1013         """enables us to export into a dict
1014         :param descr: parent :class:`tiramisu.option.OptionDescription`
1015         """
1016         if descr is None:
1017             self.impl_build_cache()
1018             descr = self
1019         super(OptionDescription, self)._impl_getstate(descr)
1020         self._state_group_type = str(self._group_type)
1021         for option in self.impl_getchildren():
1022             option._impl_getstate(descr)
1023
1024     def __getstate__(self, export=False):
1025         if not export:
1026             self._impl_getstate()
1027         states = super(OptionDescription, self).__getstate__(True)
1028         states['_state_children'] = []
1029         for option in self.impl_getchildren():
1030             states['_state_children'].append(option.__getstate__(True))
1031         return states
1032
1033
1034 def validate_requires_arg(requires, name):
1035     """check malformed requirements
1036     and tranform dict to internal tuple
1037
1038     :param requires: have a look at the
1039                      :meth:`tiramisu.setting.Settings.apply_requires` method to
1040                      know more about
1041                      the description of the requires dictionary
1042     """
1043     if requires is None:
1044         return None, None
1045     ret_requires = {}
1046     config_action = {}
1047
1048     for require in requires:
1049         if not type(require) == dict:
1050             raise ValueError(_("malformed requirements type for option:"
1051                                " {0}, must be a dict").format(name))
1052         valid_keys = ('option', 'expected', 'action', 'inverse', 'transitive',
1053                       'same_action')
1054         unknown_keys = frozenset(require.keys()) - frozenset(valid_keys)
1055         if unknown_keys != frozenset():
1056             raise ValueError('malformed requirements for option: {0}'
1057                              ' unknown keys {1}, must only '
1058                              '{2}'.format(name,
1059                                           unknown_keys,
1060                                           valid_keys))
1061         try:
1062             option = require['option']
1063             expected = require['expected']
1064             action = require['action']
1065         except KeyError:
1066             raise ValueError(_("malformed requirements for option: {0}"
1067                                " require must have option, expected and"
1068                                " action keys").format(name))
1069         inverse = require.get('inverse', False)
1070         if inverse not in [True, False]:
1071             raise ValueError(_('malformed requirements for option: {0}'
1072                                ' inverse must be boolean'))
1073         transitive = require.get('transitive', True)
1074         if transitive not in [True, False]:
1075             raise ValueError(_('malformed requirements for option: {0}'
1076                                ' transitive must be boolean'))
1077         same_action = require.get('same_action', True)
1078         if same_action not in [True, False]:
1079             raise ValueError(_('malformed requirements for option: {0}'
1080                                ' same_action must be boolean'))
1081
1082         if not isinstance(option, Option):
1083             raise ValueError(_('malformed requirements '
1084                                'must be an option in option {0}').format(name))
1085         if option.impl_is_multi():
1086             raise ValueError(_('malformed requirements option {0} '
1087                                'should not be a multi').format(name))
1088         if expected is not None:
1089             try:
1090                 option._validate(expected)
1091             except ValueError as err:
1092                 raise ValueError(_('malformed requirements second argument '
1093                                    'must be valid for option {0}'
1094                                    ': {1}').format(name, err))
1095         if action in config_action:
1096             if inverse != config_action[action]:
1097                 raise ValueError(_("inconsistency in action types"
1098                                    " for option: {0}"
1099                                    " action: {1}").format(name, action))
1100         else:
1101             config_action[action] = inverse
1102         if action not in ret_requires:
1103             ret_requires[action] = {}
1104         if option not in ret_requires[action]:
1105             ret_requires[action][option] = (option, [expected], action,
1106                                             inverse, transitive, same_action)
1107         else:
1108             ret_requires[action][option][1].append(expected)
1109     ret = []
1110     for opt_requires in ret_requires.values():
1111         ret_action = []
1112         for require in opt_requires.values():
1113             req = (require[0], tuple(require[1]), require[2], require[3],
1114                    require[4], require[5])
1115             ret_action.append(req)
1116         ret.append(tuple(ret_action))
1117     return frozenset(config_action.keys()), tuple(ret)