Important behavior change : to add default_multi value, now use Multi.append(), not...
[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 Undefined():
247     pass
248
249
250 undefined = Undefined()
251 # ____________________________________________________________
252 class Property(object):
253     "a property is responsible of the option's value access rules"
254     __slots__ = ('_setting', '_properties', '_opt', '_path')
255
256     def __init__(self, setting, prop, opt=None, path=None):
257         self._opt = opt
258         self._path = path
259         self._setting = setting
260         self._properties = prop
261
262     def append(self, propname):
263         if self._opt is not None and self._opt._calc_properties is not None \
264                 and propname in self._opt._calc_properties:
265             raise ValueError(_('cannot append {0} property for option {1}: '
266                                'this property is calculated').format(
267                                    propname, self._opt._name))
268         self._properties.add(propname)
269         self._setting._setproperties(self._properties, self._opt, self._path)
270
271     def remove(self, propname):
272         if propname in self._properties:
273             self._properties.remove(propname)
274             self._setting._setproperties(self._properties, self._opt,
275                                          self._path)
276
277     def reset(self):
278         self._setting.reset(_path=self._path)
279
280     def __contains__(self, propname):
281         return propname in self._properties
282
283     def __repr__(self):
284         return str(list(self._properties))
285
286
287 #____________________________________________________________
288 class Settings(object):
289     "``Config()``'s configuration options"
290     __slots__ = ('context', '_owner', '_p_', '__weakref__')
291
292     def __init__(self, context, storage):
293         """
294         initializer
295
296         :param context: the root config
297         :param storage: the storage type
298
299                         - dictionary -> in memory
300                         - sqlite3 -> persistent
301         """
302         # generic owner
303         self._owner = owners.user
304         self.context = weakref.ref(context)
305         self._p_ = storage
306
307     #____________________________________________________________
308     # properties methods
309     def __contains__(self, propname):
310         "enables the pythonic 'in' syntaxic sugar"
311         return propname in self._getproperties()
312
313     def __repr__(self):
314         return str(list(self._getproperties()))
315
316     def __getitem__(self, opt):
317         path = self._get_path_by_opt(opt)
318         return self._getitem(opt, path)
319
320     def _getitem(self, opt, path):
321         return Property(self, self._getproperties(opt, path), opt, path)
322
323     def __setitem__(self, opt, value):
324         raise ValueError('you should only append/remove properties')
325
326     def reset(self, opt=None, _path=None, all_properties=False):
327         if all_properties and (_path or opt):
328             raise ValueError(_('opt and all_properties must not be set '
329                                'together in reset'))
330         if all_properties:
331             self._p_.reset_all_propertives()
332         else:
333             if opt is not None and _path is None:
334                 _path = self._get_path_by_opt(opt)
335             self._p_.reset_properties(_path)
336         self.context().cfgimpl_reset_cache()
337
338     def _getproperties(self, opt=None, path=None, is_apply_req=True):
339         if opt is None:
340             props = self._p_.getproperties(path, default_properties)
341         else:
342             if path is None:
343                 raise ValueError(_('if opt is not None, path should not be'
344                                    ' None in _getproperties'))
345             ntime = None
346             if 'cache' in self and self._p_.hascache(path):
347                 if 'expire' in self:
348                     ntime = int(time())
349                 is_cached, props = self._p_.getcache(path, ntime)
350                 if is_cached:
351                     return props
352             props = self._p_.getproperties(path, opt._properties)
353             if is_apply_req:
354                 props |= self.apply_requires(opt, path)
355             if 'cache' in self:
356                 if 'expire' in self:
357                     if  ntime is None:
358                         ntime = int(time())
359                     ntime = ntime + expires_time
360                 self._p_.setcache(path, props, ntime)
361         return props
362
363     def append(self, propname):
364         "puts property propname in the Config's properties attribute"
365         props = self._p_.getproperties(None, default_properties)
366         props.add(propname)
367         self._setproperties(props, None, None)
368
369     def remove(self, propname):
370         "deletes property propname in the Config's properties attribute"
371         props = self._p_.getproperties(None, default_properties)
372         if propname in props:
373             props.remove(propname)
374             self._setproperties(props, None, None)
375
376     def _setproperties(self, properties, opt, path):
377         """save properties for specified opt
378         (never save properties if same has option properties)
379         """
380         if opt is None:
381             self._p_.setproperties(None, properties)
382         else:
383             if opt._calc_properties is not None:
384                 properties -= opt._calc_properties
385             if set(opt._properties) == properties:
386                 self._p_.reset_properties(path)
387             else:
388                 self._p_.setproperties(path, properties)
389         self.context().cfgimpl_reset_cache()
390
391     #____________________________________________________________
392     def validate_properties(self, opt_or_descr, is_descr, is_write, path,
393                             value=None, force_permissive=False,
394                             force_properties=None, force_permissives=None):
395         """
396         validation upon the properties related to `opt_or_descr`
397
398         :param opt_or_descr: an option or an option description object
399         :param force_permissive: behaves as if the permissive property
400                                  was present
401         :param force_properties: set() with properties that is force to add
402                                  in global properties
403         :param force_permissives: set() with permissives that is force to add
404                                  in global permissives
405         :param is_descr: we have to know if we are in an option description,
406                          just because the mandatory property
407                          doesn't exist here
408
409         :param is_write: in the validation process, an option is to be modified,
410                          the behavior can be different
411                          (typically with the `frozen` property)
412         """
413         # opt properties
414         properties = copy(self._getproperties(opt_or_descr, path))
415         self_properties = copy(self._getproperties())
416         # remove opt permissive
417         if force_permissive is True or 'permissive' in self_properties:
418             properties -= self._p_.getpermissive(path)
419         # remove global permissive if need
420         if force_permissive is True or 'permissive' in self_properties:
421             properties -= self._p_.getpermissive()
422         if force_permissives is not None:
423             properties -= force_permissives
424
425         # global properties
426         if force_properties is not None:
427             self_properties.update(force_properties)
428
429         # calc properties
430         properties &= self_properties
431         # mandatory and frozen are special properties
432         if is_descr:
433             properties -= frozenset(('mandatory', 'frozen'))
434         else:
435             if 'mandatory' in properties and \
436                     not self.context().cfgimpl_get_values()._isempty(
437                         opt_or_descr, value):
438                 properties.remove('mandatory')
439             if is_write and 'everything_frozen' in self_properties:
440                 properties.add('frozen')
441             elif 'frozen' in properties and not is_write:
442                 properties.remove('frozen')
443         # at this point an option should not remain in properties
444         if properties != frozenset():
445             props = list(properties)
446             if 'frozen' in properties:
447                 raise PropertiesOptionError(_('cannot change the value for '
448                                               'option {0} this option is'
449                                               ' frozen').format(
450                                                   opt_or_descr._name),
451                                             props)
452             else:
453                 raise PropertiesOptionError(_("trying to access to an option "
454                                               "named: {0} with properties {1}"
455                                               "").format(opt_or_descr._name,
456                                                          str(props)), props)
457
458     def setpermissive(self, permissive, opt=None, path=None):
459         """
460         enables us to put the permissives in the storage
461
462         :param path: the option's path
463         :param type: str
464         :param opt: if an option object is set, the path is extracted.
465                     it is better (faster) to set the path parameter
466                     instead of passing a :class:`tiramisu.option.Option()` object.
467         """
468         if opt is not None and path is None:
469             path = self._get_path_by_opt(opt)
470         if not isinstance(permissive, tuple):
471             raise TypeError(_('permissive must be a tuple'))
472         self._p_.setpermissive(path, permissive)
473
474     #____________________________________________________________
475     def setowner(self, owner):
476         ":param owner: sets the default value for owner at the Config level"
477         if not isinstance(owner, owners.Owner):
478             raise TypeError(_("invalid generic owner {0}").format(str(owner)))
479         self._owner = owner
480
481     def getowner(self):
482         return self._owner
483
484     #____________________________________________________________
485     def _read(self, remove, append):
486         for prop in remove:
487             self.remove(prop)
488         for prop in append:
489             self.append(prop)
490
491     def read_only(self):
492         "convenience method to freeze, hide and disable"
493         self._read(ro_remove, ro_append)
494
495     def read_write(self):
496         "convenience method to freeze, hide and disable"
497         self._read(rw_remove, rw_append)
498
499     def reset_cache(self, only_expired):
500         """reset all settings in cache
501
502         :param only_expired: if True reset only expired cached values
503         :type only_expired: boolean
504         """
505         if only_expired:
506             self._p_.reset_expired_cache(int(time()))
507         else:
508             self._p_.reset_all_cache()
509
510     def apply_requires(self, opt, path):
511         """carries out the jit (just in time) requirements between options
512
513         a requirement is a tuple of this form that comes from the option's
514         requirements validation::
515
516             (option, expected, action, inverse, transitive, same_action)
517
518         let's have a look at all the tuple's items:
519
520         - **option** is the target option's
521
522         - **expected** is the target option's value that is going to trigger
523           an action
524
525         - **action** is the (property) action to be accomplished if the target
526           option happens to have the expected value
527
528         - if **inverse** is `True` and if the target option's value does not
529           apply, then the property action must be removed from the option's
530           properties list (wich means that the property is inverted)
531
532         - **transitive**: but what happens if the target option cannot be
533           accessed ? We don't kown the target option's value. Actually if some
534           property in the target option is not present in the permissive, the
535           target option's value cannot be accessed. In this case, the
536           **action** have to be applied to the option. (the **action** property
537           is then added to the option).
538
539         - **same_action**: actually, if **same_action** is `True`, the
540           transitivity is not accomplished. The transitivity is accomplished
541           only if the target option **has the same property** that the demanded
542           action. If the target option's value is not accessible because of
543           another reason, because of a property of another type, then an
544           exception :exc:`~error.RequirementError` is raised.
545
546         And at last, if no target option matches the expected values, the
547         action must be removed from the option's properties list.
548
549         :param opt: the option on wich the requirement occurs
550         :type opt: `option.Option()`
551         :param path: the option's path in the config
552         :type path: str
553         """
554         if opt._requires is None:
555             return frozenset()
556
557         # filters the callbacks
558         calc_properties = set()
559         for requires in opt._requires:
560             for require in requires:
561                 option, expected, action, inverse, \
562                     transitive, same_action = require
563                 reqpath = self._get_path_by_opt(option)
564                 if reqpath == path or reqpath.startswith(path + '.'):
565                     raise RequirementError(_("malformed requirements "
566                                              "imbrication detected for option:"
567                                              " '{0}' with requirement on: "
568                                              "'{1}'").format(path, reqpath))
569                 try:
570                     value = self.context()._getattr(reqpath,
571                                                     force_permissive=True)
572                 except PropertiesOptionError as err:
573                     if not transitive:
574                         continue
575                     properties = err.proptype
576                     if same_action and action not in properties:
577                         raise RequirementError(_("option '{0}' has "
578                                                  "requirement's property "
579                                                  "error: "
580                                                  "{1} {2}").format(opt._name,
581                                                                    reqpath,
582                                                                    properties))
583                     # transitive action, force expected
584                     value = expected[0]
585                     inverse = False
586                 if (not inverse and
587                         value in expected or
588                         inverse and value not in expected):
589                     calc_properties.add(action)
590                     # the calculation cannot be carried out
591                     break
592         return calc_properties
593
594     def _get_path_by_opt(self, opt):
595         """just a wrapper to get path in optiondescription's cache
596
597         :param opt: `Option`'s object
598         :returns: path
599         """
600         return self.context().cfgimpl_get_description().impl_get_path_by_opt(opt)
601
602     def get_modified_properties(self):
603         return self._p_.get_modified_properties()
604
605     def get_modified_permissives(self):
606         return self._p_.get_modified_permissives()
607
608     def __getstate__(self):
609         return {'_p_': self._p_, '_owner': str(self._owner)}
610
611     def _impl_setstate(self, storage):
612         self._p_._storage = storage
613
614     def __setstate__(self, states):
615         self._p_ = states['_p_']
616         try:
617             self._owner = getattr(owners, states['_owner'])
618         except AttributeError:
619             owners.addowner(states['_owner'])
620             self._owner = getattr(owners, states['_owner'])