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