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