refactor validation
[tiramisu.git] / tiramisu / option / baseoption.py
1 # -*- coding: utf-8 -*-
2 # Copyright (C) 2014 Team tiramisu (see AUTHORS for all contributors)
3 #
4 # This program is free software: you can redistribute it and/or modify it
5 # under the terms of the GNU Lesser General Public License as published by the
6 # Free Software Foundation, either version 3 of the License, or (at your
7 # option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful, but WITHOUT
10 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
11 # FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
12 # details.
13 #
14 # You should have received a copy of the GNU Lesser General Public License
15 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
16 #
17 # The original `Config` design model is unproudly borrowed from
18 # the rough pypy's guys: http://codespeak.net/svn/pypy/dist/pypy/config/
19 # the whole pypy projet is under MIT licence
20 # ____________________________________________________________
21 import re
22 from types import FunctionType
23 import warnings
24
25 from tiramisu.i18n import _
26 from tiramisu.setting import log, undefined
27 from tiramisu.autolib import carry_out_calculation
28 from tiramisu.error import ConfigError, ValueWarning, PropertiesOptionError
29 from tiramisu.storage import get_storages_option
30
31
32 StorageBase = get_storages_option('base')
33 submulti = 2
34 allowed_character = '[a-zA-Z\d\-_]'
35 name_regexp = re.compile(r'^[a-z]{0}*$'.format(allowed_character))
36 forbidden_names = ('iter_all', 'iter_group', 'find', 'find_first',
37                    'make_dict', 'unwrap_from_path', 'read_only',
38                    'read_write', 'getowner', 'set_contexts')
39
40
41 def valid_name(name):
42     "an option's name is a str and does not start with 'impl' or 'cfgimpl'"
43     if not isinstance(name, str):  # pragma: optional cover
44         return False
45     if re.match(name_regexp, name) is not None and not name.startswith('_') \
46             and name not in forbidden_names \
47             and not name.startswith('impl_') \
48             and not name.startswith('cfgimpl_'):
49         return True
50     else:  # pragma: optional cover
51         return False
52
53
54 def validate_callback(callback, callback_params, type_):
55     if type(callback) != FunctionType:  # pragma: optional cover
56         raise ValueError(_('{0} must be a function').format(type_))
57     if callback_params is not None:
58         if not isinstance(callback_params, dict):  # pragma: optional cover
59             raise ValueError(_('{0}_params must be a dict').format(type_))
60         for key, callbacks in callback_params.items():
61             if key != '' and len(callbacks) != 1:  # pragma: optional cover
62                 raise ValueError(_("{0}_params with key {1} mustn't have "
63                                    "length different to 1").format(type_,
64                                                                    key))
65             if not isinstance(callbacks, tuple):  # pragma: optional cover
66                 raise ValueError(_('{0}_params must be tuple for key "{1}"'
67                                    ).format(type_, key))
68             for callbk in callbacks:
69                 if isinstance(callbk, tuple):
70                     if len(callbk) == 1:
71                         if callbk != (None,):  # pragma: optional cover
72                             raise ValueError(_('{0}_params with length of '
73                                                'tuple as 1 must only have '
74                                                'None as first value'))
75                     elif len(callbk) != 2:  # pragma: optional cover
76                         raise ValueError(_('{0}_params must only have 1 or 2 '
77                                            'as length'))
78                     else:
79                         option, force_permissive = callbk
80                         if type_ == 'validator' and not force_permissive:  # pragma: optional cover
81                             raise ValueError(_('validator not support tuple'))
82                         if not isinstance(option, Option) and not \
83                                 isinstance(option, SymLinkOption):  # pragma: optional cover
84                             raise ValueError(_('{0}_params must have an option'
85                                                ' not a {0} for first argument'
86                                                ).format(type_, type(option)))
87                         if force_permissive not in [True, False]:  # pragma: optional cover
88                             raise ValueError(_('{0}_params must have a boolean'
89                                                ' not a {0} for second argument'
90                                                ).format(type_, type(
91                                                    force_permissive)))
92 #____________________________________________________________
93 #
94
95
96 class Base(StorageBase):
97     __slots__ = tuple()
98
99     def __init__(self, name, doc, default=None, default_multi=None,
100                  requires=None, multi=False, callback=None,
101                  callback_params=None, validator=None, validator_params=None,
102                  properties=None, warnings_only=False, extra=None):
103         if not valid_name(name):  # pragma: optional cover
104             raise ValueError(_("invalid name: {0} for option").format(name))
105         if requires is not None:
106             calc_properties, requires = validate_requires_arg(
107                 requires, name)
108         else:
109             calc_properties = frozenset()
110             requires = undefined
111         if not multi and default_multi is not None:  # pragma: optional cover
112             raise ValueError(_("a default_multi is set whereas multi is False"
113                              " in option: {0}").format(name))
114         if multi is True:
115             _multi = 0
116         elif multi is False:
117             _multi = 1
118         elif multi is submulti:
119             _multi = submulti
120         if properties is None:
121             properties = tuple()
122         if not isinstance(properties, tuple):  # pragma: optional cover
123             raise TypeError(_('invalid properties type {0} for {1},'
124                             ' must be a tuple').format(
125                                 type(properties),
126                                 name))
127         if validator is not None:
128             validate_callback(validator, validator_params, 'validator')
129             self._set_validator(validator, validator_params)
130         if calc_properties != frozenset([]) and properties is not tuple():  # pragma: optional cover
131             set_forbidden_properties = calc_properties & set(properties)
132             if set_forbidden_properties != frozenset():
133                 raise ValueError('conflict: properties already set in '
134                                  'requirement {0}'.format(
135                                      list(set_forbidden_properties)))
136         StorageBase.__init__(self, name, _multi, warnings_only, doc, extra,
137                              calc_properties, requires, properties)
138         if multi is not False and default is None:
139             default = []
140         self.impl_validate(default)
141         self._set_default_values(default, default_multi)
142         if callback is not False:
143             self.impl_set_callback(callback, callback_params)
144         self.commit()
145
146     def impl_set_callback(self, callback, callback_params=None):
147         if callback is None and callback_params is not None:  # pragma: optional cover
148             raise ValueError(_("params defined for a callback function but "
149                                "no callback defined"
150                                " yet for option {0}").format(
151                                    self.impl_getname()))
152         if self.impl_get_callback()[0] is not None:
153             raise ConfigError(_("a callback is already set for option {0}, "
154                                 "cannot set another one's").format(self.impl_getname()))
155         self._validate_callback(callback, callback_params)
156         if callback is not None:
157             validate_callback(callback, callback_params, 'callback')
158             self._set_callback(callback, callback_params)
159
160     def impl_is_optiondescription(self):
161         return self.__class__.__name__ in ['OptionDescription',
162                                            'DynOptionDescription',
163                                            'SynDynOptionDescription']
164
165     def impl_is_dynoptiondescription(self):
166         return self.__class__.__name__ in ['DynOptionDescription',
167                                            'SynDynOptionDescription']
168
169
170 class BaseOption(Base):
171     """This abstract base class stands for attribute access
172     in options that have to be set only once, it is of course done in the
173     __setattr__ method
174     """
175     __slots__ = tuple()
176
177     # ____________________________________________________________
178     # serialize object
179     def _impl_convert_requires(self, descr, load=False):
180         """export of the requires during the serialization process
181
182         :type descr: :class:`tiramisu.option.OptionDescription`
183         :param load: `True` if we are at the init of the option description
184         :type load: bool
185         """
186         if not load and self.impl_getrequires() is None:
187             self._state_requires = None
188         elif load and self._state_requires is None:
189             del(self._state_requires)
190         else:
191             if load:
192                 _requires = self._state_requires
193             else:
194                 _requires = self.impl_getrequires()
195             new_value = []
196             for requires in _requires:
197                 new_requires = []
198                 for require in requires:
199                     if load:
200                         new_require = [descr.impl_get_opt_by_path(require[0])]
201                     else:
202                         new_require = [descr.impl_get_path_by_opt(require[0])]
203                     new_require.extend(require[1:])
204                     new_requires.append(tuple(new_require))
205                 new_value.append(tuple(new_requires))
206             if load:
207                 del(self._state_requires)
208                 if new_value != []:
209                     self._requires = new_value
210             else:
211                 self._state_requires = new_value
212
213     def _impl_convert_callback(self, descr, load=False):
214         if self.__class__.__name__ == 'OptionDescription' or \
215                 isinstance(self, SymLinkOption):
216             return
217         if not load and self.impl_get_callback() is None:
218             self._state_callback = None
219             self._state_callback_params = {}
220         elif load and self._state_callback is None:
221             del(self._state_callback)
222             del(self._state_callback_params)
223         else:
224             if load:
225                 callback = self._state_callback
226                 callback_params = self._state_callback_params
227             else:
228                 callback, callback_params = self.impl_get_callback()
229                 self._state_callback_params = {}
230             if callback_params is not None:
231                 cllbck_prms = {}
232                 for key, values in callback_params.items():
233                     vls = []
234                     for value in values:
235                         if isinstance(value, tuple):
236                             if load:
237                                 value = (descr.impl_get_opt_by_path(value[0]),
238                                          value[1])
239                             else:
240                                 value = (descr.impl_get_path_by_opt(value[0]),
241                                          value[1])
242                         vls.append(value)
243                     cllbck_prms[key] = tuple(vls)
244             else:
245                 cllbck_prms = None
246
247             if load:
248                 del(self._state_callback)
249                 del(self._state_callback_params)
250                 self._set_callback(callback, cllbck_prms)
251             else:
252                 self._state_callback = callback
253                 self._state_callback_params = cllbck_prms
254
255     # serialize
256     def _impl_getstate(self, descr):
257         """the under the hood stuff that need to be done
258         before the serialization.
259
260         :param descr: the parent :class:`tiramisu.option.OptionDescription`
261         """
262         #super(BaseOption, self)._impl_getstate()
263         self._stated = True
264         for func in dir(self):
265             if func.startswith('_impl_convert_'):
266                 getattr(self, func)(descr)
267
268     def __getstate__(self, stated=True):
269         """special method to enable the serialization with pickle
270         Usualy, a `__getstate__` method does'nt need any parameter,
271         but somme under the hood stuff need to be done before this action
272
273         :parameter stated: if stated is `True`, the serialization protocol
274                            can be performed, not ready yet otherwise
275         :parameter type: bool
276         """
277         try:
278             self._stated
279         except AttributeError:  # pragma: optional cover
280             raise SystemError(_('cannot serialize Option, '
281                                 'only in OptionDescription'))
282         if isinstance(self, SymLinkOption):
283             slots = frozenset(['_name', '_state_opt', '_stated'])
284         else:
285             slots = self._impl_getattributes()
286             slots -= frozenset(['_cache_paths', '_cache_consistencies',
287                                 '__weakref__'])
288         states = {}
289         for slot in slots:
290             # remove variable if save variable converted
291             # in _state_xxxx variable
292             if '_state' + slot not in slots:
293                 try:
294                     if slot.startswith('_state'):
295                         states[slot] = getattr(self, slot)
296                         # remove _state_xxx variable
297                         self.__delattr__(slot)
298                     else:
299                         states[slot] = getattr(self, slot)
300                 except AttributeError:
301                     pass
302         if not stated:
303             del(states['_stated'])
304         return states
305
306     # unserialize
307     def _impl_setstate(self, descr):
308         """the under the hood stuff that need to be done
309         before the serialization.
310
311         :type descr: :class:`tiramisu.option.OptionDescription`
312         """
313         for func in dir(self):
314             if func.startswith('_impl_convert_'):
315                 getattr(self, func)(descr, load=True)
316         try:
317             del(self._stated)
318         except AttributeError:  # pragma: optional cover
319             pass
320
321     def __setstate__(self, state):
322         """special method that enables us to serialize (pickle)
323
324         Usualy, a `__setstate__` method does'nt need any parameter,
325         but somme under the hood stuff need to be done before this action
326
327         :parameter state: a dict is passed to the loads, it is the attributes
328                           of the options object
329         :type state: dict
330         """
331         for key, value in state.items():
332             setattr(self, key, value)
333
334     def __setattr__(self, name, value):
335         """set once and only once some attributes in the option,
336         like `_name`. `_name` cannot be changed one the option and
337         pushed in the :class:`tiramisu.option.OptionDescription`.
338
339         if the attribute `_readonly` is set to `True`, the option is
340         "frozen" (which has noting to do with the high level "freeze"
341         propertie or "read_only" property)
342         """
343         if name not in ('_option', '_is_build_cache') \
344                 and not isinstance(value, tuple) and \
345                 not name.startswith('_state') and \
346                 not name == '_sa_instance_state':
347             is_readonly = False
348             # never change _name dans _opt
349             if name == '_name':
350                 try:
351                     if self.impl_getname() is not None:
352                         #so _name is already set
353                         is_readonly = True
354                 except (KeyError, AttributeError):
355                     pass
356             elif name == '_opt':
357                 try:
358                     if self._impl_getopt() is not None:
359                         #so _opt is already set
360                         is_readonly = True
361                 except (KeyError, AttributeError):
362                     pass
363             elif name != '_readonly':
364                 is_readonly = self.impl_is_readonly()
365             if is_readonly:  # pragma: optional cover
366                 raise AttributeError(_("'{0}' ({1}) object attribute '{2}' is"
367                                        " read-only").format(
368                                            self.__class__.__name__,
369                                            self,
370                                            #self.impl_getname(),
371                                            name))
372         super(BaseOption, self).__setattr__(name, value)
373
374     def impl_getpath(self, context):
375         return context.cfgimpl_get_description().impl_get_path_by_opt(self)
376
377     def impl_has_callback(self):
378         "to know if a callback has been defined or not"
379         return self.impl_get_callback()[0] is not None
380
381     def _is_subdyn(self):
382         try:
383             return self._subdyn is not None
384         except AttributeError:
385             return False
386
387     def impl_getproperties(self):
388         return self._properties
389
390
391 class OnlyOption(BaseOption):
392     __slots__ = tuple()
393
394
395 class Option(OnlyOption):
396     """
397     Abstract base class for configuration option's.
398
399     Reminder: an Option object is **not** a container for the value.
400     """
401 #    __slots__ = ('_multi', '_validator', '_default_multi', '_default',
402 #                 '_state_callback', '_callback',
403 #                 '_consistencies', '_warnings_only', '_master_slaves',
404 #                 '_state_consistencies', '__weakref__')
405     __slots__ = tuple()
406     _empty = ''
407
408     def _launch_consistency(self, func, option, value, context, index,
409                             submulti_index, all_cons_opts, warnings_only,
410                             transitive):
411         """Launch consistency now
412
413         :param func: function name, this name should start with _cons_
414         :type func: `str`
415         :param option: option that value is changing
416         :type option: `tiramisu.option.Option`
417         :param value: new value of this option
418         :param context: Config's context, if None, check default value instead
419         :type context: `tiramisu.config.Config`
420         :param index: only for multi option, consistency should be launch for
421                       specified index
422         :type index: `int`
423         :param all_cons_opts: all options concerne by this consistency
424         :type all_cons_opts: `list` of `tiramisu.option.Option`
425         :param warnings_only: specific raise error for warning
426         :type warnings_only: `boolean`
427         :param transitive: propertyerror is transitive
428         :type transitive: `boolean`
429         """
430         if context is not undefined:
431             descr = context.cfgimpl_get_description()
432
433         all_cons_vals = []
434         for opt in all_cons_opts:
435             try:
436                 #get value
437                 if (isinstance(opt, DynSymLinkOption) and option._dyn == opt._dyn) or \
438                         option == opt:
439                     opt_value = value
440                 else:
441                     #if context, calculate value, otherwise get default value
442                     if context is not undefined:
443                         if isinstance(opt, DynSymLinkOption):
444                             path = opt.impl_getpath(context)
445                         else:
446                             path = descr.impl_get_path_by_opt(opt)
447                         opt_value = context.getattr(path, validate=False,
448                                                     force_permissive=True)
449                     else:
450                         opt_value = opt.impl_getdefault()
451
452                 #append value
453                 if not self.impl_is_multi() or (isinstance(opt, DynSymLinkOption)
454                                                 and option._dyn == opt._dyn) or \
455                         option == opt:
456                     all_cons_vals.append(opt_value)
457                 elif self.impl_is_submulti():
458                     try:
459                         all_cons_vals.append(opt_value[index][submulti_index])
460                     except IndexError, err:
461                         log.debug('indexerror in submulti opt in _launch_consistency: {0}'.format(err))
462                         #value is not already set, could be higher index
463                         #so return if no value
464                         return
465                 else:
466                     try:
467                         all_cons_vals.append(opt_value[index])
468                     except IndexError, err:
469                         log.debug('indexerror in _launch_consistency: {0}'.format(err))
470                         #value is not already set, could be higher index
471                         #so return if no value and not default_value
472                         if context is not undefined:
473                             if isinstance(opt, DynSymLinkOption):
474                                 path = opt.impl_getpath(context)
475                             else:
476                                 path = descr.impl_get_path_by_opt(opt)
477                             default_value = context.cfgimpl_get_values()._getvalue(opt, path, True, index=index)
478                         else:
479                             default_value = opt.impl_getdefault_multi()
480                         if default_value is not None:
481                             all_cons_vals.append(default_value)
482                         else:
483                             return
484             except PropertiesOptionError as err:
485                 log.debug('propertyerror in _launch_consistency: {0}'.format(err))
486                 if transitive:
487                     raise err
488                 else:
489                     return
490         getattr(self, func)(all_cons_opts, all_cons_vals, warnings_only)
491
492     def impl_validate(self, value, context=undefined, validate=True,
493                       force_index=None, force_submulti_index=None,
494                       current_opt=undefined):
495         """
496         :param value: the option's value
497         :param context: Config's context
498         :type context: :class:`tiramisu.config.Config`
499         :param validate: if true enables ``self._validator`` validation
500         :type validate: boolean
501         :param force_index: if multi, value has to be a list
502                             not if force_index is not None
503         :type force_index: integer
504         :param force_submulti_index: if submulti, value has to be a list
505                                      not if force_submulti_index is not None
506         :type force_submulti_index: integer
507         """
508         if not validate:
509             return
510         if current_opt is undefined:
511             current_opt = self
512
513         def calculation_validator(val):
514             validator, validator_params = self.impl_get_validator()
515             if validator is not None:
516                 if validator_params != {}:
517                     validator_params_ = {}
518                     for val_param, values in validator_params.items():
519                         validator_params_[val_param] = values
520                     #inject value in calculation
521                     if '' in validator_params_:
522                         lst = list(validator_params_[''])
523                         lst.insert(0, val)
524                         validator_params_[''] = tuple(lst)
525                     else:
526                         validator_params_[''] = (val,)
527                 else:
528                     validator_params_ = {'': (val,)}
529                 # Raise ValueError if not valid
530                 carry_out_calculation(self, context=context,
531                                       callback=validator,
532                                       callback_params=validator_params_)
533
534         def do_validation(_value, _index, submulti_index):
535             if _value is None:
536                 return
537             # option validation
538             try:
539                 self._validate(_value, context)
540             except ValueError as err:  # pragma: optional cover
541                 log.debug('do_validation: value: {0}, index: {1}, '
542                           'submulti_index: {2}'.format(_value, _index,
543                                                        submulti_index),
544                           exc_info=True)
545                 raise ValueError(_('invalid value for option {0}: {1}'
546                                    '').format(self.impl_getname(), err))
547             error = None
548             warning = None
549             try:
550                 # valid with self._validator
551                 calculation_validator(_value)
552                 # if not context launch consistency validation
553                 if context is not undefined:
554                     descr._valid_consistency(current_opt, _value, context,
555                                              _index, submulti_index)
556                 self._second_level_validation(_value, self._is_warnings_only())
557             except ValueError as error:
558                 log.debug(_('do_validation for {0}: error in value').format(
559                     self.impl_getname()), exc_info=True)
560                 if self._is_warnings_only():
561                     warning = error
562                     error = None
563             except ValueWarning as warning:
564                 log.debug(_('do_validation for {0}: warning in value').format(
565                     self.impl_getname()), exc_info=True)
566                 pass
567             if error is None and warning is None:
568                 try:
569                     # if context launch consistency validation
570                     if context is not undefined:
571                         descr._valid_consistency(current_opt, _value, context,
572                                                  _index, submulti_index)
573                 except ValueError as error:
574                     log.debug(_('do_validation for {0}: error in consistency').format(
575                         self.impl_getname()), exc_info=True)
576                     pass
577                 except ValueWarning as warning:
578                     log.debug(_('do_validation for {0}: warning in consistency').format(
579                         self.impl_getname()), exc_info=True)
580                     pass
581             if warning:
582                 msg = _("warning on the value of the option {0}: {1}").format(
583                     self.impl_getname(), warning)
584                 warnings.warn_explicit(ValueWarning(msg, self),
585                                        ValueWarning,
586                                        self.__class__.__name__, 0)
587             elif error:
588                 raise ValueError(_("invalid value for option {0}: {1}").format(
589                     self.impl_getname(), error))
590
591         # generic calculation
592         if context is not undefined:
593             descr = context.cfgimpl_get_description()
594
595         if not self.impl_is_multi():
596             do_validation(value, None, None)
597         elif force_index is not None:
598             if self.impl_is_submulti() and force_submulti_index is None:
599                 if not isinstance(value, list):  # pragma: optional cover
600                     raise ValueError(_("invalid value {0} for option {1} which"
601                                        " must be a list").format(
602                                            value, self.impl_getname()))
603                 for idx, val in enumerate(value):
604                     do_validation(val, force_index, idx)
605             else:
606                 do_validation(value, force_index, force_submulti_index)
607         else:
608             if not isinstance(value, list):  # pragma: optional cover
609                 raise ValueError(_("invalid value {0} for option {1} which "
610                                    "must be a list").format(value,
611                                                             self.impl_getname()))
612             for idx, val in enumerate(value):
613                 if self.impl_is_submulti() and force_submulti_index is None:
614                     if not isinstance(val, list):  # pragma: optional cover
615                         raise ValueError(_("invalid value {0} for option {1} "
616                                            "which must be a list of list"
617                                            "").format(value,
618                                                       self.impl_getname()))
619                     for slave_idx, slave_val in enumerate(val):
620                         do_validation(slave_val, idx, slave_idx)
621                 else:
622                     do_validation(val, idx, force_submulti_index)
623
624     def impl_is_master_slaves(self, type_='both'):
625         """FIXME
626         """
627         try:
628             self._master_slaves
629             if type_ in ('both', 'master') and \
630                     self._master_slaves.is_master(self):
631                 return True
632             if type_ in ('both', 'slave') and \
633                     not self._master_slaves.is_master(self):
634                 return True
635         except:
636             pass
637         return False
638
639     def impl_get_master_slaves(self):
640         return self._master_slaves
641
642     def impl_is_empty_by_default(self):
643         "no default value has been set yet"
644         if ((not self.impl_is_multi() and self.impl_getdefault() is None) or
645                 (self.impl_is_multi() and (self.impl_getdefault() == []
646                                            or None in self.impl_getdefault()))):
647             return True
648         return False
649
650     def impl_getdoc(self):
651         "accesses the Option's doc"
652         return self.impl_get_information('doc')
653
654     #def impl_getkey(self, value):
655     #    return value
656
657     def impl_add_consistency(self, func, *other_opts, **params):
658         """Add consistency means that value will be validate with other_opts
659         option's values.
660
661         :param func: function's name
662         :type func: `str`
663         :param other_opts: options used to validate value
664         :type other_opts: `list` of `tiramisu.option.Option`
665         :param params: extra params (warnings_only and transitive are allowed)
666         """
667         if self.impl_is_readonly():  # pragma: optional cover
668             raise AttributeError(_("'{0}' ({1}) cannot add consistency, option is"
669                                    " read-only").format(
670                                        self.__class__.__name__,
671                                        self.impl_getname()))
672         warnings_only = False
673         transitive = True
674         for key, value in params.items():
675             if key == 'warnings_only':
676                 warnings_only = value
677             elif key == 'transitive':
678                 transitive = value
679             else:
680                 raise ValueError(_('unknow parameter {0} in consistency').format(key))
681         if self._is_subdyn():
682             dynod = self._impl_getsubdyn()
683         else:
684             dynod = None
685         for opt in other_opts:
686             if not isinstance(opt, Option):  # pragma: optional cover
687                 raise ConfigError(_('consistency must be set with an option'))
688             if opt._is_subdyn():
689                 if dynod is None:
690                     raise ConfigError(_('almost one option in consistency is '
691                                         'in a dynoptiondescription but not all'))
692                 if dynod != opt._impl_getsubdyn():
693                     raise ConfigError(_('option in consistency must be in same'
694                                         ' dynoptiondescription'))
695                 dynod = opt._impl_getsubdyn()
696             elif dynod is not None:
697                 raise ConfigError(_('almost one option in consistency is in a '
698                                     'dynoptiondescription but not all'))
699             if self is opt:  # pragma: optional cover
700                 raise ConfigError(_('cannot add consistency with itself'))
701             if self.impl_is_multi() != opt.impl_is_multi():  # pragma: optional cover
702                 raise ConfigError(_('every options in consistency must be '
703                                     'multi or none'))
704         func = '_cons_{0}'.format(func)
705         if func not in dir(self):
706             raise ConfigError(_('consistency {0} not available for this option').format(func))
707         all_cons_opts = tuple([self] + list(other_opts))
708         value = self.impl_getdefault()
709         if value is not None:
710             if self.impl_is_multi():
711                 for idx, val in enumerate(value):
712                     if not self.impl_is_submulti():
713                         self._launch_consistency(func, self, val, undefined, idx,
714                                                  None, all_cons_opts,
715                                                  warnings_only, transitive)
716                     else:
717                         for slave_idx, val in enumerate(value):
718                             self._launch_consistency(func, self, val, None,
719                                                      idx, slave_idx,
720                                                      all_cons_opts,
721                                                      warnings_only, transitive)
722             else:
723                 self._launch_consistency(func, self, value, undefined, None,
724                                          None, all_cons_opts, warnings_only,
725                                          transitive)
726         self._add_consistency(func, all_cons_opts, params)
727         #re validate default value when add consistency
728         self.impl_validate(self.impl_getdefault())
729
730     def _cons_not_equal(self, opts, vals, warnings_only):
731         for idx_inf, val_inf in enumerate(vals):
732             for idx_sup, val_sup in enumerate(vals[idx_inf + 1:]):
733                 if val_inf == val_sup is not None:
734                     if warnings_only:
735                         msg = _("same value for {0} and {1}, should be different")
736                     else:
737                         msg = _("same value for {0} and {1}, must be different")
738                     raise ValueError(msg.format(opts[idx_inf].impl_getname(),
739                                                 opts[idx_inf + idx_sup + 1].impl_getname()))
740
741     # serialize/unserialize
742     def _impl_convert_consistencies(self, descr, load=False):
743         """during serialization process, many things have to be done.
744         one of them is the localisation of the options.
745         The paths are set once for all.
746
747         :type descr: :class:`tiramisu.option.OptionDescription`
748         :param load: `True` if we are at the init of the option description
749         :type load: bool
750         """
751         if not load and self._get_consistencies() is None:
752             self._state_consistencies = None
753         elif load and self._state_consistencies is None:
754             del(self._state_consistencies)
755         else:
756             if load:
757                 consistencies = self._state_consistencies
758             else:
759                 consistencies = self._get_consistencies()
760             new_value = []
761             for consistency in consistencies:
762                 values = []
763                 for obj in consistency[1]:
764                     if load:
765                         values.append(descr.impl_get_opt_by_path(obj))
766                     else:
767                         values.append(descr.impl_get_path_by_opt(obj))
768                 new_value.append((consistency[0], tuple(values), consistency[2]))
769             if load:
770                 del(self._state_consistencies)
771                 for new_val in new_value:
772                     self._add_consistency(new_val[0], new_val[1], new_val[2])
773             else:
774                 self._state_consistencies = new_value
775
776     def _second_level_validation(self, value, warnings_only):
777         pass
778
779     def _impl_to_dyn(self, name, path):
780         return DynSymLinkOption(name, self, dyn=path)
781
782     def _validate_callback(self, callback, callback_params):
783         try:
784             default_multi = self.impl_getdefault_multi()
785         except AttributeError:
786             default_multi = None
787         if callback is not None and ((not self.impl_is_multi() and
788                                       (self.impl_getdefault() is not None or
789                                        default_multi is not None))
790                                      or (self.impl_is_multi() and
791                                          (self.impl_getdefault() != [] or
792                                           default_multi is not None))
793                                      ):  # pragma: optional cover
794             raise ValueError(_("default value not allowed if option: {0} "
795                              "is calculated").format(self.impl_getname()))
796
797
798 def validate_requires_arg(requires, name):
799     """check malformed requirements
800     and tranform dict to internal tuple
801
802     :param requires: have a look at the
803                      :meth:`tiramisu.setting.Settings.apply_requires` method to
804                      know more about
805                      the description of the requires dictionary
806     """
807     if requires is None:
808         return None, None
809     ret_requires = {}
810     config_action = {}
811
812     # start parsing all requires given by user (has dict)
813     # transforme it to a tuple
814     for require in requires:
815         if not isinstance(require, dict):  # pragma: optional cover
816             raise ValueError(_("malformed requirements type for option:"
817                                " {0}, must be a dict").format(name))
818         valid_keys = ('option', 'expected', 'action', 'inverse', 'transitive',
819                       'same_action')
820         unknown_keys = frozenset(require.keys()) - frozenset(valid_keys)
821         if unknown_keys != frozenset():  # pragma: optional cover
822             raise ValueError(_('malformed requirements for option: {0}'
823                              ' unknown keys {1}, must only '
824                              '{2}').format(name,
825                                            unknown_keys,
826                                            valid_keys))
827         # prepare all attributes
828         try:
829             option = require['option']
830             expected = require['expected']
831             action = require['action']
832         except KeyError:  # pragma: optional cover
833             raise ValueError(_("malformed requirements for option: {0}"
834                                " require must have option, expected and"
835                                " action keys").format(name))
836         if action == 'force_store_value':  # pragma: optional cover
837             raise ValueError(_("malformed requirements for option: {0}"
838                                " action cannot be force_store_value"
839                                ).format(name))
840         inverse = require.get('inverse', False)
841         if inverse not in [True, False]:  # pragma: optional cover
842             raise ValueError(_('malformed requirements for option: {0}'
843                                ' inverse must be boolean'))
844         transitive = require.get('transitive', True)
845         if transitive not in [True, False]:  # pragma: optional cover
846             raise ValueError(_('malformed requirements for option: {0}'
847                                ' transitive must be boolean'))
848         same_action = require.get('same_action', True)
849         if same_action not in [True, False]:  # pragma: optional cover
850             raise ValueError(_('malformed requirements for option: {0}'
851                                ' same_action must be boolean'))
852
853         if not isinstance(option, Option):  # pragma: optional cover
854             raise ValueError(_('malformed requirements '
855                                'must be an option in option {0}').format(name))
856         if option.impl_is_multi():  # pragma: optional cover
857             raise ValueError(_('malformed requirements option {0} '
858                                'must not be a multi for {1}').format(
859                                    option.impl_getname(), name))
860         if expected is not None:
861             try:
862                 option._validate(expected)
863             except ValueError as err:  # pragma: optional cover
864                 raise ValueError(_('malformed requirements second argument '
865                                    'must be valid for option {0}'
866                                    ': {1}').format(name, err))
867         if action in config_action:
868             if inverse != config_action[action]:  # pragma: optional cover
869                 raise ValueError(_("inconsistency in action types"
870                                    " for option: {0}"
871                                    " action: {1}").format(name, action))
872         else:
873             config_action[action] = inverse
874         if action not in ret_requires:
875             ret_requires[action] = {}
876         if option not in ret_requires[action]:
877             ret_requires[action][option] = (option, [expected], action,
878                                             inverse, transitive, same_action)
879         else:
880             ret_requires[action][option][1].append(expected)
881     # transform dict to tuple
882     ret = []
883     for opt_requires in ret_requires.values():
884         ret_action = []
885         for require in opt_requires.values():
886             ret_action.append((require[0], tuple(require[1]), require[2],
887                                require[3], require[4], require[5]))
888         ret.append(tuple(ret_action))
889     return frozenset(config_action.keys()), tuple(ret)
890
891
892 class SymLinkOption(OnlyOption):
893 #    __slots__ = ('_opt', '_state_opt')
894
895     def __init__(self, name, opt):
896         if not isinstance(opt, Option):  # pragma: optional cover
897             raise ValueError(_('malformed symlinkoption '
898                                'must be an option '
899                                'for symlink {0}').format(name))
900         super(Base, self).__init__(name, undefined, undefined, undefined,
901                                    undefined, undefined, undefined, undefined,
902                                    opt)
903         self.commit()
904
905     def __getattr__(self, name, context=undefined):
906         if name in ('_opt', '_opt_type', '_readonly', 'impl_getpath', '_name',
907                     '_state_opt', '_impl_setopt'):
908             return object.__getattr__(self, name)
909         else:
910             return getattr(self._impl_getopt(), name)
911
912     def _impl_getstate(self, descr):
913         self._stated = True
914         self._state_opt = descr.impl_get_path_by_opt(self._impl_getopt())
915
916     def _impl_setstate(self, descr):
917         self._impl_setopt(descr.impl_get_opt_by_path(self._state_opt))
918         del(self._state_opt)
919         try:
920             del(self._stated)
921         except AttributeError:  # pragma: optional cover
922             pass
923         self._set_readonly()
924
925     def impl_get_information(self, key, default=undefined):
926         return self._impl_getopt().impl_get_information(key, default)
927
928     def impl_is_readonly(self):
929         return True
930
931     def impl_getproperties(self):
932         return self._impl_getopt()._properties
933
934     def impl_get_callback(self):
935         return self._impl_getopt().impl_get_callback()
936
937     def impl_has_callback(self):
938         "to know if a callback has been defined or not"
939         return self._impl_getopt().impl_has_callback()
940
941     def impl_is_multi(self):
942         return self._impl_getopt().impl_is_multi()
943
944     def _is_subdyn(self):
945         try:
946             return self._impl_getopt()._subdyn is not None
947         except AttributeError:
948             return False
949
950
951 class DynSymLinkOption(object):
952     __slots__ = ('_dyn', '_opt', '_name')
953
954     def __init__(self, name, opt, dyn):
955         self._name = name
956         self._dyn = dyn
957         self._opt = opt
958
959     def __getattr__(self, name, context=undefined):
960         if name in ('_opt', '_opt_type', '_readonly', 'impl_getpath', '_name', '_state_opt'):
961             return object.__getattr__(self, name)
962         else:
963             return getattr(self._impl_getopt(), name)
964
965     def impl_getname(self):
966         return self._name
967
968     def _impl_getopt(self):
969         return self._opt
970
971     def impl_getsuffix(self):
972         return self._dyn.split('.')[-1][len(self._impl_getopt().impl_getname()):]
973
974     def impl_getpath(self, context):
975         path = self._impl_getopt().impl_getpath(context)
976         base_path = '.'.join(path.split('.')[:-2])
977         if self.impl_is_master_slaves() and base_path is not '':
978             base_path = base_path + self.impl_getsuffix()
979         if base_path == '':
980             return self._dyn
981         else:
982             return base_path + '.' + self._dyn
983
984     def impl_validate(self, value, context=undefined, validate=True,
985                       force_index=None, force_submulti_index=None):
986         return self._impl_getopt().impl_validate(value, context, validate,
987                                                  force_index,
988                                                  force_submulti_index,
989                                                  current_opt=self)