c9d4a0baf1d98ca8607b7473007425a265ab4447
[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             storage = self.default_storage
52         else:
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 add_owner(name):
136         """
137         :param name: the name of the new owner
138         """
139         setattr(owners, name, owners.Owner(name))
140     setattr(owners, 'add_owner', add_owner)
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, is_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, is_persistent)
227
228
229 #____________________________________________________________
230 class Settings(object):
231     "``Config()``'s configuration options"
232     __slots__ = ('context', '_owner', '_p_')
233
234     def __init__(self, context, storage):
235         """
236         initializer
237
238         :param context: the root config
239         :param storage: the storage type
240
241                         - dictionary -> in memory
242                         - sqlite3 -> persistent
243         """
244         # generic owner
245         self._owner = owners.user
246         self.context = context
247         import_lib = 'tiramisu.storage.{0}.setting'.format(storage.storage)
248         self._p_ = __import__(import_lib, globals(), locals(), ['Settings'],
249                               -1).Settings(storage)
250
251     #____________________________________________________________
252     # properties methods
253     def __contains__(self, propname):
254         "enables the pythonic 'in' syntaxic sugar"
255         return propname in self._getproperties()
256
257     def __repr__(self):
258         return str(list(self._getproperties()))
259
260     def __getitem__(self, opt):
261         path = self._get_opt_path(opt)
262         return self._getitem(opt, path)
263
264     def _getitem(self, opt, path):
265         return Property(self, self._getproperties(opt, path), opt, path)
266
267     def __setitem__(self, opt, value):
268         raise ValueError('you should only append/remove properties')
269
270     def reset(self, opt=None, _path=None, all_properties=False):
271         if all_properties and (_path or opt):
272             raise ValueError(_('opt and all_properties must not be set '
273                                'together in reset'))
274         if all_properties:
275             self._p_.reset_all_propertives()
276         else:
277             if opt is not None and _path is None:
278                 _path = self._get_opt_path(opt)
279             self._p_.reset_properties(_path)
280         self.context.cfgimpl_reset_cache()
281
282     def _getproperties(self, opt=None, path=None, is_apply_req=True):
283         if opt is None:
284             props = self._p_.getproperties(path, default_properties)
285         else:
286             if path is None:
287                 raise ValueError(_('if opt is not None, path should not be'
288                                    ' None in _getproperties'))
289             ntime = None
290             if self._p_.hascache('property', path):
291                 ntime = time()
292                 is_cached, props = self._p_.getcache('property', path, ntime)
293                 if is_cached:
294                     return props
295             props = self._p_.getproperties(path, opt._properties)
296             if is_apply_req:
297                 props |= self.apply_requires(opt, path)
298             if 'expire' in self:
299                 if ntime is None:
300                     ntime = time()
301                 self._p_.setcache('property', path, props, ntime + expires_time)
302         return props
303
304     def append(self, propname):
305         "puts property propname in the Config's properties attribute"
306         props = self._p_.getproperties(None, default_properties)
307         props.add(propname)
308         self._setproperties(props, None, None)
309
310     def remove(self, propname):
311         "deletes property propname in the Config's properties attribute"
312         props = self._p_.getproperties(None, default_properties)
313         if propname in props:
314             props.remove(propname)
315             self._setproperties(props, None, None)
316
317     def _setproperties(self, properties, opt, path):
318         """save properties for specified opt
319         (never save properties if same has option properties)
320         """
321         if opt is None:
322             self._p_.setproperties(None, properties)
323         else:
324             if opt._calc_properties is not None:
325                 properties -= opt._calc_properties
326             if set(opt._properties) == properties:
327                 self._p_.reset_properties(path)
328             else:
329                 self._p_.setproperties(path, properties)
330         self.context.cfgimpl_reset_cache()
331
332     #____________________________________________________________
333     def validate_properties(self, opt_or_descr, is_descr, is_write, path,
334                             value=None, force_permissive=False,
335                             force_properties=None):
336         """
337         validation upon the properties related to `opt_or_descr`
338
339         :param opt_or_descr: an option or an option description object
340         :param force_permissive: behaves as if the permissive property
341                                  was present
342         :param is_descr: we have to know if we are in an option description,
343                          just because the mandatory property
344                          doesn't exist here
345
346         :param is_write: in the validation process, an option is to be modified,
347                          the behavior can be different
348                          (typically with the `frozen` property)
349         """
350         # opt properties
351         properties = copy(self._getproperties(opt_or_descr, path))
352         # remove opt permissive
353         properties -= self._p_.getpermissive(path)
354         # remove global permissive if need
355         self_properties = copy(self._getproperties())
356         if force_permissive is True or 'permissive' in self_properties:
357             properties -= self._p_.getpermissive()
358
359         # global properties
360         if force_properties is not None:
361             self_properties.update(force_properties)
362
363         # calc properties
364         properties &= self_properties
365         # mandatory and frozen are special properties
366         if is_descr:
367             properties -= frozenset(('mandatory', 'frozen'))
368         else:
369             if 'mandatory' in properties and \
370                     not self.context.cfgimpl_get_values()._isempty(
371                         opt_or_descr, value):
372                 properties.remove('mandatory')
373             if is_write and 'everything_frozen' in self_properties:
374                 properties.add('frozen')
375             elif 'frozen' in properties and not is_write:
376                 properties.remove('frozen')
377         # at this point an option should not remain in properties
378         if properties != frozenset():
379             props = list(properties)
380             if 'frozen' in properties:
381                 raise PropertiesOptionError(_('cannot change the value for '
382                                               'option {0} this option is'
383                                               ' frozen').format(
384                                                   opt_or_descr._name),
385                                             props)
386             else:
387                 raise PropertiesOptionError(_("trying to access to an option "
388                                               "named: {0} with properties {1}"
389                                               "").format(opt_or_descr._name,
390                                                          str(props)), props)
391
392     def setpermissive(self, permissive, path=None):
393         if not isinstance(permissive, tuple):
394             raise TypeError(_('permissive must be a tuple'))
395         self._p_.setpermissive(path, permissive)
396
397     #____________________________________________________________
398     def setowner(self, owner):
399         ":param owner: sets the default value for owner at the Config level"
400         if not isinstance(owner, owners.Owner):
401             raise TypeError(_("invalid generic owner {0}").format(str(owner)))
402         self._owner = owner
403
404     def getowner(self):
405         return self._owner
406
407     #____________________________________________________________
408     def _read(self, remove, append):
409         for prop in remove:
410             self.remove(prop)
411         for prop in append:
412             self.append(prop)
413
414     def read_only(self):
415         "convenience method to freeze, hidde and disable"
416         self._read(ro_remove, ro_append)
417
418     def read_write(self):
419         "convenience method to freeze, hidde and disable"
420         self._read(rw_remove, rw_append)
421
422     def reset_cache(self, only_expired):
423         if only_expired:
424             self._p_.reset_expired_cache('property', time())
425         else:
426             self._p_.reset_all_cache('property')
427
428     def apply_requires(self, opt, path):
429         """carries out the jit (just in time) requirements between options
430
431         a requirement is a tuple of this form that comes from the option's
432         requirements validation::
433
434             (option, expected, action, inverse, transitive, same_action)
435
436         let's have a look at all the tuple's items:
437
438         - **option** is the target option's name or path
439
440         - **expected** is the target option's value that is going to trigger an action
441
442         - **action** is the (property) action to be accomplished if the target option
443           happens to have the expected value
444
445         - if **inverse** is `True` and if the target option's value does not
446           apply, then the property action must be removed from the option's
447           properties list (wich means that the property is inverted)
448
449         - **transitive**: but what happens if the target option cannot be
450           accessed ? We don't kown the target option's value. Actually if some
451           property in the target option is not present in the permissive, the
452           target option's value cannot be accessed. In this case, the
453           **action** have to be applied to the option. (the **action** property
454           is then added to the option).
455
456         - **same_action**: actually, if **same_action** is `True`, the
457           transitivity is not accomplished. The transitivity is accomplished
458           only if the target option **has the same property** that the demanded
459           action. If the target option's value is not accessible because of
460           another reason, because of a property of another type, then an
461           exception :exc:`~error.RequirementError` is raised.
462
463         And at last, if no target option matches the expected values, the
464         action must be removed from the option's properties list.
465
466         :param opt: the option on wich the requirement occurs
467         :type opt: `option.Option()`
468         :param path: the option's path in the config
469         :type path: str
470         """
471         if opt._requires is None:
472             return frozenset()
473
474         # filters the callbacks
475         calc_properties = set()
476         for requires in opt._requires:
477             for require in requires:
478                 option, expected, action, inverse, \
479                     transitive, same_action = require
480                 reqpath = self._get_opt_path(option)
481                 if reqpath == path or reqpath.startswith(path + '.'):
482                     raise RequirementError(_("malformed requirements "
483                                              "imbrication detected for option:"
484                                              " '{0}' with requirement on: "
485                                              "'{1}'").format(path, reqpath))
486                 try:
487                     value = self.context._getattr(reqpath,
488                                                   force_permissive=True)
489                 except PropertiesOptionError, err:
490                     if not transitive:
491                         continue
492                     properties = err.proptype
493                     if same_action and action not in properties:
494                         raise RequirementError(_("option '{0}' has "
495                                                  "requirement's property "
496                                                  "error: "
497                                                  "{1} {2}").format(opt._name,
498                                                                    reqpath,
499                                                                    properties))
500                     # transitive action, force expected
501                     value = expected[0]
502                     inverse = False
503                 if (not inverse and
504                         value in expected or
505                         inverse and value not in expected):
506                     calc_properties.add(action)
507                     # the calculation cannot be carried out
508                     break
509             return calc_properties
510
511     def _get_opt_path(self, opt):
512         return self.context.cfgimpl_get_description().impl_get_path_by_opt(opt)