add SlaveError for slave's length
[tiramisu.git] / tiramisu / setting.py
1 # -*- coding: utf-8 -*-
2 "sets the options of the configuration objects Config object itself"
3 # Copyright (C) 2012-2013 Team tiramisu (see AUTHORS for all contributors)
4 #
5 # This program is free software; you can redistribute it and/or modify
6 # it under the terms of the GNU General Public License as published by
7 # the Free Software Foundation; either version 2 of the License, or
8 # (at your option) any later version.
9 #
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 # GNU General Public License for more details.
14 #
15 # You should have received a copy of the GNU General Public License
16 # along with this program; if not, write to the Free Software
17 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
18 #
19 # The original `Config` design model is unproudly borrowed from
20 # the rough pypy's guys: http://codespeak.net/svn/pypy/dist/pypy/config/
21 # the whole pypy projet is under MIT licence
22 # ____________________________________________________________
23 from time import time
24 from tiramisu.error import RequirementRecursionError, PropertiesOptionError
25 from tiramisu.i18n import _
26
27
28 expires_time = 5
29
30
31 class _const:
32     """convenient class that emulates a module
33     and builds constants (that is, unique names)"""
34     class ConstError(TypeError):
35         pass
36
37     def __setattr__(self, name, value):
38         if name in self.__dict__:
39             raise self.ConstError, _("Can't rebind group ({})").format(name)
40         self.__dict__[name] = value
41
42     def __delattr__(self, name):
43         if name in self.__dict__:
44             raise self.ConstError, _("Can't unbind group ({})").format(name)
45         raise ValueError(name)
46
47
48 # ____________________________________________________________
49 class GroupModule(_const):
50     "emulates a module to manage unique group (OptionDescription) names"
51     class GroupType(str):
52         """allowed normal group (OptionDescription) names
53         *normal* means : groups that are not master
54         """
55         pass
56
57     class DefaultGroupType(GroupType):
58         """groups that are default (typically 'default')"""
59         pass
60
61     class MasterGroupType(GroupType):
62         """allowed normal group (OptionDescription) names
63         *master* means : groups that have the 'master' attribute set
64         """
65         pass
66 # setting.groups (emulates a module)
67 groups = GroupModule()
68
69
70 def populate_groups():
71     "populates the available groups in the appropriate namespaces"
72     groups.master = groups.MasterGroupType('master')
73     groups.default = groups.DefaultGroupType('default')
74     groups.family = groups.GroupType('family')
75
76 # names are in the module now
77 populate_groups()
78
79
80 # ____________________________________________________________
81 class OwnerModule(_const):
82     """emulates a module to manage unique owner names.
83
84     owners are living in `Config._cfgimpl_value_owners`
85     """
86     class Owner(str):
87         """allowed owner names
88         """
89         pass
90
91     class DefaultOwner(Owner):
92         """groups that are default (typically 'default')"""
93         pass
94 # setting.owners (emulates a module)
95 owners = OwnerModule()
96
97
98 def populate_owners():
99     """populates the available owners in the appropriate namespaces
100
101     - 'user' is the generic is the generic owner.
102     - 'default' is the config owner after init time
103     """
104     setattr(owners, 'default', owners.DefaultOwner('default'))
105     setattr(owners, 'user', owners.Owner('user'))
106
107     def add_owner(name):
108         """
109         :param name: the name of the new owner
110         """
111         setattr(owners, name, owners.Owner(name))
112     setattr(owners, 'add_owner', add_owner)
113
114 # names are in the module now
115 populate_owners()
116
117
118 class MultiTypeModule(_const):
119     class MultiType(str):
120         pass
121
122     class DefaultMultiType(MultiType):
123         pass
124
125     class MasterMultiType(MultiType):
126         pass
127
128     class SlaveMultiType(MultiType):
129         pass
130
131 multitypes = MultiTypeModule()
132
133
134 def populate_multitypes():
135     setattr(multitypes, 'default', multitypes.DefaultMultiType('default'))
136     setattr(multitypes, 'master', multitypes.MasterMultiType('master'))
137     setattr(multitypes, 'slave', multitypes.SlaveMultiType('slave'))
138
139 populate_multitypes()
140
141
142 #____________________________________________________________
143 class Setting(object):
144     "``Config()``'s configuration options"
145     __slots__ = ('properties', 'permissives', 'owner', 'context', '_cache')
146
147     def __init__(self, context):
148         # properties attribute: the name of a property enables this property
149         # key is None for global properties
150         self.properties = {None: ['expire']}
151         # permissive properties
152         self.permissives = {}
153         # generic owner
154         self.owner = owners.user
155         self.context = context
156         self._cache = {}
157
158     #____________________________________________________________
159     # properties methods
160     def has_properties(self, opt=None, is_apply_req=True):
161         "has properties means the Config's properties attribute is not empty"
162         return bool(len(self.get_properties(opt, is_apply_req)))
163
164     def get_properties(self, opt=None, is_apply_req=True):
165         if opt is None:
166             default = []
167         else:
168             if is_apply_req:
169                 apply_requires(opt, self.context)
170             default = list(opt._properties)
171         return self.properties.get(opt, default)
172
173     def has_property(self, propname, opt=None, is_apply_req=True):
174         """has property propname in the Config's properties attribute
175         :param property: string wich is the name of the property"""
176         return propname in self.get_properties(opt, is_apply_req)
177
178     def enable_property(self, propname):
179         "puts property propname in the Config's properties attribute"
180         props = self.get_properties()
181         if propname not in props:
182             props.append(propname)
183         self.set_properties(props)
184         self.context.cfgimpl_clean_cache()
185
186     def disable_property(self, propname):
187         "deletes property propname in the Config's properties attribute"
188         props = self.get_properties()
189         if propname in props:
190             props.remove(propname)
191         self.set_properties(props)
192         self.context.cfgimpl_clean_cache()
193
194     def set_properties(self, properties, opt=None):
195         """save properties for specified opt
196         (never save properties if same has option properties)
197         """
198         if opt is None:
199             self.properties[opt] = properties
200         else:
201             if opt._properties == properties:
202                 if opt in self.properties:
203                     del(self.properties[opt])
204             else:
205                 self.properties[opt] = properties
206
207     def add_property(self, propname, opt, is_apply_req=True):
208         properties = self.get_properties(opt, is_apply_req)
209         if not propname in properties:
210             properties.append(propname)
211             self.set_properties(properties, opt)
212         self.context.cfgimpl_clean_cache()
213
214     def del_property(self, propname, opt, is_apply_req=True):
215         properties = self.get_properties(opt, is_apply_req)
216         if propname in properties:
217             properties.remove(propname)
218             self.set_properties(properties, opt)
219         self.context.cfgimpl_clean_cache()
220
221     def _validate_mandatory(self, opt, value, force_properties=None):
222         set_mandatory = self.has_property('mandatory')
223         if force_properties is not None:
224             set_mandatory = ('mandatory' in force_properties or
225                              set_mandatory)
226         if set_mandatory and self.has_property('mandatory', opt, False) and \
227                 self.context.cfgimpl_get_values()._is_empty(opt, value):
228             return True
229         return False
230
231     def _calc_properties(self, opt_or_descr, force_permissive, force_properties):
232         properties = set(self.get_properties(opt_or_descr))
233         #remove this properties, those properties are validate in after
234         properties = properties - set(['mandatory', 'frozen'])
235         set_properties = self.get_properties()
236         if force_properties is not None:
237             set_properties.extend(force_properties)
238         set_properties = set(set_properties)
239         properties = properties & set_properties
240         if force_permissive is True or self.has_property('permissive', is_apply_req=False):
241             properties = properties - set(self.get_permissive())
242         properties = properties - set(self.get_permissive(opt_or_descr))
243         return list(properties)
244
245     #____________________________________________________________
246     def validate_properties(self, opt_or_descr, is_descr, is_write,
247                             value=None, force_permissive=False,
248                             force_properties=None):
249         is_cached = False
250         if opt_or_descr in self._cache:
251             t = time()
252             props, raise_text, created = self._cache[opt_or_descr]
253             if t < created:
254                 properties = props
255                 is_cached = True
256         if not is_cached:
257             properties = self._calc_properties(opt_or_descr, force_permissive,
258                                                force_properties)
259             raise_text = _("trying to access"
260                            " to an option named: {0} with properties"
261                            " {1}")
262             if not is_descr:
263                 if self._validate_mandatory(opt_or_descr, value,
264                                             force_properties=force_properties):
265                     properties.append('mandatory')
266                 #frozen
267                 if is_write and (self.has_property('everything_frozen') or (
268                         self.has_property('frozen') and
269                         self.has_property('frozen', opt_or_descr,
270                                           is_apply_req=False))):
271                     properties.append('frozen')
272                     raise_text = _('cannot change the value to {0} for '
273                                    'option {1} this option is frozen')
274             self._set_cache(opt_or_descr, properties, raise_text)
275         if properties != []:
276             raise PropertiesOptionError(raise_text.format(opt_or_descr._name,
277                                                           str(properties)),
278                                         properties)
279
280     def get_permissive(self, opt=None):
281         return self.permissives.get(opt, [])
282
283     def set_permissive(self, permissive, opt=None):
284         if not isinstance(permissive, list):
285             raise TypeError(_('permissive must be a list'))
286         self.permissives[opt] = permissive
287
288     #____________________________________________________________
289     def setowner(self, owner):
290         ":param owner: sets the default value for owner at the Config level"
291         if not isinstance(owner, owners.Owner):
292             raise TypeError(_("invalid generic owner {0}").format(str(owner)))
293         self.owner = owner
294
295     def getowner(self):
296         return self.owner
297
298     #____________________________________________________________
299     def read_only(self):
300         "convenience method to freeze, hidde and disable"
301         self.enable_property('everything_frozen')
302         self.enable_property('frozen')  # can be usefull...
303         self.disable_property('hidden')
304         self.enable_property('disabled')
305         self.enable_property('mandatory')
306         self.enable_property('validator')
307         self.disable_property('permissive')
308
309     def read_write(self):
310         "convenience method to freeze, hidde and disable"
311         self.disable_property('everything_frozen')
312         self.enable_property('frozen')  # can be usefull...
313         self.enable_property('hidden')
314         self.enable_property('disabled')
315         self.disable_property('mandatory')
316         self.enable_property('validator')
317         self.disable_property('permissive')
318
319     def _set_cache(self, opt, props, raise_text):
320         if self.has_property('expire'):
321             self._cache[opt] = (props, raise_text, time() + expires_time)
322
323     def reset_cache(self):
324         self._cache = {}
325
326
327 def apply_requires(opt, config):
328     "carries out the jit (just in time requirements between options"
329     def build_actions(requires):
330         "action are hide, show, enable, disable..."
331         trigger_actions = {}
332         for require in requires:
333             action = require[2]
334             trigger_actions.setdefault(action, []).append(require)
335         return trigger_actions
336     #for symlink
337     if hasattr(opt, '_requires') and opt._requires is not None:
338         # filters the callbacks
339         setting = config.cfgimpl_get_settings()
340         trigger_actions = build_actions(opt._requires)
341         optpath = config.cfgimpl_get_context().cfgimpl_get_description().get_path_by_opt(opt)
342         for requires in trigger_actions.values():
343             matches = False
344             for require in requires:
345                 if len(require) == 3:
346                     path, expected, action = require
347                     inverse = False
348                 elif len(require) == 4:
349                     path, expected, action, inverse = require
350                 if path == optpath or path.startswith(optpath + '.'):
351                     raise RequirementRecursionError(_("malformed requirements "
352                                                     "imbrication detected for option: '{0}' "
353                                                     "with requirement on: '{1}'").format(optpath, path))
354                 try:
355                     value = config.cfgimpl_get_context()._getattr(path, force_permissive=True)
356                 except PropertiesOptionError, err:
357                     properties = err.proptype
358                     #FIXME: AttributeError or PropertiesOptionError ?
359                     raise AttributeError(_("option '{0}' has requirement's property error: "
360                                          "{1} {2}").format(opt._name, path, properties))
361                 except AttributeError:
362                     raise AttributeError(_("required option not found: "
363                                          "{0}").format(path))
364                 if value == expected:
365                     if inverse:
366                         setting.del_property(action, opt, False)
367                     else:
368                         setting.add_property(action, opt, False)
369                     matches = True
370                     #FIXME optimisation : fait un double break non ? voire un return
371             # no requirement has been triggered, then just reverse the action
372             if not matches:
373                 if inverse:
374                     setting.add_property(action, opt, False)
375                 else:
376                     setting.del_property(action, opt, False)