# -*- coding: utf-8 -*- "sets the options of the configuration objects Config object itself" # Copyright (C) 2012-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 Lesser General Public License as published by the # Free Software Foundation, either version 3 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 Lesser General Public License for more # details. # # You should have received a copy of the GNU Lesser General Public License # along with this program. If not, see . # ____________________________________________________________ from time import time from copy import copy from logging import getLogger import weakref from tiramisu.error import (RequirementError, PropertiesOptionError, ConstError, ConfigError) from tiramisu.i18n import _ "Default encoding for display a Config if raise UnicodeEncodeError" default_encoding = 'utf-8' """If cache and expire is enable, time before cache is expired. This delay start first time value/setting is set in cache, even if user access several time to value/setting """ expires_time = 5 """List of default properties (you can add new one if needed). For common properties and personalise properties, if a propery is set for an Option and for the Config together, Setting raise a PropertiesOptionError * Common properties: hidden option with this property can only get value in read only mode. This option is not available in read write mode. disabled option with this property cannot be set/get frozen cannot set value for option with this properties if 'frozen' is set in config mandatory should set value for option with this properties if 'mandatory' is set in config * Special property: permissive option with 'permissive' cannot raise PropertiesOptionError for properties set in permissive config with 'permissive', whole option in this config cannot raise PropertiesOptionError for properties set in permissive * Special Config properties: cache if set, enable cache settings and values expire if set, settings and values in cache expire after ``expires_time`` everything_frozen whole option in config are frozen (even if option have not frozen property) validator launch validator set by user in option (this property has no effect for internal validator) """ default_properties = ('cache', 'expire', 'validator') """Config can be in two defaut mode: read_only you can get all variables not disabled but you cannot set any variables if a value has a callback without any value, callback is launch and value of this variable can change you cannot access to mandatory variable without values read_write you can get all variables not disabled and not hidden you can set all variables not frozen """ ro_append = set(['frozen', 'disabled', 'validator', 'everything_frozen', 'mandatory']) ro_remove = set(['permissive', 'hidden']) rw_append = set(['frozen', 'disabled', 'validator', 'hidden']) rw_remove = set(['permissive', 'everything_frozen', 'mandatory']) log = getLogger('tiramisu') #FIXME #import logging #logging.basicConfig(format='%(levelname)s:%(message)s', level=logging.DEBUG) # ____________________________________________________________ class _NameSpace(object): """convenient class that emulates a module and builds constants (that is, unique names) when attribute is added, we cannot delete it """ def __setattr__(self, name, value): if name in self.__dict__: raise ConstError(_("can't rebind {0}").format(name)) self.__dict__[name] = value def __delattr__(self, name): if name in self.__dict__: raise ConstError(_("can't unbind {0}").format(name)) raise ValueError(name) class GroupModule(_NameSpace): "emulates a module to manage unique group (OptionDescription) names" class GroupType(str): """allowed normal group (OptionDescription) names *normal* means : groups that are not master """ pass class DefaultGroupType(GroupType): """groups that are default (typically 'default')""" pass class MasterGroupType(GroupType): """allowed normal group (OptionDescription) names *master* means : groups that have the 'master' attribute set """ pass class OwnerModule(_NameSpace): """emulates a module to manage unique owner names. owners are living in `Config._cfgimpl_value_owners` """ class Owner(str): """allowed owner names """ pass class DefaultOwner(Owner): """groups that are default (typically 'default')""" pass class MultiTypeModule(_NameSpace): "namespace for the master/slaves" class MultiType(str): pass class DefaultMultiType(MultiType): pass class MasterMultiType(MultiType): pass class SlaveMultiType(MultiType): pass # ____________________________________________________________ def populate_groups(): """populates the available groups in the appropriate namespaces groups.default default group set when creating a new optiondescription groups.master master group is a special optiondescription, all suboptions should be multi option and all values should have same length, to find master's option, the optiondescription's name should be same than de master's option groups.family example of group, no special behavior with this group's type """ groups.default = groups.DefaultGroupType('default') groups.master = groups.MasterGroupType('master') groups.family = groups.GroupType('family') def populate_owners(): """populates the available owners in the appropriate namespaces default is the config owner after init time user is the generic is the generic owner """ setattr(owners, 'default', owners.DefaultOwner('default')) setattr(owners, 'user', owners.Owner('user')) def addowner(name): """ :param name: the name of the new owner """ setattr(owners, name, owners.Owner(name)) setattr(owners, 'addowner', addowner) # ____________________________________________________________ # populate groups and owners with default attributes groups = GroupModule() populate_groups() owners = OwnerModule() populate_owners() # ____________________________________________________________ class Undefined(): pass undefined = Undefined() # ____________________________________________________________ class Property(object): "a property is responsible of the option's value access rules" __slots__ = ('_setting', '_properties', '_opt', '_path') def __init__(self, setting, prop, opt=None, path=None): self._opt = opt self._path = path self._setting = setting self._properties = prop def append(self, propname): """Appends a property named propname :param propname: a predefined or user defined property name :type propname: string """ if self._opt is not None and self._opt.impl_getrequires() is not None \ and propname in self._opt._calc_properties: raise ValueError(_('cannot append {0} property for option {1}: ' 'this property is calculated').format( propname, self._opt.impl_getname())) self._properties.add(propname) self._setting._setproperties(self._properties, self._opt, self._path) def remove(self, propname): """Removes a property named propname :param propname: a predefined or user defined property name :type propname: string """ if propname in self._properties: self._properties.remove(propname) self._setting._setproperties(self._properties, self._opt, self._path) def extend(self, propnames): """Extends properties to the existing properties :param propnames: an iterable made of property names :type propnames: iterable of string """ for propname in propnames: self.append(propname) def reset(self): """resets the properties (does not **clear** the properties, default properties are still present) """ self._setting.reset(_path=self._path) def __contains__(self, propname): return propname in self._properties def __repr__(self): return str(list(self._properties)) #____________________________________________________________ class Settings(object): "``config.Config()``'s configuration options settings" __slots__ = ('context', '_owner', '_p_', '__weakref__') def __init__(self, context, storage): """ initializer :param context: the root config :param storage: the storage type - dictionary -> in memory - sqlite3 -> persistent """ # generic owner self._owner = owners.user self.context = weakref.ref(context) self._p_ = storage def _getcontext(self): """context could be None, we need to test it context is None only if all reference to `Config` object is deleted (for example we delete a `Config` and we manipulate a reference to old `SubConfig`, `Values`, `Multi` or `Settings`) """ context = self.context() if context is None: raise ConfigError(_('the context does not exist anymore')) return context #____________________________________________________________ # properties methods def __contains__(self, propname): "enables the pythonic 'in' syntaxic sugar" return propname in self._getproperties() def __repr__(self): return str(list(self._getproperties())) def __getitem__(self, opt): path = self._get_path_by_opt(opt) return self._getitem(opt, path) def _getitem(self, opt, path): return Property(self, self._getproperties(opt, path), opt, path) def __setitem__(self, opt, value): raise ValueError('you should only append/remove properties') def reset(self, opt=None, _path=None, all_properties=False): if all_properties and (_path or opt): raise ValueError(_('opt and all_properties must not be set ' 'together in reset')) if all_properties: self._p_.reset_all_properties() else: if opt is not None and _path is None: _path = self._get_path_by_opt(opt) self._p_.reset_properties(_path) self._getcontext().cfgimpl_reset_cache() def _getproperties(self, opt=None, path=None, _is_apply_req=True): """ be careful, _is_apply_req doesn't copy properties """ if opt is None: props = copy(self._p_.getproperties(path, default_properties)) else: if path is None: raise ValueError(_('if opt is not None, path should not be' ' None in _getproperties')) ntime = None if 'cache' in self and self._p_.hascache(path): if 'expire' in self: ntime = int(time()) is_cached, props = self._p_.getcache(path, ntime) if is_cached: return copy(props) props = self._p_.getproperties(path, opt._properties) if _is_apply_req: props = copy(props) props |= self.apply_requires(opt, path) if 'cache' in self: if 'expire' in self: if ntime is None: ntime = int(time()) ntime = ntime + expires_time self._p_.setcache(path, copy(props), ntime) return props def append(self, propname): "puts property propname in the Config's properties attribute" props = self._p_.getproperties(None, default_properties) props.add(propname) self._setproperties(props, None, None) def remove(self, propname): "deletes property propname in the Config's properties attribute" props = self._p_.getproperties(None, default_properties) if propname in props: props.remove(propname) self._setproperties(props, None, None) def extend(self, propnames): for propname in propnames: self.append(propname) def _setproperties(self, properties, opt, path): """save properties for specified opt (never save properties if same has option properties) """ if opt is None: self._p_.setproperties(None, properties) else: #if opt._calc_properties is not None: # properties -= opt._calc_properties #if set(opt._properties) == properties: # self._p_.reset_properties(path) #else: # self._p_.setproperties(path, properties) self._p_.setproperties(path, properties) self._getcontext().cfgimpl_reset_cache() #____________________________________________________________ def validate_properties(self, opt_or_descr, is_descr, is_write, path, value=None, force_permissive=False, force_properties=None, force_permissives=None): """ validation upon the properties related to `opt_or_descr` :param opt_or_descr: an option or an option description object :param force_permissive: behaves as if the permissive property was present :param force_properties: set() with properties that is force to add in global properties :param force_permissives: set() with permissives that is force to add in global permissives :param is_descr: we have to know if we are in an option description, just because the mandatory property doesn't exist here :param is_write: in the validation process, an option is to be modified, the behavior can be different (typically with the `frozen` property) """ # opt properties properties = self._getproperties(opt_or_descr, path) self_properties = self._getproperties() # remove opt permissive # permissive affect option's permission with or without permissive # global property properties -= self._p_.getpermissive(path) # remove global permissive if need if force_permissive is True or 'permissive' in self_properties: properties -= self._p_.getpermissive() if force_permissives is not None: properties -= force_permissives # global properties if force_properties is not None: self_properties.update(force_properties) # calc properties properties &= self_properties # mandatory and frozen are special properties if is_descr: properties -= frozenset(('mandatory', 'frozen')) else: if 'mandatory' in properties and \ not self._getcontext().cfgimpl_get_values()._isempty( opt_or_descr, value): properties.remove('mandatory') if is_write and 'everything_frozen' in self_properties: properties.add('frozen') elif 'frozen' in properties and not is_write: properties.remove('frozen') # at this point an option should not remain in properties if properties != frozenset(): props = list(properties) if 'frozen' in properties: raise PropertiesOptionError(_('cannot change the value for ' 'option {0} this option is' ' frozen').format( opt_or_descr.impl_getname()), props) else: raise PropertiesOptionError(_("trying to access to an option " "named: {0} with properties {1}" "").format(opt_or_descr.impl_getname(), str(props)), props) def setpermissive(self, permissive, opt=None, path=None): """ enables us to put the permissives in the storage :param path: the option's path :param type: str :param opt: if an option object is set, the path is extracted. it is better (faster) to set the path parameter instead of passing a :class:`tiramisu.option.Option()` object. """ if opt is not None and path is None: path = self._get_path_by_opt(opt) if not isinstance(permissive, tuple): raise TypeError(_('permissive must be a tuple')) self._p_.setpermissive(path, permissive) #____________________________________________________________ def setowner(self, owner): ":param owner: sets the default value for owner at the Config level" if not isinstance(owner, owners.Owner): raise TypeError(_("invalid generic owner {0}").format(str(owner))) self._owner = owner def getowner(self): return self._owner #____________________________________________________________ def _read(self, remove, append): for prop in remove: self.remove(prop) for prop in append: self.append(prop) def read_only(self): "convenience method to freeze, hide and disable" self._read(ro_remove, ro_append) def read_write(self): "convenience method to freeze, hide and disable" self._read(rw_remove, rw_append) def reset_cache(self, only_expired): """reset all settings in cache :param only_expired: if True reset only expired cached values :type only_expired: boolean """ if only_expired: self._p_.reset_expired_cache(int(time())) else: self._p_.reset_all_cache() def apply_requires(self, opt, path): """carries out the jit (just in time) requirements between options a requirement is a tuple of this form that comes from the option's requirements validation:: (option, expected, action, inverse, transitive, same_action) let's have a look at all the tuple's items: - **option** is the target option's - **expected** is the target option's value that is going to trigger an action - **action** is the (property) action to be accomplished if the target option happens to have the expected value - if **inverse** is `True` and if the target option's value does not apply, then the property action must be removed from the option's properties list (wich means that the property is inverted) - **transitive**: but what happens if the target option cannot be accessed ? We don't kown the target option's value. Actually if some property in the target option is not present in the permissive, the target option's value cannot be accessed. In this case, the **action** have to be applied to the option. (the **action** property is then added to the option). - **same_action**: actually, if **same_action** is `True`, the transitivity is not accomplished. The transitivity is accomplished only if the target option **has the same property** that the demanded action. If the target option's value is not accessible because of another reason, because of a property of another type, then an exception :exc:`~error.RequirementError` is raised. And at last, if no target option matches the expected values, the action must be removed from the option's properties list. :param opt: the option on wich the requirement occurs :type opt: `option.Option()` :param path: the option's path in the config :type path: str """ if opt._requires is None: return frozenset() # filters the callbacks calc_properties = set() context = self._getcontext() for requires in opt._requires: for require in requires: option, expected, action, inverse, \ transitive, same_action = require reqpath = self._get_path_by_opt(option) if reqpath == path or reqpath.startswith(path + '.'): raise RequirementError(_("malformed requirements " "imbrication detected for option:" " '{0}' with requirement on: " "'{1}'").format(path, reqpath)) try: value = context.getattr(reqpath, force_permissive=True) except PropertiesOptionError as err: if not transitive: continue properties = err.proptype if same_action and action not in properties: raise RequirementError(_("option '{0}' has " "requirement's property " "error: " "{1} {2}").format(opt._name, reqpath, properties)) # transitive action, force expected value = expected[0] inverse = False if (not inverse and value in expected or inverse and value not in expected): calc_properties.add(action) # the calculation cannot be carried out break return calc_properties def _get_path_by_opt(self, opt): """just a wrapper to get path in optiondescription's cache :param opt: `Option`'s object :returns: path """ return self._getcontext().cfgimpl_get_description().impl_get_path_by_opt(opt) def get_modified_properties(self): return self._p_.get_modified_properties() def get_modified_permissives(self): return self._p_.get_modified_permissives() def get_with_property(self, propname): opts, paths = self._getcontext().cfgimpl_get_description( )._cache_paths for index in range(0, len(paths)): opt = opts[index] path = paths[index] if propname in self._getproperties(opt, path, False): yield (opt, path) def __getstate__(self): return {'_p_': self._p_, '_owner': str(self._owner)} def _impl_setstate(self, storage): self._p_._storage = storage def __setstate__(self, states): self._p_ = states['_p_'] try: self._owner = getattr(owners, states['_owner']) except AttributeError: owners.addowner(states['_owner']) self._owner = getattr(owners, states['_owner'])