99fda6afc6cf281b3c97169624681d15b7190797
[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 = 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 = ('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']).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']).Storage(session_id, persistent)
227
228
229 def list_sessions():
230     return __import__(storage_type.get_storage(), globals(), locals(),
231                       ['list_sessions']).list_sessions()
232
233
234 def delete_session(session_id):
235     return __import__(storage_type.get_storage(), globals(), locals(),
236                       ['delete_session']).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                               ).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, opt=None, path=None):
403         """
404         enables us to put the permissives in the storage
405
406         :param path: the option's path
407         :param type: str
408         :param opt: if an option object is set, the path is extracted.
409                     it is better (faster) to set the path parameter
410                     instead of passing a :class:`tiramisu.option.Option()` object.
411         """
412         if opt is not None and path is None:
413             path = self._get_opt_path(opt)
414         if not isinstance(permissive, tuple):
415             raise TypeError(_('permissive must be a tuple'))
416         self._p_.setpermissive(path, permissive)
417
418     #____________________________________________________________
419     def setowner(self, owner):
420         ":param owner: sets the default value for owner at the Config level"
421         if not isinstance(owner, owners.Owner):
422             raise TypeError(_("invalid generic owner {0}").format(str(owner)))
423         self._owner = owner
424
425     def getowner(self):
426         return self._owner
427
428     #____________________________________________________________
429     def _read(self, remove, append):
430         for prop in remove:
431             self.remove(prop)
432         for prop in append:
433             self.append(prop)
434
435     def read_only(self):
436         "convenience method to freeze, hidde and disable"
437         self._read(ro_remove, ro_append)
438
439     def read_write(self):
440         "convenience method to freeze, hidde and disable"
441         self._read(rw_remove, rw_append)
442
443     def reset_cache(self, only_expired):
444         if only_expired:
445             self._p_.reset_expired_cache('property', time())
446         else:
447             self._p_.reset_all_cache('property')
448
449     def apply_requires(self, opt, path):
450         """carries out the jit (just in time) requirements between options
451
452         a requirement is a tuple of this form that comes from the option's
453         requirements validation::
454
455             (option, expected, action, inverse, transitive, same_action)
456
457         let's have a look at all the tuple's items:
458
459         - **option** is the target option's name or path
460
461         - **expected** is the target option's value that is going to trigger an action
462
463         - **action** is the (property) action to be accomplished if the target option
464           happens to have the expected value
465
466         - if **inverse** is `True` and if the target option's value does not
467           apply, then the property action must be removed from the option's
468           properties list (wich means that the property is inverted)
469
470         - **transitive**: but what happens if the target option cannot be
471           accessed ? We don't kown the target option's value. Actually if some
472           property in the target option is not present in the permissive, the
473           target option's value cannot be accessed. In this case, the
474           **action** have to be applied to the option. (the **action** property
475           is then added to the option).
476
477         - **same_action**: actually, if **same_action** is `True`, the
478           transitivity is not accomplished. The transitivity is accomplished
479           only if the target option **has the same property** that the demanded
480           action. If the target option's value is not accessible because of
481           another reason, because of a property of another type, then an
482           exception :exc:`~error.RequirementError` is raised.
483
484         And at last, if no target option matches the expected values, the
485         action must be removed from the option's properties list.
486
487         :param opt: the option on wich the requirement occurs
488         :type opt: `option.Option()`
489         :param path: the option's path in the config
490         :type path: str
491         """
492         if opt._requires is None:
493             return frozenset()
494
495         # filters the callbacks
496         calc_properties = set()
497         for requires in opt._requires:
498             for require in requires:
499                 option, expected, action, inverse, \
500                     transitive, same_action = require
501                 reqpath = self._get_opt_path(option)
502                 if reqpath == path or reqpath.startswith(path + '.'):
503                     raise RequirementError(_("malformed requirements "
504                                              "imbrication detected for option:"
505                                              " '{0}' with requirement on: "
506                                              "'{1}'").format(path, reqpath))
507                 try:
508                     value = self.context()._getattr(reqpath,
509                                                     force_permissive=True)
510                 except PropertiesOptionError as err:
511                     if not transitive:
512                         continue
513                     properties = err.proptype
514                     if same_action and action not in properties:
515                         raise RequirementError(_("option '{0}' has "
516                                                  "requirement's property "
517                                                  "error: "
518                                                  "{1} {2}").format(opt._name,
519                                                                    reqpath,
520                                                                    properties))
521                     # transitive action, force expected
522                     value = expected[0]
523                     inverse = False
524                 if (not inverse and
525                         value in expected or
526                         inverse and value not in expected):
527                     calc_properties.add(action)
528                     # the calculation cannot be carried out
529                     break
530         return calc_properties
531
532     def _get_opt_path(self, opt):
533         return self.context().cfgimpl_get_description().impl_get_path_by_opt(opt)