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