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