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