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