c3372ce8921472192c7fbba0ac0f06da6dc2e092
[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 class Property(object):
143     __slots__ = ('_setting', '_properties', '_opt')
144
145     def __init__(self, setting, prop, opt=None):
146         self._opt = opt
147         self._setting = setting
148         self._properties = prop
149
150     def append(self, propname):
151         if not propname in self._properties:
152             self._properties.append(propname)
153             self._setting._set_properties(self._properties, self._opt)
154         self._setting.context.cfgimpl_reset_cache()
155
156     def remove(self, propname):
157         if propname in self._properties:
158             self._properties.remove(propname)
159             self._setting._set_properties(self._properties, self._opt)
160         self._setting.context.cfgimpl_reset_cache()
161
162     def __contains__(self, propname):
163         return propname in self._properties
164
165
166 #____________________________________________________________
167 class Setting(object):
168     "``Config()``'s configuration options"
169     __slots__ = ('context', '_properties', '_permissives', '_owner', '_cache')
170
171     def __init__(self, context):
172         # properties attribute: the name of a property enables this property
173         # key is None for global properties
174         self._properties = {None: ['expire']}
175         # permissive properties
176         self._permissives = {}
177         # generic owner
178         self._owner = owners.user
179         self.context = context
180         self._cache = {}
181
182     #____________________________________________________________
183     # properties methods
184     def __contains__(self, propname):
185         return propname in self._get_properties()
186
187     def __getitem__(self, opt):
188         return Property(self, self._get_properties(opt), opt)
189
190     def __setitem__(self, opt, value):
191         raise ValueError('you must only append/remove properties')
192
193     def _get_properties(self, opt=None, is_apply_req=True):
194         if opt is not None and opt in self._cache:
195             exp = time()
196             props, created = self._cache[opt]
197             if exp < created:
198                 return props
199         if opt is None:
200             default = []
201         else:
202             if is_apply_req:
203                 apply_requires(opt, self.context)
204             default = list(opt._properties)
205         props = self._properties.get(opt, default)
206         if opt is not None:
207             self._set_cache(opt, props)
208         return props
209
210     def append(self, propname):
211         "puts property propname in the Config's properties attribute"
212         Property(self, self._get_properties()).append(propname)
213
214     def remove(self, propname):
215         "deletes property propname in the Config's properties attribute"
216         Property(self, self._get_properties()).remove(propname)
217
218     def _set_properties(self, properties, opt=None):
219         """save properties for specified opt
220         (never save properties if same has option properties)
221         """
222         if opt is None:
223             self._properties[opt] = properties
224         else:
225             if opt._properties == properties:
226                 if opt in self._properties:
227                     del(self._properties[opt])
228             else:
229                 self._properties[opt] = properties
230
231     def _validate_frozen(self, opt, value, is_write):
232         if not is_write:
233             return False
234         if 'permissive' in self and 'frozen' in self._get_permissive():
235             return False
236         if 'everything_frozen' in self or (
237                 'frozen' in self and 'frozen' in self[opt]):
238             return True
239         return False
240
241     def _validate_mandatory(self, opt, value, force_properties):
242         if 'permissive' in self and 'mandatory' in self._get_permissive():
243             return False
244         check_mandatory = 'mandatory' in self
245         if force_properties is not None:
246             check_mandatory = ('mandatory' in force_properties or
247                                check_mandatory)
248         if check_mandatory and 'mandatory' in self[opt] and \
249                 self.context.cfgimpl_get_values()._is_empty(opt, value):
250             return True
251         return False
252
253     def _calc_properties(self, opt_or_descr, force_permissive, force_properties):
254         properties = set(self._get_properties(opt_or_descr))
255         #remove this properties, those properties are validate in after
256         properties = properties - set(['mandatory', 'frozen'])
257         set_properties = set(self._get_properties())
258         if force_properties is not None:
259             set_properties.update(set(force_properties))
260         properties = properties & set_properties
261         if force_permissive is True or 'permissive' in self:
262             properties = properties - set(self._get_permissive())
263         properties = properties - set(self._get_permissive(opt_or_descr))
264         return list(properties)
265
266     #____________________________________________________________
267     def validate_properties(self, opt_or_descr, is_descr, is_write,
268                             value=None, force_permissive=False,
269                             force_properties=None):
270         properties = self._calc_properties(opt_or_descr, force_permissive,
271                                            force_properties)
272         raise_text = _("trying to access"
273                        " to an option named: {0} with properties"
274                        " {1}")
275         if not is_descr:
276             if self._validate_mandatory(opt_or_descr, value, force_properties):
277                 properties.append('mandatory')
278             if self._validate_frozen(opt_or_descr, value, is_write):
279                 properties.append('frozen')
280                 raise_text = _('cannot change the value to {0} for '
281                                'option {1} this option is frozen')
282         if properties != []:
283             raise PropertiesOptionError(raise_text.format(opt_or_descr._name,
284                                                           str(properties)),
285                                         properties)
286
287     def _get_permissive(self, opt=None):
288         return self._permissives.get(opt, [])
289
290     def set_permissive(self, permissive, opt=None):
291         if not isinstance(permissive, tuple):
292             raise TypeError(_('permissive must be a tuple'))
293         self._permissives[opt] = permissive
294
295     #____________________________________________________________
296     def setowner(self, owner):
297         ":param owner: sets the default value for owner at the Config level"
298         if not isinstance(owner, owners.Owner):
299             raise TypeError(_("invalid generic owner {0}").format(str(owner)))
300         self._owner = owner
301
302     def getowner(self):
303         return self._owner
304
305     #____________________________________________________________
306     def read_only(self):
307         "convenience method to freeze, hidde and disable"
308         self.append('everything_frozen')
309         self.append('frozen')  # can be usefull...
310         self.remove('hidden')
311         self.append('disabled')
312         self.append('mandatory')
313         self.append('validator')
314         self.remove('permissive')
315
316     def read_write(self):
317         "convenience method to freeze, hidde and disable"
318         self.remove('everything_frozen')
319         self.append('frozen')  # can be usefull...
320         self.append('hidden')
321         self.append('disabled')
322         self.remove('mandatory')
323         self.append('validator')
324         self.remove('permissive')
325
326     def _set_cache(self, opt, props):
327         if 'expire' in self:
328             self._cache[opt] = (props, time() + expires_time)
329             pass
330
331     def reset_cache(self, only_expired):
332         if only_expired:
333             exp = time()
334             keys = self._cache.keys()
335             for key in keys:
336                 props, created = self._cache[key]
337                 if exp > created:
338                     del(self._cache[key])
339         else:
340             self._cache.clear()
341
342
343 def apply_requires(opt, config):
344     "carries out the jit (just in time requirements between options"
345     def build_actions(requires):
346         "action are hide, show, enable, disable..."
347         trigger_actions = {}
348         for require in requires:
349             action = require[2]
350             trigger_actions.setdefault(action, []).append(require)
351         return trigger_actions
352     #for symlink
353     if hasattr(opt, '_requires') and opt._requires is not None:
354         # filters the callbacks
355         settings = config.cfgimpl_get_settings()
356         setting = Property(settings, settings._get_properties(opt, False), opt)
357         trigger_actions = build_actions(opt._requires)
358         optpath = config.cfgimpl_get_context().cfgimpl_get_description().get_path_by_opt(opt)
359         for requires in trigger_actions.values():
360             matches = False
361             for require in requires:
362                 if len(require) == 3:
363                     path, expected, action = require
364                     inverse = False
365                 elif len(require) == 4:
366                     path, expected, action, inverse = require
367                 if path == optpath or path.startswith(optpath + '.'):
368                     raise RequirementRecursionError(_("malformed requirements "
369                                                     "imbrication detected for option: '{0}' "
370                                                     "with requirement on: '{1}'").format(optpath, path))
371                 try:
372                     value = config.cfgimpl_get_context()._getattr(path, force_permissive=True)
373                 except PropertiesOptionError, err:
374                     properties = err.proptype
375                     raise PropertiesOptionError(_("option '{0}' has requirement's property error: "
376                                                   "{1} {2}").format(opt._name, path, properties))
377                 except AttributeError:
378                     raise AttributeError(_("required option not found: "
379                                          "{0}").format(path))
380                 if value == expected:
381                     if inverse:
382                         setting.remove(action)
383                     else:
384                         setting.append(action)
385                     matches = True
386             # no requirement has been triggered, then just reverse the action
387             if not matches:
388                 if inverse:
389                     setting.append(action)
390                 else:
391                     setting.remove(action)