# -*- coding: utf-8 -*- "takes care of the option's values and multi values" # Copyright (C) 2013 Team tiramisu (see AUTHORS for all contributors) # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # # ____________________________________________________________ from time import time from copy import copy import sys import weakref from tiramisu.error import ConfigError, SlaveError from tiramisu.setting import owners, multitypes, expires_time from tiramisu.autolib import carry_out_calculation from tiramisu.i18n import _ from tiramisu.option import SymLinkOption class Values(object): """The `Config`'s root is indeed in charge of the `Option()`'s values, but the values are physicaly located here, in `Values`, wich is also responsible of a caching utility. """ __slots__ = ('context', '_p_', '__weakref__') def __init__(self, context, storage): """ Initializes the values's dict. :param context: the context is the home config's values """ self.context = weakref.ref(context) # the storage type is dictionary or sqlite3 self._p_ = storage def _getdefault(self, opt): """ actually retrieves the default value :param opt: the `option.Option()` object """ meta = self.context().cfgimpl_get_meta() if meta is not None: value = meta.cfgimpl_get_values()[opt] else: value = opt.impl_getdefault() if opt.impl_is_multi(): return copy(value) else: return value def _getvalue(self, opt, path, validate=True): """actually retrieves the value :param opt: the `option.Option()` object :returns: the option's value (or the default value if not set) """ if not self._p_.hasvalue(path): # if there is no value value = self._getdefault(opt) if opt.impl_is_multi(): value = Multi(value, self.context, opt, path, validate) else: # if there is a value value = self._p_.getvalue(path) if opt.impl_is_multi() and not isinstance(value, Multi): # load value so don't need to validate if is not a Multi value = Multi(value, self.context, opt, path, validate=False) return value def get_modified_values(self): return self._p_.get_modified_values() def __contains__(self, opt): """ implements the 'in' keyword syntax in order provide a pythonic way to kow if an option have a value :param opt: the `option.Option()` object """ path = self._get_opt_path(opt) return self._contains(path) def _contains(self, path): return self._p_.hasvalue(path) def __delitem__(self, opt): """overrides the builtins `del()` instructions""" self.reset(opt) def reset(self, opt, path=None): if path is None: path = self._get_opt_path(opt) if self._p_.hasvalue(path): setting = self.context().cfgimpl_get_settings() opt.impl_validate(opt.impl_getdefault(), self.context(), 'validator' in setting) self.context().cfgimpl_reset_cache() if (opt.impl_is_multi() and opt.impl_get_multitype() == multitypes.master): for slave in opt.impl_get_master_slaves(): self.reset(slave) self._p_.resetvalue(path) def _isempty(self, opt, value): "convenience method to know if an option is empty" empty = opt._empty if (not opt.impl_is_multi() and (value is None or value == empty)) or \ (opt.impl_is_multi() and (value == [] or None in value or empty in value)): return True return False def _getcallback_value(self, opt, index=None, max_len=None): """ retrieves a value for the options that have a callback :param opt: the `option.Option()` object :param index: if an option is multi, only calculates the nth value :type index: int :returns: a calculated value """ callback, callback_params = opt._callback if callback_params is None: callback_params = {} return carry_out_calculation(opt, config=self.context(), callback=callback, callback_params=callback_params, index=index, max_len=max_len) def __getitem__(self, opt): "enables us to use the pythonic dictionary-like access to values" return self.getitem(opt) def getitem(self, opt, path=None, validate=True, force_permissive=False, force_properties=None, validate_properties=True): if path is None: path = self._get_opt_path(opt) ntime = None setting = self.context().cfgimpl_get_settings() if 'cache' in setting and self._p_.hascache(path): if 'expire' in setting: ntime = int(time()) is_cached, value = self._p_.getcache(path, ntime) if is_cached: if opt.impl_is_multi() and not isinstance(value, Multi): #load value so don't need to validate if is not a Multi value = Multi(value, self.context, opt, path, validate=False) return value val = self._getitem(opt, path, validate, force_permissive, force_properties, validate_properties) if 'cache' in setting and validate and validate_properties and \ force_permissive is False and force_properties is None: if 'expire' in setting: if ntime is None: ntime = int(time()) ntime = ntime + expires_time self._p_.setcache(path, val, ntime) return val def _getitem(self, opt, path, validate, force_permissive, force_properties, validate_properties): # options with callbacks setting = self.context().cfgimpl_get_settings() is_frozen = 'frozen' in setting[opt] # For calculating properties, we need value (ie for mandatory value). # If value is calculating with a PropertiesOptionError's option # _getcallback_value raise a ConfigError. # We can not raise ConfigError if this option should raise # PropertiesOptionError too. So we get config_error and raise # ConfigError if properties did not raise. config_error = None force_permissives = None # if value is callback and is not set # or frozen with force_default_on_freeze if opt.impl_has_callback() and ( self._is_default_owner(path) or (is_frozen and 'force_default_on_freeze' in setting[opt])): lenmaster = None no_value_slave = False if (opt.impl_is_multi() and opt.impl_get_multitype() == multitypes.slave): masterp = self._get_opt_path(opt.impl_get_master_slaves()) mastervalue = getattr(self.context(), masterp) lenmaster = len(mastervalue) if lenmaster == 0: value = [] no_value_slave = True if not no_value_slave: try: value = self._getcallback_value(opt, max_len=lenmaster) except ConfigError as err: # cannot assign config_err directly in python 3.3 config_error = err value = None # should not raise PropertiesOptionError if option is # mandatory force_permissives = set(['mandatory']) else: if (opt.impl_is_multi() and opt.impl_get_multitype() == multitypes.slave): if not isinstance(value, list): value = [value for i in range(lenmaster)] if config_error is None: if opt.impl_is_multi(): value = Multi(value, self.context, opt, path, validate) # suppress value if already set self.reset(opt, path) # frozen and force default elif is_frozen and 'force_default_on_freeze' in setting[opt]: value = self._getdefault(opt) if opt.impl_is_multi(): value = Multi(value, self.context, opt, path, validate) else: value = self._getvalue(opt, path, validate) if config_error is None and validate: opt.impl_validate(value, self.context(), 'validator' in setting) if config_error is None and self._is_default_owner(path) and \ 'force_store_value' in setting[opt]: self.setitem(opt, value, path, is_write=False) if validate_properties: setting.validate_properties(opt, False, False, value=value, path=path, force_permissive=force_permissive, force_properties=force_properties, force_permissives=force_permissives) if config_error is not None: raise config_error return value def __setitem__(self, opt, value): raise ValueError('you should only set value with config') def setitem(self, opt, value, path, force_permissive=False, is_write=True): # is_write is, for example, used with "force_store_value" # user didn't change value, so not write # valid opt opt.impl_validate(value, self.context(), 'validator' in self.context().cfgimpl_get_settings()) if opt.impl_is_multi() and not isinstance(value, Multi): value = Multi(value, self.context, opt, path, setitem=True) self._setvalue(opt, path, value, force_permissive=force_permissive, is_write=is_write) def _setvalue(self, opt, path, value, force_permissive=False, force_properties=None, is_write=True, validate_properties=True): self.context().cfgimpl_reset_cache() if validate_properties: setting = self.context().cfgimpl_get_settings() setting.validate_properties(opt, False, is_write, value=value, path=path, force_permissive=force_permissive, force_properties=force_properties) owner = self.context().cfgimpl_get_settings().getowner() self._p_.setvalue(path, value, owner) def getowner(self, opt): """ retrieves the option's owner :param opt: the `option.Option` object :returns: a `setting.owners.Owner` object """ if isinstance(opt, SymLinkOption): opt = opt._opt path = self._get_opt_path(opt) return self._getowner(path) def _getowner(self, path): owner = self._p_.getowner(path, owners.default) meta = self.context().cfgimpl_get_meta() if owner is owners.default and meta is not None: owner = meta.cfgimpl_get_values()._getowner(path) return owner def setowner(self, opt, owner): """ sets a owner to an option :param opt: the `option.Option` object :param owner: a valid owner, that is a `setting.owners.Owner` object """ if not isinstance(owner, owners.Owner): raise TypeError(_("invalid generic owner {0}").format(str(owner))) path = self._get_opt_path(opt) self._setowner(path, owner) def _setowner(self, path, owner): if self._getowner(path) == owners.default: raise ConfigError(_('no value for {0} cannot change owner to {1}' '').format(path, owner)) self._p_.setowner(path, owner) def is_default_owner(self, opt): """ :param config: *must* be only the **parent** config (not the toplevel config) :return: boolean """ path = self._get_opt_path(opt) return self._is_default_owner(path) def _is_default_owner(self, path): return self._getowner(path) == owners.default def reset_cache(self, only_expired): """ clears the cache if necessary """ if only_expired: self._p_.reset_expired_cache(int(time())) else: self._p_.reset_all_cache() def _get_opt_path(self, opt): """ retrieve the option's path in the config :param opt: the `option.Option` object :returns: a string with points like "gc.dummy.my_option" """ return self.context().cfgimpl_get_description().impl_get_path_by_opt(opt) # information def set_information(self, key, value): """updates the information's attribute :param key: information's key (ex: "help", "doc" :param value: information's value (ex: "the help string") """ self._p_.set_information(key, value) def get_information(self, key, default=None): """retrieves one information's item :param key: the item string (ex: "help") """ try: return self._p_.get_information(key) except ValueError: if default is not None: return default else: raise ValueError(_("information's item" " not found: {0}").format(key)) def __getstate__(self): return {'_p_': self._p_} def _impl_setstate(self, storage): self._p_._storage = storage def __setstate__(self, states): self._p_ = states['_p_'] # ____________________________________________________________ # multi types class Multi(list): """multi options values container that support item notation for the values of multi options""" __slots__ = ('opt', 'path', 'context') def __init__(self, value, context, opt, path, validate=True, setitem=False): """ :param value: the Multi wraps a list value :param context: the home config that has the values :param opt: the option object that have this Multi value :param setitem: only if set a value """ self.opt = opt self.path = path if not isinstance(context, weakref.ReferenceType): raise ValueError('context must be a Weakref') self.context = context if not isinstance(value, list): value = [value] if validate and self.opt.impl_get_multitype() == multitypes.slave: value = self._valid_slave(value, setitem) elif validate and self.opt.impl_get_multitype() == multitypes.master: self._valid_master(value) super(Multi, self).__init__(value) def _valid_slave(self, value, setitem): #if slave, had values until master's one values = self.context().cfgimpl_get_values() masterp = self.context().cfgimpl_get_description().impl_get_path_by_opt( self.opt.impl_get_master_slaves()) mastervalue = getattr(self.context(), masterp) masterlen = len(mastervalue) valuelen = len(value) is_default_owner = not values._is_default_owner(self.path) or setitem if valuelen > masterlen or (valuelen < masterlen and is_default_owner): raise SlaveError(_("invalid len for the slave: {0}" " which has {1} as master").format( self.opt._name, masterp)) elif valuelen < masterlen: for num in range(0, masterlen - valuelen): if self.opt.impl_has_callback(): # if callback add a value, but this value will not change # anymore automaticly (because this value has owner) index = value.__len__() value.append(values._getcallback_value(self.opt, index=index)) else: value.append(self.opt.impl_getdefault_multi()) #else: same len so do nothing return value def _valid_master(self, value): masterlen = len(value) values = self.context().cfgimpl_get_values() for slave in self.opt._master_slaves: path = values._get_opt_path(slave) if not values._is_default_owner(path): value_slave = values._getvalue(slave, path) if len(value_slave) > masterlen: raise SlaveError(_("invalid len for the master: {0}" " which has {1} as slave with" " greater len").format( self.opt._name, slave._name)) elif len(value_slave) < masterlen: for num in range(0, masterlen - len(value_slave)): if slave.impl_has_callback(): # if callback add a value, but this value will not # change anymore automaticly (because this value # has owner) index = value_slave.__len__() value_slave.append( values._getcallback_value(slave, index=index), force=True) else: value_slave.append(slave.impl_getdefault_multi(), force=True) def __setitem__(self, index, value): self._validate(value, index) #assume not checking mandatory property super(Multi, self).__setitem__(index, value) self.context().cfgimpl_get_values()._setvalue(self.opt, self.path, self) def append(self, value, force=False): """the list value can be updated (appened) only if the option is a master """ if not force: if self.opt.impl_get_multitype() == multitypes.slave: raise SlaveError(_("cannot append a value on a multi option {0}" " which is a slave").format(self.opt._name)) elif self.opt.impl_get_multitype() == multitypes.master: values = self.context().cfgimpl_get_values() if value is None and self.opt.impl_has_callback(): value = values._getcallback_value(self.opt) #Force None il return a list if isinstance(value, list): value = None index = self.__len__() self._validate(value, index) super(Multi, self).append(value) self.context().cfgimpl_get_values()._setvalue(self.opt, self.path, self, validate_properties=not force) if not force and self.opt.impl_get_multitype() == multitypes.master: for slave in self.opt.impl_get_master_slaves(): path = values._get_opt_path(slave) if not values._is_default_owner(path): if slave.impl_has_callback(): dvalue = values._getcallback_value(slave, index=index) else: dvalue = slave.impl_getdefault_multi() old_value = values.getitem(slave, path, validate_properties=False) if len(old_value) < self.__len__(): values.getitem(slave, path, validate_properties=False).append( dvalue, force=True) else: values.getitem(slave, path, validate_properties=False)[ index] = dvalue def sort(self, cmp=None, key=None, reverse=False): if self.opt.impl_get_multitype() in [multitypes.slave, multitypes.master]: raise SlaveError(_("cannot sort multi option {0} if master or slave" "").format(self.opt._name)) if sys.version_info[0] >= 3: if cmp is not None: raise ValueError(_('cmp is not permitted in python v3 or greater')) super(Multi, self).sort(key=key, reverse=reverse) else: super(Multi, self).sort(cmp=cmp, key=key, reverse=reverse) self.context().cfgimpl_get_values()._setvalue(self.opt, self.path, self) def reverse(self): if self.opt.impl_get_multitype() in [multitypes.slave, multitypes.master]: raise SlaveError(_("cannot reverse multi option {0} if master or " "slave").format(self.opt._name)) super(Multi, self).reverse() self.context().cfgimpl_get_values()._setvalue(self.opt, self.path, self) def insert(self, index, obj): if self.opt.impl_get_multitype() in [multitypes.slave, multitypes.master]: raise SlaveError(_("cannot insert multi option {0} if master or " "slave").format(self.opt._name)) super(Multi, self).insert(index, obj) self.context().cfgimpl_get_values()._setvalue(self.opt, self.path, self) def extend(self, iterable): if self.opt.impl_get_multitype() in [multitypes.slave, multitypes.master]: raise SlaveError(_("cannot extend multi option {0} if master or " "slave").format(self.opt._name)) super(Multi, self).extend(iterable) self.context().cfgimpl_get_values()._setvalue(self.opt, self.path, self) def _validate(self, value, force_index): if value is not None: try: self.opt.impl_validate(value, context=self.context(), force_index=force_index) except ValueError as err: raise ValueError(_("invalid value {0} " "for option {1}: {2}" "").format(str(value), self.opt._name, err)) def pop(self, index, force=False): """the list value can be updated (poped) only if the option is a master :param index: remove item a index :type index: int :param force: force pop item (withoud check master/slave) :type force: boolean :returns: item at index """ if not force: if self.opt.impl_get_multitype() == multitypes.slave: raise SlaveError(_("cannot pop a value on a multi option {0}" " which is a slave").format(self.opt._name)) elif self.opt.impl_get_multitype() == multitypes.master: for slave in self.opt.impl_get_master_slaves(): values = self.context().cfgimpl_get_values() if not values.is_default_owner(slave): #get multi without valid properties values.getitem(slave, validate_properties=False ).pop(index, force=True) #set value without valid properties ret = super(Multi, self).pop(index) self.context().cfgimpl_get_values()._setvalue(self.opt, self.path, self, validate_properties=not force) return ret