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