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