separate baseoption and option
[tiramisu.git] / tiramisu / option / baseoption.py
1 # -*- coding: utf-8 -*-
2 # Copyright (C) 2014-2017 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 sys
24
25 from ..i18n import _
26 from ..setting import undefined
27 from ..error import ConfigError
28
29 if sys.version_info[0] >= 3:  # pragma: no cover
30     from inspect import signature
31 else:
32     from inspect import getargspec
33
34 STATIC_TUPLE = tuple()
35
36
37 submulti = 2
38 NAME_REGEXP = re.compile(r'^[a-z][a-zA-Z\d_]*$')
39 FORBIDDEN_NAMES = frozenset(['iter_all', 'iter_group', 'find', 'find_first',
40                              'make_dict', 'unwrap_from_path', 'read_only',
41                              'read_write', 'getowner', 'set_contexts'])
42
43
44 def valid_name(name):
45     """an option's name is a str and does not start with 'impl' or 'cfgimpl'
46     and name is not a function name"""
47     if not isinstance(name, str):
48         return False
49     return re.match(NAME_REGEXP, name) is not None and \
50             name not in FORBIDDEN_NAMES and \
51             not name.startswith('impl_') and \
52             not name.startswith('cfgimpl_')
53
54
55 def validate_callback(callback, callback_params, type_, callbackoption):
56     """validate function and parameter set for callback, validation, ...
57     """
58     def _validate_option(option):
59         #validate option
60         if hasattr(option, '_is_symlinkoption'):
61             if option._is_symlinkoption():
62                 cur_opt = option._impl_getopt()
63             else:
64                 cur_opt = option
65         else:
66             raise ValueError(_('{}_params must have an option'
67                                ' not a {} for first argument'
68                               ).format(type_, type(option)))
69         if cur_opt != callbackoption:
70             cur_opt._add_dependencies(callbackoption)
71
72     def _validate_force_permissive(force_permissive):
73         #validate force_permissive
74         if not isinstance(force_permissive, bool):
75             raise ValueError(_('{}_params must have a boolean'
76                                ' not a {} for second argument'
77                               ).format(type_, type(
78                                   force_permissive)))
79
80     def _validate_callback(callbk):
81         if isinstance(callbk, tuple):
82             if len(callbk) == 1:
83                 if callbk not in ((None,), ('index',)):
84                     raise ValueError(_('{0}_params with length of '
85                                        'tuple as 1 must only have '
86                                        'None as first value').format(type_))
87                 return
88             elif len(callbk) != 2:
89                 raise ValueError(_('{0}_params must only have 1 or 2 '
90                                    'as length').format(type_))
91             option, force_permissive = callbk
92             _validate_option(option)
93             _validate_force_permissive(force_permissive)
94
95     if not isinstance(callback, FunctionType):
96         raise ValueError(_('{0} must be a function').format(type_))
97     if callback_params is not None:
98         if not isinstance(callback_params, dict):
99             raise ValueError(_('{0}_params must be a dict').format(type_))
100         for key, callbacks in callback_params.items():
101             if key != '' and len(callbacks) != 1:
102                 raise ValueError(_("{0}_params with key {1} mustn't have "
103                                    "length different to 1").format(type_,
104                                                                    key))
105             if not isinstance(callbacks, tuple):
106                 raise ValueError(_('{0}_params must be tuple for key "{1}"'
107                                   ).format(type_, key))
108             for callbk in callbacks:
109                 _validate_callback(callbk)
110
111
112 #____________________________________________________________
113 #
114 class Base(object):
115     """Base use by all *Option* classes (Option, OptionDescription, SymLinkOption, ...)
116     """
117     __slots__ = ('_name',
118                  '_informations',
119                  #calcul
120                  '_subdyn',
121                  '_requires',
122                  '_properties',
123                  '_calc_properties',
124                  #
125                  '_consistencies',
126                  #other
127                  '_has_dependency',
128                  '_dependencies',
129                  '__weakref__'
130                 )
131
132     def __init__(self, name, doc, requires=None, properties=None, is_multi=False):
133         if not valid_name(name):
134             raise ValueError(_("invalid name: {0} for option").format(name))
135         if requires is not None:
136             calc_properties, requires = validate_requires_arg(self, is_multi,
137                                                               requires, name)
138         else:
139             calc_properties = frozenset()
140             requires = undefined
141         if properties is None:
142             properties = tuple()
143         if not isinstance(properties, tuple):
144             raise TypeError(_('invalid properties type {0} for {1},'
145                               ' must be a tuple').format(
146                                   type(properties),
147                                   name))
148         if calc_properties != frozenset([]) and properties is not tuple():
149             set_forbidden_properties = calc_properties & set(properties)
150             if set_forbidden_properties != frozenset():
151                 raise ValueError('conflict: properties already set in '
152                                  'requirement {0}'.format(
153                                      list(set_forbidden_properties)))
154         _setattr = object.__setattr__
155         _setattr(self, '_name', name)
156         if sys.version_info[0] < 3 and isinstance(doc, str):
157             doc = doc.decode('utf8')
158         _setattr(self, '_informations', {'doc': doc})
159         if calc_properties is not undefined:
160             _setattr(self, '_calc_properties', calc_properties)
161         if requires is not undefined:
162             _setattr(self, '_requires', requires)
163         if properties is not undefined:
164             _setattr(self, '_properties', properties)
165
166     def _build_validator_params(self, validator, validator_params):
167         if sys.version_info[0] < 3:
168             func_args = getargspec(validator)
169             defaults = func_args.defaults
170             if defaults is None:
171                 defaults = []
172             args = func_args.args[0:len(func_args.args)-len(defaults)]
173         else:  # pragma: no cover
174             func_params = signature(validator).parameters
175             args = [f.name for f in func_params.values() if f.default is f.empty]
176         if validator_params is not None:
177             kwargs = list(validator_params.keys())
178             if '' in kwargs:
179                 kwargs.remove('')
180             for kwarg in kwargs:
181                 if kwarg in args:
182                     args = args[0:args.index(kwarg)]
183             len_args = len(validator_params.get('', []))
184             if len_args != 0 and len(args) >= len_args:
185                 args = args[0:len(args)-len_args]
186         if len(args) >= 2:
187             if validator_params is not None and '' in validator_params:
188                 params = list(validator_params[''])
189                 params.append((self, False))
190                 validator_params[''] = tuple(params)
191             else:
192                 if validator_params is None:
193                     validator_params = {}
194                 validator_params[''] = ((self, False),)
195         if len(args) == 3 and args[2] not in validator_params:
196             params = list(validator_params[''])
197             params.append(('index',))
198             validator_params[''] = tuple(params)
199         return validator_params
200
201     def _set_has_dependency(self):
202         if not self._is_symlinkoption():
203             self._has_dependency = True
204
205     def impl_has_dependency(self):
206         return getattr(self, '_has_dependency', False)
207
208     def impl_set_callback(self, callback, callback_params=None, _init=False):
209         if callback is None and callback_params is not None:
210             raise ValueError(_("params defined for a callback function but "
211                                "no callback defined"
212                                " yet for option {0}").format(
213                                    self.impl_getname()))
214         if not _init and self.impl_get_callback()[0] is not None:
215             raise ConfigError(_("a callback is already set for {0}, "
216                                 "cannot set another one's").format(self.impl_getname()))
217         self._validate_callback(callback, callback_params)
218         if callback is not None:
219             validate_callback(callback, callback_params, 'callback', self)
220             val = getattr(self, '_val_call', (None,))[0]
221             if callback_params is None or callback_params == {}:
222                 val_call = (callback,)
223             else:
224                 val_call = tuple([callback, callback_params])
225             self._val_call = (val, val_call)
226
227     def impl_is_optiondescription(self):
228         return self.__class__.__name__ in ['OptionDescription',
229                                            'DynOptionDescription',
230                                            'SynDynOptionDescription']
231
232     def impl_is_dynoptiondescription(self):
233         return self.__class__.__name__ in ['DynOptionDescription',
234                                            'SynDynOptionDescription']
235
236     def impl_getname(self):
237         return self._name
238
239     def impl_is_readonly(self):
240         return not isinstance(getattr(self, '_informations', dict()), dict)
241
242     def impl_getproperties(self):
243         return self._properties
244
245     def _set_readonly(self, has_extra):
246         if not self.impl_is_readonly():
247             _setattr = object.__setattr__
248             dico = self._informations
249             keys = tuple(dico.keys())
250             if len(keys) == 1:
251                 dico = dico['doc']
252             else:
253                 dico = tuple([keys, tuple(dico.values())])
254             _setattr(self, '_informations', dico)
255             if has_extra:
256                 extra = getattr(self, '_extra', None)
257                 if extra is not None:
258                     _setattr(self, '_extra', tuple([tuple(extra.keys()), tuple(extra.values())]))
259
260     def _impl_setsubdyn(self, subdyn):
261         self._subdyn = subdyn
262
263     def impl_getrequires(self):
264         return getattr(self, '_requires', STATIC_TUPLE)
265
266     def impl_get_callback(self):
267         call = getattr(self, '_val_call', (None, None))[1]
268         if call is None:
269             ret_call = (None, {})
270         elif len(call) == 1:
271             ret_call = (call[0], {})
272         else:
273             ret_call = call
274         return ret_call
275
276     # ____________________________________________________________
277     # information
278     def impl_get_information(self, key, default=undefined):
279         """retrieves one information's item
280
281         :param key: the item string (ex: "help")
282         """
283         def _is_string(infos):
284             if sys.version_info[0] >= 3:  # pragma: no cover
285                 return isinstance(infos, str)
286             else:
287                 return isinstance(infos, str) or isinstance(infos, unicode)
288
289         dico = self._informations
290         if isinstance(dico, tuple):
291             if key in dico[0]:
292                 return dico[1][dico[0].index(key)]
293         elif _is_string(dico):
294             if key == 'doc':
295                 return dico
296         elif isinstance(dico, dict):
297             if key in dico:
298                 return dico[key]
299         if default is not undefined:
300             return default
301         raise ValueError(_("information's item not found: {0}").format(
302             key))
303
304     def impl_set_information(self, key, value):
305         """updates the information's attribute
306         (which is a dictionary)
307
308         :param key: information's key (ex: "help", "doc"
309         :param value: information's value (ex: "the help string")
310         """
311         if self.impl_is_readonly():
312             raise AttributeError(_("'{0}' ({1}) object attribute '{2}' is"
313                                    " read-only").format(
314                                        self.__class__.__name__,
315                                        self,
316                                        #self.impl_getname(),
317                                        key))
318         self._informations[key] = value
319
320
321 class BaseOption(Base):
322     """This abstract base class stands for attribute access
323     in options that have to be set only once, it is of course done in the
324     __setattr__ method
325     """
326     __slots__ = tuple()
327
328     def __getstate__(self):
329         raise NotImplementedError()
330
331     def __setattr__(self, name, value):
332         """set once and only once some attributes in the option,
333         like `_name`. `_name` cannot be changed one the option and
334         pushed in the :class:`tiramisu.option.OptionDescription`.
335
336         if the attribute `_readonly` is set to `True`, the option is
337         "frozen" (which has noting to do with the high level "freeze"
338         propertie or "read_only" property)
339         """
340         if name != '_option' and \
341                 not isinstance(value, tuple):
342             is_readonly = False
343             # never change _name dans _opt
344             if name == '_name':
345                 if self.impl_getname() is not None:
346                     #so _name is already set
347                     is_readonly = True
348             elif name != '_readonly':
349                 is_readonly = self.impl_is_readonly()
350             if is_readonly:
351                 raise AttributeError(_("'{0}' ({1}) object attribute '{2}' is"
352                                        " read-only").format(
353                                            self.__class__.__name__,
354                                            self,
355                                            #self.impl_getname(),
356                                            name))
357         super(BaseOption, self).__setattr__(name, value)
358
359     def impl_getpath(self, context):
360         return context.cfgimpl_get_description().impl_get_path_by_opt(self)
361
362     def impl_has_callback(self):
363         "to know if a callback has been defined or not"
364         return self.impl_get_callback()[0] is not None
365
366     def _is_subdyn(self):
367         return getattr(self, '_subdyn', None) is not None
368
369     def _impl_valid_unicode(self, value):
370         if sys.version_info[0] >= 3:  # pragma: no cover
371             if not isinstance(value, str):
372                 return ValueError(_('invalid string'))
373         else:
374             if not isinstance(value, unicode) and not isinstance(value, str):
375                 return ValueError(_('invalid unicode or string'))
376
377     def impl_get_display_name(self, dyn_name=None):
378         name = self.impl_getdoc()
379         if name is None or name == '':
380             if dyn_name is not None:
381                 name = dyn_name
382             else:
383                 name = self.impl_getname()
384         if sys.version_info[0] < 3 and isinstance(name, unicode):
385             name = name.encode('utf8')
386         return name
387
388     def reset_cache(self, opt, obj, type_):
389         context = obj._getcontext()
390         path = self.impl_getpath(context)
391         obj._p_.delcache(path)
392         context.cfgimpl_reset_cache(only=(type_,),
393                                     opt=self,
394                                     path=path)
395
396     def _is_symlinkoption(self):
397         return False
398
399
400 class OnlyOption(BaseOption):
401     __slots__ = tuple()
402
403
404 def validate_requires_arg(new_option, multi, requires, name):
405     """check malformed requirements
406     and tranform dict to internal tuple
407
408     :param requires: have a look at the
409                      :meth:`tiramisu.setting.Settings.apply_requires` method to
410                      know more about
411                      the description of the requires dictionary
412     """
413     def set_dependency(option):
414         if not getattr(option, '_dependencies', None):
415             options = set()
416         else:
417             options = set(option._dependencies)
418         options.add(new_option)
419         option._dependencies = tuple(options)
420
421     def get_option(require):
422         option = require['option']
423         if not hasattr(option, '_is_symlinkoption'):
424             raise ValueError(_('malformed requirements '
425                                'must be an option in option {0}').format(name))
426         if not multi and option.impl_is_multi():
427             raise ValueError(_('malformed requirements '
428                                'multi option must not set '
429                                'as requires of non multi option {0}').format(name))
430         set_dependency(option)
431         return option
432
433     def _set_expected(action, inverse, transitive, same_action, option, expected, operator):
434         if inverse not in ret_requires[action]:
435             ret_requires[action][inverse] = ([(option, [expected])], action, inverse, transitive, same_action, operator)
436         else:
437             for exp in ret_requires[action][inverse][0]:
438                 if exp[0] == option:
439                     exp[1].append(expected)
440                     break
441             else:
442                 ret_requires[action][inverse][0].append((option, [expected]))
443
444     def set_expected(require, ret_requires):
445         expected = require['expected']
446         inverse = get_inverse(require)
447         transitive = get_transitive(require)
448         same_action = get_sameaction(require)
449         operator = get_operator(require)
450         if isinstance(expected, list):
451             for exp in expected:
452                 if set(exp.keys()) != {'option', 'value'}:
453                     raise ValueError(_('malformed requirements expected must have '
454                                        'option and value for option {0}').format(name))
455                 option = exp['option']
456                 set_dependency(option)
457                 if option is not None:
458                     err = option._validate(exp['value'])
459                     if err:
460                         raise ValueError(_('malformed requirements expected value '
461                                            'must be valid for option {0}'
462                                            ': {1}').format(name, err))
463                 _set_expected(action, inverse, transitive, same_action, option, exp['value'], operator)
464         else:
465             option = get_option(require)
466             if expected is not None:
467                 err = option._validate(expected)
468                 if err:
469                     raise ValueError(_('malformed requirements expected value '
470                                        'must be valid for option {0}'
471                                        ': {1}').format(name, err))
472             _set_expected(action, inverse, transitive, same_action, option, expected, operator)
473
474     def get_action(require):
475         action = require['action']
476         if action == 'force_store_value':
477             raise ValueError(_("malformed requirements for option: {0}"
478                                " action cannot be force_store_value"
479                                ).format(name))
480         return action
481
482     def get_inverse(require):
483         inverse = require.get('inverse', False)
484         if inverse not in [True, False]:
485             raise ValueError(_('malformed requirements for option: {0}'
486                                ' inverse must be boolean'))
487         return inverse
488
489     def get_transitive(require):
490         transitive = require.get('transitive', True)
491         if transitive not in [True, False]:
492             raise ValueError(_('malformed requirements for option: {0}'
493                                ' transitive must be boolean'))
494         return transitive
495
496     def get_sameaction(require):
497         same_action = require.get('same_action', True)
498         if same_action not in [True, False]:
499             raise ValueError(_('malformed requirements for option: {0}'
500                                ' same_action must be boolean'))
501         return same_action
502
503     def get_operator(require):
504         operator = require.get('operator', 'or')
505         if operator not in ['and', 'or']:
506             raise ValueError(_('malformed requirements for option: "{0}"'
507                                ' operator must be "or" or "and"').format(operator))
508         return operator
509
510
511     ret_requires = {}
512     config_action = set()
513
514     # start parsing all requires given by user (has dict)
515     # transforme it to a tuple
516     for require in requires:
517         if not isinstance(require, dict):
518             raise ValueError(_("malformed requirements type for option:"
519                                " {0}, must be a dict").format(name))
520         valid_keys = ('option', 'expected', 'action', 'inverse', 'transitive',
521                       'same_action', 'operator')
522         unknown_keys = frozenset(require.keys()) - frozenset(valid_keys)
523         if unknown_keys != frozenset():
524             raise ValueError(_('malformed requirements for option: {0}'
525                              ' unknown keys {1}, must only '
526                              '{2}').format(name,
527                                            unknown_keys,
528                                            valid_keys))
529         # prepare all attributes
530         if not ('expected' in require and isinstance(require['expected'], list)) and \
531                 not ('option' in require and 'expected' in require) or \
532                 'action' not in require:
533             raise ValueError(_("malformed requirements for option: {0}"
534                                " require must have option, expected and"
535                                " action keys").format(name))
536         action = get_action(require)
537         config_action.add(action)
538         if action not in ret_requires:
539             ret_requires[action] = {}
540         set_expected(require, ret_requires)
541
542     # transform dict to tuple
543     ret = []
544     for requires in ret_requires.values():
545         ret_action = []
546         for require in requires.values():
547             ret_action.append((tuple(require[0]), require[1],
548                                require[2], require[3], require[4], require[5]))
549         ret.append(tuple(ret_action))
550     return frozenset(config_action), tuple(ret)
551
552
553 class SymLinkOption(OnlyOption):
554
555     def __init__(self, name, opt):
556         if not isinstance(opt, OnlyOption) or \
557                 opt._is_symlinkoption():
558             raise ValueError(_('malformed symlinkoption '
559                                'must be an option '
560                                'for symlink {0}').format(name))
561         _setattr = object.__setattr__
562         _setattr(self, '_name', name)
563         _setattr(self, '_opt', opt)
564         opt._set_has_dependency()
565
566     def _is_symlinkoption(self):
567         return True
568
569     def __getattr__(self, name, context=undefined):
570         return getattr(self._impl_getopt(), name)
571
572     def _impl_getopt(self):
573         return self._opt
574
575     def impl_get_information(self, key, default=undefined):
576         return self._impl_getopt().impl_get_information(key, default)
577
578     def impl_is_readonly(self):
579         return True
580
581     def impl_getproperties(self):
582         return self._impl_getopt()._properties
583
584     def impl_get_callback(self):
585         return self._impl_getopt().impl_get_callback()
586
587     def impl_has_callback(self):
588         "to know if a callback has been defined or not"
589         return self._impl_getopt().impl_has_callback()
590
591     def impl_is_multi(self):
592         return self._impl_getopt().impl_is_multi()
593
594     def _is_subdyn(self):
595         return getattr(self._impl_getopt(), '_subdyn', None) is not None
596
597     def _get_consistencies(self):
598         return ()
599
600     def _has_consistencies(self):
601         return False
602
603
604 class DynSymLinkOption(object):
605     __slots__ = ('_dyn', '_opt', '_name')
606
607     def __init__(self, name, opt, dyn):
608         self._name = name
609         self._dyn = dyn
610         self._opt = opt
611
612     def __getattr__(self, name, context=undefined):
613         return getattr(self._impl_getopt(), name)
614
615     def impl_getname(self):
616         return self._name
617
618     def impl_get_display_name(self):
619         return self._impl_getopt().impl_get_display_name(dyn_name=self.impl_getname())
620
621     def _impl_getopt(self):
622         return self._opt
623
624     def impl_getsuffix(self):
625         return self._dyn.split('.')[-1][len(self._impl_getopt().impl_getname()):]
626
627     def impl_getpath(self, context):
628         path = self._impl_getopt().impl_getpath(context)
629         base_path = '.'.join(path.split('.')[:-2])
630         if self.impl_is_master_slaves() and base_path is not '':
631             base_path = base_path + self.impl_getsuffix()
632         if base_path == '':
633             return self._dyn
634         else:
635             return base_path + '.' + self._dyn
636
637     def impl_validate(self, value, context=undefined, validate=True,
638                       force_index=None, force_submulti_index=None, is_multi=None,
639                       display_error=True, display_warnings=True, multi=None,
640                       setting_properties=undefined):
641         return self._impl_getopt().impl_validate(value, context, validate,
642                                                  force_index,
643                                                  force_submulti_index,
644                                                  current_opt=self,
645                                                  is_multi=is_multi,
646                                                  display_error=display_error,
647                                                  display_warnings=display_warnings,
648                                                  multi=multi,
649                                                  setting_properties=setting_properties)
650
651     def impl_is_dynsymlinkoption(self):
652         return True