coverage
[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 ..storage import get_storages_option
31 from ..autolib import carry_out_calculation
32
33
34 StorageOptionDescription = get_storages_option('optiondescription')
35
36 name_regexp = re.compile(r'^[a-zA-Z\d\-_]*$')
37
38 import sys
39 if sys.version_info[0] >= 3:  # pragma: no cover
40     xrange = range
41 del(sys)
42
43
44 class OptionDescription(BaseOption, StorageOptionDescription):
45     """Config's schema (organisation, group) and container of Options
46     The `OptionsDescription` objects lives in the `tiramisu.config.Config`.
47     """
48     __slots__ = tuple()
49
50     def __init__(self, name, doc, children, requires=None, properties=None):
51         """
52         :param children: a list of options (including optiondescriptions)
53
54         """
55         super(OptionDescription, self).__init__(name, doc=doc,
56                                                 requires=requires,
57                                                 properties=properties,
58                                                 callback=False)
59         child_names = []
60         dynopt_names = []
61         for child in children:
62             name = child.impl_getname()
63             child_names.append(name)
64             if isinstance(child, DynOptionDescription):
65                 dynopt_names.append(name)
66
67         #better performance like this
68         valid_child = copy(child_names)
69         valid_child.sort()
70         old = None
71         for child in valid_child:
72             if child == old:  # pragma: optional cover
73                 raise ConflictError(_('duplicate option name: '
74                                       '{0}').format(child))
75             if dynopt_names:
76                 for dynopt in dynopt_names:
77                     if child != dynopt and child.startswith(dynopt):
78                         raise ConflictError(_('option must not start as '
79                                               'dynoptiondescription'))
80             old = child
81         self._add_children(child_names, children)
82         _setattr = object.__setattr__
83         _setattr(self, '_cache_consistencies', None)
84         # the group_type is useful for filtering OptionDescriptions in a config
85         _setattr(self, '_group_type', groups.default)
86
87     def impl_getdoc(self):
88         return self.impl_get_information('doc')
89
90     def impl_validate(self, *args, **kwargs):
91         """usefull for OptionDescription"""
92         pass
93
94     def impl_getpaths(self, include_groups=False, _currpath=None):
95         """returns a list of all paths in self, recursively
96            _currpath should not be provided (helps with recursion)
97         """
98         return _impl_getpaths(self, include_groups, _currpath)
99
100     def impl_build_cache(self, config, path='', _consistencies=None,
101                          cache_option=None, force_store_values=None):
102         """validate duplicate option and set option has readonly option
103         """
104         if cache_option is None:
105             if self.impl_is_readonly():
106                 raise ConfigError(_('option description seems to be part of an other '
107                                     'config'))
108             init = True
109             _consistencies = {}
110             cache_option = []
111             force_store_values = []
112         else:
113             init = False
114         for option in self._impl_getchildren(dyn=False):
115             #FIXME specifique id for sqlalchemy?
116             #FIXME avec sqlalchemy ca marche le multi parent ? (dans des configs diffĂ©rentes)
117             cache_option.append(option._get_id())
118             if path == '':
119                 subpath = option.impl_getname()
120             else:
121                 subpath = path + '.' + option.impl_getname()
122             if isinstance(option, OptionDescription):
123                 option._set_readonly(False)
124                 option.impl_build_cache(config, subpath, _consistencies,
125                                         cache_option, force_store_values)
126                 #cannot set multi option as OptionDescription requires
127             else:
128                 if option.impl_is_master_slaves('master'):
129                     if not getattr(option, '_dependencies', None):
130                         options = set()
131                     else:
132                         options = set(option._dependencies)
133                     options.add(option.impl_get_master_slaves())
134                     option._dependencies = tuple(options)
135                 option._set_readonly(True)
136                 is_multi = option.impl_is_multi()
137                 if not isinstance(option, SymLinkOption) and 'force_store_value' in option.impl_getproperties():
138                     force_store_values.append((subpath, option))
139                 for func, all_cons_opts, params in option._get_consistencies():
140                     option._valid_consistencies(all_cons_opts[1:], init=False)
141                     if func not in allowed_const_list and is_multi:
142                         is_masterslaves = option.impl_is_master_slaves()
143                         if not is_masterslaves:
144                             raise ConfigError(_('malformed consistency option "{0}" '
145                                                'must be a master/slaves').format(
146                                                    option.impl_getname()))
147                         masterslaves = option.impl_get_master_slaves()
148                     for opt in all_cons_opts:
149                         if func not in allowed_const_list and is_multi:
150                             if not opt.impl_is_master_slaves():
151                                 raise ConfigError(_('malformed consistency option "{0}" '
152                                                    'must not be a multi for "{1}"').format(
153                                                        option.impl_getname(), opt.impl_getname()))
154                             elif masterslaves != opt.impl_get_master_slaves():
155                                 raise ConfigError(_('malformed consistency option "{0}" '
156                                                    'must be in same master/slaves for "{1}"').format(
157                                                        option.impl_getname(), opt.impl_getname()))
158                         _consistencies.setdefault(opt,
159                                                   []).append((func,
160                                                              all_cons_opts,
161                                                              params))
162                 is_slave = None
163                 if is_multi:
164                     all_requires = option.impl_getrequires()
165                     if all_requires != tuple():
166                         for requires in all_requires:
167                             for require in requires:
168                                 #if option in require is a multi:
169                                 # * option in require must be a master or a slave
170                                 # * current option must be a slave (and only a slave)
171                                 # * option in require and current option must be in same master/slaves
172                                 for require_opt, values in require[0]:
173                                     if require_opt.impl_is_multi():
174                                         if is_slave is None:
175                                             is_slave = option.impl_is_master_slaves('slave')
176                                             if is_slave:
177                                                 masterslaves = option.impl_get_master_slaves()
178                                         if is_slave and require_opt.impl_is_master_slaves():
179                                             if masterslaves != require_opt.impl_get_master_slaves():
180                                                 raise ValueError(_('malformed requirements option {0} '
181                                                                    'must be in same master/slaves for {1}').format(
182                                                                        require_opt.impl_getname(), option.impl_getname()))
183                                         else:
184                                             raise ValueError(_('malformed requirements option {0} '
185                                                                'must not be a multi for {1}').format(
186                                                                    require_opt.impl_getname(), option.impl_getname()))
187         if init:
188             if len(cache_option) != len(set(cache_option)):
189                 for idx in xrange(1, len(cache_option) + 1):
190                     opt = cache_option.pop(0)
191                     if opt in cache_option:
192                         raise ConflictError(_('duplicate option: {0}').format(opt))
193             if _consistencies != {}:
194                 self._cache_consistencies = {}
195                 for opt, cons in _consistencies.items():
196                     if opt._get_id() not in cache_option:  # pragma: optional cover
197                         raise ConfigError(_('consistency with option {0} '
198                                             'which is not in Config').format(
199                                                 opt.impl_getname()))
200                     self._cache_consistencies[opt] = tuple(cons)
201             self._cache_force_store_values = force_store_values
202             self._set_readonly(False)
203
204
205     def impl_build_force_store_values(self, config, force_store_values):
206         session = config._impl_values._p_.getsession()
207         value_set = False
208         for subpath, option in self._cache_force_store_values:
209             if option.impl_is_master_slaves('slave'):
210                 # problem with index
211                 raise ConfigError(_('a slave ({0}) cannot have '
212                                     'force_store_value property').format(subpath))
213             if option._is_subdyn():
214                 raise ConfigError(_('a dynoption ({0}) cannot have '
215                                     'force_store_value property').format(subpath))
216             if force_store_values and not config._impl_values._p_.hasvalue(subpath, session):
217                 value = config.cfgimpl_get_values()._get_cached_value(option,
218                                                                       path=subpath,
219                                                                       validate=False,
220                                                                       trusted_cached_properties=False,
221                                                                       validate_properties=True)
222                 value_set = True
223                 config._impl_values._p_.setvalue(subpath, value,
224                                                  owners.forced, None, session, False)
225
226         if value_set:
227             config._impl_values._p_.commit()
228
229     # ____________________________________________________________
230     def impl_set_group_type(self, group_type):
231         """sets a given group object to an OptionDescription
232
233         :param group_type: an instance of `GroupType` or `MasterGroupType`
234                               that lives in `setting.groups`
235         """
236         if self._group_type != groups.default:  # pragma: optional cover
237             raise TypeError(_('cannot change group_type if already set '
238                             '(old {0}, new {1})').format(self._group_type,
239                                                          group_type))
240         if isinstance(group_type, groups.GroupType):
241             self._group_type = group_type
242             if isinstance(group_type, groups.MasterGroupType):
243                 children = self.impl_getchildren()
244                 for child in children:
245                     if isinstance(child, SymLinkOption):  # pragma: optional cover
246                         raise ValueError(_("master group {0} shall not have "
247                                            "a symlinkoption").format(self.impl_getname()))
248                     if not isinstance(child, Option):  # pragma: optional cover
249                         raise ValueError(_("master group {0} shall not have "
250                                            "a subgroup").format(self.impl_getname()))
251                     if not child.impl_is_multi():  # pragma: optional cover
252                         raise ValueError(_("not allowed option {0} "
253                                            "in group {1}"
254                                            ": this option is not a multi"
255                                            "").format(child.impl_getname(), self.impl_getname()))
256                 #length of master change slaves length
257                 self._set_has_dependency()
258                 MasterSlaves(self.impl_getname(), children)
259         else:  # pragma: optional cover
260             raise ValueError(_('group_type: {0}'
261                                ' not allowed').format(group_type))
262
263     def _impl_getstate(self, descr=None):
264         """enables us to export into a dict
265         :param descr: parent :class:`tiramisu.option.OptionDescription`
266         """
267         if descr is None:
268             self.impl_build_cache_option()
269             descr = self
270         super(OptionDescription, self)._impl_getstate(descr)
271         self._state_group_type = str(self._group_type)
272         for option in self._impl_getchildren():
273             option._impl_getstate(descr)
274
275     def __getstate__(self):
276         """special method to enable the serialization with pickle
277         """
278         stated = True
279         try:
280             # the `_state` attribute is a flag that which tells us if
281             # the serialization can be performed
282             self._stated
283         except AttributeError:
284             # if cannot delete, _impl_getstate never launch
285             # launch it recursivement
286             # _stated prevent __getstate__ launch more than one time
287             # _stated is delete, if re-serialize, re-lauch _impl_getstate
288             self._impl_getstate()
289             stated = False
290         return super(OptionDescription, self).__getstate__(stated)
291
292     def _impl_setstate(self, descr=None):
293         """enables us to import from a dict
294         :param descr: parent :class:`tiramisu.option.OptionDescription`
295         """
296         if descr is None:
297             self._cache_consistencies = None
298             self.impl_build_cache_option()
299             descr = self
300         self._group_type = getattr(groups, self._state_group_type)
301         if isinstance(self._group_type, groups.MasterGroupType):
302             MasterSlaves(self.impl_getname(), self.impl_getchildren(),
303                          validate=False)
304         del(self._state_group_type)
305         super(OptionDescription, self)._impl_setstate(descr)
306         for option in self._impl_getchildren(dyn=False):
307             option._impl_setstate(descr)
308
309     def __setstate__(self, state):
310         super(OptionDescription, self).__setstate__(state)
311         try:
312             self._stated
313         except AttributeError:
314             self._impl_setstate()
315
316     def _impl_get_suffixes(self, context):
317         callback, callback_params = self.impl_get_callback()
318         values = carry_out_calculation(self, context=context,
319                                        callback=callback,
320                                        callback_params=callback_params)
321         if len(values) > len(set(values)):
322             raise ConfigError(_('DynOptionDescription callback return not unique value'))
323         for val in values:
324             if not isinstance(val, str) or re.match(name_regexp, val) is None:
325                 raise ValueError(_("invalid suffix: {0} for option").format(val))
326         return values
327
328     def _impl_search_dynchild(self, name, context):
329         ret = []
330         for child in self._impl_st_getchildren(context, only_dyn=True):
331             cname = child.impl_getname()
332             if name.startswith(cname):
333                 path = cname
334                 for value in child._impl_get_suffixes(context):
335                     if name == cname + value:
336                         return SynDynOptionDescription(child, name, path + value, value)
337         return ret
338
339     def _impl_get_dynchild(self, child, suffix):
340         name = child.impl_getname() + suffix
341         path = self.impl_getname() + suffix + '.' + name
342         if isinstance(child, OptionDescription):
343             return SynDynOptionDescription(child, name, path, suffix)
344         else:
345             return child._impl_to_dyn(name, path)
346
347     def _impl_getchildren(self, dyn=True, context=undefined):
348         for child in self._impl_st_getchildren(context):
349             cname = child.impl_getname()
350             if dyn and child.impl_is_dynoptiondescription():
351                 path = cname
352                 for value in child._impl_get_suffixes(context):
353                     yield SynDynOptionDescription(child,
354                                                   cname + value,
355                                                   path + value, value)
356             else:
357                 yield child
358
359     def impl_getchildren(self):
360         return list(self._impl_getchildren())
361
362     def __getattr__(self, name, context=undefined):
363         if name.startswith('_'):  # or name.startswith('impl_'):
364             return object.__getattribute__(self, name)
365         if '.' in name:
366             path = name.split('.')[0]
367             subpath = '.'.join(name.split('.')[1:])
368             return self.__getattr__(path, context=context).__getattr__(subpath, context=context)
369         return self._getattr(name, context=context)
370
371
372 class DynOptionDescription(OptionDescription):
373     def __init__(self, name, doc, children, requires=None, properties=None,
374                  callback=None, callback_params=None):
375         super(DynOptionDescription, self).__init__(name, doc, children,
376                                                    requires, properties)
377         for child in children:
378             if isinstance(child, OptionDescription):
379                 if child.impl_get_group_type() != groups.master:
380                     raise ConfigError(_('cannot set optiondescription in a '
381                                         'dynoptiondescription'))
382                 for chld in child._impl_getchildren():
383                     chld._impl_setsubdyn(self)
384             if isinstance(child, SymLinkOption):
385                 raise ConfigError(_('cannot set symlinkoption in a '
386                                     'dynoptiondescription'))
387             child._impl_setsubdyn(self)
388         self.impl_set_callback(callback, callback_params)
389
390     def _validate_callback(self, callback, callback_params):
391         if callback is None:
392             raise ConfigError(_('callback is mandatory for dynoptiondescription'))
393
394
395 class SynDynOptionDescription(object):
396     __slots__ = ('_opt', '_name', '_path', '_suffix')
397
398     def __init__(self, opt, name, path, suffix):
399         self._opt = opt
400         self._name = name
401         self._path = path
402         self._suffix = suffix
403
404     def __getattr__(self, name, context=undefined):
405         if name in dir(self._opt):
406             return getattr(self._opt, name)
407         return self._opt._getattr(name, suffix=self._suffix, context=context)
408
409     def impl_getname(self):
410         return self._name
411
412     def _impl_getchildren(self, dyn=True, context=undefined):
413         children = []
414         for child in self._opt._impl_getchildren():
415             yield(self._opt._impl_get_dynchild(child, self._suffix))
416
417     def impl_getchildren(self):
418         return self._impl_getchildren()
419
420     def impl_getpath(self, context):
421         return self._path
422
423     def impl_getpaths(self, include_groups=False, _currpath=None):
424         return _impl_getpaths(self, include_groups, _currpath)
425
426     def _impl_getopt(self):
427         return self._opt
428
429
430 def _impl_getpaths(klass, include_groups, _currpath):
431         """returns a list of all paths in klass, recursively
432            _currpath should not be provided (helps with recursion)
433         """
434         if _currpath is None:
435             _currpath = []
436         paths = []
437         for option in klass._impl_getchildren():
438             attr = option.impl_getname()
439             if option.impl_is_optiondescription():
440                 if include_groups:
441                     paths.append('.'.join(_currpath + [attr]))
442                 paths += option.impl_getpaths(include_groups=include_groups,
443                                               _currpath=_currpath + [attr])
444             else:
445                 paths.append('.'.join(_currpath + [attr]))
446         return paths