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