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