split cache/value/setting in plugin
[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 from tiramisu.plugins.dictionary.setting import PluginSettings
28
29
30 default_encoding = 'utf-8'
31 expires_time = 5
32 ro_remove = ('permissive', 'hidden')
33 ro_append = ('frozen', 'disabled', 'validator', 'everything_frozen', 'mandatory')
34 rw_remove = ('permissive', 'everything_frozen', 'mandatory')
35 rw_append = ('frozen', 'disabled', 'validator', 'hidden')
36 default_properties = ('expire', 'validator')
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(PluginSettings):
182     "``Config()``'s configuration options"
183     __slots__ = ('context', '_owner')
184
185     def __init__(self, context):
186         # generic owner
187         self._owner = owners.user
188         self.context = context
189         super(Settings, self).__init__()
190
191     #____________________________________________________________
192     # properties methods
193     def __contains__(self, propname):
194         return propname in self._getproperties()
195
196     def __repr__(self):
197         return str(list(self._getproperties()))
198
199     def __getitem__(self, opt):
200         return Property(self, self._getproperties(opt), opt)
201
202     def __setitem__(self, opt, value):
203         raise ValueError('you must only append/remove properties')
204
205     def reset(self, opt=None, all_properties=False):
206         if all_properties and opt:
207             raise ValueError(_('opt and all_properties must not be set '
208                                'together in reset'))
209         if all_properties:
210             self._p_reset_all_propertives()
211         else:
212             self._p_reset_properties(opt)
213         self.context.cfgimpl_reset_cache()
214
215     def _getproperties(self, opt=None, is_apply_req=True):
216         if opt is None:
217             props = self._p_getproperties(opt, default_properties)
218         else:
219             ntime = None
220             if self._p_hascache('properties', opt):
221                 ntime = time()
222                 is_cached, props = self._p_getcache('properties', opt, ntime)
223                 if is_cached:
224                     return props
225             if is_apply_req:
226                 self.apply_requires(opt)
227             props = self._p_getproperties(opt, opt._properties)
228             if 'expire' in self:
229                 if ntime is None:
230                     ntime = time()
231                 self._p_setcache('properties', opt, props, ntime + expires_time)
232         return props
233
234     def append(self, propname):
235         "puts property propname in the Config's properties attribute"
236         Property(self, self._getproperties()).append(propname)
237
238     def remove(self, propname):
239         "deletes property propname in the Config's properties attribute"
240         Property(self, self._getproperties()).remove(propname)
241
242     def _setproperties(self, properties, opt=None):
243         """save properties for specified opt
244         (never save properties if same has option properties)
245         """
246         if opt is None:
247             self._p_setproperties(opt, properties)
248         else:
249             if set(opt._properties) == properties:
250                 self._p_reset_properties(opt)
251             else:
252                 self._p_setproperties(opt, properties)
253         self.context.cfgimpl_reset_cache()
254
255     #____________________________________________________________
256     def validate_properties(self, opt_or_descr, is_descr, is_write,
257                             value=None, force_permissive=False,
258                             force_properties=None):
259         #opt properties
260         properties = copy(self._getproperties(opt_or_descr))
261         #remove opt permissive
262         properties -= self._p_getpermissive(opt_or_descr)
263         #remove global permissive if need
264         self_properties = copy(self._getproperties())
265         if force_permissive is True or 'permissive' in self_properties:
266             properties -= self._p_getpermissive()
267
268         #global properties
269         if force_properties is not None:
270             self_properties.update(force_properties)
271
272         #calc properties
273         properties &= self_properties
274         #mandatory and frozen are special properties
275         if is_descr:
276             properties -= frozenset(('mandatory', 'frozen'))
277         else:
278             if 'mandatory' in properties and \
279                     not self.context.cfgimpl_get_values()._isempty(opt_or_descr,
280                                                                    value):
281                 properties.remove('mandatory')
282             if is_write and 'everything_frozen' in self_properties:
283                 properties.add('frozen')
284             elif 'frozen' in properties and not is_write:
285                 properties.remove('frozen')
286
287         if properties != frozenset():
288             props = list(properties)
289             if 'frozen' in properties:
290                 raise PropertiesOptionError(_('cannot change the value for '
291                                               'option {0} this option is frozen'
292                                               '').format(opt_or_descr._name), props)
293             else:
294                 raise PropertiesOptionError(_("trying to access to an option "
295                                               "named: {0} with properties {1}"
296                                               "").format(opt_or_descr._name, str(props)), props)
297
298     #FIXME should be setpermissive
299     def set_permissive(self, permissive, opt=None):
300         if not isinstance(permissive, tuple):
301             raise TypeError(_('permissive must be a tuple'))
302         self._p_setpermissive(opt, permissive)
303
304     #____________________________________________________________
305     def setowner(self, owner):
306         ":param owner: sets the default value for owner at the Config level"
307         if not isinstance(owner, owners.Owner):
308             raise TypeError(_("invalid generic owner {0}").format(str(owner)))
309         self._owner = owner
310
311     def getowner(self):
312         return self._owner
313
314     #____________________________________________________________
315     def _read(self, remove, append):
316         for prop in remove:
317             self.remove(prop)
318         for prop in append:
319             self.append(prop)
320
321     def read_only(self):
322         "convenience method to freeze, hidde and disable"
323         self._read(ro_remove, ro_append)
324
325     def read_write(self):
326         "convenience method to freeze, hidde and disable"
327         self._read(rw_remove, rw_append)
328
329     def reset_cache(self, only_expired):
330         if only_expired:
331             self._p_reset_expired_cache('properties', time())
332         else:
333             self._p_reset_all_cache('properties')
334
335     def apply_requires(self, opt):
336         "carries out the jit (just in time requirements between options"
337         if opt._requires is None:
338             return
339
340         # filters the callbacks
341         setting = Property(self, self._getproperties(opt, False), opt)
342         descr = self.context.cfgimpl_get_description()
343         optpath = descr.impl_get_path_by_opt(opt)
344         for requires in opt._requires:
345             matches = False
346             for require in requires:
347                 option, expected, action, inverse, transitive, same_action = require
348                 path = descr.impl_get_path_by_opt(option)
349                 if path == optpath or path.startswith(optpath + '.'):
350                     raise RequirementError(_("malformed requirements "
351                                              "imbrication detected for option: '{0}' "
352                                              "with requirement on: '{1}'").format(optpath, path))
353                 try:
354                     value = self.context._getattr(path, force_permissive=True)
355                 except PropertiesOptionError, err:
356                     if not transitive:
357                         continue
358                     properties = err.proptype
359                     if same_action and action not in properties:
360                         raise RequirementError(_("option '{0}' has requirement's property error: "
361                                                  "{1} {2}").format(opt._name, path, properties))
362                     #transitive action, force expected
363                     value = expected[0]
364                     inverse = False
365                 except AttributeError:
366                     raise AttributeError(_("required option not found: "
367                                            "{0}").format(path))
368                 if not inverse and value in expected or inverse and value not in expected:
369                     matches = True
370                     setting.append(action)
371                     ## the calculation cannot be carried out
372                     break
373             # no requirement has been triggered, then just reverse the action
374             if not matches:
375                 setting.remove(action)