Merge branch 'master' into force-cache
[tiramisu.git] / tiramisu / value.py
1 # -*- coding: utf-8 -*-
2 "takes care of the option's values and multi values"
3 # Copyright (C) 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 # ____________________________________________________________
20 from time import time
21 from copy import copy
22 import sys
23 import weakref
24 from tiramisu.error import ConfigError, SlaveError, PropertiesOptionError
25 from tiramisu.setting import owners, multitypes, expires_time, undefined
26 from tiramisu.autolib import carry_out_calculation
27 from tiramisu.i18n import _
28 from tiramisu.option import SymLinkOption
29
30
31 class Values(object):
32     """The `Config`'s root is indeed  in charge of the `Option()`'s values,
33     but the values are physicaly located here, in `Values`, wich is also
34     responsible of a caching utility.
35     """
36     __slots__ = ('context', '_p_', '__weakref__')
37
38     def __init__(self, context, storage):
39         """
40         Initializes the values's dict.
41
42         :param context: the context is the home config's values
43
44         """
45         self.context = weakref.ref(context)
46         # the storage type is dictionary or sqlite3
47         self._p_ = storage
48
49     def _getcontext(self):
50         """context could be None, we need to test it
51         context is None only if all reference to `Config` object is deleted
52         (for example we delete a `Config` and we manipulate a reference to
53         old `SubConfig`, `Values`, `Multi` or `Settings`)
54         """
55         context = self.context()
56         if context is None:
57             raise ConfigError(_('the context does not exist anymore'))
58         return context
59
60     def _getdefault(self, opt):
61         """
62         actually retrieves the default value
63
64         :param opt: the `option.Option()` object
65         """
66         meta = self._getcontext().cfgimpl_get_meta()
67         if meta is not None:
68             value = meta.cfgimpl_get_values()[opt]
69             if isinstance(value, Multi):
70                 value = list(value)
71         else:
72             value = opt.impl_getdefault()
73         if opt.impl_is_multi():
74             return copy(value)
75         else:
76             return value
77
78     def _getvalue(self, opt, path):
79         """actually retrieves the value
80
81         :param opt: the `option.Option()` object
82         :returns: the option's value (or the default value if not set)
83         """
84         if not self._p_.hasvalue(path):
85             # if there is no value
86             value = self._getdefault(opt)
87         else:
88             # if there is a value
89             value = self._p_.getvalue(path)
90         return value
91
92     def get_modified_values(self):
93         return self._p_.get_modified_values()
94
95     def __contains__(self, opt):
96         """
97         implements the 'in' keyword syntax in order provide a pythonic way
98         to kow if an option have a value
99
100         :param opt: the `option.Option()` object
101         """
102         path = self._get_opt_path(opt)
103         return self._contains(path)
104
105     def _contains(self, path):
106         return self._p_.hasvalue(path)
107
108     def __delitem__(self, opt):
109         """overrides the builtins `del()` instructions"""
110         self.reset(opt)
111
112     def reset(self, opt, path=None):
113         if path is None:
114             path = self._get_opt_path(opt)
115         if self._p_.hasvalue(path):
116             context = self._getcontext()
117             setting = context.cfgimpl_get_settings()
118             opt.impl_validate(opt.impl_getdefault(),
119                               context, 'validator' in setting)
120             context.cfgimpl_reset_cache()
121             if (opt.impl_is_multi() and
122                     opt.impl_get_multitype() == multitypes.master):
123                 for slave in opt.impl_get_master_slaves():
124                     self.reset(slave)
125             self._p_.resetvalue(path)
126
127     def _isempty(self, opt, value):
128         "convenience method to know if an option is empty"
129         empty = opt._empty
130         if (not opt.impl_is_multi() and (value is None or value == empty)) or \
131            (opt.impl_is_multi() and (value == [] or
132                                      None in value or empty in value)):
133             return True
134         return False
135
136     def _getcallback_value(self, opt, index=None, max_len=None):
137         """
138         retrieves a value for the options that have a callback
139
140         :param opt: the `option.Option()` object
141         :param index: if an option is multi, only calculates the nth value
142         :type index: int
143         :returns: a calculated value
144         """
145         callback, callback_params = opt._callback
146         if callback_params is None:
147             callback_params = {}
148         return carry_out_calculation(opt, config=self._getcontext(),
149                                      callback=callback,
150                                      callback_params=callback_params,
151                                      index=index, max_len=max_len)
152
153     def __getitem__(self, opt):
154         "enables us to use the pythonic dictionary-like access to values"
155         return self.getitem(opt)
156
157     def getitem(self, opt, path=None, validate=True, force_permissive=False,
158                 force_properties=None, validate_properties=True):
159         if path is None:
160             path = self._get_opt_path(opt)
161         ntime = None
162         setting = self._getcontext().cfgimpl_get_settings()
163         if 'cache' in setting and self._p_.hascache(path):
164             if 'expire' in setting:
165                 ntime = int(time())
166             is_cached, value = self._p_.getcache(path, ntime)
167             if is_cached:
168                 if opt.impl_is_multi() and not isinstance(value, Multi):
169                     #load value so don't need to validate if is not a Multi
170                     value = Multi(value, self.context, opt, path, validate=False)
171                 return value
172         val = self._getitem(opt, path, validate, force_permissive,
173                             force_properties, validate_properties)
174         if 'cache' in setting and validate and validate_properties and \
175                 force_permissive is False and force_properties is None:
176             if 'expire' in setting:
177                 if ntime is None:
178                     ntime = int(time())
179                 ntime = ntime + expires_time
180             self._p_.setcache(path, val, ntime)
181
182         return val
183
184     def _getitem(self, opt, path, validate, force_permissive, force_properties,
185                  validate_properties):
186         # options with callbacks
187         context = self._getcontext()
188         setting = context.cfgimpl_get_settings()
189         is_frozen = 'frozen' in setting[opt]
190         # For calculating properties, we need value (ie for mandatory value).
191         # If value is calculating with a PropertiesOptionError's option
192         # _getcallback_value raise a ConfigError.
193         # We can not raise ConfigError if this option should raise
194         # PropertiesOptionError too. So we get config_error and raise
195         # ConfigError if properties did not raise.
196         config_error = None
197         force_permissives = None
198         # if value has callback and is not set
199         # or frozen with force_default_on_freeze
200         if opt.impl_has_callback() and (
201                 self._is_default_owner(path) or
202                 (is_frozen and 'force_default_on_freeze' in setting[opt])):
203             lenmaster = None
204             no_value_slave = False
205             if (opt.impl_is_multi() and
206                     opt.impl_get_multitype() == multitypes.slave):
207                 masterp = self._get_opt_path(opt.impl_get_master_slaves())
208                 mastervalue = context._getattr(masterp, validate=validate)
209                 lenmaster = len(mastervalue)
210                 if lenmaster == 0:
211                     value = []
212                     no_value_slave = True
213
214             if not no_value_slave:
215                 try:
216                     value = self._getcallback_value(opt, max_len=lenmaster)
217                 except ConfigError as err:
218                     # cannot assign config_err directly in python 3.3
219                     config_error = err
220                     value = None
221                     # should not raise PropertiesOptionError if option is
222                     # mandatory
223                     force_permissives = set(['mandatory'])
224                 else:
225                     if (opt.impl_is_multi() and
226                             opt.impl_get_multitype() == multitypes.slave):
227                         if not isinstance(value, list):
228                             value = [value for i in range(lenmaster)]
229             if config_error is None:
230                 if opt.impl_is_multi():
231                     value = Multi(value, self.context, opt, path, validate)
232                 # suppress value if already set
233                 self.reset(opt, path)
234         # frozen and force default
235         elif is_frozen and 'force_default_on_freeze' in setting[opt]:
236             value = self._getdefault(opt)
237             if opt.impl_is_multi():
238                 value = Multi(value, self.context, opt, path, validate)
239         else:
240             value = self._getvalue(opt, path)
241             if opt.impl_is_multi():
242                 # load value so don't need to validate if is not a Multi
243                 value = Multi(value, self.context, opt, path, validate=validate)
244         if config_error is None and validate:
245             opt.impl_validate(value, context, 'validator' in setting)
246         if config_error is None and self._is_default_owner(path) and \
247                 'force_store_value' in setting[opt]:
248             self.setitem(opt, value, path, is_write=False)
249         if validate_properties:
250             setting.validate_properties(opt, False, False, value=value, path=path,
251                                         force_permissive=force_permissive,
252                                         force_properties=force_properties,
253                                         force_permissives=force_permissives)
254         if config_error is not None:
255             raise config_error
256         return value
257
258     def __setitem__(self, opt, value):
259         raise ValueError('you should only set value with config')
260
261     def setitem(self, opt, value, path, force_permissive=False,
262                 is_write=True):
263         # is_write is, for example, used with "force_store_value"
264         # user didn't change value, so not write
265         # valid opt
266         context = self._getcontext()
267         opt.impl_validate(value, context,
268                           'validator' in context.cfgimpl_get_settings())
269         if opt.impl_is_multi():
270             value = Multi(value, self.context, opt, path, setitem=True)
271             # Save old value
272             if opt.impl_get_multitype() == multitypes.master and \
273                     self._p_.hasvalue(path):
274                 old_value = self._p_.getvalue(path)
275                 old_owner = self._p_.getowner(path, None)
276             else:
277                 old_value = undefined
278                 old_owner = undefined
279         self._setvalue(opt, path, value, force_permissive=force_permissive,
280                        is_write=is_write)
281         if opt.impl_is_multi() and opt.impl_get_multitype() == multitypes.master:
282             try:
283                 value._valid_master()
284             except Exception, err:
285                 if old_value is not undefined:
286                     self._p_.setvalue(path, old_value, old_owner)
287                 else:
288                     self._p_.resetvalue(path)
289                 raise err
290
291     def _setvalue(self, opt, path, value, force_permissive=False,
292                   force_properties=None,
293                   is_write=True, validate_properties=True):
294         context = self._getcontext()
295         context.cfgimpl_reset_cache()
296         if validate_properties:
297             setting = context.cfgimpl_get_settings()
298             setting.validate_properties(opt, False, is_write,
299                                         value=value, path=path,
300                                         force_permissive=force_permissive,
301                                         force_properties=force_properties)
302         owner = context.cfgimpl_get_settings().getowner()
303         if isinstance(value, Multi):
304             value = list(value)
305         self._p_.setvalue(path, value, owner)
306
307     def getowner(self, opt):
308         """
309         retrieves the option's owner
310
311         :param opt: the `option.Option` object
312         :returns: a `setting.owners.Owner` object
313         """
314         if isinstance(opt, SymLinkOption):
315             opt = opt._opt
316         path = self._get_opt_path(opt)
317         return self._getowner(path)
318
319     def _getowner(self, path):
320         owner = self._p_.getowner(path, owners.default)
321         meta = self._getcontext().cfgimpl_get_meta()
322         if owner is owners.default and meta is not None:
323             owner = meta.cfgimpl_get_values()._getowner(path)
324         return owner
325
326     def setowner(self, opt, owner):
327         """
328         sets a owner to an option
329
330         :param opt: the `option.Option` object
331         :param owner: a valid owner, that is a `setting.owners.Owner` object
332         """
333         if not isinstance(owner, owners.Owner):
334             raise TypeError(_("invalid generic owner {0}").format(str(owner)))
335
336         path = self._get_opt_path(opt)
337         self._setowner(path, owner)
338
339     def _setowner(self, path, owner):
340         if self._getowner(path) == owners.default:
341             raise ConfigError(_('no value for {0} cannot change owner to {1}'
342                                 '').format(path, owner))
343         self._p_.setowner(path, owner)
344
345     def is_default_owner(self, opt):
346         """
347         :param config: *must* be only the **parent** config
348                        (not the toplevel config)
349         :return: boolean
350         """
351         path = self._get_opt_path(opt)
352         return self._is_default_owner(path)
353
354     def _is_default_owner(self, path):
355         return self._getowner(path) == owners.default
356
357     def reset_cache(self, only_expired):
358         """
359         clears the cache if necessary
360         """
361         if only_expired:
362             self._p_.reset_expired_cache(int(time()))
363         else:
364             self._p_.reset_all_cache()
365
366     def _get_opt_path(self, opt):
367         """
368         retrieve the option's path in the config
369
370         :param opt: the `option.Option` object
371         :returns: a string with points like "gc.dummy.my_option"
372         """
373         return self._getcontext().cfgimpl_get_description().impl_get_path_by_opt(opt)
374
375     # information
376     def set_information(self, key, value):
377         """updates the information's attribute
378
379         :param key: information's key (ex: "help", "doc"
380         :param value: information's value (ex: "the help string")
381         """
382         self._p_.set_information(key, value)
383
384     def get_information(self, key, default=None):
385         """retrieves one information's item
386
387         :param key: the item string (ex: "help")
388         """
389         try:
390             return self._p_.get_information(key)
391         except ValueError:
392             if default is not None:
393                 return default
394             else:
395                 raise ValueError(_("information's item"
396                                    " not found: {0}").format(key))
397
398     def mandatory_warnings(self):
399         """convenience function to trace Options that are mandatory and
400         where no value has been set
401
402         :returns: generator of mandatory Option's path
403
404         """
405         #if value in cache, properties are not calculated
406         self.reset_cache(False)
407         context = self.context()
408         for path in context.cfgimpl_get_description().impl_getpaths(
409                 include_groups=True):
410             try:
411                 context._getattr(path,
412                                  force_properties=frozenset(('mandatory',)))
413             except PropertiesOptionError as err:
414                 if err.proptype == ['mandatory']:
415                     yield path
416         self.reset_cache(False)
417
418     def force_cache(self):
419         """parse all option to force data in cache
420         """
421         context = self.context()
422         if not 'cache' in context.cfgimpl_get_settings():
423             raise ConfigError(_('can force cache only if cache '
424                                 'is actived in config'))
425         #remove all cached properties and value to update "expired" time
426         context.cfgimpl_reset_cache()
427         for path in context.cfgimpl_get_description().impl_getpaths(
428                 include_groups=True):
429             try:
430                 context._getattr(path)
431             except PropertiesOptionError:
432                 pass
433
434     def __getstate__(self):
435         return {'_p_': self._p_}
436
437     def _impl_setstate(self, storage):
438         self._p_._storage = storage
439
440     def __setstate__(self, states):
441         self._p_ = states['_p_']
442
443
444 # ____________________________________________________________
445 # multi types
446
447
448 class Multi(list):
449     """multi options values container
450     that support item notation for the values of multi options"""
451     __slots__ = ('opt', 'path', 'context')
452
453     def __init__(self, value, context, opt, path, validate=True,
454                  setitem=False):
455         """
456         :param value: the Multi wraps a list value
457         :param context: the home config that has the values
458         :param opt: the option object that have this Multi value
459         :param setitem: only if set a value
460         """
461         if isinstance(value, Multi):
462             raise ValueError(_('{0} is already a Multi ').format(opt._name))
463         self.opt = opt
464         self.path = path
465         if not isinstance(context, weakref.ReferenceType):
466             raise ValueError('context must be a Weakref')
467         self.context = context
468         if not isinstance(value, list):
469             value = [value]
470         if validate and self.opt.impl_get_multitype() == multitypes.slave:
471             value = self._valid_slave(value, setitem)
472         elif not setitem and validate and \
473                 self.opt.impl_get_multitype() == multitypes.master:
474             self._valid_master()
475         super(Multi, self).__init__(value)
476
477     def _getcontext(self):
478         """context could be None, we need to test it
479         context is None only if all reference to `Config` object is deleted
480         (for example we delete a `Config` and we manipulate a reference to
481         old `SubConfig`, `Values`, `Multi` or `Settings`)
482         """
483         context = self.context()
484         if context is None:
485             raise ConfigError(_('the context does not exist anymore'))
486         return context
487
488     def _valid_slave(self, value, setitem):
489         #if slave, had values until master's one
490         context = self._getcontext()
491         values = context.cfgimpl_get_values()
492         masterp = context.cfgimpl_get_description().impl_get_path_by_opt(
493             self.opt.impl_get_master_slaves())
494         mastervalue = context._getattr(masterp, validate=False)
495         masterlen = len(mastervalue)
496         valuelen = len(value)
497         if valuelen > masterlen or (valuelen < masterlen and setitem):
498             raise SlaveError(_("invalid len for the slave: {0}"
499                                " which has {1} as master").format(
500                                    self.opt._name, masterp))
501         elif valuelen < masterlen:
502             for num in range(0, masterlen - valuelen):
503                 if self.opt.impl_has_callback():
504                     # if callback add a value, but this value will not change
505                     # anymore automaticly (because this value has owner)
506                     index = value.__len__()
507                     value.append(values._getcallback_value(self.opt,
508                                                            index=index))
509                 else:
510                     value.append(self.opt.impl_getdefault_multi())
511         #else: same len so do nothing
512         return value
513
514     def _valid_master(self):
515         #masterlen = len(value)
516         values = self._getcontext().cfgimpl_get_values()
517         for slave in self.opt._master_slaves:
518             path = values._get_opt_path(slave)
519             Multi(values._getvalue(slave, path), self.context, slave, path)
520
521     def __setitem__(self, index, value):
522         self._validate(value, index)
523         #assume not checking mandatory property
524         super(Multi, self).__setitem__(index, value)
525         self._getcontext().cfgimpl_get_values()._setvalue(self.opt, self.path, self)
526
527     def append(self, value=undefined, force=False):
528         """the list value can be updated (appened)
529         only if the option is a master
530         """
531         context = self._getcontext()
532         if not force:
533             if self.opt.impl_get_multitype() == multitypes.slave:
534                 raise SlaveError(_("cannot append a value on a multi option {0}"
535                                    " which is a slave").format(self.opt._name))
536             elif self.opt.impl_get_multitype() == multitypes.master:
537                 values = context.cfgimpl_get_values()
538                 if value is undefined and self.opt.impl_has_callback():
539                     value = values._getcallback_value(self.opt)
540                     #Force None il return a list
541                     if isinstance(value, list):
542                         value = None
543         index = self.__len__()
544         if value is undefined:
545             value = self.opt.impl_getdefault_multi()
546         self._validate(value, index)
547         super(Multi, self).append(value)
548         context.cfgimpl_get_values()._setvalue(self.opt, self.path,
549                                                self,
550                                                validate_properties=not force)
551         if not force and self.opt.impl_get_multitype() == multitypes.master:
552             for slave in self.opt.impl_get_master_slaves():
553                 path = values._get_opt_path(slave)
554                 if not values._is_default_owner(path):
555                     if slave.impl_has_callback():
556                         dvalue = values._getcallback_value(slave, index=index)
557                     else:
558                         dvalue = slave.impl_getdefault_multi()
559                     old_value = values.getitem(slave, path, validate=False,
560                                                validate_properties=False)
561                     if len(old_value) + 1 != self.__len__():
562                         raise SlaveError(_("invalid len for the slave: {0}"
563                                            " which has {1} as master").format(
564                                                self.opt._name, self.__len__()))
565                     values.getitem(slave, path, validate=False,
566                                    validate_properties=False).append(
567                                        dvalue, force=True)
568
569     def sort(self, cmp=None, key=None, reverse=False):
570         if self.opt.impl_get_multitype() in [multitypes.slave,
571                                              multitypes.master]:
572             raise SlaveError(_("cannot sort multi option {0} if master or slave"
573                                "").format(self.opt._name))
574         if sys.version_info[0] >= 3:
575             if cmp is not None:
576                 raise ValueError(_('cmp is not permitted in python v3 or greater'))
577             super(Multi, self).sort(key=key, reverse=reverse)
578         else:
579             super(Multi, self).sort(cmp=cmp, key=key, reverse=reverse)
580         self._getcontext().cfgimpl_get_values()._setvalue(self.opt, self.path, self)
581
582     def reverse(self):
583         if self.opt.impl_get_multitype() in [multitypes.slave,
584                                              multitypes.master]:
585             raise SlaveError(_("cannot reverse multi option {0} if master or "
586                                "slave").format(self.opt._name))
587         super(Multi, self).reverse()
588         self._getcontext().cfgimpl_get_values()._setvalue(self.opt, self.path, self)
589
590     def insert(self, index, obj):
591         if self.opt.impl_get_multitype() in [multitypes.slave,
592                                              multitypes.master]:
593             raise SlaveError(_("cannot insert multi option {0} if master or "
594                                "slave").format(self.opt._name))
595         super(Multi, self).insert(index, obj)
596         self._getcontext().cfgimpl_get_values()._setvalue(self.opt, self.path, self)
597
598     def extend(self, iterable):
599         if self.opt.impl_get_multitype() in [multitypes.slave,
600                                              multitypes.master]:
601             raise SlaveError(_("cannot extend multi option {0} if master or "
602                                "slave").format(self.opt._name))
603         super(Multi, self).extend(iterable)
604         self._getcontext().cfgimpl_get_values()._setvalue(self.opt, self.path, self)
605
606     def _validate(self, value, force_index):
607         if value is not None:
608             try:
609                 self.opt.impl_validate(value, context=self._getcontext(),
610                                        force_index=force_index)
611             except ValueError as err:
612                 raise ValueError(_("invalid value {0} "
613                                    "for option {1}: {2}"
614                                    "").format(str(value),
615                                               self.opt._name, err))
616
617     def pop(self, index, force=False):
618         """the list value can be updated (poped)
619         only if the option is a master
620
621         :param index: remove item a index
622         :type index: int
623         :param force: force pop item (withoud check master/slave)
624         :type force: boolean
625         :returns: item at index
626         """
627         context = self._getcontext()
628         if not force:
629             if self.opt.impl_get_multitype() == multitypes.slave:
630                 raise SlaveError(_("cannot pop a value on a multi option {0}"
631                                    " which is a slave").format(self.opt._name))
632             if self.opt.impl_get_multitype() == multitypes.master:
633                 for slave in self.opt.impl_get_master_slaves():
634                     values = context.cfgimpl_get_values()
635                     if not values.is_default_owner(slave):
636                         #get multi without valid properties
637                         values.getitem(slave, validate=False,
638                                        validate_properties=False
639                                        ).pop(index, force=True)
640         #set value without valid properties
641         ret = super(Multi, self).pop(index)
642         context.cfgimpl_get_values()._setvalue(self.opt, self.path, self, validate_properties=not force)
643         return ret