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