adds test for an API
[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
6 # it under the terms of the GNU General Public License as published by
7 # the Free Software Foundation; either version 2 of the License, or
8 # (at your option) any later version.
9 #
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 # GNU General Public License for more details.
14 #
15 # You should have received a copy of the GNU General Public License
16 # along with this program; if not, write to the Free Software
17 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
18 #
19 # The original `Config` design model is unproudly borrowed from
20 # the rough pypy's guys: http://codespeak.net/svn/pypy/dist/pypy/config/
21 # the whole pypy projet is under MIT licence
22 # ____________________________________________________________
23 from time import time
24 from copy import copy
25 import weakref
26 from tiramisu.error import (RequirementError, PropertiesOptionError,
27                             ConstError)
28 from tiramisu.i18n import _
29
30
31 "Default encoding for display a Config if raise UnicodeEncodeError"
32 default_encoding = 'utf-8'
33
34 """If cache and expire is enable, time before cache is expired.
35 This delay start first time value/setting is set in cache, even if
36 user access several time to value/setting
37 """
38 expires_time = 5
39 """List of default properties (you can add new one if needed).
40
41 For common properties and personalise properties, if a propery is set for
42 an Option and for the Config together, Setting raise a PropertiesOptionError
43
44 * Common properties:
45
46 hidden
47     option with this property can only get value in read only mode. This
48     option is not available in read write mode.
49
50 disabled
51     option with this property cannot be set/get
52
53 frozen
54     cannot set value for option with this properties if 'frozen' is set in
55     config
56
57 mandatory
58     should set value for option with this properties if 'mandatory' is set in
59     config
60
61
62 * Special property:
63
64 permissive
65     option with 'permissive' cannot raise PropertiesOptionError for properties
66     set in permissive
67     config with 'permissive', whole option in this config cannot raise
68     PropertiesOptionError for properties set in permissive
69
70 * Special Config properties:
71
72 cache
73     if set, enable cache settings and values
74
75 expire
76     if set, settings and values in cache expire after ``expires_time``
77
78 everything_frozen
79     whole option in config are frozen (even if option have not frozen
80     property)
81
82 validator
83     launch validator set by user in option (this property has no effect
84     for internal validator)
85 """
86 default_properties = ('cache', 'expire', 'validator')
87
88 """Config can be in two defaut mode:
89
90 read_only
91     you can get all variables not disabled but you cannot set any variables
92     if a value has a callback without any value, callback is launch and value
93     of this variable can change
94     you cannot access to mandatory variable without values
95
96 read_write
97     you can get all variables not disabled and not hidden
98     you can set all variables not frozen
99 """
100 ro_append = set(['frozen', 'disabled', 'validator', 'everything_frozen',
101                 'mandatory'])
102 ro_remove = set(['permissive', 'hidden'])
103 rw_append = set(['frozen', 'disabled', 'validator', 'hidden'])
104 rw_remove = set(['permissive', 'everything_frozen', 'mandatory'])
105
106
107 # ____________________________________________________________
108 class _NameSpace(object):
109     """convenient class that emulates a module
110     and builds constants (that is, unique names)
111     when attribute is added, we cannot delete it
112     """
113
114     def __setattr__(self, name, value):
115         if name in self.__dict__:
116             raise ConstError(_("can't rebind {0}").format(name))
117         self.__dict__[name] = value
118
119     def __delattr__(self, name):
120         if name in self.__dict__:
121             raise ConstError(_("can't unbind {0}").format(name))
122         raise ValueError(name)
123
124
125 class GroupModule(_NameSpace):
126     "emulates a module to manage unique group (OptionDescription) names"
127     class GroupType(str):
128         """allowed normal group (OptionDescription) names
129         *normal* means : groups that are not master
130         """
131         pass
132
133     class DefaultGroupType(GroupType):
134         """groups that are default (typically 'default')"""
135         pass
136
137     class MasterGroupType(GroupType):
138         """allowed normal group (OptionDescription) names
139         *master* means : groups that have the 'master' attribute set
140         """
141         pass
142
143
144 class OwnerModule(_NameSpace):
145     """emulates a module to manage unique owner names.
146
147     owners are living in `Config._cfgimpl_value_owners`
148     """
149     class Owner(str):
150         """allowed owner names
151         """
152         pass
153
154     class DefaultOwner(Owner):
155         """groups that are default (typically 'default')"""
156         pass
157
158
159 class MultiTypeModule(_NameSpace):
160     "namespace for the master/slaves"
161     class MultiType(str):
162         pass
163
164     class DefaultMultiType(MultiType):
165         pass
166
167     class MasterMultiType(MultiType):
168         pass
169
170     class SlaveMultiType(MultiType):
171         pass
172
173
174 # ____________________________________________________________
175 def populate_groups():
176     """populates the available groups in the appropriate namespaces
177
178     groups.default
179         default group set when creating a new optiondescription
180
181     groups.master
182         master group is a special optiondescription, all suboptions should be
183         multi option and all values should have same length, to find master's
184         option, the optiondescription's name should be same than de master's
185         option
186
187     groups.family
188         example of group, no special behavior with this group's type
189     """
190     groups.default = groups.DefaultGroupType('default')
191     groups.master = groups.MasterGroupType('master')
192     groups.family = groups.GroupType('family')
193
194
195 def populate_owners():
196     """populates the available owners in the appropriate namespaces
197
198     default
199         is the config owner after init time
200
201     user
202         is the generic is the generic owner
203     """
204     setattr(owners, 'default', owners.DefaultOwner('default'))
205     setattr(owners, 'user', owners.Owner('user'))
206
207     def addowner(name):
208         """
209         :param name: the name of the new owner
210         """
211         setattr(owners, name, owners.Owner(name))
212     setattr(owners, 'addowner', addowner)
213
214
215 def populate_multitypes():
216     """all multi option should have a type, this type is automaticly set do
217     not touch this
218
219     default
220         default's multi option set if not master or slave
221
222     master
223         master's option in a group with master's type, name of this option
224         should be the same name of the optiondescription
225
226     slave
227         slave's option in a group with master's type
228
229     """
230     setattr(multitypes, 'default', multitypes.DefaultMultiType('default'))
231     setattr(multitypes, 'master', multitypes.MasterMultiType('master'))
232     setattr(multitypes, 'slave', multitypes.SlaveMultiType('slave'))
233
234
235 # ____________________________________________________________
236 # populate groups, owners and multitypes with default attributes
237 groups = GroupModule()
238 populate_groups()
239 owners = OwnerModule()
240 populate_owners()
241 multitypes = MultiTypeModule()
242 populate_multitypes()
243
244
245 # ____________________________________________________________
246 class Undefined():
247     pass
248
249
250 undefined = Undefined()
251 # ____________________________________________________________
252 class Property(object):
253     "a property is responsible of the option's value access rules"
254     __slots__ = ('_setting', '_properties', '_opt', '_path')
255
256     def __init__(self, setting, prop, opt=None, path=None):
257         self._opt = opt
258         self._path = path
259         self._setting = setting
260         self._properties = prop
261
262     def append(self, propname):
263         """Appends a property named propname
264
265         :param propname: a predefined or user defined property name
266         :type propname: string
267         """
268         if self._opt is not None and self._opt._calc_properties is not None \
269                 and propname in self._opt._calc_properties:
270             raise ValueError(_('cannot append {0} property for option {1}: '
271                                'this property is calculated').format(
272                                    propname, self._opt._name))
273         self._properties.add(propname)
274         self._setting._setproperties(self._properties, self._opt, self._path)
275
276     def remove(self, propname):
277         """Removes a property named propname
278
279         :param propname: a predefined or user defined property name
280         :type propname: string
281         """
282         if propname in self._properties:
283             self._properties.remove(propname)
284             self._setting._setproperties(self._properties, self._opt,
285                                          self._path)
286     def extend(self, propnames):
287         """Extends properties to the existing properties
288
289         :param propnames: an iterable made of property names
290         :type propnames: iterable of string
291         """
292         for propname in propnames:
293             self.append(propname)
294
295     def reset(self):
296         """resets the properties (does not **clear** the properties,
297         default properties are still present)
298         """
299         self._setting.reset(_path=self._path)
300
301     def __contains__(self, propname):
302         return propname in self._properties
303
304     def __repr__(self):
305         return str(list(self._properties))
306
307
308 #____________________________________________________________
309 class Settings(object):
310     "``Config()``'s configuration options"
311     __slots__ = ('context', '_owner', '_p_', '__weakref__')
312
313     def __init__(self, context, storage):
314         """
315         initializer
316
317         :param context: the root config
318         :param storage: the storage type
319
320                         - dictionary -> in memory
321                         - sqlite3 -> persistent
322         """
323         # generic owner
324         self._owner = owners.user
325         self.context = weakref.ref(context)
326         self._p_ = storage
327
328     #____________________________________________________________
329     # properties methods
330     def __contains__(self, propname):
331         "enables the pythonic 'in' syntaxic sugar"
332         return propname in self._getproperties()
333
334     def __repr__(self):
335         return str(list(self._getproperties()))
336
337     def __getitem__(self, opt):
338         path = self._get_path_by_opt(opt)
339         return self._getitem(opt, path)
340
341     def _getitem(self, opt, path):
342         return Property(self, self._getproperties(opt, path), opt, path)
343
344     def __setitem__(self, opt, value):
345         raise ValueError('you should only append/remove properties')
346
347     def reset(self, opt=None, _path=None, all_properties=False):
348         if all_properties and (_path or opt):
349             raise ValueError(_('opt and all_properties must not be set '
350                                'together in reset'))
351         if all_properties:
352             self._p_.reset_all_propertives()
353         else:
354             if opt is not None and _path is None:
355                 _path = self._get_path_by_opt(opt)
356             self._p_.reset_properties(_path)
357         self.context().cfgimpl_reset_cache()
358
359     def _getproperties(self, opt=None, path=None, is_apply_req=True):
360         if opt is None:
361             props = self._p_.getproperties(path, default_properties)
362         else:
363             if path is None:
364                 raise ValueError(_('if opt is not None, path should not be'
365                                    ' None in _getproperties'))
366             ntime = None
367             if 'cache' in self and self._p_.hascache(path):
368                 if 'expire' in self:
369                     ntime = int(time())
370                 is_cached, props = self._p_.getcache(path, ntime)
371                 if is_cached:
372                     return props
373             props = self._p_.getproperties(path, opt._properties)
374             if is_apply_req:
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, 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, 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, None)
396
397     def _setproperties(self, properties, opt, path):
398         """save properties for specified opt
399         (never save properties if same has option properties)
400         """
401         if opt is None:
402             self._p_.setproperties(None, properties)
403         else:
404             if opt._calc_properties is not None:
405                 properties -= opt._calc_properties
406             if set(opt._properties) == properties:
407                 self._p_.reset_properties(path)
408             else:
409                 self._p_.setproperties(path, properties)
410         self.context().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 = copy(self._getproperties(opt_or_descr, path))
436         self_properties = copy(self._getproperties())
437         # remove opt permissive
438         if force_permissive is True or 'permissive' in self_properties:
439             properties -= self._p_.getpermissive(path)
440         # remove global permissive if need
441         if force_permissive is True or 'permissive' in self_properties:
442             properties -= self._p_.getpermissive()
443         if force_permissives is not None:
444             properties -= force_permissives
445
446         # global properties
447         if force_properties is not None:
448             self_properties.update(force_properties)
449
450         # calc properties
451         properties &= self_properties
452         # mandatory and frozen are special properties
453         if is_descr:
454             properties -= frozenset(('mandatory', 'frozen'))
455         else:
456             if 'mandatory' in properties and \
457                     not self.context().cfgimpl_get_values()._isempty(
458                         opt_or_descr, value):
459                 properties.remove('mandatory')
460             if is_write and 'everything_frozen' in self_properties:
461                 properties.add('frozen')
462             elif 'frozen' in properties and not is_write:
463                 properties.remove('frozen')
464         # at this point an option should not remain in properties
465         if properties != frozenset():
466             props = list(properties)
467             if 'frozen' in properties:
468                 raise PropertiesOptionError(_('cannot change the value for '
469                                               'option {0} this option is'
470                                               ' frozen').format(
471                                                   opt_or_descr._name),
472                                             props)
473             else:
474                 raise PropertiesOptionError(_("trying to access to an option "
475                                               "named: {0} with properties {1}"
476                                               "").format(opt_or_descr._name,
477                                                          str(props)), props)
478
479     def setpermissive(self, permissive, opt=None, path=None):
480         """
481         enables us to put the permissives in the storage
482
483         :param path: the option's path
484         :param type: str
485         :param opt: if an option object is set, the path is extracted.
486                     it is better (faster) to set the path parameter
487                     instead of passing a :class:`tiramisu.option.Option()` object.
488         """
489         if opt is not None and path is None:
490             path = self._get_path_by_opt(opt)
491         if not isinstance(permissive, tuple):
492             raise TypeError(_('permissive must be a tuple'))
493         self._p_.setpermissive(path, permissive)
494
495     #____________________________________________________________
496     def setowner(self, owner):
497         ":param owner: sets the default value for owner at the Config level"
498         if not isinstance(owner, owners.Owner):
499             raise TypeError(_("invalid generic owner {0}").format(str(owner)))
500         self._owner = owner
501
502     def getowner(self):
503         return self._owner
504
505     #____________________________________________________________
506     def _read(self, remove, append):
507         for prop in remove:
508             self.remove(prop)
509         for prop in append:
510             self.append(prop)
511
512     def read_only(self):
513         "convenience method to freeze, hide and disable"
514         self._read(ro_remove, ro_append)
515
516     def read_write(self):
517         "convenience method to freeze, hide and disable"
518         self._read(rw_remove, rw_append)
519
520     def reset_cache(self, only_expired):
521         """reset all settings in cache
522
523         :param only_expired: if True reset only expired cached values
524         :type only_expired: boolean
525         """
526         if only_expired:
527             self._p_.reset_expired_cache(int(time()))
528         else:
529             self._p_.reset_all_cache()
530
531     def apply_requires(self, opt, path):
532         """carries out the jit (just in time) requirements between options
533
534         a requirement is a tuple of this form that comes from the option's
535         requirements validation::
536
537             (option, expected, action, inverse, transitive, same_action)
538
539         let's have a look at all the tuple's items:
540
541         - **option** is the target option's
542
543         - **expected** is the target option's value that is going to trigger
544           an action
545
546         - **action** is the (property) action to be accomplished if the target
547           option happens to have the expected value
548
549         - if **inverse** is `True` and if the target option's value does not
550           apply, then the property action must be removed from the option's
551           properties list (wich means that the property is inverted)
552
553         - **transitive**: but what happens if the target option cannot be
554           accessed ? We don't kown the target option's value. Actually if some
555           property in the target option is not present in the permissive, the
556           target option's value cannot be accessed. In this case, the
557           **action** have to be applied to the option. (the **action** property
558           is then added to the option).
559
560         - **same_action**: actually, if **same_action** is `True`, the
561           transitivity is not accomplished. The transitivity is accomplished
562           only if the target option **has the same property** that the demanded
563           action. If the target option's value is not accessible because of
564           another reason, because of a property of another type, then an
565           exception :exc:`~error.RequirementError` is raised.
566
567         And at last, if no target option matches the expected values, the
568         action must be removed from the option's properties list.
569
570         :param opt: the option on wich the requirement occurs
571         :type opt: `option.Option()`
572         :param path: the option's path in the config
573         :type path: str
574         """
575         if opt._requires is None:
576             return frozenset()
577
578         # filters the callbacks
579         calc_properties = set()
580         for requires in opt._requires:
581             for require in requires:
582                 option, expected, action, inverse, \
583                     transitive, same_action = require
584                 reqpath = self._get_path_by_opt(option)
585                 if reqpath == path or reqpath.startswith(path + '.'):
586                     raise RequirementError(_("malformed requirements "
587                                              "imbrication detected for option:"
588                                              " '{0}' with requirement on: "
589                                              "'{1}'").format(path, reqpath))
590                 try:
591                     value = self.context()._getattr(reqpath,
592                                                     force_permissive=True)
593                 except PropertiesOptionError as err:
594                     if not transitive:
595                         continue
596                     properties = err.proptype
597                     if same_action and action not in properties:
598                         raise RequirementError(_("option '{0}' has "
599                                                  "requirement's property "
600                                                  "error: "
601                                                  "{1} {2}").format(opt._name,
602                                                                    reqpath,
603                                                                    properties))
604                     # transitive action, force expected
605                     value = expected[0]
606                     inverse = False
607                 if (not inverse and
608                         value in expected or
609                         inverse and value not in expected):
610                     calc_properties.add(action)
611                     # the calculation cannot be carried out
612                     break
613         return calc_properties
614
615     def _get_path_by_opt(self, opt):
616         """just a wrapper to get path in optiondescription's cache
617
618         :param opt: `Option`'s object
619         :returns: path
620         """
621         return self.context().cfgimpl_get_description().impl_get_path_by_opt(opt)
622
623     def get_modified_properties(self):
624         return self._p_.get_modified_properties()
625
626     def get_modified_permissives(self):
627         return self._p_.get_modified_permissives()
628
629     def __getstate__(self):
630         return {'_p_': self._p_, '_owner': str(self._owner)}
631
632     def _impl_setstate(self, storage):
633         self._p_._storage = storage
634
635     def __setstate__(self, states):
636         self._p_ = states['_p_']
637         try:
638             self._owner = getattr(owners, states['_owner'])
639         except AttributeError:
640             owners.addowner(states['_owner'])
641             self._owner = getattr(owners, states['_owner'])