we can personalise storage easily
[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 copy import copy
25 from tiramisu.error import RequirementError, PropertiesOptionError
26 from tiramisu.i18n import _
27
28
29 default_encoding = 'utf-8'
30 expires_time = 5
31 ro_remove = ('permissive', 'hidden')
32 ro_append = ('frozen', 'disabled', 'validator', 'everything_frozen', 'mandatory')
33 rw_remove = ('permissive', 'everything_frozen', 'mandatory')
34 rw_append = ('frozen', 'disabled', 'validator', 'hidden')
35 default_properties = ('expire', 'validator')
36 default_storage = 'dictionary'
37
38
39 class _const:
40     """convenient class that emulates a module
41     and builds constants (that is, unique names)"""
42     class ConstError(TypeError):
43         pass
44
45     def __setattr__(self, name, value):
46         if name in self.__dict__:
47             raise self.ConstError, _("can't rebind group ({})").format(name)
48         self.__dict__[name] = value
49
50     def __delattr__(self, name):
51         if name in self.__dict__:
52             raise self.ConstError, _("can't unbind group ({})").format(name)
53         raise ValueError(name)
54
55
56 # ____________________________________________________________
57 class GroupModule(_const):
58     "emulates a module to manage unique group (OptionDescription) names"
59     class GroupType(str):
60         """allowed normal group (OptionDescription) names
61         *normal* means : groups that are not master
62         """
63         pass
64
65     class DefaultGroupType(GroupType):
66         """groups that are default (typically 'default')"""
67         pass
68
69     class MasterGroupType(GroupType):
70         """allowed normal group (OptionDescription) names
71         *master* means : groups that have the 'master' attribute set
72         """
73         pass
74 # setting.groups (emulates a module)
75 groups = GroupModule()
76
77
78 def populate_groups():
79     "populates the available groups in the appropriate namespaces"
80     groups.master = groups.MasterGroupType('master')
81     groups.default = groups.DefaultGroupType('default')
82     groups.family = groups.GroupType('family')
83
84 # names are in the module now
85 populate_groups()
86
87
88 # ____________________________________________________________
89 class OwnerModule(_const):
90     """emulates a module to manage unique owner names.
91
92     owners are living in `Config._cfgimpl_value_owners`
93     """
94     class Owner(str):
95         """allowed owner names
96         """
97         pass
98
99     class DefaultOwner(Owner):
100         """groups that are default (typically 'default')"""
101         pass
102 # setting.owners (emulates a module)
103 owners = OwnerModule()
104
105
106 def populate_owners():
107     """populates the available owners in the appropriate namespaces
108
109     - 'user' is the generic is the generic owner.
110     - 'default' is the config owner after init time
111     """
112     setattr(owners, 'default', owners.DefaultOwner('default'))
113     setattr(owners, 'user', owners.Owner('user'))
114
115     def add_owner(name):
116         """
117         :param name: the name of the new owner
118         """
119         setattr(owners, name, owners.Owner(name))
120     setattr(owners, 'add_owner', add_owner)
121
122 # names are in the module now
123 populate_owners()
124
125
126 class MultiTypeModule(_const):
127     "namespace for the master/slaves"
128     class MultiType(str):
129         pass
130
131     class DefaultMultiType(MultiType):
132         pass
133
134     class MasterMultiType(MultiType):
135         pass
136
137     class SlaveMultiType(MultiType):
138         pass
139
140 multitypes = MultiTypeModule()
141
142
143 def populate_multitypes():
144     "populates the master/slave namespace"
145     setattr(multitypes, 'default', multitypes.DefaultMultiType('default'))
146     setattr(multitypes, 'master', multitypes.MasterMultiType('master'))
147     setattr(multitypes, 'slave', multitypes.SlaveMultiType('slave'))
148
149 populate_multitypes()
150
151
152 class Property(object):
153     "a property is responsible of the option's value access rules"
154     __slots__ = ('_setting', '_properties', '_opt')
155
156     def __init__(self, setting, prop, opt=None):
157         self._opt = opt
158         self._setting = setting
159         self._properties = prop
160
161     def append(self, propname):
162         self._properties.add(propname)
163         self._setting._setproperties(self._properties, self._opt)
164
165     def remove(self, propname):
166         if propname in self._properties:
167             self._properties.remove(propname)
168             self._setting._setproperties(self._properties, self._opt)
169
170     def reset(self):
171         self._setting.reset(opt=self._opt)
172
173     def __contains__(self, propname):
174         return propname in self._properties
175
176     def __repr__(self):
177         return str(list(self._properties))
178
179
180 #____________________________________________________________
181 class Settings(object):
182     "``Config()``'s configuration options"
183     __slots__ = ('context', '_owner', '_p_')
184
185     def __init__(self, context, config_id, plugin_name):
186         # generic owner
187         self._owner = owners.user
188         self.context = context
189         import_lib = 'tiramisu.plugins.{0}.setting'.format(plugin_name)
190         self._p_ = __import__(import_lib, globals(), locals(), ['Settings'],
191                               -1).Settings(config_id)
192
193     def _getkey(self, opt):
194         if self._p_.key_is_path:
195             if opt is None:
196                 return '_none'
197             else:
198                 return self._get_opt_path(opt)
199         else:
200             return opt
201
202     #____________________________________________________________
203     # properties methods
204     def __contains__(self, propname):
205         return propname in self._getproperties()
206
207     def __repr__(self):
208         return str(list(self._getproperties()))
209
210     def __getitem__(self, opt):
211         return Property(self, self._getproperties(opt), opt)
212
213     def __setitem__(self, opt, value):
214         raise ValueError('you must only append/remove properties')
215
216     def reset(self, opt=None, all_properties=False):
217         if all_properties and opt:
218             raise ValueError(_('opt and all_properties must not be set '
219                                'together in reset'))
220         if all_properties:
221             self._p_.reset_all_propertives()
222         else:
223             self._p_.reset_properties(self._getkey(opt))
224         self.context.cfgimpl_reset_cache()
225
226     def _getproperties(self, opt=None, is_apply_req=True):
227         if opt is None:
228             props = self._p_.getproperties(self._getkey(opt), default_properties)
229         else:
230             ntime = None
231             if self._p_.hascache('property', self._getkey(opt)):
232                 ntime = time()
233                 is_cached, props = self._p_.getcache('property', self._getkey(opt), ntime)
234                 if is_cached:
235                     return props
236             if is_apply_req:
237                 self.apply_requires(opt)
238             props = self._p_.getproperties(self._getkey(opt), opt._properties)
239             if 'expire' in self:
240                 if ntime is None:
241                     ntime = time()
242                 self._p_.setcache('property', self._getkey(opt), props, ntime + expires_time)
243         return props
244
245     def append(self, propname):
246         "puts property propname in the Config's properties attribute"
247         Property(self, self._getproperties()).append(propname)
248
249     def remove(self, propname):
250         "deletes property propname in the Config's properties attribute"
251         Property(self, self._getproperties()).remove(propname)
252
253     def _setproperties(self, properties, opt=None):
254         """save properties for specified opt
255         (never save properties if same has option properties)
256         """
257         if opt is None:
258             self._p_.setproperties(self._getkey(opt), properties)
259         else:
260             if set(opt._properties) == properties:
261                 self._p_.reset_properties(self._getkey(opt))
262             else:
263                 self._p_.setproperties(self._getkey(opt), properties)
264         self.context.cfgimpl_reset_cache()
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         #opt properties
271         properties = copy(self._getproperties(opt_or_descr))
272         #remove opt permissive
273         properties -= self._p_.getpermissive(self._getkey(opt_or_descr))
274         #remove global permissive if need
275         self_properties = copy(self._getproperties())
276         if force_permissive is True or 'permissive' in self_properties:
277             properties -= self._p_.getpermissive()
278
279         #global properties
280         if force_properties is not None:
281             self_properties.update(force_properties)
282
283         #calc properties
284         properties &= self_properties
285         #mandatory and frozen are special properties
286         if is_descr:
287             properties -= frozenset(('mandatory', 'frozen'))
288         else:
289             if 'mandatory' in properties and \
290                     not self.context.cfgimpl_get_values()._isempty(opt_or_descr,
291                                                                    value):
292                 properties.remove('mandatory')
293             if is_write and 'everything_frozen' in self_properties:
294                 properties.add('frozen')
295             elif 'frozen' in properties and not is_write:
296                 properties.remove('frozen')
297
298         if properties != frozenset():
299             props = list(properties)
300             if 'frozen' in properties:
301                 raise PropertiesOptionError(_('cannot change the value for '
302                                               'option {0} this option is frozen'
303                                               '').format(opt_or_descr._name), props)
304             else:
305                 raise PropertiesOptionError(_("trying to access to an option "
306                                               "named: {0} with properties {1}"
307                                               "").format(opt_or_descr._name, str(props)), props)
308
309     #FIXME should be setpermissive
310     def set_permissive(self, permissive, opt=None):
311         if not isinstance(permissive, tuple):
312             raise TypeError(_('permissive must be a tuple'))
313         self._p_.setpermissive(self._getkey(opt), permissive)
314
315     #____________________________________________________________
316     def setowner(self, owner):
317         ":param owner: sets the default value for owner at the Config level"
318         if not isinstance(owner, owners.Owner):
319             raise TypeError(_("invalid generic owner {0}").format(str(owner)))
320         self._owner = owner
321
322     def getowner(self):
323         return self._owner
324
325     #____________________________________________________________
326     def _read(self, remove, append):
327         for prop in remove:
328             self.remove(prop)
329         for prop in append:
330             self.append(prop)
331
332     def read_only(self):
333         "convenience method to freeze, hidde and disable"
334         self._read(ro_remove, ro_append)
335
336     def read_write(self):
337         "convenience method to freeze, hidde and disable"
338         self._read(rw_remove, rw_append)
339
340     def reset_cache(self, only_expired):
341         if only_expired:
342             self._p_.reset_expired_cache('property', time())
343         else:
344             self._p_.reset_all_cache('property')
345
346     def apply_requires(self, opt):
347         "carries out the jit (just in time requirements between options"
348         if opt._requires is None:
349             return
350
351         # filters the callbacks
352         setting = Property(self, self._getproperties(opt, False), opt)
353         descr = self.context.cfgimpl_get_description()
354         optpath = descr.impl_get_path_by_opt(opt)
355         for requires in opt._requires:
356             matches = False
357             for require in requires:
358                 option, expected, action, inverse, transitive, same_action = require
359                 path = descr.impl_get_path_by_opt(option)
360                 if path == optpath or path.startswith(optpath + '.'):
361                     raise RequirementError(_("malformed requirements "
362                                              "imbrication detected for option: '{0}' "
363                                              "with requirement on: '{1}'").format(optpath, path))
364                 try:
365                     value = self.context._getattr(path, force_permissive=True)
366                 except PropertiesOptionError, err:
367                     if not transitive:
368                         continue
369                     properties = err.proptype
370                     if same_action and action not in properties:
371                         raise RequirementError(_("option '{0}' has requirement's property error: "
372                                                  "{1} {2}").format(opt._name, path, properties))
373                     #transitive action, force expected
374                     value = expected[0]
375                     inverse = False
376                 except AttributeError:
377                     raise AttributeError(_("required option not found: "
378                                            "{0}").format(path))
379                 if not inverse and value in expected or inverse and value not in expected:
380                     matches = True
381                     setting.append(action)
382                     ## the calculation cannot be carried out
383                     break
384             # no requirement has been triggered, then just reverse the action
385             if not matches:
386                 setting.remove(action)
387
388     def _get_opt_path(self, opt):
389         return self.context.cfgimpl_get_description().impl_get_path_by_opt(opt)