# -*- coding: utf-8 -*- "sets the options of the configuration objects Config object itself" # Copyright (C) 2012-2018 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 .error import (RequirementError, PropertiesOptionError, ConstError, ConfigError, display_list) from .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) empty raise mandatory PropertiesOptionError if multi or master have empty value validator launch validator set by user in option (this property has no effect for internal validator) warnings display warnings during validation """ default_properties = ('cache', 'validator', 'warnings') """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', 'empty']) ro_remove = set(['permissive', 'hidden']) rw_append = set(['frozen', 'disabled', 'validator', 'hidden']) rw_remove = set(['permissive', 'everything_frozen', 'mandatory', 'empty']) FORBIDDEN_SET_PROPERTIES = frozenset(['force_store_value']) FORBIDDEN_SET_PERMISSIVES = frozenset(['force_default_on_freeze']) log = getLogger('tiramisu') #FIXME #import logging #logging.basicConfig(format='%(levelname)s:%(message)s', level=logging.DEBUG) debug = False static_set = frozenset() class ConfigBag(object): __slots__ = ('default', 'config', 'option', 'ori_option', 'properties', 'validate', 'setting_properties', 'force_permissive', 'force_unrestraint', 'display_warnings', 'trusted_cached_properties', 'fromconsistency', ) def __init__(self, config, **kwargs): self.default = {'force_permissive': False, 'force_unrestraint': False, 'display_warnings': True, 'trusted_cached_properties': True, } self.config = config self.fromconsistency = [] for key, value in kwargs.items(): if value != self.default.get(key): setattr(self, key, value) def __getattr__(self, key): if key in ['validate', 'validate_properties']: return not self.force_unrestraint if key == 'setting_properties': if self.force_unrestraint: return None self.setting_properties = self.config.cfgimpl_get_settings().get_context_properties() return self.setting_properties return self.default.get(key) def copy(self, filters='all'): kwargs = {} for key in self.__slots__: if filters == 'nooption' and (key.startswith('option') or \ key == 'properties'): continue if key == 'fromconsistency': kwargs['fromconsistency'] = copy(self.fromconsistency) elif key != 'default': value = getattr(self, key) if value != self.default.get(key): kwargs[key] = value return ConfigBag(**kwargs) # ____________________________________________________________ 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): raise ConstError(_("can't unbind {0}").format(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 def addowner(self, name): """ :param name: the name of the new owner """ setattr(owners, name, owners.Owner(name)) # ____________________________________________________________ # populate groups groups = GroupModule() """groups.default default group set when creating a new optiondescription""" groups.default = groups.DefaultGroupType('default') """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.master = groups.MasterGroupType('master') """ groups.family example of group, no special behavior with this group's type""" groups.family = groups.GroupType('family') # ____________________________________________________________ # populate owners with default attributes owners = OwnerModule() """default is the config owner after init time""" owners.default = owners.DefaultOwner('default') """user is the generic is the generic owner""" owners.user = owners.Owner('user') """forced special owner when value is forced""" owners.forced = owners.Owner('forced') """meta special owner when value comes from metaconfig""" owners.meta = owners.Owner('meta') forbidden_owners = (owners.default, owners.forced, owners.meta) # ____________________________________________________________ class Undefined(object): def __str__(self): return 'Undefined' __repr__ = __str__ undefined = Undefined() #____________________________________________________________ class Settings(object): "``config.Config()``'s configuration options settings" __slots__ = ('context', '_owner', '_p_', '_pp_', '__weakref__') def __init__(self, context, properties, permissives): """ 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_ = properties self._pp_ = permissives 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: # pragma: no cover raise ConfigError(_('the context does not exist anymore')) return context #____________________________________________________________ # get properties and permissive methods def get_context_properties(self): ntime = int(time()) if self._p_.hascache(None, None): is_cached, props = self._p_.getcache(None, ntime, None) else: is_cached = False if not is_cached or 'cache' not in props: meta = self._getcontext().cfgimpl_get_meta() if meta is None: props = self._p_.getproperties(None, default_properties) else: props = meta.cfgimpl_get_settings().get_context_properties() if 'cache' in props: if 'expire' in props: ntime = ntime + expires_time else: ntime = None self._p_.setcache(None, props, ntime, None) return props def getproperties(self, path, index, config_bag, apply_requires=True): """ """ opt = config_bag.option if opt.impl_is_symlinkoption(): opt = opt.impl_getopt() path = opt.impl_getpath(self._getcontext()) is_cached = False if apply_requires and config_bag.setting_properties is not None: if 'cache' in config_bag.setting_properties and \ 'expire' in config_bag.setting_properties: ntime = int(time()) else: ntime = None if 'cache' in config_bag.setting_properties and self._p_.hascache(path, index): is_cached, props = self._p_.getcache(path, ntime, index) if not is_cached: meta = self._getcontext().cfgimpl_get_meta() if meta is None: props = self._p_.getproperties(path, opt.impl_getproperties()) else: props = meta.cfgimpl_get_settings().getproperties(path, index, config_bag, apply_requires) if apply_requires: props |= self.apply_requires(path, opt.impl_getrequires(), index, False, config_bag, opt.impl_get_display_name()) props -= self.getpermissive(opt, path) if apply_requires and config_bag.setting_properties is not None and \ 'cache' in config_bag.setting_properties: if 'expire' in config_bag.setting_properties: ntime = ntime + expires_time self._p_.setcache(path, props, ntime, index) return props def get_context_permissive(self): return self.getpermissive(None, None) def getpermissive(self, opt, path): if opt and opt.impl_is_symlinkoption(): opt = opt.impl_getopt() path = opt.impl_getpath(self._getcontext()) meta = self._getcontext().cfgimpl_get_meta() if meta is not None: return meta.cfgimpl_get_settings().getpermissive(opt, path) return self._pp_.getpermissive(path) def apply_requires(self, path, current_requires, index, readable, config_bag, name): """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 will not add to 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 """ #current_requires = opt.impl_getrequires() # filters the callbacks if readable: calc_properties = {} else: calc_properties = set() if not current_requires: return calc_properties context = self._getcontext() all_properties = None for requires in current_requires: for require in requires: exps, action, inverse, transitive, same_action, operator = require breaked = False for option, expected in exps: reqpath = option.impl_getpath(context) #FIXME c'est un peut tard ! if reqpath == path or reqpath.startswith(path + '.'): raise RequirementError(_("malformed requirements " "imbrication detected for option:" " '{0}' with requirement on: " "'{1}'").format(path, reqpath)) idx = None is_indexed = False if option.impl_is_master_slaves('slave'): idx = index elif option.impl_is_multi(): is_indexed = True sconfig_bag = config_bag.copy('nooption') sconfig_bag.option = option sconfig_bag.force_permissive = True try: value = context.getattr(reqpath, idx, sconfig_bag) if is_indexed: value = value[index] except PropertiesOptionError as err: properties = err.proptype if not transitive: if all_properties is None: all_properties = [] for requires_ in current_requires: for require_ in requires_: all_properties.append(require_[1]) if not set(properties) - set(all_properties): continue if same_action and action not in properties: if len(properties) == 1: prop_msg = _('property') else: prop_msg = _('properties') raise RequirementError(_('cannot access to option "{0}" because ' 'required option "{1}" has {2} {3}' '').format(name, option.impl_get_display_name(), prop_msg, display_list(list(properties), add_quote=True))) # transitive action, add action if operator != 'and': if readable: for msg in self.apply_requires(err._path, err._requires, err._index, True, err._config_bag, err._name).values(): calc_properties.setdefault(action, []).extend(msg) else: calc_properties.add(action) breaked = True break else: if (not inverse and value in expected or inverse and value not in expected): if operator != 'and': if readable: if not inverse: msg = _('the value of "{0}" is {1}') else: msg = _('the value of "{0}" is not {1}') calc_properties.setdefault(action, []).append( msg.format(option.impl_get_display_name(), display_list(expected, 'or', add_quote=True))) else: calc_properties.add(action) breaked = True break elif operator == 'and': break else: if operator == 'and': calc_properties.add(action) continue # pragma: no cover if breaked: break return calc_properties #____________________________________________________________ # set methods def set_context_properties(self, properties): self.setproperties(None, properties, None) def setproperties(self, path, properties, config_bag): """save properties for specified path (never save properties if same has option properties) """ if self._getcontext().cfgimpl_get_meta() is not None: raise ConfigError(_('cannot change property with metaconfig')) if path is not None and config_bag.option.impl_getrequires() is not None: not_allowed_props = properties & getattr(config_bag.option, '_calc_properties', static_set) if not_allowed_props: raise ValueError(_('cannot set property {} for option "{}" this property is calculated' '').format(display_list(list(not_allowed_props), add_quote=True), config_bag.option.impl_get_display_name())) if config_bag is None: opt = None else: opt = config_bag.option if opt and opt.impl_is_symlinkoption(): raise TypeError(_("can't assign property to the symlinkoption \"{}\"" "").format(opt.impl_get_display_name())) if 'force_default_on_freeze' in properties and \ 'frozen' not in properties and \ opt.impl_is_master_slaves('master'): raise ConfigError(_('a master ({0}) cannot have ' '"force_default_on_freeze" property without "frozen"' '').format(opt.impl_get_display_name())) self._p_.setproperties(path, properties) #values too because of slave values could have a PropertiesOptionError has value self._getcontext().cfgimpl_reset_cache(opt, path, config_bag) def set_context_permissive(self, permissive): self.setpermissive(None, None, None, permissive) def setpermissive(self, opt, path, config_bag, permissives): """ 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 self._getcontext().cfgimpl_get_meta() is not None: raise ConfigError(_('cannot change permissive with metaconfig')) if not isinstance(permissives, frozenset): raise TypeError(_('permissive must be a frozenset')) if opt and opt.impl_is_symlinkoption(): raise TypeError(_("can't assign permissive to the symlinkoption \"{}\"" "").format(opt.impl_get_display_name())) forbidden_permissives = FORBIDDEN_SET_PERMISSIVES & permissives if forbidden_permissives: raise ConfigError(_('cannot add those permissives: {0}').format( ' '.join(forbidden_permissives))) self._pp_.setpermissive(path, permissives) self._getcontext().cfgimpl_reset_cache(opt, path, config_bag) #____________________________________________________________ # reset methods def reset(self, opt, path, config_bag, all_properties=False): if self._getcontext().cfgimpl_get_meta() is not None: raise ConfigError(_('cannot change property with metaconfig')) if opt and opt.impl_is_symlinkoption(): raise TypeError(_("can't reset properties to the symlinkoption \"{}\"" "").format(opt.impl_get_display_name())) 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 = opt.impl_getpath(self._getcontext()) self._p_.delproperties(path) self._getcontext().cfgimpl_reset_cache(opt, path, config_bag) #____________________________________________________________ # validate properties def validate_properties(self, path, index, config_bag): """ validation upon the properties related to `opt` :param opt: an option or an option description object :param force_permissive: behaves as if the permissive property was present """ opt = config_bag.option # calc properties self_properties = config_bag.properties if self_properties is None: self_properties = self.getproperties(path, index, config_bag) config_bag.properties = self_properties properties = self_properties & config_bag.setting_properties - {'frozen', 'mandatory', 'empty'} # remove permissive properties if (config_bag.force_permissive is True or 'permissive' in config_bag.setting_properties) and properties: # remove global permissive if need properties -= self.get_context_permissive() # at this point an option should not remain in properties if properties != frozenset(): raise PropertiesOptionError(path, index, config_bag, properties, self) def validate_mandatory(self, path, index, value, config_bag): values = self._getcontext().cfgimpl_get_values() opt = config_bag.option is_mandatory = False if config_bag.setting_properties and 'mandatory' in config_bag.setting_properties: if (config_bag.force_permissive is True or 'permissive' in config_bag.setting_properties) and \ 'mandatory' in self.get_context_permissive(): pass elif 'mandatory' in config_bag.properties and values.isempty(opt, value, index=index): is_mandatory = True if 'empty' in config_bag.properties and values.isempty(opt, value, force_allow_empty_list=True, index=index): is_mandatory = True if is_mandatory: raise PropertiesOptionError(path, index, config_bag, ['mandatory'], self) def validate_frozen(self, path, index, config_bag): if config_bag.setting_properties and \ ('everything_frozen' in config_bag.setting_properties or 'frozen' in config_bag.properties) and \ not ((config_bag.force_permissive is True or 'permissive' in config_bag.setting_properties) and 'frozen' in self.get_context_permissive()): raise PropertiesOptionError(path, index, config_bag, ['frozen'], self) return False #____________________________________________________________ # read only/read write def _read(self, remove, append): props = self._p_.getproperties(None, default_properties) modified = False if remove & props: props = props - remove modified = True if append & props != append: props = props | append modified = True if modified: self.set_context_properties(frozenset(props)) 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) #____________________________________________________________ # default owner methods def setowner(self, owner): ":param owner: sets the default value for owner at the Config level" self._owner = owner def getowner(self): return self._owner