add 'cache' property
[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 import weakref
26 from tiramisu.error import (RequirementError, PropertiesOptionError,
27                             ConstError)
28 from tiramisu.i18n import _
29
30
31 default_encoding = 'utf-8'
32 expires_time = 5
33 ro_remove = set(['permissive', 'hidden'])
34 ro_append = set(['frozen', 'disabled', 'validator', 'everything_frozen',
35                 'mandatory'])
36 rw_remove = set(['permissive', 'everything_frozen', 'mandatory'])
37 rw_append = set(['frozen', 'disabled', 'validator', 'hidden'])
38 default_properties = ('cache', 'expire', 'validator')
39
40
41 class _NameSpace:
42     """convenient class that emulates a module
43     and builds constants (that is, unique names)"""
44
45     def __setattr__(self, name, value):
46         if name in self.__dict__:
47             raise ConstError(_("can't rebind {0}").format(name))
48         self.__dict__[name] = value
49
50     def __delattr__(self, name):
51         if name in self.__dict__:
52             raise ConstError(_("can't unbind {0}").format(name))
53         raise ValueError(name)
54
55
56 # ____________________________________________________________
57 class GroupModule(_NameSpace):
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(_NameSpace):
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 addowner(name):
116         """
117         :param name: the name of the new owner
118         """
119         setattr(owners, name, owners.Owner(name))
120     setattr(owners, 'addowner', addowner)
121
122 # names are in the module now
123 populate_owners()
124
125
126 class MultiTypeModule(_NameSpace):
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', '_path')
155
156     def __init__(self, setting, prop, opt=None, path=None):
157         self._opt = opt
158         self._path = path
159         self._setting = setting
160         self._properties = prop
161
162     def append(self, propname):
163         if self._opt is not None and self._opt._calc_properties is not None \
164                 and propname in self._opt._calc_properties:
165             raise ValueError(_('cannot append {0} property for option {1}: '
166                                'this property is calculated').format(
167                                    propname, self._opt._name))
168         self._properties.add(propname)
169         self._setting._setproperties(self._properties, self._opt, self._path)
170
171     def remove(self, propname):
172         if propname in self._properties:
173             self._properties.remove(propname)
174             self._setting._setproperties(self._properties, self._opt, self._path)
175
176     def reset(self):
177         self._setting.reset(_path=self._path)
178
179     def __contains__(self, propname):
180         return propname in self._properties
181
182     def __repr__(self):
183         return str(list(self._properties))
184
185
186 #____________________________________________________________
187 class Settings(object):
188     "``Config()``'s configuration options"
189     __slots__ = ('context', '_owner', '_p_', '__weakref__')
190
191     def __init__(self, context, storage):
192         """
193         initializer
194
195         :param context: the root config
196         :param storage: the storage type
197
198                         - dictionary -> in memory
199                         - sqlite3 -> persistent
200         """
201         # generic owner
202         self._owner = owners.user
203         self.context = weakref.ref(context)
204         self._p_ = storage
205
206     #____________________________________________________________
207     # properties methods
208     def __contains__(self, propname):
209         "enables the pythonic 'in' syntaxic sugar"
210         return propname in self._getproperties()
211
212     def __repr__(self):
213         return str(list(self._getproperties()))
214
215     def __getitem__(self, opt):
216         path = self._get_opt_path(opt)
217         return self._getitem(opt, path)
218
219     def _getitem(self, opt, path):
220         return Property(self, self._getproperties(opt, path), opt, path)
221
222     def __setitem__(self, opt, value):
223         raise ValueError('you should only append/remove properties')
224
225     def reset(self, opt=None, _path=None, all_properties=False):
226         if all_properties and (_path or opt):
227             raise ValueError(_('opt and all_properties must not be set '
228                                'together in reset'))
229         if all_properties:
230             self._p_.reset_all_propertives()
231         else:
232             if opt is not None and _path is None:
233                 _path = self._get_opt_path(opt)
234             self._p_.reset_properties(_path)
235         self.context().cfgimpl_reset_cache()
236
237     def _getproperties(self, opt=None, path=None, is_apply_req=True):
238         if opt is None:
239             props = self._p_.getproperties(path, default_properties)
240         else:
241             if path is None:
242                 raise ValueError(_('if opt is not None, path should not be'
243                                    ' None in _getproperties'))
244             ntime = None
245             if 'cache' in self and self._p_.hascache(path):
246                 if 'expire' in self:
247                     ntime = int(time())
248                 is_cached, props = self._p_.getcache(path, ntime)
249                 if is_cached:
250                     return props
251             props = self._p_.getproperties(path, opt._properties)
252             if is_apply_req:
253                 props |= self.apply_requires(opt, path)
254             if 'cache' in self:
255                 if 'expire' in self:
256                     if  ntime is None:
257                         ntime = int(time())
258                     ntime = ntime + expires_time
259                 self._p_.setcache(path, props, ntime)
260         return props
261
262     def append(self, propname):
263         "puts property propname in the Config's properties attribute"
264         props = self._p_.getproperties(None, default_properties)
265         props.add(propname)
266         self._setproperties(props, None, None)
267
268     def remove(self, propname):
269         "deletes property propname in the Config's properties attribute"
270         props = self._p_.getproperties(None, default_properties)
271         if propname in props:
272             props.remove(propname)
273             self._setproperties(props, None, None)
274
275     def _setproperties(self, properties, opt, path):
276         """save properties for specified opt
277         (never save properties if same has option properties)
278         """
279         if opt is None:
280             self._p_.setproperties(None, properties)
281         else:
282             if opt._calc_properties is not None:
283                 properties -= opt._calc_properties
284             if set(opt._properties) == properties:
285                 self._p_.reset_properties(path)
286             else:
287                 self._p_.setproperties(path, properties)
288         self.context().cfgimpl_reset_cache()
289
290     #____________________________________________________________
291     def validate_properties(self, opt_or_descr, is_descr, is_write, path,
292                             value=None, force_permissive=False,
293                             force_properties=None):
294         """
295         validation upon the properties related to `opt_or_descr`
296
297         :param opt_or_descr: an option or an option description object
298         :param force_permissive: behaves as if the permissive property
299                                  was present
300         :param is_descr: we have to know if we are in an option description,
301                          just because the mandatory property
302                          doesn't exist here
303
304         :param is_write: in the validation process, an option is to be modified,
305                          the behavior can be different
306                          (typically with the `frozen` property)
307         """
308         # opt properties
309         properties = copy(self._getproperties(opt_or_descr, path))
310         # remove opt permissive
311         properties -= self._p_.getpermissive(path)
312         # remove global permissive if need
313         self_properties = copy(self._getproperties())
314         if force_permissive is True or 'permissive' in self_properties:
315             properties -= self._p_.getpermissive()
316
317         # global properties
318         if force_properties is not None:
319             self_properties.update(force_properties)
320
321         # calc properties
322         properties &= self_properties
323         # mandatory and frozen are special properties
324         if is_descr:
325             properties -= frozenset(('mandatory', 'frozen'))
326         else:
327             if 'mandatory' in properties and \
328                     not self.context().cfgimpl_get_values()._isempty(
329                         opt_or_descr, value):
330                 properties.remove('mandatory')
331             if is_write and 'everything_frozen' in self_properties:
332                 properties.add('frozen')
333             elif 'frozen' in properties and not is_write:
334                 properties.remove('frozen')
335         # at this point an option should not remain in properties
336         if properties != frozenset():
337             props = list(properties)
338             if 'frozen' in properties:
339                 raise PropertiesOptionError(_('cannot change the value for '
340                                               'option {0} this option is'
341                                               ' frozen').format(
342                                                   opt_or_descr._name),
343                                             props)
344             else:
345                 raise PropertiesOptionError(_("trying to access to an option "
346                                               "named: {0} with properties {1}"
347                                               "").format(opt_or_descr._name,
348                                                          str(props)), props)
349
350     def setpermissive(self, permissive, opt=None, path=None):
351         """
352         enables us to put the permissives in the storage
353
354         :param path: the option's path
355         :param type: str
356         :param opt: if an option object is set, the path is extracted.
357                     it is better (faster) to set the path parameter
358                     instead of passing a :class:`tiramisu.option.Option()` object.
359         """
360         if opt is not None and path is None:
361             path = self._get_opt_path(opt)
362         if not isinstance(permissive, tuple):
363             raise TypeError(_('permissive must be a tuple'))
364         self._p_.setpermissive(path, permissive)
365
366     #____________________________________________________________
367     def setowner(self, owner):
368         ":param owner: sets the default value for owner at the Config level"
369         if not isinstance(owner, owners.Owner):
370             raise TypeError(_("invalid generic owner {0}").format(str(owner)))
371         self._owner = owner
372
373     def getowner(self):
374         return self._owner
375
376     #____________________________________________________________
377     def _read(self, remove, append):
378         for prop in remove:
379             self.remove(prop)
380         for prop in append:
381             self.append(prop)
382
383     def read_only(self):
384         "convenience method to freeze, hidde and disable"
385         self._read(ro_remove, ro_append)
386
387     def read_write(self):
388         "convenience method to freeze, hidde and disable"
389         self._read(rw_remove, rw_append)
390
391     def reset_cache(self, only_expired):
392         if only_expired:
393             self._p_.reset_expired_cache(int(time()))
394         else:
395             self._p_.reset_all_cache()
396
397     def apply_requires(self, opt, path):
398         """carries out the jit (just in time) requirements between options
399
400         a requirement is a tuple of this form that comes from the option's
401         requirements validation::
402
403             (option, expected, action, inverse, transitive, same_action)
404
405         let's have a look at all the tuple's items:
406
407         - **option** is the target option's name or path
408
409         - **expected** is the target option's value that is going to trigger an action
410
411         - **action** is the (property) action to be accomplished if the target option
412           happens to have the expected value
413
414         - if **inverse** is `True` and if the target option's value does not
415           apply, then the property action must be removed from the option's
416           properties list (wich means that the property is inverted)
417
418         - **transitive**: but what happens if the target option cannot be
419           accessed ? We don't kown the target option's value. Actually if some
420           property in the target option is not present in the permissive, the
421           target option's value cannot be accessed. In this case, the
422           **action** have to be applied to the option. (the **action** property
423           is then added to the option).
424
425         - **same_action**: actually, if **same_action** is `True`, the
426           transitivity is not accomplished. The transitivity is accomplished
427           only if the target option **has the same property** that the demanded
428           action. If the target option's value is not accessible because of
429           another reason, because of a property of another type, then an
430           exception :exc:`~error.RequirementError` is raised.
431
432         And at last, if no target option matches the expected values, the
433         action must be removed from the option's properties list.
434
435         :param opt: the option on wich the requirement occurs
436         :type opt: `option.Option()`
437         :param path: the option's path in the config
438         :type path: str
439         """
440         if opt._requires is None:
441             return frozenset()
442
443         # filters the callbacks
444         calc_properties = set()
445         for requires in opt._requires:
446             for require in requires:
447                 option, expected, action, inverse, \
448                     transitive, same_action = require
449                 reqpath = self._get_opt_path(option)
450                 if reqpath == path or reqpath.startswith(path + '.'):
451                     raise RequirementError(_("malformed requirements "
452                                              "imbrication detected for option:"
453                                              " '{0}' with requirement on: "
454                                              "'{1}'").format(path, reqpath))
455                 try:
456                     value = self.context()._getattr(reqpath,
457                                                     force_permissive=True)
458                 except PropertiesOptionError as err:
459                     if not transitive:
460                         continue
461                     properties = err.proptype
462                     if same_action and action not in properties:
463                         raise RequirementError(_("option '{0}' has "
464                                                  "requirement's property "
465                                                  "error: "
466                                                  "{1} {2}").format(opt._name,
467                                                                    reqpath,
468                                                                    properties))
469                     # transitive action, force expected
470                     value = expected[0]
471                     inverse = False
472                 if (not inverse and
473                         value in expected or
474                         inverse and value not in expected):
475                     calc_properties.add(action)
476                     # the calculation cannot be carried out
477                     break
478         return calc_properties
479
480     def _get_opt_path(self, opt):
481         return self.context().cfgimpl_get_description().impl_get_path_by_opt(opt)