* to "reset" a value, now you just have to delete it
[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     #____________________________________________________________
222     def validate_properties(self, opt_or_descr, is_descr, is_write,
223                             value=None, force_permissive=False,
224                             force_properties=None):
225         is_cached = False
226         if opt_or_descr in self._cache:
227             t = time()
228             props, raise_text, created = self._cache[opt_or_descr]
229             if t - created < expires_time:
230                 properties = props
231                 is_cached = True
232         if not is_cached:
233             properties = set(self.get_properties(opt_or_descr))
234             #remove this properties, those properties are validate in after
235             properties = properties - set(['mandatory', 'frozen'])
236             set_properties = self.get_properties()
237             if force_properties is not None:
238                 set_properties.extend(force_properties)
239             set_properties = set(set_properties)
240             properties = properties & set_properties
241             if force_permissive is True or self.has_property('permissive', is_apply_req=False):
242                 properties = properties - set(self.get_permissive())
243             properties = properties - set(self.get_permissive(opt_or_descr))
244             properties = list(properties)
245             raise_text = _("trying to access"
246                            " to an option named: {0} with properties"
247                            " {1}")
248             if not is_descr:
249                 if self.context.cfgimpl_get_values().is_mandatory_err(opt_or_descr,
250                                                                       value,
251                                                                       force_properties=force_properties):
252                     properties.append('mandatory')
253                 if is_write and (self.has_property('everything_frozen') or (
254                         self.has_property('frozen') and
255                         self.has_property('frozen', opt_or_descr,
256                                           is_apply_req=False))):
257                     properties.append('frozen')
258                     raise_text = _('cannot change the value to {0} for '
259                                    'option {1} this option is frozen')
260             self._set_cache(opt_or_descr, properties, raise_text)
261         if properties != []:
262             raise PropertiesOptionError(raise_text.format(opt_or_descr._name,
263                                                           str(properties)),
264                                         properties)
265
266     def get_permissive(self, opt=None):
267         return self.permissives.get(opt, [])
268
269     def set_permissive(self, permissive, opt=None):
270         if not isinstance(permissive, list):
271             raise TypeError(_('permissive must be a list'))
272         self.permissives[opt] = permissive
273
274     #____________________________________________________________
275     def setowner(self, owner):
276         ":param owner: sets the default value for owner at the Config level"
277         if not isinstance(owner, owners.Owner):
278             raise TypeError(_("invalid generic owner {0}").format(str(owner)))
279         self.owner = owner
280
281     def getowner(self):
282         return self.owner
283
284     #____________________________________________________________
285     def read_only(self):
286         "convenience method to freeze, hidde and disable"
287         self.enable_property('everything_frozen')
288         self.enable_property('frozen')  # can be usefull...
289         self.disable_property('hidden')
290         self.enable_property('disabled')
291         self.enable_property('mandatory')
292         self.enable_property('validator')
293         self.disable_property('permissive')
294
295     def read_write(self):
296         "convenience method to freeze, hidde and disable"
297         self.disable_property('everything_frozen')
298         self.enable_property('frozen')  # can be usefull...
299         self.enable_property('hidden')
300         self.enable_property('disabled')
301         self.disable_property('mandatory')
302         self.enable_property('validator')
303         self.disable_property('permissive')
304
305     def _set_cache(self, opt, props, raise_text):
306         if self.has_property('expire'):
307             self._cache[opt] = (props, raise_text, time())
308
309     def reset_cache(self):
310         self._cache = {}
311
312
313 def apply_requires(opt, config):
314     "carries out the jit (just in time requirements between options"
315     def build_actions(requires):
316         "action are hide, show, enable, disable..."
317         trigger_actions = {}
318         for require in requires:
319             action = require[2]
320             trigger_actions.setdefault(action, []).append(require)
321         return trigger_actions
322     #for symlink
323     if hasattr(opt, '_requires') and opt._requires is not None:
324         # filters the callbacks
325         setting = config.cfgimpl_get_settings()
326         trigger_actions = build_actions(opt._requires)
327         optpath = config.cfgimpl_get_context().cfgimpl_get_description().get_path_by_opt(opt)
328         for requires in trigger_actions.values():
329             matches = False
330             for require in requires:
331                 if len(require) == 3:
332                     path, expected, action = require
333                     inverse = False
334                 elif len(require) == 4:
335                     path, expected, action, inverse = require
336                 if path == optpath or path.startswith(optpath + '.'):
337                     raise RequirementRecursionError(_("malformed requirements "
338                                                     "imbrication detected for option: '{0}' "
339                                                     "with requirement on: '{1}'").format(optpath, path))
340                 try:
341                     value = config.cfgimpl_get_context()._getattr(path, force_permissive=True)
342                 except PropertiesOptionError, err:
343                     properties = err.proptype
344                     #FIXME: AttributeError or PropertiesOptionError ?
345                     raise AttributeError(_("option '{0}' has requirement's property error: "
346                                          "{1} {2}").format(opt._name, path, properties))
347                 except AttributeError:
348                     raise AttributeError(_("required option not found: "
349                                          "{0}").format(path))
350                 if value == expected:
351                     if inverse:
352                         setting.del_property(action, opt, False)
353                     else:
354                         setting.add_property(action, opt, False)
355                     matches = True
356                     #FIXME optimisation : fait un double break non ? voire un return
357             # no requirement has been triggered, then just reverse the action
358             if not matches:
359                 if inverse:
360                     setting.add_property(action, opt, False)
361                 else:
362                     setting.del_property(action, opt, False)