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