fb7644453fbedf448041fa33add5fbdedbe3cd40
[tiramisu.git] / tiramisu / option.py
1 # -*- coding: utf-8 -*-
2 "option types and option description"
3 # Copyright (C) 2012-2013 Team tiramisu (see AUTHORS for all contributors)
4 #
5 # This program is free software; you can redistribute it and/or modify
6 # it under the terms of the GNU General Public License as published by
7 # the Free Software Foundation; either version 2 of the License, or
8 # (at your option) any later version.
9 #
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 # GNU General Public License for more details.
14 #
15 # You should have received a copy of the GNU General Public License
16 # along with this program; if not, write to the Free Software
17 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
18 #
19 # The original `Config` design model is unproudly borrowed from
20 # the rough pypy's guys: http://codespeak.net/svn/pypy/dist/pypy/config/
21 # the whole pypy projet is under MIT licence
22 # ____________________________________________________________
23 import re
24 import sys
25 from copy import copy, deepcopy
26 from types import FunctionType
27 from IPy import IP
28
29 from tiramisu.error import ConflictError, ConfigError
30 from tiramisu.setting import groups, multitypes
31 from tiramisu.i18n import _
32 from tiramisu.autolib import carry_out_calculation
33
34 name_regexp = re.compile(r'^\d+')
35 forbidden_names = ('iter_all', 'iter_group', 'find', 'find_first',
36                    'make_dict', 'unwrap_from_path', 'read_only',
37                    'read_write', 'getowner', 'set_contexts')
38
39
40 def valid_name(name):
41     "an option's name is a str and does not start with 'impl' or 'cfgimpl'"
42     try:
43         name = str(name)
44     except:
45         return False
46     if re.match(name_regexp, name) is None and not name.startswith('_') \
47             and name not in forbidden_names \
48             and not name.startswith('impl_') \
49             and not name.startswith('cfgimpl_'):
50         return True
51     else:
52         return False
53 #____________________________________________________________
54 #
55
56
57 class BaseOption(object):
58     """This abstract base class stands for attribute access
59     in options that have to be set only once, it is of course done in the
60     __setattr__ method
61     """
62     __slots__ = ('_name', '_requires', '_properties', '_readonly',
63                  '_consistencies', '_calc_properties', '_impl_informations',
64                  '_state_consistencies', '_state_readonly', '_state_requires',
65                  '_stated')
66
67     def __init__(self, name, doc, requires, properties):
68         if not valid_name(name):
69             raise ValueError(_("invalid name: {0} for option").format(name))
70         self._name = name
71         self._impl_informations = {}
72         self.impl_set_information('doc', doc)
73         self._calc_properties, self._requires = validate_requires_arg(
74             requires, self._name)
75         self._consistencies = None
76         if properties is None:
77             properties = tuple()
78         if not isinstance(properties, tuple):
79             raise TypeError(_('invalid properties type {0} for {1},'
80                             ' must be a tuple').format(
81                                 type(properties),
82                                 self._name))
83         if self._calc_properties is not None and properties is not tuple():
84             set_forbidden_properties = set(properties) & self._calc_properties
85             if set_forbidden_properties != frozenset():
86                 raise ValueError('conflict: properties already set in '
87                                  'requirement {0}'.format(
88                                      list(set_forbidden_properties)))
89         self._properties = properties  # 'hidden', 'disabled'...
90
91     def __setattr__(self, name, value):
92         """set once and only once some attributes in the option,
93         like `_name`. `_name` cannot be changed one the option and
94         pushed in the :class:`tiramisu.option.OptionDescription`.
95
96         if the attribute `_readonly` is set to `True`, the option is
97         "frozen" (which has noting to do with the high level "freeze"
98         propertie or "read_only" property)
99         """
100         if not name.startswith('_state') and name not in ('_cache_paths',
101                                                           '_consistencies'):
102             is_readonly = False
103             # never change _name
104             if name == '_name':
105                 try:
106                     self._name
107                     #so _name is already set
108                     is_readonly = True
109                 except:
110                     pass
111             try:
112                 if self._readonly is True:
113                     if value is True:
114                         # already readonly and try to re set readonly
115                         # don't raise, just exit
116                         return
117                     is_readonly = True
118             except AttributeError:
119                 pass
120             if is_readonly:
121                 raise AttributeError(_("'{0}' ({1}) object attribute '{2}' is"
122                                        " read-only").format(
123                                            self.__class__.__name__,
124                                            self._name,
125                                            name))
126         object.__setattr__(self, name, value)
127
128     # information
129     def impl_set_information(self, key, value):
130         """updates the information's attribute
131         (which is a dictionary)
132
133         :param key: information's key (ex: "help", "doc"
134         :param value: information's value (ex: "the help string")
135         """
136         self._impl_informations[key] = value
137
138     def impl_get_information(self, key, default=None):
139         """retrieves one information's item
140
141         :param key: the item string (ex: "help")
142         """
143         if key in self._impl_informations:
144             return self._impl_informations[key]
145         elif default is not None:
146             return default
147         else:
148             raise ValueError(_("information's item not found: {0}").format(
149                 key))
150
151     # serialize/unserialize
152     def _impl_convert_consistencies(self, descr, load=False):
153         """during serialization process, many things have to be done.
154         one of them is the localisation of the options.
155         The paths are set once for all.
156
157         :type descr: :class:`tiramisu.option.OptionDescription`
158         :param load: `True` if we are at the init of the option description
159         :type load: bool
160         """
161         if not load and self._consistencies is None:
162             self._state_consistencies = None
163         elif load and self._state_consistencies is None:
164             self._consistencies = None
165             del(self._state_consistencies)
166         else:
167             if load:
168                 consistencies = self._state_consistencies
169             else:
170                 consistencies = self._consistencies
171             if isinstance(consistencies, list):
172                 new_value = []
173                 for consistency in consistencies:
174                     if load:
175                         new_value.append((consistency[0],
176                                           descr.impl_get_opt_by_path(
177                                               consistency[1])))
178                     else:
179                         new_value.append((consistency[0],
180                                           descr.impl_get_path_by_opt(
181                                               consistency[1])))
182
183             else:
184                 new_value = {}
185                 for key, _consistencies in consistencies.items():
186                     new_value[key] = []
187                     for key_cons, _cons in _consistencies:
188                         _list_cons = []
189                         for _con in _cons:
190                             if load:
191                                 _list_cons.append(
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                  '_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):
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
355         """
356         super(Option, self).__init__(name, doc, requires, properties)
357         self._multi = multi
358         if validator is not None:
359             validate_callback(validator, validator_params, 'validator')
360             self._validator = (validator, validator_params)
361         else:
362             self._validator = None
363         if not self._multi and default_multi is not None:
364             raise ValueError(_("a default_multi is set whereas multi is False"
365                              " in option: {0}").format(name))
366         if default_multi is not None:
367             try:
368                 self._validate(default_multi)
369             except ValueError as err:
370                 raise ValueError(_("invalid default_multi value {0} "
371                                    "for option {1}: {2}").format(
372                                        str(default_multi), name, err))
373         if callback is not None and (default is not None or
374                                      default_multi is not None):
375             raise ValueError(_("default value not allowed if option: {0} "
376                              "is calculated").format(name))
377         if callback is None and callback_params is not None:
378             raise ValueError(_("params defined for a callback function but "
379                              "no callback defined"
380                              " yet for option {0}").format(name))
381         if callback is not None:
382             validate_callback(callback, callback_params, 'callback')
383             self._callback = (callback, callback_params)
384         else:
385             self._callback = None
386         if self._multi:
387             if default is None:
388                 default = []
389             self._multitype = multitypes.default
390             self._default_multi = default_multi
391         self.impl_validate(default)
392         self._default = default
393
394     def _launch_consistency(self, func, opt, vals, context, index, opt_):
395         if context is not None:
396             descr = context.cfgimpl_get_description()
397         if opt is self:
398             #values are for self, search opt_ values
399             values = vals
400             if context is not None:
401                 path = descr.impl_get_path_by_opt(opt_)
402                 values_ = context._getattr(path, validate=False)
403             else:
404                 values_ = opt_.impl_getdefault()
405             if index is not None:
406                 #value is not already set, could be higher
407                 try:
408                     values_ = values_[index]
409                 except IndexError:
410                     values_ = None
411         else:
412             #values are for opt_, search self values
413             values_ = vals
414             if context is not None:
415                 path = descr.impl_get_path_by_opt(self)
416                 values = context._getattr(path, validate=False)
417             else:
418                 values = self.impl_getdefault()
419             if index is not None:
420                 #value is not already set, could be higher
421                 try:
422                     values = values[index]
423                 except IndexError:
424                     values = None
425         if index is None and self.impl_is_multi():
426             for index in range(0, len(values)):
427                 try:
428                     value = values[index]
429                     value_ = values_[index]
430                 except IndexError:
431                     value = None
432                     value_ = None
433                 if None not in (value, value_):
434                     getattr(self, func)(opt_._name, value, value_)
435         else:
436             if None not in (values, values_):
437                 getattr(self, func)(opt_._name, values, values_)
438
439     def impl_validate(self, value, context=None, validate=True):
440         """
441         :param value: the option's value
442         :param validate: if true enables ``self._validator`` validation
443         """
444         if not validate:
445             return
446
447         def val_validator(val):
448             if self._validator is not None:
449                 if self._validator[1] is not None:
450                     validator_params = deepcopy(self._validator[1])
451                     if '' in validator_params:
452                         lst = list(validator_params[''])
453                         lst.insert(0, val)
454                         validator_params[''] = tuple(lst)
455                     else:
456                         validator_params[''] = (val,)
457                 else:
458                     validator_params = {'': (val,)}
459                 ret = carry_out_calculation(self._name, config=context,
460                                             callback=self._validator[0],
461                                             callback_params=validator_params)
462                 if ret not in [False, True]:
463                     raise ConfigError(_('validator should return a boolean, '
464                                         'not {0}').format(ret))
465                 return ret
466             else:
467                 return True
468
469         def do_validation(_value, _index=None):
470             if _value is None:
471                 return True
472             if not val_validator(_value):
473                 raise ValueError(_("invalid value {0} "
474                                    "for option {1} for object {2}"
475                                    ).format(_value,
476                                             self._name,
477                                             self.__class__.__name__))
478             try:
479                 self._validate(_value)
480             except ValueError as err:
481                 raise ValueError(_("invalid value {0} for option {1}: {2}"
482                                    "").format(_value, self._name, err))
483             if context is not None:
484                 descr._valid_consistency(self, _value, context, _index)
485
486         # generic calculation
487         if context is not None:
488             descr = context.cfgimpl_get_description()
489         if not self._multi:
490             do_validation(value)
491         else:
492             if not isinstance(value, list):
493                 raise ValueError(_("invalid value {0} for option {1} "
494                                    "which must be a list").format(value,
495                                                                   self._name))
496             for index in range(0, len(value)):
497                 val = value[index]
498                 do_validation(val, index)
499
500     def impl_getdefault(self, default_multi=False):
501         "accessing the default value"
502         if not default_multi or not self.impl_is_multi():
503             return self._default
504         else:
505             return self.getdefault_multi()
506
507     def impl_getdefault_multi(self):
508         "accessing the default value for a multi"
509         return self._default_multi
510
511     def impl_get_multitype(self):
512         return self._multitype
513
514     def impl_get_master_slaves(self):
515         return self._master_slaves
516
517     def impl_is_empty_by_default(self):
518         "no default value has been set yet"
519         if ((not self.impl_is_multi() and self._default is None) or
520                 (self.impl_is_multi() and (self._default == []
521                                            or None in self._default))):
522             return True
523         return False
524
525     def impl_getdoc(self):
526         "accesses the Option's doc"
527         return self.impl_get_information('doc')
528
529     def impl_has_callback(self):
530         "to know if a callback has been defined or not"
531         if self._callback is None:
532             return False
533         else:
534             return True
535
536     def impl_getkey(self, value):
537         return value
538
539     def impl_is_multi(self):
540         return self._multi
541
542     def impl_add_consistency(self, func, opt):
543         if self._consistencies is None:
544             self._consistencies = []
545         if not isinstance(opt, Option):
546             raise ValueError('consistency must be set with an option')
547         if self is opt:
548             raise ValueError('cannot add consistency with itself')
549         if self.impl_is_multi() != opt.impl_is_multi():
550             raise ValueError('options in consistency'
551                              ' should be multi in two sides')
552         func = '_cons_{0}'.format(func)
553         self._launch_consistency(func,
554                                  self,
555                                  self.impl_getdefault(),
556                                  None, None, opt)
557         self._consistencies.append((func, opt))
558         self.impl_validate(self.impl_getdefault())
559
560     def _cons_not_equal(self, optname, value, value_):
561         if value == value_:
562             raise ValueError(_("invalid value {0} for option {1} "
563                                "must be different as {2} option"
564                                "").format(value, self._name, optname))
565
566     def _impl_convert_callbacks(self, descr, load=False):
567         if not load and self._callback is None:
568             self._state_callback = None
569         elif load and self._state_callback is None:
570             self._callback = None
571             del(self._state_callback)
572         else:
573             if load:
574                 callback, callback_params = self._state_callback
575             else:
576                 callback, callback_params = self._callback
577             if callback_params is not None:
578                 cllbck_prms = {}
579                 for key, values in callback_params.items():
580                     vls = []
581                     for value in values:
582                         if isinstance(value, tuple):
583                             if load:
584                                 value = (descr.impl_get_opt_by_path(value[0]),
585                                          value[1])
586                             else:
587                                 value = (descr.impl_get_path_by_opt(value[0]),
588                                          value[1])
589                         vls.append(value)
590                     cllbck_prms[key] = tuple(vls)
591             else:
592                 cllbck_prms = None
593
594             if load:
595                 del(self._state_callback)
596                 self._callback = (callback, cllbck_prms)
597             else:
598                 self._state_callback = (callback, cllbck_prms)
599
600
601 class ChoiceOption(Option):
602     """represents a choice out of several objects.
603
604     The option can also have the value ``None``
605     """
606
607     __slots__ = ('_values', '_open_values')
608     _opt_type = 'string'
609
610     def __init__(self, name, doc, values, default=None, default_multi=None,
611                  requires=None, multi=False, callback=None,
612                  callback_params=None, open_values=False, validator=None,
613                  validator_params=None, properties=()):
614         """
615         :param values: is a list of values the option can possibly take
616         """
617         if not isinstance(values, tuple):
618             raise TypeError(_('values must be a tuple for {0}').format(name))
619         self._values = values
620         if open_values not in (True, False):
621             raise TypeError(_('open_values must be a boolean for '
622                             '{0}').format(name))
623         self._open_values = open_values
624         super(ChoiceOption, self).__init__(name, doc, default=default,
625                                            default_multi=default_multi,
626                                            callback=callback,
627                                            callback_params=callback_params,
628                                            requires=requires,
629                                            multi=multi,
630                                            validator=validator,
631                                            validator_params=validator_params,
632                                            properties=properties)
633
634     def impl_get_values(self):
635         return self._values
636
637     def impl_is_openvalues(self):
638         return self._open_values
639
640     def _validate(self, value):
641         if not self._open_values and not value in self._values:
642             raise ValueError(_('value {0} is not permitted, '
643                                'only {1} is allowed'
644                                '').format(value, self._values))
645
646
647 class BoolOption(Option):
648     "represents a choice between ``True`` and ``False``"
649     __slots__ = tuple()
650     _opt_type = 'bool'
651
652     def _validate(self, value):
653         if not isinstance(value, bool):
654             raise ValueError(_('value must be a boolean'))
655
656
657 class IntOption(Option):
658     "represents a choice of an integer"
659     __slots__ = tuple()
660     _opt_type = 'int'
661
662     def _validate(self, value):
663         if not isinstance(value, int):
664             raise ValueError(_('value must be an integer'))
665
666
667 class FloatOption(Option):
668     "represents a choice of a floating point number"
669     __slots__ = tuple()
670     _opt_type = 'float'
671
672     def _validate(self, value):
673         if not isinstance(value, float):
674             raise ValueError(_('value must be a float'))
675
676
677 class StrOption(Option):
678     "represents the choice of a string"
679     __slots__ = tuple()
680     _opt_type = 'string'
681
682     def _validate(self, value):
683         if not isinstance(value, str):
684             raise ValueError(_('value must be a string, not '
685                                '{0}').format(type(value)))
686
687
688 if sys.version_info[0] >= 3:
689     #UnicodeOption is same has StrOption in python 3+
690     class UnicodeOption(StrOption):
691         __slots__ = tuple()
692         pass
693 else:
694     class UnicodeOption(Option):
695         "represents the choice of a unicode string"
696         __slots__ = tuple()
697         _opt_type = 'unicode'
698         _empty = u''
699
700         def _validate(self, value):
701             if not isinstance(value, unicode):
702                 raise ValueError(_('value must be an unicode'))
703
704
705 class SymLinkOption(BaseOption):
706     __slots__ = ('_name', '_opt', '_state_opt')
707     _opt_type = 'symlink'
708     #not return _opt consistencies
709     _consistencies = {}
710
711     def __init__(self, name, opt):
712         self._name = name
713         if not isinstance(opt, Option):
714             raise ValueError(_('malformed symlinkoption '
715                                'must be an option '
716                                'for symlink {0}').format(name))
717         self._opt = opt
718         self._readonly = True
719
720     def __getattr__(self, name):
721         if name in ('_name', '_opt', '_opt_type', '_readonly'):
722             return object.__getattr__(self, name)
723         else:
724             return getattr(self._opt, name)
725
726     def _impl_getstate(self, descr):
727         super(SymLinkOption, self)._impl_getstate(descr)
728         self._state_opt = descr.impl_get_path_by_opt(self._opt)
729
730     def _impl_setstate(self, descr):
731         self._opt = descr.impl_get_opt_by_path(self._state_opt)
732         del(self._state_opt)
733         super(SymLinkOption, self)._impl_setstate(descr)
734
735     def _impl_convert_consistencies(self, descr, load=False):
736         if load:
737             del(self._state_consistencies)
738         else:
739             self._state_consistencies = None
740
741
742 class IPOption(Option):
743     "represents the choice of an ip"
744     __slots__ = ('_only_private', '_allow_reserved')
745     _opt_type = 'ip'
746
747     def __init__(self, name, doc, default=None, default_multi=None,
748                  requires=None, multi=False, callback=None,
749                  callback_params=None, validator=None, validator_params=None,
750                  properties=None, only_private=False, allow_reserved=False):
751         self._only_private = only_private
752         self._allow_reserved = allow_reserved
753         super(IPOption, self).__init__(name, doc, default=default,
754                                        default_multi=default_multi,
755                                        callback=callback,
756                                        callback_params=callback_params,
757                                        requires=requires,
758                                        multi=multi,
759                                        validator=validator,
760                                        validator_params=validator_params,
761                                        properties=properties)
762
763     def _validate(self, value):
764         ip = IP('{0}/32'.format(value))
765         if not self._allow_reserved and ip.iptype() == 'RESERVED':
766             raise ValueError(_("IP mustn't not be in reserved class"))
767         if self._only_private and not ip.iptype() == 'PRIVATE':
768             raise ValueError(_("IP must be in private class"))
769
770
771 class PortOption(Option):
772     """represents the choice of a port
773     The port numbers are divided into three ranges:
774     the well-known ports,
775     the registered ports,
776     and the dynamic or private ports.
777     You can actived this three range.
778     Port number 0 is reserved and can't be used.
779     see: http://en.wikipedia.org/wiki/Port_numbers
780     """
781     __slots__ = ('_allow_range', '_allow_zero', '_min_value', '_max_value')
782     _opt_type = 'port'
783
784     def __init__(self, name, doc, default=None, default_multi=None,
785                  requires=None, multi=False, callback=None,
786                  callback_params=None, validator=None, validator_params=None,
787                  properties=None, allow_range=False, allow_zero=False,
788                  allow_wellknown=True, allow_registred=True,
789                  allow_private=False):
790         self._allow_range = allow_range
791         self._min_value = None
792         self._max_value = None
793         ports_min = [0, 1, 1024, 49152]
794         ports_max = [0, 1023, 49151, 65535]
795         is_finally = False
796         for index, allowed in enumerate([allow_zero,
797                                          allow_wellknown,
798                                          allow_registred,
799                                          allow_private]):
800             if self._min_value is None:
801                 if allowed:
802                     self._min_value = ports_min[index]
803             elif not allowed:
804                 is_finally = True
805             elif allowed and is_finally:
806                 raise ValueError(_('inconsistency in allowed range'))
807             if allowed:
808                 self._max_value = ports_max[index]
809
810         if self._max_value is None:
811             raise ValueError(_('max value is empty'))
812
813         super(PortOption, self).__init__(name, doc, default=default,
814                                          default_multi=default_multi,
815                                          callback=callback,
816                                          callback_params=callback_params,
817                                          requires=requires,
818                                          multi=multi,
819                                          validator=validator,
820                                          validator_params=validator_params,
821                                          properties=properties)
822
823     def _validate(self, value):
824         if self._allow_range and ":" in str(value):
825             value = str(value).split(':')
826             if len(value) != 2:
827                 raise ValueError('range must have two values only')
828             if not value[0] < value[1]:
829                 raise ValueError('first port in range must be'
830                                  ' smaller than the second one')
831         else:
832             value = [value]
833
834         for val in value:
835             if not self._min_value <= int(val) <= self._max_value:
836                 raise ValueError('port must be an between {0} and {1}'
837                                  ''.format(self._min_value, self._max_value))
838
839
840 class NetworkOption(Option):
841     "represents the choice of a network"
842     __slots__ = tuple()
843     _opt_type = 'network'
844
845     def _validate(self, value):
846         ip = IP(value)
847         if ip.iptype() == 'RESERVED':
848             raise ValueError(_("network shall not be in reserved class"))
849
850
851 class NetmaskOption(Option):
852     "represents the choice of a netmask"
853     __slots__ = tuple()
854     _opt_type = 'netmask'
855
856     def _validate(self, value):
857         IP('0.0.0.0/{0}'.format(value))
858
859     def _cons_network_netmask(self, optname, value, value_):
860         #opts must be (netmask, network) options
861         self.__cons_netmask(optname, value, value_, False)
862
863     def _cons_ip_netmask(self, optname, value, value_):
864         #opts must be (netmask, ip) options
865         self.__cons_netmask(optname, value, value_, True)
866
867     #def __cons_netmask(self, opt, value, context, index, opts, make_net):
868     def __cons_netmask(self, optname, val_netmask, val_ipnetwork, make_net):
869         msg = None
870         try:
871             ip = IP('{0}/{1}'.format(val_ipnetwork, val_netmask),
872                     make_net=make_net)
873             #if cidr == 32, ip same has network
874             if ip.prefixlen() != 32:
875                 try:
876                     IP('{0}/{1}'.format(val_ipnetwork, val_netmask),
877                         make_net=not make_net)
878                 except ValueError:
879                     if not make_net:
880                         msg = _("invalid network {0} ({1}) "
881                                 "with netmask {2} ({3}),"
882                                 " this network is an IP")
883                 else:
884                     if make_net:
885                         msg = _("invalid IP {0} ({1}) with netmask {2} ({3}),"
886                                 " this IP is a network")
887
888         except ValueError:
889             if make_net:
890                 msg = _("invalid IP {0} ({1}) with netmask {2} ({3})")
891             else:
892                 msg = _("invalid network {0} ({1}) with netmask {2} ({3})")
893         if msg is not None:
894             raise ValueError(msg.format(val_ipnetwork, optname,
895                                         val_netmask, self._name))
896
897
898 class DomainnameOption(Option):
899     "represents the choice of a domain name"
900     __slots__ = ('_type', '_allow_ip')
901     _opt_type = 'domainname'
902
903     def __init__(self, name, doc, default=None, default_multi=None,
904                  requires=None, multi=False, callback=None,
905                  callback_params=None, validator=None, validator_params=None,
906                  properties=None, allow_ip=False, type_='domainname'):
907         #netbios: for MS domain
908         #hostname: to identify the device
909         #domainname:
910         #fqdn: with tld, not supported yet
911         if type_ not in ['netbios', 'hostname', 'domainname']:
912             raise ValueError(_('unknown type_ {0} for hostname').format(type_))
913         self._type = type_
914         if allow_ip not in [True, False]:
915             raise ValueError(_('allow_ip must be a boolean'))
916         self._allow_ip = allow_ip
917         super(DomainnameOption, self).__init__(name, doc, default=default,
918                                                default_multi=default_multi,
919                                                callback=callback,
920                                                callback_params=callback_params,
921                                                requires=requires,
922                                                multi=multi,
923                                                validator=validator,
924                                                validator_params=validator_params,
925                                                properties=properties)
926
927     def _validate(self, value):
928         if self._allow_ip is True:
929             try:
930                 IP('{0}/32'.format(value))
931                 return
932             except ValueError:
933                 pass
934         if self._type == 'netbios':
935             length = 15
936             extrachar = ''
937         elif self._type == 'hostname':
938             length = 63
939             extrachar = ''
940         elif self._type == 'domainname':
941             length = 255
942             extrachar = '\.'
943             if '.' not in value:
944                 raise ValueError(_("invalid value for {0}, must have dot"
945                                    "").format(self._name))
946         if len(value) > length:
947             raise ValueError(_("invalid domainname's length for"
948                                " {0} (max {1})").format(self._name, length))
949         if len(value) == 1:
950             raise ValueError(_("invalid domainname's length for {0} (min 2)"
951                                "").format(self._name))
952         regexp = r'^[a-z]([a-z\d{0}-])*[a-z\d]$'.format(extrachar)
953         if re.match(regexp, value) is None:
954             raise ValueError(_('invalid domainname'))
955
956
957 class OptionDescription(BaseOption):
958     """Config's schema (organisation, group) and container of Options
959     The `OptionsDescription` objects lives in the `tiramisu.config.Config`.
960     """
961     __slots__ = ('_name', '_requires', '_cache_paths', '_group_type',
962                  '_state_group_type', '_properties', '_children',
963                  '_consistencies', '_calc_properties', '__weakref__',
964                  '_readonly', '_impl_informations', '_state_requires',
965                  '_state_consistencies', '_stated', '_state_readonly')
966     _opt_type = 'optiondescription'
967
968     def __init__(self, name, doc, children, requires=None, properties=None):
969         """
970         :param children: a list of options (including optiondescriptions)
971
972         """
973         super(OptionDescription, self).__init__(name, doc, requires, properties)
974         child_names = [child._name for child in children]
975         #better performance like this
976         valid_child = copy(child_names)
977         valid_child.sort()
978         old = None
979         for child in valid_child:
980             if child == old:
981                 raise ConflictError(_('duplicate option name: '
982                                       '{0}').format(child))
983             old = child
984         self._children = (tuple(child_names), tuple(children))
985         self._cache_paths = None
986         # the group_type is useful for filtering OptionDescriptions in a config
987         self._group_type = groups.default
988
989     def impl_getdoc(self):
990         return self.impl_get_information('doc')
991
992     def __getattr__(self, name):
993         if name in self.__slots__:
994             return object.__getattribute__(self, name)
995         try:
996             return self._children[1][self._children[0].index(name)]
997         except ValueError:
998             raise AttributeError(_('unknown Option {0} '
999                                    'in OptionDescription {1}'
1000                                    '').format(name, self._name))
1001
1002     def impl_getkey(self, config):
1003         return tuple([child.impl_getkey(getattr(config, child._name))
1004                       for child in self.impl_getchildren()])
1005
1006     def impl_getpaths(self, include_groups=False, _currpath=None):
1007         """returns a list of all paths in self, recursively
1008            _currpath should not be provided (helps with recursion)
1009         """
1010         if _currpath is None:
1011             _currpath = []
1012         paths = []
1013         for option in self.impl_getchildren():
1014             attr = option._name
1015             if isinstance(option, OptionDescription):
1016                 if include_groups:
1017                     paths.append('.'.join(_currpath + [attr]))
1018                 paths += option.impl_getpaths(include_groups=include_groups,
1019                                               _currpath=_currpath + [attr])
1020             else:
1021                 paths.append('.'.join(_currpath + [attr]))
1022         return paths
1023
1024     def impl_getchildren(self):
1025         return self._children[1]
1026
1027     def impl_build_cache(self,
1028                          cache_path=None,
1029                          cache_option=None,
1030                          _currpath=None,
1031                          _consistencies=None,
1032                          force_no_consistencies=False):
1033         if _currpath is None and self._cache_paths is not None:
1034             # cache already set
1035             return
1036         if _currpath is None:
1037             save = True
1038             _currpath = []
1039             if not force_no_consistencies:
1040                 _consistencies = {}
1041         else:
1042             save = False
1043         if cache_path is None:
1044             cache_path = []
1045             cache_option = []
1046         for option in self.impl_getchildren():
1047             attr = option._name
1048             if option in cache_option:
1049                 raise ConflictError(_('duplicate option: {0}').format(option))
1050
1051             cache_option.append(option)
1052             if not force_no_consistencies:
1053                 option._readonly = True
1054             cache_path.append(str('.'.join(_currpath + [attr])))
1055             if not isinstance(option, OptionDescription):
1056                 if not force_no_consistencies and \
1057                         option._consistencies is not None:
1058                     for consistency in option._consistencies:
1059                         func, opt = consistency
1060                         opts = (option, opt)
1061                         _consistencies.setdefault(opt,
1062                                                   []).append((func, opts))
1063                         _consistencies.setdefault(option,
1064                                                   []).append((func, opts))
1065             else:
1066                 _currpath.append(attr)
1067                 option.impl_build_cache(cache_path,
1068                                         cache_option,
1069                                         _currpath,
1070                                         _consistencies,
1071                                         force_no_consistencies)
1072                 _currpath.pop()
1073         if save:
1074             self._cache_paths = (tuple(cache_option), tuple(cache_path))
1075             if not force_no_consistencies:
1076                 self._consistencies = _consistencies
1077                 self._readonly = True
1078
1079     def impl_get_opt_by_path(self, path):
1080         try:
1081             return self._cache_paths[0][self._cache_paths[1].index(path)]
1082         except ValueError:
1083             raise AttributeError(_('no option for path {0}').format(path))
1084
1085     def impl_get_path_by_opt(self, opt):
1086         try:
1087             return self._cache_paths[1][self._cache_paths[0].index(opt)]
1088         except ValueError:
1089             raise AttributeError(_('no option {0} found').format(opt))
1090
1091     # ____________________________________________________________
1092     def impl_set_group_type(self, group_type):
1093         """sets a given group object to an OptionDescription
1094
1095         :param group_type: an instance of `GroupType` or `MasterGroupType`
1096                               that lives in `setting.groups`
1097         """
1098         if self._group_type != groups.default:
1099             raise TypeError(_('cannot change group_type if already set '
1100                             '(old {0}, new {1})').format(self._group_type,
1101                                                          group_type))
1102         if isinstance(group_type, groups.GroupType):
1103             self._group_type = group_type
1104             if isinstance(group_type, groups.MasterGroupType):
1105                 #if master (same name has group) is set
1106                 identical_master_child_name = False
1107                 #for collect all slaves
1108                 slaves = []
1109                 master = None
1110                 for child in self.impl_getchildren():
1111                     if isinstance(child, OptionDescription):
1112                         raise ValueError(_("master group {0} shall not have "
1113                                          "a subgroup").format(self._name))
1114                     if isinstance(child, SymLinkOption):
1115                         raise ValueError(_("master group {0} shall not have "
1116                                          "a symlinkoption").format(self._name))
1117                     if not child.impl_is_multi():
1118                         raise ValueError(_("not allowed option {0} "
1119                                          "in group {1}"
1120                                          ": this option is not a multi"
1121                                          "").format(child._name, self._name))
1122                     if child._name == self._name:
1123                         identical_master_child_name = True
1124                         child._multitype = multitypes.master
1125                         master = child
1126                     else:
1127                         slaves.append(child)
1128                 if master is None:
1129                     raise ValueError(_('master group with wrong'
1130                                        ' master name for {0}'
1131                                        ).format(self._name))
1132                 master._master_slaves = tuple(slaves)
1133                 for child in self.impl_getchildren():
1134                     if child != master:
1135                         child._master_slaves = master
1136                         child._multitype = multitypes.slave
1137                 if not identical_master_child_name:
1138                     raise ValueError(_("no child has same nom has master group"
1139                                        " for: {0}").format(self._name))
1140         else:
1141             raise ValueError(_('group_type: {0}'
1142                                ' not allowed').format(group_type))
1143
1144     def impl_get_group_type(self):
1145         return self._group_type
1146
1147     def _valid_consistency(self, opt, value, context=None, index=None):
1148         consistencies = self._consistencies.get(opt)
1149         if consistencies is not None:
1150             for consistency in consistencies:
1151                 opt_ = consistency[1]
1152                 ret = opt_[0]._launch_consistency(consistency[0],
1153                                                   opt,
1154                                                   value,
1155                                                   context,
1156                                                   index,
1157                                                   opt_[1])
1158                 if ret is False:
1159                     return False
1160         return True
1161
1162     def _impl_getstate(self, descr=None):
1163         """enables us to export into a dict
1164         :param descr: parent :class:`tiramisu.option.OptionDescription`
1165         """
1166         if descr is None:
1167             self.impl_build_cache()
1168             descr = self
1169         super(OptionDescription, self)._impl_getstate(descr)
1170         self._state_group_type = str(self._group_type)
1171         for option in self.impl_getchildren():
1172             option._impl_getstate(descr)
1173
1174     def __getstate__(self):
1175         """special method to enable the serialization with pickle
1176         """
1177         stated = True
1178         try:
1179             # the `_state` attribute is a flag that which tells us if
1180             # the serialization can be performed
1181             self._stated
1182         except AttributeError:
1183             # if cannot delete, _impl_getstate never launch
1184             # launch it recursivement
1185             # _stated prevent __getstate__ launch more than one time
1186             # _stated is delete, if re-serialize, re-lauch _impl_getstate
1187             self._impl_getstate()
1188             stated = False
1189         return super(OptionDescription, self).__getstate__(stated)
1190
1191     def _impl_setstate(self, descr=None):
1192         """enables us to import from a dict
1193         :param descr: parent :class:`tiramisu.option.OptionDescription`
1194         """
1195         if descr is None:
1196             self._cache_paths = None
1197             self.impl_build_cache(force_no_consistencies=True)
1198             descr = self
1199         self._group_type = getattr(groups, self._state_group_type)
1200         del(self._state_group_type)
1201         super(OptionDescription, self)._impl_setstate(descr)
1202         for option in self.impl_getchildren():
1203             option._impl_setstate(descr)
1204
1205     def __setstate__(self, state):
1206         super(OptionDescription, self).__setstate__(state)
1207         try:
1208             self._stated
1209         except AttributeError:
1210             self._impl_setstate()
1211
1212
1213 def validate_requires_arg(requires, name):
1214     """check malformed requirements
1215     and tranform dict to internal tuple
1216
1217     :param requires: have a look at the
1218                      :meth:`tiramisu.setting.Settings.apply_requires` method to
1219                      know more about
1220                      the description of the requires dictionary
1221     """
1222     if requires is None:
1223         return None, None
1224     ret_requires = {}
1225     config_action = {}
1226
1227     # start parsing all requires given by user (has dict)
1228     # transforme it to a tuple
1229     for require in requires:
1230         if not type(require) == dict:
1231             raise ValueError(_("malformed requirements type for option:"
1232                                " {0}, must be a dict").format(name))
1233         valid_keys = ('option', 'expected', 'action', 'inverse', 'transitive',
1234                       'same_action')
1235         unknown_keys = frozenset(require.keys()) - frozenset(valid_keys)
1236         if unknown_keys != frozenset():
1237             raise ValueError('malformed requirements for option: {0}'
1238                              ' unknown keys {1}, must only '
1239                              '{2}'.format(name,
1240                                           unknown_keys,
1241                                           valid_keys))
1242         # prepare all attributes
1243         try:
1244             option = require['option']
1245             expected = require['expected']
1246             action = require['action']
1247         except KeyError:
1248             raise ValueError(_("malformed requirements for option: {0}"
1249                                " require must have option, expected and"
1250                                " action keys").format(name))
1251         inverse = require.get('inverse', False)
1252         if inverse not in [True, False]:
1253             raise ValueError(_('malformed requirements for option: {0}'
1254                                ' inverse must be boolean'))
1255         transitive = require.get('transitive', True)
1256         if transitive not in [True, False]:
1257             raise ValueError(_('malformed requirements for option: {0}'
1258                                ' transitive must be boolean'))
1259         same_action = require.get('same_action', True)
1260         if same_action not in [True, False]:
1261             raise ValueError(_('malformed requirements for option: {0}'
1262                                ' same_action must be boolean'))
1263
1264         if not isinstance(option, Option):
1265             raise ValueError(_('malformed requirements '
1266                                'must be an option in option {0}').format(name))
1267         if option.impl_is_multi():
1268             raise ValueError(_('malformed requirements option {0} '
1269                                'should not be a multi').format(name))
1270         if expected is not None:
1271             try:
1272                 option._validate(expected)
1273             except ValueError as err:
1274                 raise ValueError(_('malformed requirements second argument '
1275                                    'must be valid for option {0}'
1276                                    ': {1}').format(name, err))
1277         if action in config_action:
1278             if inverse != config_action[action]:
1279                 raise ValueError(_("inconsistency in action types"
1280                                    " for option: {0}"
1281                                    " action: {1}").format(name, action))
1282         else:
1283             config_action[action] = inverse
1284         if action not in ret_requires:
1285             ret_requires[action] = {}
1286         if option not in ret_requires[action]:
1287             ret_requires[action][option] = (option, [expected], action,
1288                                             inverse, transitive, same_action)
1289         else:
1290             ret_requires[action][option][1].append(expected)
1291     # transform dict to tuple
1292     ret = []
1293     for opt_requires in ret_requires.values():
1294         ret_action = []
1295         for require in opt_requires.values():
1296             ret_action.append((require[0], tuple(require[1]), require[2],
1297                                require[3], require[4], require[5]))
1298         ret.append(tuple(ret_action))
1299     return frozenset(config_action.keys()), tuple(ret)
1300
1301
1302 def validate_callback(callback, callback_params, type_):
1303     if type(callback) != FunctionType:
1304         raise ValueError(_('{0} should be a function').format(type_))
1305     if callback_params is not None:
1306         if not isinstance(callback_params, dict):
1307             raise ValueError(_('{0}_params should be a dict').format(type_))
1308         for key, callbacks in callback_params.items():
1309             if key != '' and len(callbacks) != 1:
1310                 raise ValueError(_('{0}_params with key {1} should not have '
1311                                    'length different to 1').format(type_,
1312                                                                    key))
1313             if not isinstance(callbacks, tuple):
1314                 raise ValueError(_('{0}_params should be tuple for key "{1}"'
1315                                    ).format(type_, key))
1316             for callbk in callbacks:
1317                 if isinstance(callbk, tuple):
1318                     option, force_permissive = callbk
1319                     if type_ == 'validator' and not force_permissive:
1320                         raise ValueError(_('validator not support tuple'))
1321                     if not isinstance(option, Option) and not \
1322                             isinstance(option, SymLinkOption):
1323                         raise ValueError(_('{0}_params should have an option '
1324                                            'not a {0} for first argument'
1325                                            ).format(type_, type(option)))
1326                     if force_permissive not in [True, False]:
1327                         raise ValueError(_('{0}_params should have a boolean'
1328                                            ' not a {0} for second argument'
1329                                            ).format(type_, type(
1330                                                force_permissive)))