mandatories masterslaves with consistency and default value is acceptable
[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)), props,
523                                                           self, datas, opt_type)
524
525     def setpermissive(self, permissive, opt=None, path=None):
526         """
527         enables us to put the permissives in the storage
528
529         :param path: the option's path
530         :param type: str
531         :param opt: if an option object is set, the path is extracted.
532                     it is better (faster) to set the path parameter
533                     instead of passing a :class:`tiramisu.option.Option()` object.
534         """
535         if opt is not None and path is None:
536             path = opt.impl_getpath(self._getcontext())
537         if not isinstance(permissive, tuple):  # pragma: optional cover
538             raise TypeError(_('permissive must be a tuple'))
539         self._p_.setpermissive(path, permissive)
540
541     #____________________________________________________________
542     def setowner(self, owner):
543         ":param owner: sets the default value for owner at the Config level"
544         if not isinstance(owner, owners.Owner):  # pragma: optional cover
545             raise TypeError(_("invalid generic owner {0}").format(str(owner)))
546         self._owner = owner
547
548     def getowner(self):
549         return self._owner
550
551     #____________________________________________________________
552     def _read(self, remove, append):
553         props = self._p_.getproperties(None, default_properties)
554         modified = False
555         if remove & props != set([]):
556             props = props - remove
557             modified = True
558         if append & props != append:
559             props = props | append
560             modified = True
561         if modified:
562             self._setproperties(props, None)
563
564     def read_only(self):
565         "convenience method to freeze, hide and disable"
566         self._read(ro_remove, ro_append)
567
568     def read_write(self):
569         "convenience method to freeze, hide and disable"
570         self._read(rw_remove, rw_append)
571
572     def reset_cache(self, only_expired):
573         """reset all settings in cache
574
575         :param only_expired: if True reset only expired cached values
576         :type only_expired: boolean
577         """
578         if only_expired:
579             self._p_.reset_expired_cache(int(time()))
580         else:
581             self._p_.reset_all_cache()
582
583     def apply_requires(self, opt, path, setting_properties, index, debug):
584         """carries out the jit (just in time) requirements between options
585
586         a requirement is a tuple of this form that comes from the option's
587         requirements validation::
588
589             (option, expected, action, inverse, transitive, same_action)
590
591         let's have a look at all the tuple's items:
592
593         - **option** is the target option's
594
595         - **expected** is the target option's value that is going to trigger
596           an action
597
598         - **action** is the (property) action to be accomplished if the target
599           option happens to have the expected value
600
601         - if **inverse** is `True` and if the target option's value does not
602           apply, then the property action must be removed from the option's
603           properties list (wich means that the property is inverted)
604
605         - **transitive**: but what happens if the target option cannot be
606           accessed ? We don't kown the target option's value. Actually if some
607           property in the target option is not present in the permissive, the
608           target option's value cannot be accessed. In this case, the
609           **action** have to be applied to the option. (the **action** property
610           is then added to the option).
611
612         - **same_action**: actually, if **same_action** is `True`, the
613           transitivity is not accomplished. The transitivity is accomplished
614           only if the target option **has the same property** that the demanded
615           action. If the target option's value is not accessible because of
616           another reason, because of a property of another type, then an
617           exception :exc:`~error.RequirementError` is raised.
618
619         And at last, if no target option matches the expected values, the
620         action must be removed from the option's properties list.
621
622         :param opt: the option on wich the requirement occurs
623         :type opt: `option.Option()`
624         :param path: the option's path in the config
625         :type path: str
626         """
627         current_requires = opt.impl_getrequires()
628
629         # filters the callbacks
630         if debug:
631             calc_properties = {}
632         else:
633             calc_properties = set()
634
635         if not current_requires:
636             return calc_properties
637
638         context = self._getcontext()
639         all_properties = None
640         for requires in current_requires:
641             for require in requires:
642                 option, expected, action, inverse, \
643                     transitive, same_action = require
644                 reqpath = option.impl_getpath(context)
645                 if reqpath == path or reqpath.startswith(path + '.'):  # pragma: optional cover
646                     raise RequirementError(_("malformed requirements "
647                                              "imbrication detected for option:"
648                                              " '{0}' with requirement on: "
649                                              "'{1}'").format(path, reqpath))
650                 if option.impl_is_multi():
651                     idx = index
652                 else:
653                     idx = None
654                 value = context.getattr(reqpath, force_permissive=True,
655                                         _setting_properties=setting_properties,
656                                         index=idx, returns_raise=True)
657                 if isinstance(value, Exception):
658                     if isinstance(value, PropertiesOptionError):
659                         if not transitive:
660                             if all_properties is None:
661                                 all_properties = []
662                                 for requires in opt.impl_getrequires():
663                                     for require in requires:
664                                         all_properties.append(require[2])
665                             if not set(value.proptype) - set(all_properties):
666                                 continue
667                         properties = value.proptype
668                         if same_action and action not in properties:  # pragma: optional cover
669                             raise RequirementError(_("option '{0}' has "
670                                                      "requirement's property "
671                                                      "error: "
672                                                      "{1} {2}").format(opt._name,
673                                                                        reqpath,
674                                                                        properties))
675                         orig_value = value
676                         # transitive action, force expected
677                         value = expected[0]
678                         inverse = False
679                     else:
680                         raise value
681                 else:
682                     orig_value = value
683                 if (not inverse and
684                         value in expected or
685                         inverse and value not in expected):
686                     if debug:
687                         if isinstance(orig_value, PropertiesOptionError):
688                             for act, msg in orig_value._settings.apply_requires(**orig_value._datas).items():
689                                 calc_properties.setdefault(action, []).extend(msg)
690                         else:
691                             if not inverse:
692                                 msg = _('the value of "{0}" is "{1}"')
693                             else:
694                                 msg = _('the value of "{0}" is not "{1}"')
695                             calc_properties.setdefault(action, []).append(msg.format(option.impl_get_display_name(), display_list(expected, 'or')))
696                     else:
697                         calc_properties.add(action)
698                         break
699         return calc_properties
700
701     def get_modified_properties(self):
702         return self._p_.get_modified_properties()
703
704     def get_modified_permissives(self):
705         return self._p_.get_modified_permissives()
706
707     def __getstate__(self):
708         return {'_p_': self._p_, '_owner': str(self._owner)}
709
710     def _impl_setstate(self, storage):
711         self._p_._storage = storage
712
713     def __setstate__(self, states):
714         self._p_ = states['_p_']
715         try:
716             self._owner = getattr(owners, states['_owner'])
717         except AttributeError:
718             owners.addowner(states['_owner'])
719             self._owner = getattr(owners, states['_owner'])