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