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