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