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