968f0e4fc471dff8701f09d5d4e7ce66456235d1
[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         if validate_properties:
248             setting.validate_properties(opt, False, False, value=value, path=path,
249                                         force_permissive=force_permissive,
250                                         force_properties=force_properties,
251                                         force_permissives=force_permissives)
252         if config_error is not None:
253             raise config_error
254         return value
255
256     def __setitem__(self, opt, value):
257         raise ValueError('you should only set value with config')
258
259     def setitem(self, opt, value, path, force_permissive=False,
260                 is_write=True):
261         # is_write is, for example, used with "force_store_value"
262         # user didn't change value, so not write
263         # valid opt
264         context = self._getcontext()
265         opt.impl_validate(value, context,
266                           'validator' in context.cfgimpl_get_settings())
267         if opt.impl_is_multi():
268             value = Multi(value, self.context, opt, path, setitem=True)
269             # Save old value
270             if opt.impl_get_multitype() == multitypes.master and \
271                     self._p_.hasvalue(path):
272                 old_value = self._p_.getvalue(path)
273                 old_owner = self._p_.getowner(path, None)
274             else:
275                 old_value = undefined
276                 old_owner = undefined
277         self._setvalue(opt, path, value, force_permissive=force_permissive,
278                        is_write=is_write)
279         if opt.impl_is_multi() and opt.impl_get_multitype() == multitypes.master:
280             try:
281                 value._valid_master()
282             except Exception, err:
283                 if old_value is not undefined:
284                     self._p_.setvalue(path, old_value, old_owner)
285                 else:
286                     self._p_.resetvalue(path)
287                 raise err
288
289     def _setvalue(self, opt, path, value, force_permissive=False,
290                   force_properties=None,
291                   is_write=True, validate_properties=True):
292         context = self._getcontext()
293         context.cfgimpl_reset_cache()
294         if validate_properties:
295             setting = context.cfgimpl_get_settings()
296             setting.validate_properties(opt, False, is_write,
297                                         value=value, path=path,
298                                         force_permissive=force_permissive,
299                                         force_properties=force_properties)
300         owner = context.cfgimpl_get_settings().getowner()
301         if isinstance(value, Multi):
302             value = list(value)
303         self._p_.setvalue(path, value, owner)
304
305     def getowner(self, opt):
306         """
307         retrieves the option's owner
308
309         :param opt: the `option.Option` object
310         :returns: a `setting.owners.Owner` object
311         """
312         if isinstance(opt, SymLinkOption):
313             opt = opt._opt
314         path = self._get_opt_path(opt)
315         return self._getowner(path)
316
317     def _getowner(self, path):
318         owner = self._p_.getowner(path, owners.default)
319         meta = self._getcontext().cfgimpl_get_meta()
320         if owner is owners.default and meta is not None:
321             owner = meta.cfgimpl_get_values()._getowner(path)
322         return owner
323
324     def setowner(self, opt, owner):
325         """
326         sets a owner to an option
327
328         :param opt: the `option.Option` object
329         :param owner: a valid owner, that is a `setting.owners.Owner` object
330         """
331         if not isinstance(owner, owners.Owner):
332             raise TypeError(_("invalid generic owner {0}").format(str(owner)))
333
334         path = self._get_opt_path(opt)
335         self._setowner(path, owner)
336
337     def _setowner(self, path, owner):
338         if self._getowner(path) == owners.default:
339             raise ConfigError(_('no value for {0} cannot change owner to {1}'
340                                 '').format(path, owner))
341         self._p_.setowner(path, owner)
342
343     def is_default_owner(self, opt):
344         """
345         :param config: *must* be only the **parent** config
346                        (not the toplevel config)
347         :return: boolean
348         """
349         path = self._get_opt_path(opt)
350         return self._is_default_owner(path)
351
352     def _is_default_owner(self, path):
353         return self._getowner(path) == owners.default
354
355     def reset_cache(self, only_expired):
356         """
357         clears the cache if necessary
358         """
359         if only_expired:
360             self._p_.reset_expired_cache(int(time()))
361         else:
362             self._p_.reset_all_cache()
363
364     def _get_opt_path(self, opt):
365         """
366         retrieve the option's path in the config
367
368         :param opt: the `option.Option` object
369         :returns: a string with points like "gc.dummy.my_option"
370         """
371         return self._getcontext().cfgimpl_get_description().impl_get_path_by_opt(opt)
372
373     # information
374     def set_information(self, key, value):
375         """updates the information's attribute
376
377         :param key: information's key (ex: "help", "doc"
378         :param value: information's value (ex: "the help string")
379         """
380         self._p_.set_information(key, value)
381
382     def get_information(self, key, default=None):
383         """retrieves one information's item
384
385         :param key: the item string (ex: "help")
386         """
387         try:
388             return self._p_.get_information(key)
389         except ValueError:
390             if default is not None:
391                 return default
392             else:
393                 raise ValueError(_("information's item"
394                                    " not found: {0}").format(key))
395
396     def mandatory_warnings(self):
397         """convenience function to trace Options that are mandatory and
398         where no value has been set
399
400         :returns: generator of mandatory Option's path
401
402         """
403         #if value in cache, properties are not calculated
404         self.reset_cache(False)
405         context = self.context()
406         for path in context.cfgimpl_get_description().impl_getpaths(
407                 include_groups=True):
408             try:
409                 context._getattr(path,
410                                  force_properties=frozenset(('mandatory',)))
411             except PropertiesOptionError as err:
412                 if err.proptype == ['mandatory']:
413                     yield path
414         self.reset_cache(False)
415
416     def force_cache(self):
417         """parse all option to force data in cache
418         """
419         context = self.context()
420         if not 'cache' in context.cfgimpl_get_settings():
421             raise ConfigError(_('can force cache only if cache '
422                                 'is actived in config'))
423         #remove all cached properties and value to update "expired" time
424         context.cfgimpl_reset_cache()
425         for path in context.cfgimpl_get_description().impl_getpaths(
426                 include_groups=True):
427             try:
428                 context._getattr(path)
429             except PropertiesOptionError:
430                 pass
431
432     def __getstate__(self):
433         return {'_p_': self._p_}
434
435     def _impl_setstate(self, storage):
436         self._p_._storage = storage
437
438     def __setstate__(self, states):
439         self._p_ = states['_p_']
440
441
442 # ____________________________________________________________
443 # multi types
444
445
446 class Multi(list):
447     """multi options values container
448     that support item notation for the values of multi options"""
449     __slots__ = ('opt', 'path', 'context')
450
451     def __init__(self, value, context, opt, path, validate=True,
452                  setitem=False):
453         """
454         :param value: the Multi wraps a list value
455         :param context: the home config that has the values
456         :param opt: the option object that have this Multi value
457         :param setitem: only if set a value
458         """
459         if isinstance(value, Multi):
460             raise ValueError(_('{0} is already a Multi ').format(opt._name))
461         self.opt = opt
462         self.path = path
463         if not isinstance(context, weakref.ReferenceType):
464             raise ValueError('context must be a Weakref')
465         self.context = context
466         if not isinstance(value, list):
467             value = [value]
468         if validate and self.opt.impl_get_multitype() == multitypes.slave:
469             value = self._valid_slave(value, setitem)
470         elif not setitem and validate and \
471                 self.opt.impl_get_multitype() == multitypes.master:
472             self._valid_master()
473         super(Multi, self).__init__(value)
474
475     def _getcontext(self):
476         """context could be None, we need to test it
477         context is None only if all reference to `Config` object is deleted
478         (for example we delete a `Config` and we manipulate a reference to
479         old `SubConfig`, `Values`, `Multi` or `Settings`)
480         """
481         context = self.context()
482         if context is None:
483             raise ConfigError(_('the context does not exist anymore'))
484         return context
485
486     def _valid_slave(self, value, setitem):
487         #if slave, had values until master's one
488         context = self._getcontext()
489         values = context.cfgimpl_get_values()
490         masterp = context.cfgimpl_get_description().impl_get_path_by_opt(
491             self.opt.impl_get_master_slaves())
492         mastervalue = context._getattr(masterp, validate=False)
493         masterlen = len(mastervalue)
494         valuelen = len(value)
495         if valuelen > masterlen or (valuelen < masterlen and setitem):
496             raise SlaveError(_("invalid len for the slave: {0}"
497                                " which has {1} as master").format(
498                                    self.opt._name, masterp))
499         elif valuelen < masterlen:
500             for num in range(0, masterlen - valuelen):
501                 if self.opt.impl_has_callback():
502                     # if callback add a value, but this value will not change
503                     # anymore automaticly (because this value has owner)
504                     index = value.__len__()
505                     value.append(values._getcallback_value(self.opt,
506                                                            index=index))
507                 else:
508                     value.append(self.opt.impl_getdefault_multi())
509         #else: same len so do nothing
510         return value
511
512     def _valid_master(self):
513         #masterlen = len(value)
514         values = self._getcontext().cfgimpl_get_values()
515         for slave in self.opt._master_slaves:
516             path = values._get_opt_path(slave)
517             Multi(values._getvalue(slave, path), self.context, slave, path)
518
519     def __setitem__(self, index, value):
520         self._validate(value, index)
521         #assume not checking mandatory property
522         super(Multi, self).__setitem__(index, value)
523         self._getcontext().cfgimpl_get_values()._setvalue(self.opt, self.path, self)
524
525     def append(self, value=undefined, force=False):
526         """the list value can be updated (appened)
527         only if the option is a master
528         """
529         context = self._getcontext()
530         if not force:
531             if self.opt.impl_get_multitype() == multitypes.slave:
532                 raise SlaveError(_("cannot append a value on a multi option {0}"
533                                    " which is a slave").format(self.opt._name))
534             elif self.opt.impl_get_multitype() == multitypes.master:
535                 values = context.cfgimpl_get_values()
536                 if value is undefined and self.opt.impl_has_callback():
537                     value = values._getcallback_value(self.opt)
538                     #Force None il return a list
539                     if isinstance(value, list):
540                         value = None
541         index = self.__len__()
542         if value is undefined:
543             value = self.opt.impl_getdefault_multi()
544         self._validate(value, index)
545         super(Multi, self).append(value)
546         context.cfgimpl_get_values()._setvalue(self.opt, self.path,
547                                                self,
548                                                validate_properties=not force)
549         if not force and self.opt.impl_get_multitype() == multitypes.master:
550             for slave in self.opt.impl_get_master_slaves():
551                 path = values._get_opt_path(slave)
552                 if not values._is_default_owner(path):
553                     if slave.impl_has_callback():
554                         dvalue = values._getcallback_value(slave, index=index)
555                     else:
556                         dvalue = slave.impl_getdefault_multi()
557                     old_value = values.getitem(slave, path, validate=False,
558                                                validate_properties=False)
559                     if len(old_value) + 1 != self.__len__():
560                         raise SlaveError(_("invalid len for the slave: {0}"
561                                            " which has {1} as master").format(
562                                                self.opt._name, self.__len__()))
563                     values.getitem(slave, path, validate=False,
564                                    validate_properties=False).append(
565                                        dvalue, force=True)
566
567     def sort(self, cmp=None, key=None, reverse=False):
568         if self.opt.impl_get_multitype() in [multitypes.slave,
569                                              multitypes.master]:
570             raise SlaveError(_("cannot sort multi option {0} if master or slave"
571                                "").format(self.opt._name))
572         if sys.version_info[0] >= 3:
573             if cmp is not None:
574                 raise ValueError(_('cmp is not permitted in python v3 or greater'))
575             super(Multi, self).sort(key=key, reverse=reverse)
576         else:
577             super(Multi, self).sort(cmp=cmp, key=key, reverse=reverse)
578         self._getcontext().cfgimpl_get_values()._setvalue(self.opt, self.path, self)
579
580     def reverse(self):
581         if self.opt.impl_get_multitype() in [multitypes.slave,
582                                              multitypes.master]:
583             raise SlaveError(_("cannot reverse multi option {0} if master or "
584                                "slave").format(self.opt._name))
585         super(Multi, self).reverse()
586         self._getcontext().cfgimpl_get_values()._setvalue(self.opt, self.path, self)
587
588     def insert(self, index, obj):
589         if self.opt.impl_get_multitype() in [multitypes.slave,
590                                              multitypes.master]:
591             raise SlaveError(_("cannot insert multi option {0} if master or "
592                                "slave").format(self.opt._name))
593         super(Multi, self).insert(index, obj)
594         self._getcontext().cfgimpl_get_values()._setvalue(self.opt, self.path, self)
595
596     def extend(self, iterable):
597         if self.opt.impl_get_multitype() in [multitypes.slave,
598                                              multitypes.master]:
599             raise SlaveError(_("cannot extend multi option {0} if master or "
600                                "slave").format(self.opt._name))
601         super(Multi, self).extend(iterable)
602         self._getcontext().cfgimpl_get_values()._setvalue(self.opt, self.path, self)
603
604     def _validate(self, value, force_index):
605         if value is not None:
606             try:
607                 self.opt.impl_validate(value, context=self._getcontext(),
608                                        force_index=force_index)
609             except ValueError as err:
610                 raise ValueError(_("invalid value {0} "
611                                    "for option {1}: {2}"
612                                    "").format(str(value),
613                                               self.opt._name, err))
614
615     def pop(self, index, force=False):
616         """the list value can be updated (poped)
617         only if the option is a master
618
619         :param index: remove item a index
620         :type index: int
621         :param force: force pop item (withoud check master/slave)
622         :type force: boolean
623         :returns: item at index
624         """
625         context = self._getcontext()
626         if not force:
627             if self.opt.impl_get_multitype() == multitypes.slave:
628                 raise SlaveError(_("cannot pop a value on a multi option {0}"
629                                    " which is a slave").format(self.opt._name))
630             if self.opt.impl_get_multitype() == multitypes.master:
631                 for slave in self.opt.impl_get_master_slaves():
632                     values = context.cfgimpl_get_values()
633                     if not values.is_default_owner(slave):
634                         #get multi without valid properties
635                         values.getitem(slave, validate=False,
636                                        validate_properties=False
637                                        ).pop(index, force=True)
638         #set value without valid properties
639         ret = super(Multi, self).pop(index)
640         context.cfgimpl_get_values()._setvalue(self.opt, self.path, self, validate_properties=not force)
641         return ret