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