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