reorganise Base and Option
[tiramisu.git] / tiramisu / option / optiondescription.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 from copy import copy
22 import re
23
24
25 from ..i18n import _
26 from ..setting import groups, undefined, owners  # , log
27 from .baseoption import BaseOption, SymLinkOption, Option, ALLOWED_CONST_LIST
28 from . import MasterSlaves
29 from ..error import ConfigError, ConflictError
30 from ..autolib import carry_out_calculation
31
32
33 name_regexp = re.compile(r'^[a-zA-Z\d\-_]*$')
34
35 import sys
36 if sys.version_info[0] >= 3:  # pragma: no cover
37     xrange = range
38 del(sys)
39
40
41 class CacheOptionDescription(BaseOption):
42     __slots__ = ('_cache_paths', '_cache_consistencies', '_cache_force_store_values')
43
44     def impl_build_cache(self, config, path='', _consistencies=None,
45                          cache_option=None, force_store_values=None):
46         """validate duplicate option and set option has readonly option
47         """
48         if cache_option is None:
49             if self.impl_is_readonly():
50                 raise ConfigError(_('option description seems to be part of an other '
51                                     'config'))
52             init = True
53             _consistencies = {}
54             cache_option = []
55             force_store_values = []
56         else:
57             init = False
58         for option in self._impl_getchildren(dyn=False):
59             cache_option.append(option)
60             if path == '':
61                 subpath = option.impl_getname()
62             else:
63                 subpath = path + '.' + option.impl_getname()
64             if isinstance(option, OptionDescription):
65                 option._set_readonly(False)
66                 option.impl_build_cache(config, subpath, _consistencies,
67                                         cache_option, force_store_values)
68                 #cannot set multi option as OptionDescription requires
69             else:
70                 if option.impl_is_master_slaves('master'):
71                     if not getattr(option, '_dependencies', None):
72                         options = set()
73                     else:
74                         options = set(option._dependencies)
75                     options.add(option.impl_get_master_slaves())
76                     option._dependencies = tuple(options)
77                 option._set_readonly(True)
78                 is_multi = option.impl_is_multi()
79                 if not isinstance(option, SymLinkOption) and 'force_store_value' in option.impl_getproperties():
80                     force_store_values.append((subpath, option))
81                 for func, all_cons_opts, params in option._get_consistencies():
82                     option._valid_consistencies(all_cons_opts[1:], init=False)
83                     if func not in ALLOWED_CONST_LIST and is_multi:
84                         is_masterslaves = option.impl_is_master_slaves()
85                         if not is_masterslaves:
86                             raise ConfigError(_('malformed consistency option "{0}" '
87                                                'must be a master/slaves').format(
88                                                    option.impl_getname()))
89                         masterslaves = option.impl_get_master_slaves()
90                     for opt in all_cons_opts:
91                         if func not in ALLOWED_CONST_LIST and is_multi:
92                             if not opt.impl_is_master_slaves():
93                                 raise ConfigError(_('malformed consistency option "{0}" '
94                                                    'must not be a multi for "{1}"').format(
95                                                        option.impl_getname(), opt.impl_getname()))
96                             elif masterslaves != opt.impl_get_master_slaves():
97                                 raise ConfigError(_('malformed consistency option "{0}" '
98                                                    'must be in same master/slaves for "{1}"').format(
99                                                        option.impl_getname(), opt.impl_getname()))
100                         _consistencies.setdefault(opt,
101                                                   []).append((func,
102                                                              all_cons_opts,
103                                                              params))
104                 is_slave = None
105                 if is_multi:
106                     all_requires = option.impl_getrequires()
107                     if all_requires != tuple():
108                         for requires in all_requires:
109                             for require in requires:
110                                 #if option in require is a multi:
111                                 # * option in require must be a master or a slave
112                                 # * current option must be a slave (and only a slave)
113                                 # * option in require and current option must be in same master/slaves
114                                 for require_opt, values in require[0]:
115                                     if require_opt.impl_is_multi():
116                                         if is_slave is None:
117                                             is_slave = option.impl_is_master_slaves('slave')
118                                             if is_slave:
119                                                 masterslaves = option.impl_get_master_slaves()
120                                         if is_slave and require_opt.impl_is_master_slaves():
121                                             if masterslaves != require_opt.impl_get_master_slaves():
122                                                 raise ValueError(_('malformed requirements option {0} '
123                                                                    'must be in same master/slaves for {1}').format(
124                                                                        require_opt.impl_getname(), option.impl_getname()))
125                                         else:
126                                             raise ValueError(_('malformed requirements option {0} '
127                                                                'must not be a multi for {1}').format(
128                                                                    require_opt.impl_getname(), option.impl_getname()))
129         if init:
130             if len(cache_option) != len(set(cache_option)):
131                 for idx in xrange(1, len(cache_option) + 1):
132                     opt = cache_option.pop(0)
133                     if opt in cache_option:
134                         raise ConflictError(_('duplicate option: {0}').format(opt))
135             if _consistencies != {}:
136                 self._cache_consistencies = {}
137                 for opt, cons in _consistencies.items():
138                     if opt not in cache_option:  # pragma: optional cover
139                         raise ConfigError(_('consistency with option {0} '
140                                             'which is not in Config').format(
141                                                 opt.impl_getname()))
142                     self._cache_consistencies[opt] = tuple(cons)
143             self._cache_force_store_values = force_store_values
144             self._set_readonly(False)
145
146     def impl_already_build_caches(self):
147         return getattr(self, '_cache_paths', None) is not None
148
149     def impl_build_force_store_values(self, config, force_store_values):
150         session = config._impl_values._p_.getsession()
151         value_set = False
152         for subpath, option in self._cache_force_store_values:
153             if option.impl_is_master_slaves('slave'):
154                 # problem with index
155                 raise ConfigError(_('a slave ({0}) cannot have '
156                                     'force_store_value property').format(subpath))
157             if option._is_subdyn():
158                 raise ConfigError(_('a dynoption ({0}) cannot have '
159                                     'force_store_value property').format(subpath))
160             if force_store_values and not config._impl_values._p_.hasvalue(subpath, session):
161                 value = config.cfgimpl_get_values()._get_cached_value(option,
162                                                                       path=subpath,
163                                                                       validate=False,
164                                                                       trusted_cached_properties=False,
165                                                                       validate_properties=True)
166                 value_set = True
167                 config._impl_values._p_.setvalue(subpath, value,
168                                                  owners.forced, None, session, False)
169
170         if value_set:
171             config._impl_values._p_.commit()
172
173     def impl_build_cache_option(self, _currpath=None, cache_path=None,
174                                 cache_option=None):
175
176         if self.impl_is_readonly() or (_currpath is None and getattr(self, '_cache_paths', None) is not None):
177             # cache already set
178             return
179         if _currpath is None:
180             save = True
181             _currpath = []
182         else:
183             save = False
184         if cache_path is None:
185             cache_path = []
186             cache_option = []
187         for option in self._impl_getchildren(dyn=False):
188             attr = option.impl_getname()
189             path = str('.'.join(_currpath + [attr]))
190             cache_option.append(option)
191             cache_path.append(path)
192             if option.impl_is_optiondescription():
193                 _currpath.append(attr)
194                 option.impl_build_cache_option(_currpath, cache_path,
195                                                cache_option)
196                 _currpath.pop()
197         if save:
198             _setattr = object.__setattr__
199             _setattr(self, '_cache_paths', (tuple(cache_option), tuple(cache_path)))
200
201
202 class OptionDescriptionWalk(CacheOptionDescription):
203     __slots__ = ('_children',)
204
205     def impl_get_options_paths(self, bytype, byname, _subpath, only_first, context):
206         find_results = []
207
208         def _rebuild_dynpath(path, suffix, dynopt):
209             found = False
210             spath = path.split('.')
211             for length in xrange(1, len(spath)):
212                 subpath = '.'.join(spath[0:length])
213                 subopt = self.impl_get_opt_by_path(subpath)
214                 if dynopt == subopt:
215                     found = True
216                     break
217             if not found:  # pragma: no cover
218                 raise ConfigError(_('cannot find dynpath'))
219             subpath = subpath + suffix
220             for slength in xrange(length, len(spath)):
221                 subpath = subpath + '.' + spath[slength] + suffix
222             return subpath
223
224         def _filter_by_name(path, option):
225             name = option.impl_getname()
226             if option._is_subdyn():
227                 if byname.startswith(name):
228                     found = False
229                     for suffix in option._subdyn._impl_get_suffixes(
230                             context):
231                         if byname == name + suffix:
232                             found = True
233                             path = _rebuild_dynpath(path, suffix,
234                                                     option._subdyn)
235                             option = option._impl_to_dyn(
236                                 name + suffix, path)
237                             break
238                     if not found:
239                         return False
240             else:
241                 if not byname == name:
242                     return False
243             find_results.append((path, option))
244             return True
245
246         def _filter_by_type(path, option):
247             if isinstance(option, bytype):
248                 #if byname is not None, check option byname in _filter_by_name
249                 #not here
250                 if byname is None:
251                     if option._is_subdyn():
252                         name = option.impl_getname()
253                         for suffix in option._subdyn._impl_get_suffixes(
254                                 context):
255                             spath = _rebuild_dynpath(path, suffix,
256                                                      option._subdyn)
257                             find_results.append((spath, option._impl_to_dyn(
258                                 name + suffix, spath)))
259                     else:
260                         find_results.append((path, option))
261                 return True
262             return False
263
264         def _filter(path, option):
265             if bytype is not None:
266                 retval = _filter_by_type(path, option)
267                 if byname is None:
268                     return retval
269             if byname is not None:
270                 return _filter_by_name(path, option)
271
272         opts, paths = self._cache_paths
273         for index in xrange(0, len(paths)):
274             option = opts[index]
275             if option.impl_is_optiondescription():
276                 continue
277             path = paths[index]
278             if _subpath is not None and not path.startswith(_subpath + '.'):
279                 continue
280             if bytype == byname is None:
281                 if option._is_subdyn():
282                     name = option.impl_getname()
283                     for suffix in option._subdyn._impl_get_suffixes(
284                             context):
285                         spath = _rebuild_dynpath(path, suffix,
286                                                  option._subdyn)
287                         find_results.append((spath, option._impl_to_dyn(
288                             name + suffix, spath)))
289                 else:
290                     find_results.append((path, option))
291             else:
292                 if _filter(path, option) is False:
293                     continue
294             if only_first:
295                 return find_results
296         return find_results
297
298     def _impl_st_getchildren(self, context, only_dyn=False):
299         for child in self._children[1]:
300             if only_dyn is False or child.impl_is_dynoptiondescription():
301                 yield(child)
302
303     def _getattr(self, name, suffix=undefined, context=undefined, dyn=True):
304         error = False
305         if suffix is not undefined:
306             if undefined in [suffix, context]:  # pragma: no cover
307                 raise ConfigError(_("suffix and context needed if "
308                                     "it's a dyn option"))
309             if name.endswith(suffix):
310                 oname = name[:-len(suffix)]
311                 child = self._children[1][self._children[0].index(oname)]
312                 return self._impl_get_dynchild(child, suffix)
313             else:
314                 error = True
315         else:
316             if name in self._children[0]:
317                 child = self._children[1][self._children[0].index(name)]
318                 if dyn and child.impl_is_dynoptiondescription():
319                     error = True
320                 else:
321                     return child
322             else:
323                 child = self._impl_search_dynchild(name, context=context)
324                 if child != []:
325                     return child
326                 error = True
327         if error:
328             raise AttributeError(_('unknown Option {0} '
329                                    'in OptionDescription {1}'
330                                    '').format(name, self.impl_getname()))
331
332     def impl_getpaths(self, include_groups=False, _currpath=None):
333         """returns a list of all paths in self, recursively
334            _currpath should not be provided (helps with recursion)
335         """
336         return _impl_getpaths(self, include_groups, _currpath)
337
338     def impl_get_opt_by_path(self, path):
339         if getattr(self, '_cache_paths', None) is None:
340             raise ConfigError(_('use impl_get_opt_by_path only with root OptionDescription'))
341         if path not in self._cache_paths[1]:
342             raise AttributeError(_('no option for path {0}').format(path))
343         return self._cache_paths[0][self._cache_paths[1].index(path)]
344
345     def impl_get_path_by_opt(self, opt):
346         if getattr(self, '_cache_paths', None) is None:
347             raise ConfigError(_('use impl_get_path_by_opt only with root OptionDescription'))
348         if opt not in self._cache_paths[0]:
349             raise AttributeError(_('no option {0} found').format(opt))
350         return self._cache_paths[1][self._cache_paths[0].index(opt)]
351
352     def _impl_getchildren(self, dyn=True, context=undefined):
353         for child in self._impl_st_getchildren(context):
354             cname = child.impl_getname()
355             if dyn and child.impl_is_dynoptiondescription():
356                 path = cname
357                 for value in child._impl_get_suffixes(context):
358                     yield SynDynOptionDescription(child,
359                                                   cname + value,
360                                                   path + value, value)
361             else:
362                 yield child
363
364     def impl_getchildren(self):
365         return list(self._impl_getchildren())
366
367     def __getattr__(self, name, context=undefined):
368         if name.startswith('_'):  # or name.startswith('impl_'):
369             return object.__getattribute__(self, name)
370         if '.' in name:
371             path = name.split('.')[0]
372             subpath = '.'.join(name.split('.')[1:])
373             return self.__getattr__(path, context=context).__getattr__(subpath, context=context)
374         return self._getattr(name, context=context)
375
376     def _impl_search_dynchild(self, name, context):
377         ret = []
378         for child in self._impl_st_getchildren(context, only_dyn=True):
379             cname = child.impl_getname()
380             if name.startswith(cname):
381                 path = cname
382                 for value in child._impl_get_suffixes(context):
383                     if name == cname + value:
384                         return SynDynOptionDescription(child, name, path + value, value)
385         return ret
386
387     def _impl_get_dynchild(self, child, suffix):
388         name = child.impl_getname() + suffix
389         path = self.impl_getname() + suffix + '.' + name
390         if isinstance(child, OptionDescription):
391             return SynDynOptionDescription(child, name, path, suffix)
392         else:
393             return child._impl_to_dyn(name, path)
394
395
396 class OptionDescription(OptionDescriptionWalk):
397     """Config's schema (organisation, group) and container of Options
398     The `OptionsDescription` objects lives in the `tiramisu.config.Config`.
399     """
400     __slots__ = ('_group_type',)
401
402     def __init__(self, name, doc, children, requires=None, properties=None):
403         """
404         :param children: a list of options (including optiondescriptions)
405
406         """
407         super(OptionDescription, self).__init__(name, doc=doc,
408                                                 requires=requires,
409                                                 properties=properties)
410         child_names = []
411         dynopt_names = []
412         for child in children:
413             name = child.impl_getname()
414             child_names.append(name)
415             if isinstance(child, DynOptionDescription):
416                 dynopt_names.append(name)
417
418         #better performance like this
419         valid_child = copy(child_names)
420         valid_child.sort()
421         old = None
422         for child in valid_child:
423             if child == old:  # pragma: optional cover
424                 raise ConflictError(_('duplicate option name: '
425                                       '{0}').format(child))
426             if dynopt_names:
427                 for dynopt in dynopt_names:
428                     if child != dynopt and child.startswith(dynopt):
429                         raise ConflictError(_('option must not start as '
430                                               'dynoptiondescription'))
431             old = child
432         _setattr = object.__setattr__
433         _setattr(self, '_children', (tuple(child_names), tuple(children)))
434         _setattr(self, '_cache_consistencies', None)
435         # the group_type is useful for filtering OptionDescriptions in a config
436         _setattr(self, '_group_type', groups.default)
437
438     def impl_getdoc(self):
439         return self.impl_get_information('doc')
440
441     def impl_validate(self, *args, **kwargs):
442         """usefull for OptionDescription"""
443         pass
444
445     # ____________________________________________________________
446     def impl_set_group_type(self, group_type):
447         """sets a given group object to an OptionDescription
448
449         :param group_type: an instance of `GroupType` or `MasterGroupType`
450                               that lives in `setting.groups`
451         """
452         if self._group_type != groups.default:  # pragma: optional cover
453             raise TypeError(_('cannot change group_type if already set '
454                             '(old {0}, new {1})').format(self._group_type,
455                                                          group_type))
456         if isinstance(group_type, groups.GroupType):
457             self._group_type = group_type
458             if isinstance(group_type, groups.MasterGroupType):
459                 children = self.impl_getchildren()
460                 for child in children:
461                     if isinstance(child, SymLinkOption):  # pragma: optional cover
462                         raise ValueError(_("master group {0} shall not have "
463                                            "a symlinkoption").format(self.impl_getname()))
464                     if not isinstance(child, Option):  # pragma: optional cover
465                         raise ValueError(_("master group {0} shall not have "
466                                            "a subgroup").format(self.impl_getname()))
467                     if not child.impl_is_multi():  # pragma: optional cover
468                         raise ValueError(_("not allowed option {0} "
469                                            "in group {1}"
470                                            ": this option is not a multi"
471                                            "").format(child.impl_getname(), self.impl_getname()))
472                 #length of master change slaves length
473                 self._set_has_dependency()
474                 MasterSlaves(self.impl_getname(), children)
475         else:  # pragma: optional cover
476             raise ValueError(_('group_type: {0}'
477                                ' not allowed').format(group_type))
478
479     def impl_get_group_type(self):
480         return self._group_type
481
482     def __getstate__(self):
483         raise NotImplementedError()
484
485     def _impl_get_suffixes(self, context):
486         callback, callback_params = self.impl_get_callback()
487         values = carry_out_calculation(self, context=context,
488                                        callback=callback,
489                                        callback_params=callback_params)
490         if len(values) > len(set(values)):
491             raise ConfigError(_('DynOptionDescription callback return not unique value'))
492         for val in values:
493             if not isinstance(val, str) or re.match(name_regexp, val) is None:
494                 raise ValueError(_("invalid suffix: {0} for option").format(val))
495         return values
496
497
498 class DynOptionDescription(OptionDescription):
499     def __init__(self, name, doc, children, requires=None, properties=None,
500                  callback=None, callback_params=None):
501         super(DynOptionDescription, self).__init__(name, doc, children,
502                                                    requires, properties)
503         for child in children:
504             if isinstance(child, OptionDescription):
505                 if child.impl_get_group_type() != groups.master:
506                     raise ConfigError(_('cannot set optiondescription in a '
507                                         'dynoptiondescription'))
508                 for chld in child._impl_getchildren():
509                     chld._impl_setsubdyn(self)
510             if isinstance(child, SymLinkOption):
511                 raise ConfigError(_('cannot set symlinkoption in a '
512                                     'dynoptiondescription'))
513             child._impl_setsubdyn(self)
514         self.impl_set_callback(callback, callback_params)
515
516     def _validate_callback(self, callback, callback_params):
517         if callback is None:
518             raise ConfigError(_('callback is mandatory for dynoptiondescription'))
519
520
521 class SynDynOptionDescription(object):
522     __slots__ = ('_opt', '_name', '_path', '_suffix')
523
524     def __init__(self, opt, name, path, suffix):
525         self._opt = opt
526         self._name = name
527         self._path = path
528         self._suffix = suffix
529
530     def __getattr__(self, name, context=undefined):
531         if name in dir(self._opt):
532             return getattr(self._opt, name)
533         return self._opt._getattr(name, suffix=self._suffix, context=context)
534
535     def impl_getname(self):
536         return self._name
537
538     def _impl_getchildren(self, dyn=True, context=undefined):
539         children = []
540         for child in self._opt._impl_getchildren():
541             yield(self._opt._impl_get_dynchild(child, self._suffix))
542
543     def impl_getchildren(self):
544         return self._impl_getchildren()
545
546     def impl_getpath(self, context):
547         return self._path
548
549     def impl_getpaths(self, include_groups=False, _currpath=None):
550         return _impl_getpaths(self, include_groups, _currpath)
551
552     def _impl_getopt(self):
553         return self._opt
554
555
556 def _impl_getpaths(klass, include_groups, _currpath):
557     """returns a list of all paths in klass, recursively
558        _currpath should not be provided (helps with recursion)
559     """
560     if _currpath is None:
561         _currpath = []
562     paths = []
563     for option in klass._impl_getchildren():
564         attr = option.impl_getname()
565         if option.impl_is_optiondescription():
566             if include_groups:
567                 paths.append('.'.join(_currpath + [attr]))
568             paths += option.impl_getpaths(include_groups=include_groups,
569                                           _currpath=_currpath + [attr])
570         else:
571             paths.append('.'.join(_currpath + [attr]))
572     return paths