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