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