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