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