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