huge use of weakrefs to remove memoryleaks due to circular references
[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 add_owner(name):
136         """
137         :param name: the name of the new owner
138         """
139         setattr(owners, name, owners.Owner(name))
140     setattr(owners, 'add_owner', add_owner)
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'], -1).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     a=__import__(storage_type.get_storage(), globals(), locals(),
226                       ['Storage'], -1).Storage(session_id, persistent)
227     return a
228
229
230 def list_sessions():
231     return __import__(storage_type.get_storage(), globals(), locals(),
232                       ['list_sessions'], -1).list_sessions()
233
234
235 def delete_session(session_id):
236     return __import__(storage_type.get_storage(), globals(), locals(),
237                       ['delete_session'], -1).delete_session(session_id)
238
239
240 #____________________________________________________________
241 class Settings(object):
242     "``Config()``'s configuration options"
243     __slots__ = ('context', '_owner', '_p_')
244
245     def __init__(self, context, storage):
246         """
247         initializer
248
249         :param context: the root config
250         :param storage: the storage type
251
252                         - dictionary -> in memory
253                         - sqlite3 -> persistent
254         """
255         # generic owner
256         self._owner = owners.user
257         self.context = weakref.ref(context)
258         import_lib = 'tiramisu.storage.{0}.setting'.format(storage.storage)
259         self._p_ = __import__(import_lib, globals(), locals(), ['Settings'],
260                               -1).Settings(storage)
261
262     #____________________________________________________________
263     # properties methods
264     def __contains__(self, propname):
265         "enables the pythonic 'in' syntaxic sugar"
266         return propname in self._getproperties()
267
268     def __repr__(self):
269         return str(list(self._getproperties()))
270
271     def __getitem__(self, opt):
272         path = self._get_opt_path(opt)
273         return self._getitem(opt, path)
274
275     def _getitem(self, opt, path):
276         return Property(self, self._getproperties(opt, path), opt, path)
277
278     def __setitem__(self, opt, value):
279         raise ValueError('you should only append/remove properties')
280
281     def reset(self, opt=None, _path=None, all_properties=False):
282         if all_properties and (_path or opt):
283             raise ValueError(_('opt and all_properties must not be set '
284                                'together in reset'))
285         if all_properties:
286             self._p_.reset_all_propertives()
287         else:
288             if opt is not None and _path is None:
289                 _path = self._get_opt_path(opt)
290             self._p_.reset_properties(_path)
291         self.context().cfgimpl_reset_cache()
292
293     def _getproperties(self, opt=None, path=None, is_apply_req=True):
294         if opt is None:
295             props = self._p_.getproperties(path, default_properties)
296         else:
297             if path is None:
298                 raise ValueError(_('if opt is not None, path should not be'
299                                    ' None in _getproperties'))
300             ntime = None
301             if self._p_.hascache('property', path):
302                 ntime = time()
303                 is_cached, props = self._p_.getcache('property', path, ntime)
304                 if is_cached:
305                     return props
306             props = self._p_.getproperties(path, opt._properties)
307             if is_apply_req:
308                 props |= self.apply_requires(opt, path)
309             if 'expire' in self:
310                 if ntime is None:
311                     ntime = time()
312                 self._p_.setcache('property', path, props, ntime + expires_time)
313         return props
314
315     def append(self, propname):
316         "puts property propname in the Config's properties attribute"
317         props = self._p_.getproperties(None, default_properties)
318         props.add(propname)
319         self._setproperties(props, None, None)
320
321     def remove(self, propname):
322         "deletes property propname in the Config's properties attribute"
323         props = self._p_.getproperties(None, default_properties)
324         if propname in props:
325             props.remove(propname)
326             self._setproperties(props, None, None)
327
328     def _setproperties(self, properties, opt, path):
329         """save properties for specified opt
330         (never save properties if same has option properties)
331         """
332         if opt is None:
333             self._p_.setproperties(None, properties)
334         else:
335             if opt._calc_properties is not None:
336                 properties -= opt._calc_properties
337             if set(opt._properties) == properties:
338                 self._p_.reset_properties(path)
339             else:
340                 self._p_.setproperties(path, properties)
341         self.context().cfgimpl_reset_cache()
342
343     #____________________________________________________________
344     def validate_properties(self, opt_or_descr, is_descr, is_write, path,
345                             value=None, force_permissive=False,
346                             force_properties=None):
347         """
348         validation upon the properties related to `opt_or_descr`
349
350         :param opt_or_descr: an option or an option description object
351         :param force_permissive: behaves as if the permissive property
352                                  was present
353         :param is_descr: we have to know if we are in an option description,
354                          just because the mandatory property
355                          doesn't exist here
356
357         :param is_write: in the validation process, an option is to be modified,
358                          the behavior can be different
359                          (typically with the `frozen` property)
360         """
361         # opt properties
362         properties = copy(self._getproperties(opt_or_descr, path))
363         # remove opt permissive
364         properties -= self._p_.getpermissive(path)
365         # remove global permissive if need
366         self_properties = copy(self._getproperties())
367         if force_permissive is True or 'permissive' in self_properties:
368             properties -= self._p_.getpermissive()
369
370         # global properties
371         if force_properties is not None:
372             self_properties.update(force_properties)
373
374         # calc properties
375         properties &= self_properties
376         # mandatory and frozen are special properties
377         if is_descr:
378             properties -= frozenset(('mandatory', 'frozen'))
379         else:
380             if 'mandatory' in properties and \
381                     not self.context().cfgimpl_get_values()._isempty(
382                         opt_or_descr, value):
383                 properties.remove('mandatory')
384             if is_write and 'everything_frozen' in self_properties:
385                 properties.add('frozen')
386             elif 'frozen' in properties and not is_write:
387                 properties.remove('frozen')
388         # at this point an option should not remain in properties
389         if properties != frozenset():
390             props = list(properties)
391             if 'frozen' in properties:
392                 raise PropertiesOptionError(_('cannot change the value for '
393                                               'option {0} this option is'
394                                               ' frozen').format(
395                                                   opt_or_descr._name),
396                                             props)
397             else:
398                 raise PropertiesOptionError(_("trying to access to an option "
399                                               "named: {0} with properties {1}"
400                                               "").format(opt_or_descr._name,
401                                                          str(props)), props)
402
403     def setpermissive(self, permissive, path=None):
404         if not isinstance(permissive, tuple):
405             raise TypeError(_('permissive must be a tuple'))
406         self._p_.setpermissive(path, permissive)
407
408     #____________________________________________________________
409     def setowner(self, owner):
410         ":param owner: sets the default value for owner at the Config level"
411         if not isinstance(owner, owners.Owner):
412             raise TypeError(_("invalid generic owner {0}").format(str(owner)))
413         self._owner = owner
414
415     def getowner(self):
416         return self._owner
417
418     #____________________________________________________________
419     def _read(self, remove, append):
420         for prop in remove:
421             self.remove(prop)
422         for prop in append:
423             self.append(prop)
424
425     def read_only(self):
426         "convenience method to freeze, hidde and disable"
427         self._read(ro_remove, ro_append)
428
429     def read_write(self):
430         "convenience method to freeze, hidde and disable"
431         self._read(rw_remove, rw_append)
432
433     def reset_cache(self, only_expired):
434         if only_expired:
435             self._p_.reset_expired_cache('property', time())
436         else:
437             self._p_.reset_all_cache('property')
438
439     def apply_requires(self, opt, path):
440         """carries out the jit (just in time) requirements between options
441
442         a requirement is a tuple of this form that comes from the option's
443         requirements validation::
444
445             (option, expected, action, inverse, transitive, same_action)
446
447         let's have a look at all the tuple's items:
448
449         - **option** is the target option's name or path
450
451         - **expected** is the target option's value that is going to trigger an action
452
453         - **action** is the (property) action to be accomplished if the target option
454           happens to have the expected value
455
456         - if **inverse** is `True` and if the target option's value does not
457           apply, then the property action must be removed from the option's
458           properties list (wich means that the property is inverted)
459
460         - **transitive**: but what happens if the target option cannot be
461           accessed ? We don't kown the target option's value. Actually if some
462           property in the target option is not present in the permissive, the
463           target option's value cannot be accessed. In this case, the
464           **action** have to be applied to the option. (the **action** property
465           is then added to the option).
466
467         - **same_action**: actually, if **same_action** is `True`, the
468           transitivity is not accomplished. The transitivity is accomplished
469           only if the target option **has the same property** that the demanded
470           action. If the target option's value is not accessible because of
471           another reason, because of a property of another type, then an
472           exception :exc:`~error.RequirementError` is raised.
473
474         And at last, if no target option matches the expected values, the
475         action must be removed from the option's properties list.
476
477         :param opt: the option on wich the requirement occurs
478         :type opt: `option.Option()`
479         :param path: the option's path in the config
480         :type path: str
481         """
482         if opt._requires is None:
483             return frozenset()
484
485         # filters the callbacks
486         calc_properties = set()
487         for requires in opt._requires:
488             for require in requires:
489                 option, expected, action, inverse, \
490                     transitive, same_action = require
491                 reqpath = self._get_opt_path(option)
492                 if reqpath == path or reqpath.startswith(path + '.'):
493                     raise RequirementError(_("malformed requirements "
494                                              "imbrication detected for option:"
495                                              " '{0}' with requirement on: "
496                                              "'{1}'").format(path, reqpath))
497                 try:
498                     value = self.context()._getattr(reqpath,
499                                                   force_permissive=True)
500                 except PropertiesOptionError, err:
501                     if not transitive:
502                         continue
503                     properties = err.proptype
504                     if same_action and action not in properties:
505                         raise RequirementError(_("option '{0}' has "
506                                                  "requirement's property "
507                                                  "error: "
508                                                  "{1} {2}").format(opt._name,
509                                                                    reqpath,
510                                                                    properties))
511                     # transitive action, force expected
512                     value = expected[0]
513                     inverse = False
514                 if (not inverse and
515                         value in expected or
516                         inverse and value not in expected):
517                     calc_properties.add(action)
518                     # the calculation cannot be carried out
519                     break
520             return calc_properties
521
522     def _get_opt_path(self, opt):
523         return self.context().cfgimpl_get_description().impl_get_path_by_opt(opt)