f8f3f75bb52d40b1ed96f8e49035a0ab21546031
[tiramisu.git] / tiramisu / option / optiondescription.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 from copy import copy
22 import re
23
24
25 from tiramisu.i18n import _
26 from tiramisu.setting import groups, undefined  # , log
27 from .baseoption import BaseOption, DynSymLinkOption, SymLinkOption, \
28     allowed_character
29 from . import MasterSlaves
30 from tiramisu.error import ConfigError, ConflictError, ValueWarning
31 from tiramisu.storage import get_storages_option
32 from tiramisu.autolib import carry_out_calculation
33
34
35 StorageOptionDescription = get_storages_option('optiondescription')
36 name_regexp = re.compile(r'^{0}*$'.format(allowed_character))
37
38
39 class OptionDescription(BaseOption, StorageOptionDescription):
40     """Config's schema (organisation, group) and container of Options
41     The `OptionsDescription` objects lives in the `tiramisu.config.Config`.
42     """
43     __slots__ = tuple()
44
45     def __init__(self, name, doc, children, requires=None, properties=None):
46         """
47         :param children: a list of options (including optiondescriptions)
48
49         """
50         super(OptionDescription, self).__init__(name, doc=doc,
51                                                 requires=requires,
52                                                 properties=properties,
53                                                 callback=False)
54         child_names = []
55         dynopt_names = []
56         for child in children:
57             name = child.impl_getname()
58             child_names.append(name)
59             if isinstance(child, DynOptionDescription):
60                 dynopt_names.append(name)
61
62         #better performance like this
63         valid_child = copy(child_names)
64         valid_child.sort()
65         old = None
66         for child in valid_child:
67             if child == old:  # pragma: optional cover
68                 raise ConflictError(_('duplicate option name: '
69                                       '{0}').format(child))
70             if dynopt_names:
71                 for dynopt in dynopt_names:
72                     if child != dynopt and child.startswith(dynopt):
73                         raise ConflictError(_('option must not start as '
74                                               'dynoptiondescription'))
75             old = child
76         self._add_children(child_names, children)
77         self._cache_paths = None
78         self._cache_consistencies = None
79         # the group_type is useful for filtering OptionDescriptions in a config
80         self._group_type = groups.default
81         self._is_build_cache = False
82
83     def impl_getrequires(self):
84         return self._requires
85
86     def impl_getdoc(self):
87         return self.impl_get_information('doc')
88
89     def impl_validate(self, *args):
90         """usefull for OptionDescription"""
91         pass
92
93     def impl_getpaths(self, include_groups=False, _currpath=None):
94         """returns a list of all paths in self, recursively
95            _currpath should not be provided (helps with recursion)
96         """
97         return _impl_getpaths(self, include_groups, _currpath)
98
99     def impl_build_cache_consistency(self, _consistencies=None, cache_option=None):
100         #FIXME cache_option !
101         if _consistencies is None:
102             init = True
103             _consistencies = {}
104             cache_option = []
105         else:
106             init = False
107         for option in self._impl_getchildren(dyn=False):
108             cache_option.append(option._get_id())
109             if not isinstance(option, OptionDescription):
110                 for func, all_cons_opts, params in option._get_consistencies():
111                     for opt in all_cons_opts:
112                         _consistencies.setdefault(opt,
113                                                   []).append((func,
114                                                              all_cons_opts,
115                                                              params))
116             else:
117                 option.impl_build_cache_consistency(_consistencies, cache_option)
118         if init and _consistencies != {}:
119             self._cache_consistencies = {}
120             for opt, cons in _consistencies.items():
121                 #FIXME dans le cache ...
122                 if opt._get_id() not in cache_option:  # pragma: optional cover
123                     raise ConfigError(_('consistency with option {0} '
124                                         'which is not in Config').format(
125                                             opt.impl_getname()))
126                 self._cache_consistencies[opt] = tuple(cons)
127
128     def impl_validate_options(self, cache_option=None):
129         """validate duplicate option and set option has readonly option
130         """
131         if cache_option is None:
132             init = True
133             cache_option = []
134         else:
135             init = False
136         for option in self._impl_getchildren(dyn=False):
137             #FIXME specifique id for sqlalchemy?
138             #FIXME avec sqlalchemy ca marche le multi parent ? (dans des configs diffĂ©rentes)
139             #if option.id is None:
140             #    raise SystemError(_("an option's id should not be None "
141             #                        "for {0}").format(option.impl_getname()))
142             if option._get_id() in cache_option:  # pragma: optional cover
143                 raise ConflictError(_('duplicate option: {0}').format(option))
144             cache_option.append(option._get_id())
145             option._readonly = True
146             if isinstance(option, OptionDescription):
147                 option.impl_validate_options(cache_option)
148         if init:
149             self._readonly = True
150
151     # ____________________________________________________________
152     def impl_set_group_type(self, group_type):
153         """sets a given group object to an OptionDescription
154
155         :param group_type: an instance of `GroupType` or `MasterGroupType`
156                               that lives in `setting.groups`
157         """
158         if self._group_type != groups.default:  # pragma: optional cover
159             raise TypeError(_('cannot change group_type if already set '
160                             '(old {0}, new {1})').format(self._group_type,
161                                                          group_type))
162         if isinstance(group_type, groups.GroupType):
163             self._group_type = group_type
164             if isinstance(group_type, groups.MasterGroupType):
165                 MasterSlaves(self.impl_getname(), self.impl_getchildren())
166         else:  # pragma: optional cover
167             raise ValueError(_('group_type: {0}'
168                                ' not allowed').format(group_type))
169
170     def impl_get_group_type(self):
171         return self._group_type
172
173     def _valid_consistency(self, option, value, context, index, submulti_idx):
174         if self._cache_consistencies is None:
175             return True
176         #consistencies is something like [('_cons_not_equal', (opt1, opt2))]
177         if isinstance(option, DynSymLinkOption):
178             consistencies = self._cache_consistencies.get(option._opt)
179         else:
180             consistencies = self._cache_consistencies.get(option)
181         if consistencies is not None:
182             for func, all_cons_opts, params in consistencies:
183                 warnings_only = params.get('warnings_only', False)
184                 #all_cons_opts[0] is the option where func is set
185                 if isinstance(option, DynSymLinkOption):
186                     subpath = '.'.join(option._dyn.split('.')[:-1])
187                     namelen = len(option._opt.impl_getname())
188                     suffix = option.impl_getname()[namelen:]
189                     opts = []
190                     for opt in all_cons_opts:
191                         name = opt.impl_getname() + suffix
192                         path = subpath + '.' + name
193                         opts.append(opt._impl_to_dyn(name, path))
194                 else:
195                     opts = all_cons_opts
196                 try:
197                     opts[0]._launch_consistency(func, option, value, context,
198                                                 index, submulti_idx, opts,
199                                                 warnings_only)
200                 except ValueError as err:
201                     if warnings_only:
202                         raise ValueWarning(err.message, option)
203                     else:
204                         raise err
205
206     def _impl_getstate(self, descr=None):
207         """enables us to export into a dict
208         :param descr: parent :class:`tiramisu.option.OptionDescription`
209         """
210         if descr is None:
211             #FIXME faut le desactiver ?
212             #self.impl_build_cache_consistency()
213             self.impl_build_cache_option()
214             descr = self
215         super(OptionDescription, self)._impl_getstate(descr)
216         self._state_group_type = str(self._group_type)
217         for option in self._impl_getchildren():
218             option._impl_getstate(descr)
219
220     def __getstate__(self):
221         """special method to enable the serialization with pickle
222         """
223         stated = True
224         try:
225             # the `_state` attribute is a flag that which tells us if
226             # the serialization can be performed
227             self._stated
228         except AttributeError:
229             # if cannot delete, _impl_getstate never launch
230             # launch it recursivement
231             # _stated prevent __getstate__ launch more than one time
232             # _stated is delete, if re-serialize, re-lauch _impl_getstate
233             self._impl_getstate()
234             stated = False
235         return super(OptionDescription, self).__getstate__(stated)
236
237     def _impl_setstate(self, descr=None):
238         """enables us to import from a dict
239         :param descr: parent :class:`tiramisu.option.OptionDescription`
240         """
241         if descr is None:
242             self._cache_paths = None
243             self._cache_consistencies = None
244             self.impl_build_cache_option()
245             descr = self
246         self._group_type = getattr(groups, self._state_group_type)
247         if isinstance(self._group_type, groups.MasterGroupType):
248             MasterSlaves(self.impl_getname(), self.impl_getchildren(),
249                          validate=False)
250         del(self._state_group_type)
251         super(OptionDescription, self)._impl_setstate(descr)
252         for option in self._impl_getchildren(dyn=False):
253             option._impl_setstate(descr)
254
255     def __setstate__(self, state):
256         super(OptionDescription, self).__setstate__(state)
257         try:
258             self._stated
259         except AttributeError:
260             self._impl_setstate()
261
262     def _impl_get_suffixes(self, context):
263         callback, callback_params = self.impl_get_callback()
264         if callback_params is None:
265             callback_params = {}
266         values = carry_out_calculation(self, config=context,
267                                        callback=callback,
268                                        callback_params=callback_params)
269         if len(values) > len(set(values)):
270             raise ConfigError(_('DynOptionDescription callback return not uniq value'))
271         for val in values:
272             if not isinstance(val, str) or re.match(name_regexp, val) is None:
273                 raise ValueError(_("invalid suffix: {0} for option").format(val))
274         return values
275
276     def _impl_search_dynchild(self, name=undefined, context=undefined):
277         ret = []
278         for child in self._impl_st_getchildren():
279             cname = child.impl_getname()
280             if isinstance(child, DynOptionDescription) and \
281                     (name is undefined or name.startswith(cname)):
282                 path = cname
283                 for value in child._impl_get_suffixes(context):
284                     if name is undefined:
285                         ret.append(SynDynOptionDescription(child, cname + value, path + value, value))
286                     elif name == cname + value:
287                         return SynDynOptionDescription(child, name, path + value, value)
288         return ret
289
290     def _impl_get_dynchild(self, child, suffix):
291         name = child.impl_getname() + suffix
292         path = self._name + suffix + '.' + name
293         if isinstance(child, OptionDescription):
294             return SynDynOptionDescription(child, name, path, suffix)
295         else:
296             return child._impl_to_dyn(name, path)
297
298     def _impl_getchildren(self, dyn=True, context=undefined):
299         for child in self._impl_st_getchildren():
300             cname = child._name
301             if dyn and child.impl_is_dynoptiondescription():
302                 path = cname
303                 for value in child._impl_get_suffixes(context):
304                     yield SynDynOptionDescription(child,
305                                                   cname + value,
306                                                   path + value, value)
307             else:
308                 yield child
309
310     def impl_getchildren(self):
311         return list(self._impl_getchildren())
312
313
314 class DynOptionDescription(OptionDescription):
315     def __init__(self, name, doc, children, requires=None, properties=None,
316                  callback=None, callback_params=None):
317         for child in children:
318             if isinstance(child, OptionDescription):
319                 if child.impl_get_group_type() != groups.master:
320                     raise ConfigError(_('cannot set optiondescription in an '
321                                         'dynoptiondescription'))
322                 for chld in child._impl_getchildren():
323                     chld._subdyn = self
324             if isinstance(child, SymLinkOption):
325                 raise ConfigError(_('cannot set symlinkoption in an '
326                                     'dynoptiondescription'))
327             child._subdyn = self
328         super(DynOptionDescription, self).__init__(name, doc, children,
329                                                    requires, properties)
330         self.impl_set_callback(callback, callback_params)
331
332     def _validate_callback(self, callback, callback_params):
333         if callback is None:
334             raise ConfigError(_('callback is mandatory for dynoptiondescription'))
335
336
337 class SynDynOptionDescription(object):
338     __slots__ = ('_opt', '_name', '_path', '_suffix')
339
340     def __init__(self, opt, name, path, suffix):
341         self._opt = opt
342         self._name = name
343         self._path = path
344         self._suffix = suffix
345
346     def __getattr__(self, name, context=undefined):
347         if name in dir(self._opt):
348             return getattr(self._opt, name)
349         return self._opt._getattr(name, self._name, self._suffix, context)
350
351     def impl_getname(self):
352         return self._name
353
354     def _impl_getchildren(self, dyn=True, context=undefined):
355         children = []
356         for child in self._opt._impl_getchildren():
357             children.append(self._opt._impl_get_dynchild(child, self._suffix))
358         return children
359
360     def impl_getchildren(self):
361         return self._impl_getchildren()
362
363     def impl_getpath(self, context):
364         return self._path
365
366     def impl_getpaths(self, include_groups=False, _currpath=None):
367         return _impl_getpaths(self, include_groups, _currpath)
368
369
370 def _impl_getpaths(klass, include_groups, _currpath):
371         """returns a list of all paths in klass, recursively
372            _currpath should not be provided (helps with recursion)
373         """
374         if _currpath is None:
375             _currpath = []
376         paths = []
377         for option in klass._impl_getchildren():
378             attr = option.impl_getname()
379             if option.impl_is_optiondescription():
380                 if include_groups:
381                     paths.append('.'.join(_currpath + [attr]))
382                 paths += option.impl_getpaths(include_groups=include_groups,
383                                               _currpath=_currpath + [attr])
384             else:
385                 paths.append('.'.join(_currpath + [attr]))
386         return paths