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