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