# -*- 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 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', 'force_default_on_freeze']) log = getLogger('tiramisu') #FIXME #import logging #logging.basicConfig(format='%(levelname)s:%(message)s', level=logging.DEBUG) static_set = frozenset() class OptionBag: __slots__ = ('option', # current option 'path', 'index', 'config_bag', 'ori_option', # original option (for example useful for symlinkoption) 'properties', # properties of current option 'apply_requires', # apply requires or not for this option 'fromconsistency' # history for consistency ) def __init__(self): self.option = None self.fromconsistency = [] def set_option(self, option, path, index, config_bag): if path is None: path = option.impl_getpath() self.path = path self.index = index self.option = option self.config_bag = config_bag def __getattr__(self, key): if key == 'properties': settings = self.config_bag.context.cfgimpl_get_settings() self.properties = settings.getproperties(self, apply_requires=self.apply_requires) return self.properties elif key == 'ori_option': return self.option elif key == 'apply_requires': return True raise KeyError('unknown key {} for OptionBag'.format(key)) # pragma: no cover def __delattr__(self, key): if key in ['properties', 'permissives']: return raise KeyError('unknown key {} for ConfigBag'.format(key)) # pragma: no cover def copy(self): kwargs = {} option_bag = OptionBag() for key in self.__slots__: if key == 'properties' and self.config_bag is undefined: continue setattr(option_bag, key, getattr(self, key)) return option_bag class ConfigBag: __slots__ = ('context', # link to the current context 'properties', # properties for current context 'permissives', # permissives for current context ) def __init__(self, context, **kwargs): self.context = context for key, value in kwargs.items(): setattr(self, key, value) def __getattr__(self, key): if key == 'properties': self.properties = self.context.cfgimpl_get_settings().get_context_properties() return self.properties if key == 'permissives': self.permissives = self.context.cfgimpl_get_settings().get_context_permissives() return self.permissives raise KeyError('unknown key {} for ConfigBag'.format(key)) # pragma: no cover def remove_warnings(self): self.properties = frozenset(self.properties - {'warnings'}) def remove_validation(self): self.properties = frozenset(self.properties - {'validator'}) def set_permissive(self): self.properties = frozenset(self.properties | {'permissive'}) def __delattr__(self, key): if key in ['properties', 'permissives']: try: super().__delattr__(key) except AttributeError: pass return raise KeyError('unknown key {} for ConfigBag'.format(key)) # pragma: no cover #def __setattr__(self, key, value): # super().__setattr__(key, value) def copy(self): kwargs = {} for key in self.__slots__: if key in ['properties', 'permissives'] and \ not hasattr(self.context, '_impl_settings'): continue kwargs[key] = getattr(self, key) 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') forbidden_owners = (owners.default, owners.forced) # ____________________________________________________________ class Undefined(object): def __str__(self): return 'Undefined' __repr__ = __str__ undefined = Undefined() # ____________________________________________________________ class Settings(object): "``config.Config()``'s configuration options settings" __slots__ = ('_p_', '_pp_', '__weakref__') def __init__(self, properties, permissives): """ initializer :param context: the root config :param storage: the storage type - dictionary -> in memory - sqlite3 -> persistent """ # generic owner self._p_ = properties self._pp_ = permissives # ____________________________________________________________ # get properties and permissive methods def get_context_properties(self): is_cached, props = self._p_.getcache(None, None, None, {}, {}, 'context_props') if not is_cached: props = self._p_.getproperties(None, default_properties) self._p_.setcache(None, None, props, {}, props) return props def getproperties(self, option_bag, apply_requires=True): """ """ opt = option_bag.option config_bag = option_bag.config_bag path = option_bag.path index = option_bag.index if opt.impl_is_symlinkoption(): opt = opt.impl_getopt() path = opt.impl_getpath() if apply_requires: props = config_bag.properties is_cached, props = self._p_.getcache(path, expires_time, index, props, {}, 'self_props') else: is_cached = False if not is_cached: props = self._p_.getproperties(path, opt.impl_getproperties()) if apply_requires: props |= self.apply_requires(option_bag, False) props -= self.getpermissives(opt, path) if apply_requires: self._p_.setcache(path, index, props, props, config_bag.properties) return props def get_context_permissives(self): return self.getpermissives(None, None) def getpermissives(self, opt, path): if opt and opt.impl_is_symlinkoption(): opt = opt.impl_getopt() path = opt.impl_getpath() return self._pp_.getpermissives(path) def apply_requires(self, option_bag, readable): """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 = option_bag.option.impl_getrequires() # filters the callbacks if readable: calc_properties = {} else: calc_properties = set() if not current_requires: return calc_properties context = option_bag.config_bag.context 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: if option.issubdyn(): option = option.to_dynoption(option_bag.option._rootpath, option_bag.option.impl_getsuffix()) reqpath = option.impl_getpath() #FIXME too later! if reqpath.startswith(option_bag.path + '.'): raise RequirementError(_("malformed requirements " "imbrication detected for option:" " '{0}' with requirement on: " "'{1}'").format(option_bag.path, reqpath)) idx = None is_indexed = False if option.impl_is_master_slaves('slave'): idx = option_bag.index elif option.impl_is_multi() and option_bag.index is not None: is_indexed = True config_bag = option_bag.config_bag.copy() config_bag.set_permissive() soption_bag = OptionBag() soption_bag.set_option(option, reqpath, idx, config_bag) if option_bag.option == option: soption_bag.config_bag.properties = frozenset() soption_bag.config_bag.remove_validation() soption_bag.apply_requires = False try: value = context.getattr(reqpath, soption_bag) if is_indexed: value = value[option_bag.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(option_bag.option.impl_get_display_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._option_bag, True).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 if breaked: break return calc_properties #____________________________________________________________ # set methods def set_context_properties(self, properties, context): self.setproperties(None, properties, None, context) def setproperties(self, path, properties, option_bag, context): """save properties for specified path (never save properties if same has option properties) """ # should have index !!! if path is not None and option_bag.option.impl_getrequires() is not None: not_allowed_props = properties & \ getattr(option_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), option_bag.option.impl_get_display_name())) if option_bag is None: opt = None else: opt = option_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 context.cfgimpl_reset_cache(option_bag) if option_bag is not None: del option_bag.properties def set_context_permissives(self, permissives): self.setpermissives(None, permissives) def setpermissives(self, option_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 not isinstance(permissives, frozenset): raise TypeError(_('permissive must be a frozenset')) if option_bag is not None: opt = option_bag.option if opt and opt.impl_is_symlinkoption(): raise TypeError(_("can't assign permissive to the symlinkoption \"{}\"" "").format(opt.impl_get_display_name())) path = option_bag.path else: path = None forbidden_permissives = FORBIDDEN_SET_PERMISSIVES & permissives if forbidden_permissives: raise ConfigError(_('cannot add those permissives: {0}').format( ' '.join(forbidden_permissives))) self._pp_.setpermissives(path, permissives) if option_bag is not None: option_bag.config_bag.context.cfgimpl_reset_cache(option_bag) #____________________________________________________________ # reset methods def reset(self, option_bag, context): if option_bag is None: opt = None path = None else: opt = option_bag.option assert not opt.impl_is_symlinkoption(), _("can't reset properties to " "the symlinkoption \"{}\"" "").format(opt.impl_get_display_name()) path = option_bag.path self._p_.delproperties(path) context.cfgimpl_reset_cache(option_bag) def reset_permissives(self, option_bag, context): if option_bag is None: opt = None path = None else: opt = option_bag.option assert not opt.impl_is_symlinkoption(), _("can't reset permissives to " "the symlinkoption \"{}\"" "").format(opt.impl_get_display_name()) path = option_bag.path self._pp_.delpermissive(path) context.cfgimpl_reset_cache(option_bag) #____________________________________________________________ # validate properties def calc_raises_properties(self, option_properties, config_properties, config_permissives): properties = option_properties & config_properties - {'frozen', 'mandatory', 'empty'} # remove global permissive properties if properties and ('permissive' in config_properties): properties -= config_permissives # at this point an option should not remain in properties return properties def validate_properties(self, option_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 """ config_bag = option_bag.config_bag if not config_bag.properties: return properties = self.calc_raises_properties(option_bag.properties, config_bag.properties, config_bag.permissives) if properties != frozenset(): raise PropertiesOptionError(option_bag, properties, self) def validate_mandatory(self, value, option_bag): if 'mandatory' in option_bag.config_bag.properties: values = option_bag.config_bag.context.cfgimpl_get_values() is_mandatory = False if ('permissive' in option_bag.config_bag.properties) and \ 'mandatory' in option_bag.config_bag.permissives: pass elif 'mandatory' in option_bag.properties and values.isempty(option_bag.option, value, index=option_bag.index): is_mandatory = True if 'empty' in option_bag.properties and values.isempty(option_bag.option, value, force_allow_empty_list=True, index=option_bag.index): is_mandatory = True if is_mandatory: raise PropertiesOptionError(option_bag, ['mandatory'], self) def validate_frozen(self, option_bag): if option_bag.config_bag.properties and \ ('everything_frozen' in option_bag.config_bag.properties or 'frozen' in option_bag.properties) and \ not (('permissive' in option_bag.config_bag.properties) and 'frozen' in option_bag.config_bag.permissives): raise PropertiesOptionError(option_bag, ['frozen'], self) return False #____________________________________________________________ # read only/read write def _read(self, remove, append, context): 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), context) def read_only(self, context): "convenience method to freeze, hide and disable" self._read(ro_remove, ro_append, context) def read_write(self, context): "convenience method to freeze, hide and disable" self._read(rw_remove, rw_append, context)