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