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