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