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