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