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