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