add logger in validation
[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         if opt is None:
374             props = copy(self._p_.getproperties(path, default_properties))
375         else:
376             if path is None:
377                 raise ValueError(_('if opt is not None, path should not be'
378                                    ' None in _getproperties'))
379             ntime = None
380             if 'cache' in self and self._p_.hascache(path):
381                 if 'expire' in self:
382                     ntime = int(time())
383                 is_cached, props = self._p_.getcache(path, ntime)
384                 if is_cached:
385                     return copy(props)
386             props = copy(self._p_.getproperties(path, opt._properties))
387             if is_apply_req:
388                 props |= self.apply_requires(opt, path)
389             if 'cache' in self:
390                 if 'expire' in self:
391                     if  ntime is None:
392                         ntime = int(time())
393                     ntime = ntime + expires_time
394                 self._p_.setcache(path, copy(props), ntime)
395         return props
396
397     def append(self, propname):
398         "puts property propname in the Config's properties attribute"
399         props = self._p_.getproperties(None, default_properties)
400         props.add(propname)
401         self._setproperties(props, None, None)
402
403     def remove(self, propname):
404         "deletes property propname in the Config's properties attribute"
405         props = self._p_.getproperties(None, default_properties)
406         if propname in props:
407             props.remove(propname)
408             self._setproperties(props, None, None)
409
410     def extend(self, propnames):
411         for propname in propnames:
412             self.append(propname)
413
414     def _setproperties(self, properties, opt, path):
415         """save properties for specified opt
416         (never save properties if same has option properties)
417         """
418         if opt is None:
419             self._p_.setproperties(None, properties)
420         else:
421             if opt._calc_properties is not None:
422                 properties -= opt._calc_properties
423             if set(opt._properties) == properties:
424                 self._p_.reset_properties(path)
425             else:
426                 self._p_.setproperties(path, properties)
427         self._getcontext().cfgimpl_reset_cache()
428
429     #____________________________________________________________
430     def validate_properties(self, opt_or_descr, is_descr, is_write, path,
431                             value=None, force_permissive=False,
432                             force_properties=None, force_permissives=None):
433         """
434         validation upon the properties related to `opt_or_descr`
435
436         :param opt_or_descr: an option or an option description object
437         :param force_permissive: behaves as if the permissive property
438                                  was present
439         :param force_properties: set() with properties that is force to add
440                                  in global properties
441         :param force_permissives: set() with permissives that is force to add
442                                  in global permissives
443         :param is_descr: we have to know if we are in an option description,
444                          just because the mandatory property
445                          doesn't exist here
446
447         :param is_write: in the validation process, an option is to be modified,
448                          the behavior can be different
449                          (typically with the `frozen` property)
450         """
451         # opt properties
452         properties = self._getproperties(opt_or_descr, path)
453         self_properties = self._getproperties()
454         # remove opt permissive
455         # permissive affect option's permission with or without permissive
456         # global property
457         properties -= self._p_.getpermissive(path)
458         # remove global permissive if need
459         if force_permissive is True or 'permissive' in self_properties:
460             properties -= self._p_.getpermissive()
461         if force_permissives is not None:
462             properties -= force_permissives
463
464         # global properties
465         if force_properties is not None:
466             self_properties.update(force_properties)
467
468         # calc properties
469         properties &= self_properties
470         # mandatory and frozen are special properties
471         if is_descr:
472             properties -= frozenset(('mandatory', 'frozen'))
473         else:
474             if 'mandatory' in properties and \
475                     not self._getcontext().cfgimpl_get_values()._isempty(
476                         opt_or_descr, value):
477                 properties.remove('mandatory')
478             if is_write and 'everything_frozen' in self_properties:
479                 properties.add('frozen')
480             elif 'frozen' in properties and not is_write:
481                 properties.remove('frozen')
482         # at this point an option should not remain in properties
483         if properties != frozenset():
484             props = list(properties)
485             if 'frozen' in properties:
486                 raise PropertiesOptionError(_('cannot change the value for '
487                                               'option {0} this option is'
488                                               ' frozen').format(
489                                                   opt_or_descr._name),
490                                             props)
491             else:
492                 raise PropertiesOptionError(_("trying to access to an option "
493                                               "named: {0} with properties {1}"
494                                               "").format(opt_or_descr._name,
495                                                          str(props)), props)
496
497     def setpermissive(self, permissive, opt=None, path=None):
498         """
499         enables us to put the permissives in the storage
500
501         :param path: the option's path
502         :param type: str
503         :param opt: if an option object is set, the path is extracted.
504                     it is better (faster) to set the path parameter
505                     instead of passing a :class:`tiramisu.option.Option()` object.
506         """
507         if opt is not None and path is None:
508             path = self._get_path_by_opt(opt)
509         if not isinstance(permissive, tuple):
510             raise TypeError(_('permissive must be a tuple'))
511         self._p_.setpermissive(path, permissive)
512
513     #____________________________________________________________
514     def setowner(self, owner):
515         ":param owner: sets the default value for owner at the Config level"
516         if not isinstance(owner, owners.Owner):
517             raise TypeError(_("invalid generic owner {0}").format(str(owner)))
518         self._owner = owner
519
520     def getowner(self):
521         return self._owner
522
523     #____________________________________________________________
524     def _read(self, remove, append):
525         for prop in remove:
526             self.remove(prop)
527         for prop in append:
528             self.append(prop)
529
530     def read_only(self):
531         "convenience method to freeze, hide and disable"
532         self._read(ro_remove, ro_append)
533
534     def read_write(self):
535         "convenience method to freeze, hide and disable"
536         self._read(rw_remove, rw_append)
537
538     def reset_cache(self, only_expired):
539         """reset all settings in cache
540
541         :param only_expired: if True reset only expired cached values
542         :type only_expired: boolean
543         """
544         if only_expired:
545             self._p_.reset_expired_cache(int(time()))
546         else:
547             self._p_.reset_all_cache()
548
549     def apply_requires(self, opt, path):
550         """carries out the jit (just in time) requirements between options
551
552         a requirement is a tuple of this form that comes from the option's
553         requirements validation::
554
555             (option, expected, action, inverse, transitive, same_action)
556
557         let's have a look at all the tuple's items:
558
559         - **option** is the target option's
560
561         - **expected** is the target option's value that is going to trigger
562           an action
563
564         - **action** is the (property) action to be accomplished if the target
565           option happens to have the expected value
566
567         - if **inverse** is `True` and if the target option's value does not
568           apply, then the property action must be removed from the option's
569           properties list (wich means that the property is inverted)
570
571         - **transitive**: but what happens if the target option cannot be
572           accessed ? We don't kown the target option's value. Actually if some
573           property in the target option is not present in the permissive, the
574           target option's value cannot be accessed. In this case, the
575           **action** have to be applied to the option. (the **action** property
576           is then added to the option).
577
578         - **same_action**: actually, if **same_action** is `True`, the
579           transitivity is not accomplished. The transitivity is accomplished
580           only if the target option **has the same property** that the demanded
581           action. If the target option's value is not accessible because of
582           another reason, because of a property of another type, then an
583           exception :exc:`~error.RequirementError` is raised.
584
585         And at last, if no target option matches the expected values, the
586         action must be removed from the option's properties list.
587
588         :param opt: the option on wich the requirement occurs
589         :type opt: `option.Option()`
590         :param path: the option's path in the config
591         :type path: str
592         """
593         if opt._requires is None:
594             return frozenset()
595
596         # filters the callbacks
597         calc_properties = set()
598         context = self._getcontext()
599         for requires in opt._requires:
600             for require in requires:
601                 option, expected, action, inverse, \
602                     transitive, same_action = require
603                 reqpath = self._get_path_by_opt(option)
604                 if reqpath == path or reqpath.startswith(path + '.'):
605                     raise RequirementError(_("malformed requirements "
606                                              "imbrication detected for option:"
607                                              " '{0}' with requirement on: "
608                                              "'{1}'").format(path, reqpath))
609                 try:
610                     value = context.getattr(reqpath, force_permissive=True)
611                 except PropertiesOptionError as err:
612                     if not transitive:
613                         continue
614                     properties = err.proptype
615                     if same_action and action not in properties:
616                         raise RequirementError(_("option '{0}' has "
617                                                  "requirement's property "
618                                                  "error: "
619                                                  "{1} {2}").format(opt._name,
620                                                                    reqpath,
621                                                                    properties))
622                     # transitive action, force expected
623                     value = expected[0]
624                     inverse = False
625                 if (not inverse and
626                         value in expected or
627                         inverse and value not in expected):
628                     calc_properties.add(action)
629                     # the calculation cannot be carried out
630                     break
631         return calc_properties
632
633     def _get_path_by_opt(self, opt):
634         """just a wrapper to get path in optiondescription's cache
635
636         :param opt: `Option`'s object
637         :returns: path
638         """
639         return self._getcontext().cfgimpl_get_description().impl_get_path_by_opt(opt)
640
641     def get_modified_properties(self):
642         return self._p_.get_modified_properties()
643
644     def get_modified_permissives(self):
645         return self._p_.get_modified_permissives()
646
647     def __getstate__(self):
648         return {'_p_': self._p_, '_owner': str(self._owner)}
649
650     def _impl_setstate(self, storage):
651         self._p_._storage = storage
652
653     def __setstate__(self, states):
654         self._p_ = states['_p_']
655         try:
656             self._owner = getattr(owners, states['_owner'])
657         except AttributeError:
658             owners.addowner(states['_owner'])
659             self._owner = getattr(owners, states['_owner'])