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