setpermissive should have opt has option
[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 import weakref
26 from tiramisu.error import (RequirementError, PropertiesOptionError,
27                             ConstError, ConfigError)
28 from tiramisu.i18n import _
29
30
31 default_encoding = 'utf-8'
32 expires_time = 5
33 ro_remove = ('permissive', 'hidden')
34 ro_append = ('frozen', 'disabled', 'validator', 'everything_frozen',
35              'mandatory')
36 rw_remove = ('permissive', 'everything_frozen', 'mandatory')
37 rw_append = ('frozen', 'disabled', 'validator', 'hidden')
38 default_properties = ('expire', 'validator')
39
40
41 class StorageType:
42     default_storage = 'dictionary'
43     storage_type = None
44
45     def set_storage(self, name):
46         if self.storage_type is not None:
47             raise ConfigError(_('storage_type is already set, cannot rebind it'))
48         self.storage_type = name
49
50     def get_storage(self):
51         if self.storage_type is None:
52             self.storage_type = self.default_storage
53         storage = self.storage_type
54         return 'tiramisu.storage.{0}.storage'.format(
55             storage)
56
57
58 storage_type = StorageType()
59
60
61 class _NameSpace:
62     """convenient class that emulates a module
63     and builds constants (that is, unique names)"""
64
65     def __setattr__(self, name, value):
66         if name in self.__dict__:
67             raise ConstError(_("can't rebind {0}").format(name))
68         self.__dict__[name] = value
69
70     def __delattr__(self, name):
71         if name in self.__dict__:
72             raise ConstError(_("can't unbind {0}").format(name))
73         raise ValueError(name)
74
75
76 # ____________________________________________________________
77 class GroupModule(_NameSpace):
78     "emulates a module to manage unique group (OptionDescription) names"
79     class GroupType(str):
80         """allowed normal group (OptionDescription) names
81         *normal* means : groups that are not master
82         """
83         pass
84
85     class DefaultGroupType(GroupType):
86         """groups that are default (typically 'default')"""
87         pass
88
89     class MasterGroupType(GroupType):
90         """allowed normal group (OptionDescription) names
91         *master* means : groups that have the 'master' attribute set
92         """
93         pass
94 # setting.groups (emulates a module)
95 groups = GroupModule()
96
97
98 def populate_groups():
99     "populates the available groups in the appropriate namespaces"
100     groups.master = groups.MasterGroupType('master')
101     groups.default = groups.DefaultGroupType('default')
102     groups.family = groups.GroupType('family')
103
104 # names are in the module now
105 populate_groups()
106
107
108 # ____________________________________________________________
109 class OwnerModule(_NameSpace):
110     """emulates a module to manage unique owner names.
111
112     owners are living in `Config._cfgimpl_value_owners`
113     """
114     class Owner(str):
115         """allowed owner names
116         """
117         pass
118
119     class DefaultOwner(Owner):
120         """groups that are default (typically 'default')"""
121         pass
122 # setting.owners (emulates a module)
123 owners = OwnerModule()
124
125
126 def populate_owners():
127     """populates the available owners in the appropriate namespaces
128
129     - 'user' is the generic is the generic owner.
130     - 'default' is the config owner after init time
131     """
132     setattr(owners, 'default', owners.DefaultOwner('default'))
133     setattr(owners, 'user', owners.Owner('user'))
134
135     def addowner(name):
136         """
137         :param name: the name of the new owner
138         """
139         setattr(owners, name, owners.Owner(name))
140     setattr(owners, 'addowner', addowner)
141
142 # names are in the module now
143 populate_owners()
144
145
146 class MultiTypeModule(_NameSpace):
147     "namespace for the master/slaves"
148     class MultiType(str):
149         pass
150
151     class DefaultMultiType(MultiType):
152         pass
153
154     class MasterMultiType(MultiType):
155         pass
156
157     class SlaveMultiType(MultiType):
158         pass
159
160 multitypes = MultiTypeModule()
161
162
163 def populate_multitypes():
164     "populates the master/slave namespace"
165     setattr(multitypes, 'default', multitypes.DefaultMultiType('default'))
166     setattr(multitypes, 'master', multitypes.MasterMultiType('master'))
167     setattr(multitypes, 'slave', multitypes.SlaveMultiType('slave'))
168
169 populate_multitypes()
170
171
172 class Property(object):
173     "a property is responsible of the option's value access rules"
174     __slots__ = ('_setting', '_properties', '_opt', '_path')
175
176     def __init__(self, setting, prop, opt=None, path=None):
177         self._opt = opt
178         self._path = path
179         self._setting = setting
180         self._properties = prop
181
182     def append(self, propname):
183         if self._opt is not None and self._opt._calc_properties is not None \
184                 and propname in self._opt._calc_properties:
185             raise ValueError(_('cannot append {0} property for option {1}: '
186                                'this property is calculated').format(
187                                    propname, self._opt._name))
188         self._properties.add(propname)
189         self._setting._setproperties(self._properties, self._opt, self._path)
190
191     def remove(self, propname):
192         if propname in self._properties:
193             self._properties.remove(propname)
194             self._setting._setproperties(self._properties, self._opt, self._path)
195
196     def reset(self):
197         self._setting.reset(_path=self._path)
198
199     def __contains__(self, propname):
200         return propname in self._properties
201
202     def __repr__(self):
203         return str(list(self._properties))
204
205
206 def set_storage(name, **args):
207     storage_type.set_storage(name)
208     settings = __import__(storage_type.get_storage(), globals(), locals(),
209                           ['Setting']).Setting()
210     for option, value in args.items():
211         try:
212             getattr(settings, option)
213             setattr(settings, option, value)
214         except AttributeError:
215             raise ValueError(_('option {0} not already exists in storage {1}'
216                                '').format(option, name))
217
218
219 def get_storage(context, session_id, persistent):
220     def gen_id(config):
221         return str(id(config)) + str(time())
222
223     if session_id is None:
224         session_id = gen_id(context)
225     return __import__(storage_type.get_storage(), globals(), locals(),
226                       ['Storage']).Storage(session_id, persistent)
227
228
229 def list_sessions():
230     return __import__(storage_type.get_storage(), globals(), locals(),
231                       ['list_sessions']).list_sessions()
232
233
234 def delete_session(session_id):
235     return __import__(storage_type.get_storage(), globals(), locals(),
236                       ['delete_session']).delete_session(session_id)
237
238
239 #____________________________________________________________
240 class Settings(object):
241     "``Config()``'s configuration options"
242     __slots__ = ('context', '_owner', '_p_', '__weakref__')
243
244     def __init__(self, context, storage):
245         """
246         initializer
247
248         :param context: the root config
249         :param storage: the storage type
250
251                         - dictionary -> in memory
252                         - sqlite3 -> persistent
253         """
254         # generic owner
255         self._owner = owners.user
256         self.context = weakref.ref(context)
257         import_lib = 'tiramisu.storage.{0}.setting'.format(storage.storage)
258         self._p_ = __import__(import_lib, globals(), locals(), ['Settings']
259                               ).Settings(storage)
260
261     #____________________________________________________________
262     # properties methods
263     def __contains__(self, propname):
264         "enables the pythonic 'in' syntaxic sugar"
265         return propname in self._getproperties()
266
267     def __repr__(self):
268         return str(list(self._getproperties()))
269
270     def __getitem__(self, opt):
271         path = self._get_opt_path(opt)
272         return self._getitem(opt, path)
273
274     def _getitem(self, opt, path):
275         return Property(self, self._getproperties(opt, path), opt, path)
276
277     def __setitem__(self, opt, value):
278         raise ValueError('you should only append/remove properties')
279
280     def reset(self, opt=None, _path=None, all_properties=False):
281         if all_properties and (_path or opt):
282             raise ValueError(_('opt and all_properties must not be set '
283                                'together in reset'))
284         if all_properties:
285             self._p_.reset_all_propertives()
286         else:
287             if opt is not None and _path is None:
288                 _path = self._get_opt_path(opt)
289             self._p_.reset_properties(_path)
290         self.context().cfgimpl_reset_cache()
291
292     def _getproperties(self, opt=None, path=None, is_apply_req=True):
293         if opt is None:
294             props = self._p_.getproperties(path, default_properties)
295         else:
296             if path is None:
297                 raise ValueError(_('if opt is not None, path should not be'
298                                    ' None in _getproperties'))
299             ntime = None
300             if self._p_.hascache('property', path):
301                 ntime = time()
302                 is_cached, props = self._p_.getcache('property', path, ntime)
303                 if is_cached:
304                     return props
305             props = self._p_.getproperties(path, opt._properties)
306             if is_apply_req:
307                 props |= self.apply_requires(opt, path)
308             if 'expire' in self:
309                 if ntime is None:
310                     ntime = time()
311                 self._p_.setcache('property', path, props, ntime + expires_time)
312         return props
313
314     def append(self, propname):
315         "puts property propname in the Config's properties attribute"
316         props = self._p_.getproperties(None, default_properties)
317         props.add(propname)
318         self._setproperties(props, None, None)
319
320     def remove(self, propname):
321         "deletes property propname in the Config's properties attribute"
322         props = self._p_.getproperties(None, default_properties)
323         if propname in props:
324             props.remove(propname)
325             self._setproperties(props, None, None)
326
327     def _setproperties(self, properties, opt, path):
328         """save properties for specified opt
329         (never save properties if same has option properties)
330         """
331         if opt is None:
332             self._p_.setproperties(None, properties)
333         else:
334             if opt._calc_properties is not None:
335                 properties -= opt._calc_properties
336             if set(opt._properties) == properties:
337                 self._p_.reset_properties(path)
338             else:
339                 self._p_.setproperties(path, properties)
340         self.context().cfgimpl_reset_cache()
341
342     #____________________________________________________________
343     def validate_properties(self, opt_or_descr, is_descr, is_write, path,
344                             value=None, force_permissive=False,
345                             force_properties=None):
346         """
347         validation upon the properties related to `opt_or_descr`
348
349         :param opt_or_descr: an option or an option description object
350         :param force_permissive: behaves as if the permissive property
351                                  was present
352         :param is_descr: we have to know if we are in an option description,
353                          just because the mandatory property
354                          doesn't exist here
355
356         :param is_write: in the validation process, an option is to be modified,
357                          the behavior can be different
358                          (typically with the `frozen` property)
359         """
360         # opt properties
361         properties = copy(self._getproperties(opt_or_descr, path))
362         # remove opt permissive
363         properties -= self._p_.getpermissive(path)
364         # remove global permissive if need
365         self_properties = copy(self._getproperties())
366         if force_permissive is True or 'permissive' in self_properties:
367             properties -= self._p_.getpermissive()
368
369         # global properties
370         if force_properties is not None:
371             self_properties.update(force_properties)
372
373         # calc properties
374         properties &= self_properties
375         # mandatory and frozen are special properties
376         if is_descr:
377             properties -= frozenset(('mandatory', 'frozen'))
378         else:
379             if 'mandatory' in properties and \
380                     not self.context().cfgimpl_get_values()._isempty(
381                         opt_or_descr, value):
382                 properties.remove('mandatory')
383             if is_write and 'everything_frozen' in self_properties:
384                 properties.add('frozen')
385             elif 'frozen' in properties and not is_write:
386                 properties.remove('frozen')
387         # at this point an option should not remain in properties
388         if properties != frozenset():
389             props = list(properties)
390             if 'frozen' in properties:
391                 raise PropertiesOptionError(_('cannot change the value for '
392                                               'option {0} this option is'
393                                               ' frozen').format(
394                                                   opt_or_descr._name),
395                                             props)
396             else:
397                 raise PropertiesOptionError(_("trying to access to an option "
398                                               "named: {0} with properties {1}"
399                                               "").format(opt_or_descr._name,
400                                                          str(props)), props)
401
402     def setpermissive(self, permissive, opt=None, path=None):
403         if opt is not None and path is None:
404             path = self._get_opt_path(opt)
405         if not isinstance(permissive, tuple):
406             raise TypeError(_('permissive must be a tuple'))
407         self._p_.setpermissive(path, permissive)
408
409     #____________________________________________________________
410     def setowner(self, owner):
411         ":param owner: sets the default value for owner at the Config level"
412         if not isinstance(owner, owners.Owner):
413             raise TypeError(_("invalid generic owner {0}").format(str(owner)))
414         self._owner = owner
415
416     def getowner(self):
417         return self._owner
418
419     #____________________________________________________________
420     def _read(self, remove, append):
421         for prop in remove:
422             self.remove(prop)
423         for prop in append:
424             self.append(prop)
425
426     def read_only(self):
427         "convenience method to freeze, hidde and disable"
428         self._read(ro_remove, ro_append)
429
430     def read_write(self):
431         "convenience method to freeze, hidde and disable"
432         self._read(rw_remove, rw_append)
433
434     def reset_cache(self, only_expired):
435         if only_expired:
436             self._p_.reset_expired_cache('property', time())
437         else:
438             self._p_.reset_all_cache('property')
439
440     def apply_requires(self, opt, path):
441         """carries out the jit (just in time) requirements between options
442
443         a requirement is a tuple of this form that comes from the option's
444         requirements validation::
445
446             (option, expected, action, inverse, transitive, same_action)
447
448         let's have a look at all the tuple's items:
449
450         - **option** is the target option's name or path
451
452         - **expected** is the target option's value that is going to trigger an action
453
454         - **action** is the (property) action to be accomplished if the target option
455           happens to have the expected value
456
457         - if **inverse** is `True` and if the target option's value does not
458           apply, then the property action must be removed from the option's
459           properties list (wich means that the property is inverted)
460
461         - **transitive**: but what happens if the target option cannot be
462           accessed ? We don't kown the target option's value. Actually if some
463           property in the target option is not present in the permissive, the
464           target option's value cannot be accessed. In this case, the
465           **action** have to be applied to the option. (the **action** property
466           is then added to the option).
467
468         - **same_action**: actually, if **same_action** is `True`, the
469           transitivity is not accomplished. The transitivity is accomplished
470           only if the target option **has the same property** that the demanded
471           action. If the target option's value is not accessible because of
472           another reason, because of a property of another type, then an
473           exception :exc:`~error.RequirementError` is raised.
474
475         And at last, if no target option matches the expected values, the
476         action must be removed from the option's properties list.
477
478         :param opt: the option on wich the requirement occurs
479         :type opt: `option.Option()`
480         :param path: the option's path in the config
481         :type path: str
482         """
483         if opt._requires is None:
484             return frozenset()
485
486         # filters the callbacks
487         calc_properties = set()
488         for requires in opt._requires:
489             for require in requires:
490                 option, expected, action, inverse, \
491                     transitive, same_action = require
492                 reqpath = self._get_opt_path(option)
493                 if reqpath == path or reqpath.startswith(path + '.'):
494                     raise RequirementError(_("malformed requirements "
495                                              "imbrication detected for option:"
496                                              " '{0}' with requirement on: "
497                                              "'{1}'").format(path, reqpath))
498                 try:
499                     value = self.context()._getattr(reqpath,
500                                                     force_permissive=True)
501                 except PropertiesOptionError as err:
502                     if not transitive:
503                         continue
504                     properties = err.proptype
505                     if same_action and action not in properties:
506                         raise RequirementError(_("option '{0}' has "
507                                                  "requirement's property "
508                                                  "error: "
509                                                  "{1} {2}").format(opt._name,
510                                                                    reqpath,
511                                                                    properties))
512                     # transitive action, force expected
513                     value = expected[0]
514                     inverse = False
515                 if (not inverse and
516                         value in expected or
517                         inverse and value not in expected):
518                     calc_properties.add(action)
519                     # the calculation cannot be carried out
520                     break
521             return calc_properties
522
523     def _get_opt_path(self, opt):
524         return self.context().cfgimpl_get_description().impl_get_path_by_opt(opt)