f0e4d8442cc7ec582b0c54be853676fb486701df
[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')
156
157     def __init__(self, setting, prop, opt=None):
158         self._opt = opt
159         self._setting = setting
160         self._properties = prop
161
162     def append(self, propname):
163         self._properties.add(propname)
164         self._setting._setproperties(self._properties, self._opt)
165
166     def remove(self, propname):
167         if propname in self._properties:
168             self._properties.remove(propname)
169             self._setting._setproperties(self._properties, self._opt)
170
171     def reset(self):
172         self._setting.reset(opt=self._opt)
173
174     def __contains__(self, propname):
175         return propname in self._properties
176
177     def __repr__(self):
178         return str(list(self._properties))
179
180
181 #____________________________________________________________
182 class Settings(object):
183     "``Config()``'s configuration options"
184     __slots__ = ('context', '_owner', '_p_')
185
186     def __init__(self, context, storage):
187         """
188         initializer
189
190         :param context: the root config
191         :param storage: the storage type
192
193                         - dictionnary -> in memory
194                         - sqlite3 -> persistent
195         """
196         # generic owner
197         self._owner = owners.user
198         self.context = context
199         import_lib = 'tiramisu.storage.{0}.setting'.format(storage_type)
200         self._p_ = __import__(import_lib, globals(), locals(), ['Settings'],
201                               -1).Settings(storage)
202
203     def _getkey(self, opt):
204         if self._p_.key_is_path:
205             if opt is None:
206                 return '_none'
207             else:
208                 return self._get_opt_path(opt)
209         else:
210             return opt
211
212     #____________________________________________________________
213     # properties methods
214     def __contains__(self, propname):
215         "enables the pythonic 'in' syntaxic sugar"
216         return propname in self._getproperties()
217
218     def __repr__(self):
219         return str(list(self._getproperties()))
220
221     def __getitem__(self, opt):
222         return Property(self, self._getproperties(opt), opt)
223
224     def __setitem__(self, opt, value):
225         raise ValueError('you must only append/remove properties')
226
227     def reset(self, opt=None, all_properties=False):
228         if all_properties and opt:
229             raise ValueError(_('opt and all_properties must not be set '
230                                'together in reset'))
231         if all_properties:
232             self._p_.reset_all_propertives()
233         else:
234             self._p_.reset_properties(self._getkey(opt))
235         self.context.cfgimpl_reset_cache()
236
237     def _getproperties(self, opt=None, is_apply_req=True):
238         if opt is None:
239             props = self._p_.getproperties(self._getkey(opt), default_properties)
240         else:
241             ntime = None
242             if self._p_.hascache('property', self._getkey(opt)):
243                 ntime = time()
244                 is_cached, props = self._p_.getcache('property', self._getkey(opt), ntime)
245                 if is_cached:
246                     return props
247             if is_apply_req:
248                 self.apply_requires(opt)
249             props = self._p_.getproperties(self._getkey(opt), opt._properties)
250             if 'expire' in self:
251                 if ntime is None:
252                     ntime = time()
253                 self._p_.setcache('property', self._getkey(opt), props, ntime + expires_time)
254         return props
255
256     def append(self, propname):
257         "puts property propname in the Config's properties attribute"
258         Property(self, self._getproperties()).append(propname)
259
260     def remove(self, propname):
261         "deletes property propname in the Config's properties attribute"
262         Property(self, self._getproperties()).remove(propname)
263
264     def _setproperties(self, properties, opt=None):
265         """save properties for specified opt
266         (never save properties if same has option properties)
267         """
268         if opt is None:
269             self._p_.setproperties(self._getkey(opt), properties)
270         else:
271             if set(opt._properties) == properties:
272                 self._p_.reset_properties(self._getkey(opt))
273             else:
274                 self._p_.setproperties(self._getkey(opt), properties)
275         self.context.cfgimpl_reset_cache()
276
277     #____________________________________________________________
278     def validate_properties(self, opt_or_descr, is_descr, is_write,
279                             value=None, force_permissive=False,
280                             force_properties=None):
281         """
282         validation upon the properties related to `opt_or_descr`
283
284         :param opt_or_descr: an option or an option description object
285         :param force_permissive: behaves as if the permissive property was present
286         :param is_descr: we have to know if we are in an option description,
287                          just because the mandatory property doesn't exist there
288
289         :param is_write: in the validation process, an option is to be modified,
290                          the behavior can be different (typically with the `frozen`
291                          property)
292         """
293         # opt properties
294         properties = copy(self._getproperties(opt_or_descr))
295         # remove opt permissive
296         properties -= self._p_.getpermissive(self._getkey(opt_or_descr))
297         # remove global permissive if need
298         self_properties = copy(self._getproperties())
299         if force_permissive is True or 'permissive' in self_properties:
300             properties -= self._p_.getpermissive()
301
302         # global properties
303         if force_properties is not None:
304             self_properties.update(force_properties)
305
306         # calc properties
307         properties &= self_properties
308         # mandatory and frozen are special properties
309         if is_descr:
310             properties -= frozenset(('mandatory', 'frozen'))
311         else:
312             if 'mandatory' in properties and \
313                     not self.context.cfgimpl_get_values()._isempty(
314                         opt_or_descr, value):
315                 properties.remove('mandatory')
316             if is_write and 'everything_frozen' in self_properties:
317                 properties.add('frozen')
318             elif 'frozen' in properties and not is_write:
319                 properties.remove('frozen')
320         # at this point an option should not remain in properties
321         if properties != frozenset():
322             props = list(properties)
323             if 'frozen' in properties:
324                 raise PropertiesOptionError(_('cannot change the value for '
325                                               'option {0} this option is'
326                                               ' frozen').format(
327                                                   opt_or_descr._name),
328                                             props)
329             else:
330                 raise PropertiesOptionError(_("trying to access to an option "
331                                               "named: {0} with properties {1}"
332                                               "").format(opt_or_descr._name,
333                                                          str(props)), props)
334
335     # XXX should rename it to setpermissive, but kept for retro compatibility
336     def set_permissive(self, permissive, opt=None):
337         if not isinstance(permissive, tuple):
338             raise TypeError(_('permissive must be a tuple'))
339         self._p_.setpermissive(self._getkey(opt), permissive)
340
341     #____________________________________________________________
342     def setowner(self, owner):
343         ":param owner: sets the default value for owner at the Config level"
344         if not isinstance(owner, owners.Owner):
345             raise TypeError(_("invalid generic owner {0}").format(str(owner)))
346         self._owner = owner
347
348     def getowner(self):
349         return self._owner
350
351     #____________________________________________________________
352     def _read(self, remove, append):
353         for prop in remove:
354             self.remove(prop)
355         for prop in append:
356             self.append(prop)
357
358     def read_only(self):
359         "convenience method to freeze, hidde and disable"
360         self._read(ro_remove, ro_append)
361
362     def read_write(self):
363         "convenience method to freeze, hidde and disable"
364         self._read(rw_remove, rw_append)
365
366     def reset_cache(self, only_expired):
367         if only_expired:
368             self._p_.reset_expired_cache('property', time())
369         else:
370             self._p_.reset_all_cache('property')
371
372     def apply_requires(self, opt):
373         "carries out the jit (just in time requirements between options"
374         if opt._requires is None:
375             return
376
377         # filters the callbacks
378         setting = Property(self, self._getproperties(opt, False), opt)
379         descr = self.context.cfgimpl_get_description()
380         optpath = descr.impl_get_path_by_opt(opt)
381         for requires in opt._requires:
382             matches = False
383             for require in requires:
384                 option, expected, action, inverse, \
385                     transitive, same_action = require
386                 path = descr.impl_get_path_by_opt(option)
387                 if path == optpath or path.startswith(optpath + '.'):
388                     raise RequirementError(_("malformed requirements "
389                                              "imbrication detected for option:"
390                                              " '{0}' with requirement on: "
391                                              "'{1}'").format(optpath, path))
392                 try:
393                     value = self.context._getattr(path, force_permissive=True)
394                 except PropertiesOptionError, err:
395                     if not transitive:
396                         continue
397                     properties = err.proptype
398                     if same_action and action not in properties:
399                         raise RequirementError(_("option '{0}' has "
400                                                  "requirement's property "
401                                                  "error: "
402                                                  "{1} {2}").format(opt._name,
403                                                                    path,
404                                                                    properties))
405                     # transitive action, force expected
406                     value = expected[0]
407                     inverse = False
408                 except AttributeError:
409                     raise AttributeError(_("required option not found: "
410                                            "{0}").format(path))
411                 if (not inverse and
412                         value in expected or
413                         inverse and value not in expected):
414                     matches = True
415                     setting.append(action)
416                     # the calculation cannot be carried out
417                     break
418             # no requirement has been triggered, then just reverse the action
419             if not matches:
420                 setting.remove(action)
421
422     def _get_opt_path(self, opt):
423         return self.context.cfgimpl_get_description().impl_get_path_by_opt(opt)