b3720ebc6b75119bc6617360761358bfd9698edf
[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 copy import copy
23 from types import FunctionType
24 import warnings
25
26 from tiramisu.i18n import _
27 from tiramisu.setting import log, undefined
28 from tiramisu.autolib import carry_out_calculation
29 from tiramisu.error import ConfigError, ValueWarning
30 from tiramisu.storage import get_storages_option
31
32
33 StorageBase = get_storages_option('base')
34
35 name_regexp = re.compile(r'^\d+')
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):
44         return False
45     if re.match(name_regexp, name) is 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:
51         return False
52
53
54 def validate_callback(callback, callback_params, type_):
55     if type(callback) != FunctionType:
56         raise ValueError(_('{0} must be a function').format(type_))
57     if callback_params is not None:
58         if not isinstance(callback_params, dict):
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:
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):
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,):
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:
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:
81                             raise ValueError(_('validator not support tuple'))
82                         if not isinstance(option, Option) and not \
83                                 isinstance(option, SymLinkOption):
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]:
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):
103         if not valid_name(name):
104             raise ValueError(_("invalid name: {0} for option").format(name))
105         self._name = name
106         self._readonly = False
107         self._informations = {}
108         self.impl_set_information('doc', doc)
109         if requires is not None:
110             self._calc_properties, self._requires = validate_requires_arg(
111                 requires, self._name)
112         else:
113             self._calc_properties = frozenset()
114             self._requires = []
115         if not multi and default_multi is not None:
116             raise ValueError(_("a default_multi is set whereas multi is False"
117                              " in option: {0}").format(name))
118         if default_multi is not None:
119             try:
120                 self._validate(default_multi)
121             except ValueError as err:
122                 raise ValueError(_("invalid default_multi value {0} "
123                                    "for option {1}: {2}").format(
124                                        str(default_multi), name, err))
125         self._multi = multi
126         if self._multi:
127             if default is None:
128                 default = []
129             self._default_multi = default_multi
130         if callback is not None and ((not multi and (default is not None or
131                                                      default_multi is not None))
132                                      or (multi and (default != [] or
133                                                     default_multi is not None))
134                                      ):
135             raise ValueError(_("default value not allowed if option: {0} "
136                              "is calculated").format(name))
137         if properties is None:
138             properties = tuple()
139         if not isinstance(properties, tuple):
140             raise TypeError(_('invalid properties type {0} for {1},'
141                             ' must be a tuple').format(
142                                 type(properties),
143                                 self._name))
144         if validator is not None:
145             validate_callback(validator, validator_params, 'validator')
146             self._validator = validator
147             if validator_params is not None:
148                 self._validator_params = validator_params
149         if callback is None and callback_params is not None:
150             raise ValueError(_("params defined for a callback function but "
151                              "no callback defined"
152                              " yet for option {0}").format(name))
153         if callback is not None:
154             validate_callback(callback, callback_params, 'callback')
155             self._callback = callback
156             if callback_params is not None:
157                 self._callback_params = callback_params
158         if self._calc_properties != frozenset([]) and properties is not tuple():
159             set_forbidden_properties = self._calc_properties & set(properties)
160             if set_forbidden_properties != frozenset():
161                 raise ValueError('conflict: properties already set in '
162                                  'requirement {0}'.format(
163                                      list(set_forbidden_properties)))
164         if multi and default is None:
165             self._default = []
166         else:
167             self._default = default
168         self._properties = properties
169         self._warnings_only = warnings_only
170         ret = super(Base, self).__init__()
171         self.impl_validate(self._default)
172         return ret
173
174
175 class BaseOption(Base):
176     """This abstract base class stands for attribute access
177     in options that have to be set only once, it is of course done in the
178     __setattr__ method
179     """
180     __slots__ = tuple()
181
182     # information
183     def impl_set_information(self, key, value):
184         """updates the information's attribute
185         (which is a dictionary)
186
187         :param key: information's key (ex: "help", "doc"
188         :param value: information's value (ex: "the help string")
189         """
190         self._informations[key] = value
191
192     def impl_get_information(self, key, default=undefined):
193         """retrieves one information's item
194
195         :param key: the item string (ex: "help")
196         """
197         if key in self._informations:
198             return self._informations[key]
199         elif default is not undefined:
200             return default
201         else:
202             raise ValueError(_("information's item not found: {0}").format(
203                 key))
204
205     # ____________________________________________________________
206     # serialize object
207     def _impl_convert_requires(self, descr, load=False):
208         """export of the requires during the serialization process
209
210         :type descr: :class:`tiramisu.option.OptionDescription`
211         :param load: `True` if we are at the init of the option description
212         :type load: bool
213         """
214         if not load and self._requires is None:
215             self._state_requires = None
216         elif load and self._state_requires is None:
217             self._requires = None
218             del(self._state_requires)
219         else:
220             if load:
221                 _requires = self._state_requires
222             else:
223                 _requires = self._requires
224             new_value = []
225             for requires in _requires:
226                 new_requires = []
227                 for require in requires:
228                     if load:
229                         new_require = [descr.impl_get_opt_by_path(require[0])]
230                     else:
231                         new_require = [descr.impl_get_path_by_opt(require[0])]
232                     new_require.extend(require[1:])
233                     new_requires.append(tuple(new_require))
234                 new_value.append(tuple(new_requires))
235             if load:
236                 del(self._state_requires)
237                 self._requires = new_value
238             else:
239                 self._state_requires = new_value
240
241     # serialize
242     def _impl_getstate(self, descr):
243         """the under the hood stuff that need to be done
244         before the serialization.
245
246         :param descr: the parent :class:`tiramisu.option.OptionDescription`
247         """
248         self._stated = True
249         for func in dir(self):
250             if func.startswith('_impl_convert_'):
251                 getattr(self, func)(descr)
252         self._state_readonly = self._readonly
253
254     def __getstate__(self, stated=True):
255         """special method to enable the serialization with pickle
256         Usualy, a `__getstate__` method does'nt need any parameter,
257         but somme under the hood stuff need to be done before this action
258
259         :parameter stated: if stated is `True`, the serialization protocol
260                            can be performed, not ready yet otherwise
261         :parameter type: bool
262         """
263         try:
264             self._stated
265         except AttributeError:
266             raise SystemError(_('cannot serialize Option, '
267                                 'only in OptionDescription'))
268         slots = set()
269         for subclass in self.__class__.__mro__:
270             if subclass is not object:
271                 slots.update(subclass.__slots__)
272         slots -= frozenset(['_cache_paths', '_cache_consistencies',
273                             '__weakref__'])
274         states = {}
275         for slot in slots:
276             # remove variable if save variable converted
277             # in _state_xxxx variable
278             if '_state' + slot not in slots:
279                 try:
280                     if slot.startswith('_state'):
281                         states[slot] = getattr(self, slot)
282                         # remove _state_xxx variable
283                         self.__delattr__(slot)
284                     else:
285                         states[slot] = getattr(self, slot)
286                 except AttributeError:
287                     pass
288         if not stated:
289             del(states['_stated'])
290         return states
291
292     # unserialize
293     def _impl_setstate(self, descr):
294         """the under the hood stuff that need to be done
295         before the serialization.
296
297         :type descr: :class:`tiramisu.option.OptionDescription`
298         """
299         for func in dir(self):
300             if func.startswith('_impl_convert_'):
301                 getattr(self, func)(descr, load=True)
302         try:
303             self._readonly = self._state_readonly
304             del(self._state_readonly)
305             del(self._stated)
306         except AttributeError:
307             pass
308
309     def __setstate__(self, state):
310         """special method that enables us to serialize (pickle)
311
312         Usualy, a `__setstate__` method does'nt need any parameter,
313         but somme under the hood stuff need to be done before this action
314
315         :parameter state: a dict is passed to the loads, it is the attributes
316                           of the options object
317         :type state: dict
318         """
319         for key, value in state.items():
320             setattr(self, key, value)
321
322     def __setattr__(self, name, value):
323         """set once and only once some attributes in the option,
324         like `_name`. `_name` cannot be changed one the option and
325         pushed in the :class:`tiramisu.option.OptionDescription`.
326
327         if the attribute `_readonly` is set to `True`, the option is
328         "frozen" (which has noting to do with the high level "freeze"
329         propertie or "read_only" property)
330         """
331         if name not in ('_option', '_is_build_cache') \
332                 and not isinstance(value, tuple) and \
333                 not name.startswith('_state'):
334             is_readonly = False
335             # never change _name
336             if name == '_name':
337                 try:
338                     if self._name is not None:
339                         #so _name is already set
340                         is_readonly = True
341                 except (KeyError, AttributeError):
342                     pass
343             elif name != '_readonly':
344                 is_readonly = self.impl_is_readonly()
345             if is_readonly:
346                 raise AttributeError(_("'{0}' ({1}) object attribute '{2}' is"
347                                        " read-only").format(
348                                            self.__class__.__name__,
349                                            self._name,
350                                            name))
351         super(BaseOption, self).__setattr__(name, value)
352
353     def impl_is_readonly(self):
354         try:
355             if self._readonly is True:
356                 return True
357         except AttributeError:
358             pass
359         return False
360
361     def impl_getname(self):
362         return self._name
363
364
365 class OnlyOption(BaseOption):
366     __slots__ = tuple()
367
368
369 class Option(OnlyOption):
370     """
371     Abstract base class for configuration option's.
372
373     Reminder: an Option object is **not** a container for the value.
374     """
375 #    __slots__ = ('_multi', '_validator', '_default_multi', '_default',
376 #                 '_state_callback', '_callback',
377 #                 '_consistencies', '_warnings_only', '_master_slaves',
378 #                 '_state_consistencies', '__weakref__')
379     __slots__ = tuple()
380     _empty = ''
381
382     def __init__(self, name, doc, default=None, default_multi=None,
383                  requires=None, multi=False, callback=None,
384                  callback_params=None, validator=None, validator_params=None,
385                  properties=None, warnings_only=False):
386         """
387         :param name: the option's name
388         :param doc: the option's description
389         :param default: specifies the default value of the option,
390                         for a multi : ['bla', 'bla', 'bla']
391         :param default_multi: 'bla' (used in case of a reset to default only at
392                         a given index)
393         :param requires: is a list of names of options located anywhere
394                          in the configuration.
395         :param multi: if true, the option's value is a list
396         :param callback: the name of a function. If set, the function's output
397                          is responsible of the option's value
398         :param callback_params: the callback's parameter
399         :param validator: the name of a function which stands for a custom
400                           validation of the value
401         :param validator_params: the validator's parameters
402         :param properties: tuple of default properties
403         :param warnings_only: _validator and _consistencies don't raise if True
404                              Values()._warning contain message
405
406         """
407         super(Option, self).__init__(name, doc, default, default_multi,
408                                      requires, multi, callback,
409                                      callback_params, validator,
410                                      validator_params, properties,
411                                      warnings_only)
412
413     def impl_getrequires(self):
414         return self._requires
415
416     def _launch_consistency(self, func, option, value, context, index,
417                             all_cons_opts, warnings_only):
418         """Launch consistency now
419
420         :param func: function name, this name should start with _cons_
421         :type func: `str`
422         :param option: option that value is changing
423         :type option: `tiramisu.option.Option`
424         :param value: new value of this option
425         :param context: Config's context, if None, check default value instead
426         :type context: `tiramisu.config.Config`
427         :param index: only for multi option, consistency should be launch for
428                       specified index
429         :type index: `int`
430         :param all_cons_opts: all options concerne by this consistency
431         :type all_cons_opts: `list` of `tiramisu.option.Option`
432         :param warnings_only: specific raise error for warning
433         :type warnings_only: `boolean`
434         """
435         if context is not None:
436             descr = context.cfgimpl_get_description()
437
438         all_cons_vals = []
439         for opt in all_cons_opts:
440             #get value
441             if option == opt:
442                 opt_value = value
443             else:
444                 #if context, calculate value, otherwise get default value
445                 if context is not None:
446                     opt_value = context.getattr(
447                         descr.impl_get_path_by_opt(opt), 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 option == opt:
454                 all_cons_vals.append(opt_value)
455             else:
456                 #value is not already set, could be higher index
457                 try:
458                     all_cons_vals.append(opt_value[index])
459                 except IndexError:
460                     #so return if no value
461                     return
462         getattr(self, func)(all_cons_opts, all_cons_vals, warnings_only)
463
464     def impl_validate(self, value, context=None, validate=True,
465                       force_index=None):
466         """
467         :param value: the option's value
468         :param context: Config's context
469         :type context: :class:`tiramisu.config.Config`
470         :param validate: if true enables ``self._validator`` validation
471         :type validate: boolean
472         :param force_index: if multi, value has to be a list
473                                not if force_index is not None
474         :type force_index: integer
475         """
476         if not validate:
477             return
478
479         def val_validator(val):
480             if self._validator is not None:
481                 if self._validator_params is not None:
482                     validator_params = {}
483                     for val_param, values in self._validator_params.items():
484                         validator_params[val_param] = values
485                     #FIXME ... ca sert à quoi ...
486                     if '' in validator_params:
487                         lst = list(validator_params[''])
488                         lst.insert(0, val)
489                         validator_params[''] = tuple(lst)
490                     else:
491                         validator_params[''] = (val,)
492                 else:
493                     validator_params = {'': (val,)}
494                 # Raise ValueError if not valid
495                 carry_out_calculation(self, config=context,
496                                       callback=self._validator,
497                                       callback_params=validator_params)
498
499         def do_validation(_value, _index=None):
500             if _value is None:
501                 return
502             # option validation
503             try:
504                 self._validate(_value)
505             except ValueError as err:
506                 log.debug('do_validation: value: {0} index: {1}'.format(
507                     _value, _index), exc_info=True)
508                 raise ValueError(_('invalid value for option {0}: {1}'
509                                    '').format(self.impl_getname(), err))
510             error = None
511             warning = None
512             try:
513                 # valid with self._validator
514                 val_validator(_value)
515                 # if not context launch consistency validation
516                 if context is not None:
517                     descr._valid_consistency(self, _value, context, _index)
518                 self._second_level_validation(_value, self._warnings_only)
519             except ValueError as error:
520                 log.debug(_('do_validation for {0}: error in value').format(
521                     self.impl_getname()), exc_info=True)
522                 if self._warnings_only:
523                     warning = error
524                     error = None
525             except ValueWarning as warning:
526                 log.debug(_('do_validation for {0}: warning in value').format(
527                     self.impl_getname()), exc_info=True)
528                 pass
529             if error is None and warning is None:
530                 try:
531                     # if context launch consistency validation
532                     if context is not None:
533                         descr._valid_consistency(self, _value, context, _index)
534                 except ValueError as error:
535                     log.debug(_('do_validation for {0}: error in consistency').format(
536                         self.impl_getname()), exc_info=True)
537                     pass
538                 except ValueWarning as warning:
539                     log.debug(_('do_validation for {0}: warning in consistency').format(
540                         self.impl_getname()), exc_info=True)
541                     pass
542             if warning:
543                 msg = _("warning on the value of the option {0}: {1}").format(
544                     self.impl_getname(), warning)
545                 warnings.warn_explicit(ValueWarning(msg, self),
546                                        ValueWarning,
547                                        self.__class__.__name__, 0)
548             elif error:
549                 raise ValueError(_("invalid value for option {0}: {1}").format(
550                     self._name, error))
551
552         # generic calculation
553         if context is not None:
554             descr = context.cfgimpl_get_description()
555
556         if not self._multi or force_index is not None:
557             do_validation(value, force_index)
558         else:
559             if not isinstance(value, list):
560                 raise ValueError(_("invalid value {0} for option {1} which must be a list").format(value, self.impl_getname()))
561             for index, val in enumerate(value):
562                 do_validation(val, index)
563
564     def impl_getdefault(self):
565         "accessing the default value"
566         if isinstance(self._default, list):
567             return copy(self._default)
568         return self._default
569
570     def impl_getdefault_multi(self):
571         "accessing the default value for a multi"
572         return self._default_multi
573
574     def impl_is_master_slaves(self, type_='both'):
575         """FIXME
576         """
577         try:
578             self._master_slaves
579             if type_ in ('both', 'master') and \
580                     self._master_slaves.is_master(self):
581                 return True
582             if type_ in ('both', 'slave') and \
583                     not self._master_slaves.is_master(self):
584                 return True
585         except:
586             pass
587         return False
588
589     def impl_get_master_slaves(self):
590         return self._master_slaves
591
592     def impl_is_empty_by_default(self):
593         "no default value has been set yet"
594         if ((not self.impl_is_multi() and self._default is None) or
595                 (self.impl_is_multi() and (self._default == []
596                                            or None in self._default))):
597             return True
598         return False
599
600     def impl_getdoc(self):
601         "accesses the Option's doc"
602         return self.impl_get_information('doc')
603
604     def impl_has_callback(self):
605         "to know if a callback has been defined or not"
606         if self._callback is None:
607             return False
608         else:
609             return True
610
611     def impl_get_callback(self):
612         return self._callback, self._callback_params
613
614     #def impl_getkey(self, value):
615     #    return value
616
617     def impl_is_multi(self):
618         return self._multi
619
620     def impl_add_consistency(self, func, *other_opts, **params):
621         """Add consistency means that value will be validate with other_opts
622         option's values.
623
624         :param func: function's name
625         :type func: `str`
626         :param other_opts: options used to validate value
627         :type other_opts: `list` of `tiramisu.option.Option`
628         :param params: extra params (only warnings_only are allowed)
629         """
630         if self.impl_is_readonly():
631             raise AttributeError(_("'{0}' ({1}) cannont add consistency, option is"
632                                    " read-only").format(
633                                        self.__class__.__name__,
634                                        self._name))
635         warnings_only = params.get('warnings_only', False)
636         for opt in other_opts:
637             if not isinstance(opt, Option):
638                 raise ConfigError(_('consistency must be set with an option'))
639             if self is opt:
640                 raise ConfigError(_('cannot add consistency with itself'))
641             if self.impl_is_multi() != opt.impl_is_multi():
642                 raise ConfigError(_('every options in consistency must be '
643                                     'multi or none'))
644         func = '_cons_{0}'.format(func)
645         all_cons_opts = tuple([self] + list(other_opts))
646         value = self.impl_getdefault()
647         if value is not None:
648             if self.impl_is_multi():
649                 for idx, val in enumerate(value):
650                     self._launch_consistency(func, self, val, None,
651                                              idx, all_cons_opts, warnings_only)
652             else:
653                 self._launch_consistency(func, self, value, None,
654                                          None, all_cons_opts, warnings_only)
655         self._add_consistency(func, all_cons_opts, params)
656         self.impl_validate(self.impl_getdefault())
657
658     def _cons_not_equal(self, opts, vals, warnings_only):
659         for idx_inf, val_inf in enumerate(vals):
660             for idx_sup, val_sup in enumerate(vals[idx_inf + 1:]):
661                 if val_inf == val_sup is not None:
662                     if warnings_only:
663                         msg = _("same value for {0} and {1}, should be different")
664                     else:
665                         msg = _("same value for {0} and {1}, must be different")
666                     raise ValueError(msg.format(opts[idx_inf].impl_getname(),
667                                                 opts[idx_inf + idx_sup + 1].impl_getname()))
668
669     def _impl_convert_callbacks(self, descr, load=False):
670         if not load and self._callback is None:
671             self._state_callback = None
672             self._state_callback_params = None
673         elif load and self._state_callback is None:
674             self._callback = None
675             self._callback_params = None
676             del(self._state_callback)
677             del(self._state_callback_params)
678         else:
679             if load:
680                 callback = self._state_callback
681                 callback_params = self._state_callback_params
682             else:
683                 callback = self._callback
684                 callback_params = self._callback_params
685                 self._state_callback_params = None
686             if callback_params is not None:
687                 cllbck_prms = {}
688                 for key, values in callback_params.items():
689                     vls = []
690                     for value in values:
691                         if isinstance(value, tuple):
692                             if load:
693                                 value = (descr.impl_get_opt_by_path(value[0]),
694                                          value[1])
695                             else:
696                                 value = (descr.impl_get_path_by_opt(value[0]),
697                                          value[1])
698                         vls.append(value)
699                     cllbck_prms[key] = tuple(vls)
700             else:
701                 cllbck_prms = None
702
703             if load:
704                 del(self._state_callback)
705                 del(self._state_callback_params)
706                 self._callback = callback
707                 self._callback_params = cllbck_prms
708             else:
709                 self._state_callback = callback
710                 self._state_callback_params = cllbck_prms
711
712     # serialize/unserialize
713     def _impl_convert_consistencies(self, descr, load=False):
714         """during serialization process, many things have to be done.
715         one of them is the localisation of the options.
716         The paths are set once for all.
717
718         :type descr: :class:`tiramisu.option.OptionDescription`
719         :param load: `True` if we are at the init of the option description
720         :type load: bool
721         """
722         if not load and self._consistencies is None:
723             self._state_consistencies = None
724         elif load and self._state_consistencies is None:
725             self._consistencies = None
726             del(self._state_consistencies)
727         else:
728             if load:
729                 consistencies = self._state_consistencies
730             else:
731                 consistencies = self._consistencies
732             new_value = []
733             for consistency in consistencies:
734                 values = []
735                 for obj in consistency[1]:
736                     if load:
737                         values.append(descr.impl_get_opt_by_path(obj))
738                     else:
739                         values.append(descr.impl_get_path_by_opt(obj))
740                 new_value.append((consistency[0], tuple(values), consistency[2]))
741             if load:
742                 del(self._state_consistencies)
743                 self._consistencies = new_value
744             else:
745                 self._state_consistencies = new_value
746
747     def _second_level_validation(self, value, warnings_only):
748         pass
749
750
751 def validate_requires_arg(requires, name):
752     """check malformed requirements
753     and tranform dict to internal tuple
754
755     :param requires: have a look at the
756                      :meth:`tiramisu.setting.Settings.apply_requires` method to
757                      know more about
758                      the description of the requires dictionary
759     """
760     if requires is None:
761         return None, None
762     ret_requires = {}
763     config_action = {}
764
765     # start parsing all requires given by user (has dict)
766     # transforme it to a tuple
767     for require in requires:
768         if not type(require) == dict:
769             raise ValueError(_("malformed requirements type for option:"
770                                " {0}, must be a dict").format(name))
771         valid_keys = ('option', 'expected', 'action', 'inverse', 'transitive',
772                       'same_action')
773         unknown_keys = frozenset(require.keys()) - frozenset(valid_keys)
774         if unknown_keys != frozenset():
775             raise ValueError('malformed requirements for option: {0}'
776                              ' unknown keys {1}, must only '
777                              '{2}'.format(name,
778                                           unknown_keys,
779                                           valid_keys))
780         # prepare all attributes
781         try:
782             option = require['option']
783             expected = require['expected']
784             action = require['action']
785         except KeyError:
786             raise ValueError(_("malformed requirements for option: {0}"
787                                " require must have option, expected and"
788                                " action keys").format(name))
789         if action == 'force_store_value':
790             raise ValueError(_("malformed requirements for option: {0}"
791                                " action cannot be force_store_value"
792                                ).format(name))
793         inverse = require.get('inverse', False)
794         if inverse not in [True, False]:
795             raise ValueError(_('malformed requirements for option: {0}'
796                                ' inverse must be boolean'))
797         transitive = require.get('transitive', True)
798         if transitive not in [True, False]:
799             raise ValueError(_('malformed requirements for option: {0}'
800                                ' transitive must be boolean'))
801         same_action = require.get('same_action', True)
802         if same_action not in [True, False]:
803             raise ValueError(_('malformed requirements for option: {0}'
804                                ' same_action must be boolean'))
805
806         if not isinstance(option, Option):
807             raise ValueError(_('malformed requirements '
808                                'must be an option in option {0}').format(name))
809         if option.impl_is_multi():
810             raise ValueError(_('malformed requirements option {0} '
811                                'must not be a multi').format(name))
812         if expected is not None:
813             try:
814                 option._validate(expected)
815             except ValueError as err:
816                 raise ValueError(_('malformed requirements second argument '
817                                    'must be valid for option {0}'
818                                    ': {1}').format(name, err))
819         if action in config_action:
820             if inverse != config_action[action]:
821                 raise ValueError(_("inconsistency in action types"
822                                    " for option: {0}"
823                                    " action: {1}").format(name, action))
824         else:
825             config_action[action] = inverse
826         if action not in ret_requires:
827             ret_requires[action] = {}
828         if option not in ret_requires[action]:
829             ret_requires[action][option] = (option, [expected], action,
830                                             inverse, transitive, same_action)
831         else:
832             ret_requires[action][option][1].append(expected)
833     # transform dict to tuple
834     ret = []
835     for opt_requires in ret_requires.values():
836         ret_action = []
837         for require in opt_requires.values():
838             ret_action.append((require[0], tuple(require[1]), require[2],
839                                require[3], require[4], require[5]))
840         ret.append(tuple(ret_action))
841     return frozenset(config_action.keys()), tuple(ret)
842
843
844 class SymLinkOption(OnlyOption):
845     #FIXME : et avec sqlalchemy ca marche vraiment ?
846     __slots__ = ('_opt', '_state_opt')
847     #not return _opt consistencies
848     #_consistencies = None
849
850     def __init__(self, name, opt):
851         self._name = name
852         if not isinstance(opt, Option):
853             raise ValueError(_('malformed symlinkoption '
854                                'must be an option '
855                                'for symlink {0}').format(name))
856         self._opt = opt
857         self._readonly = True
858         return super(Base, self).__init__()
859
860     def __getattr__(self, name):
861         if name in ('_opt', '_opt_type', '_readonly', 'impl_getname'):
862             return object.__getattr__(self, name)
863         else:
864             return getattr(self._opt, name)
865
866     def _impl_getstate(self, descr):
867         super(SymLinkOption, self)._impl_getstate(descr)
868         self._state_opt = descr.impl_get_path_by_opt(self._opt)
869
870     def _impl_setstate(self, descr):
871         self._opt = descr.impl_get_opt_by_path(self._state_opt)
872         del(self._state_opt)
873         super(SymLinkOption, self)._impl_setstate(descr)
874
875     def impl_get_information(self, key, default=undefined):
876         return self._opt.impl_get_information(key, default)