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