tiramisu/tiramisu/setting.py

820 lines
32 KiB
Python

# -*- coding: utf-8 -*-
"sets the options of the configuration objects Config object itself"
# Copyright (C) 2012-2019 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 <http://www.gnu.org/licenses/>.
# ____________________________________________________________
from .error import (RequirementError, PropertiesOptionError,
ConstError, ConfigError, display_list)
from .i18n import _
"""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
"""
EXPIRATION_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 ``expiration_time``
everything_frozen
whole option in config are frozen (even if option have not frozen
property)
empty
raise mandatory PropertiesOptionError if multi or leader have empty value
validator
launch validator set by user in option (this property has no effect
for internal validator)
warnings
display warnings during validation
demoting_error_warning
all value errors are convert to warning (ValueErrorWarning)
"""
DEFAULT_PROPERTIES = frozenset(['cache', 'validator', 'warnings'])
SPECIAL_PROPERTIES = {'frozen', 'mandatory', 'empty', 'force_store_value'}
"""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 = frozenset(['frozen', 'disabled', 'validator', 'everything_frozen',
'mandatory', 'empty', 'force_store_value'])
RO_REMOVE = frozenset(['permissive', 'hidden'])
RW_APPEND = frozenset(['frozen', 'disabled', 'validator', 'hidden',
'force_store_value'])
RW_REMOVE = frozenset(['permissive', 'everything_frozen', 'mandatory',
'empty'])
FORBIDDEN_SET_PROPERTIES = frozenset(['force_store_value'])
FORBIDDEN_SET_PERMISSIVES = frozenset(['force_default_on_freeze',
'force_metaconfig_on_freeze',
'force_store_value'])
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']:
try:
super().__delattr__(key)
except AttributeError:
pass
return
raise KeyError('unknown key {} for ConfigBag'.format(key)) # pragma: no cover
def copy(self):
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
'true_properties', # properties for current context
'permissives', # permissives for current context
'expiration_time' # EXPIRATION_TIME
)
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':
settings = self.context.cfgimpl_get_settings()
self.properties = settings.get_context_properties()
return self.properties
if key == 'permissives':
settings = self.context.cfgimpl_get_settings()
self.permissives = settings.get_context_permissives()
return self.permissives
if key == 'true_properties':
return self.properties
if key == 'expiration_time':
self.expiration_time = EXPIRATION_TIME
return self.expiration_time
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 unrestraint(self):
self.true_properties = self.properties
self.properties = frozenset(['cache'])
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', 'true_properties'] and \
not hasattr(self.context, '_impl_settings'):
# not for GroupConfig
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 leader
"""
pass
class DefaultGroupType(GroupType):
"""groups that are default (typically 'default')"""
pass
class LeadershipGroupType(GroupType):
"""allowed normal group (OptionDescription) names
*leadership* means : groups that have the 'leadership' 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.leadership
leadership group is a special optiondescription, all suboptions should
be multi option and all values should have same length, to find
leader's option, the optiondescription's name should be same than de
leader's option"""
groups.leadership = groups.LeadershipGroupType('leadership')
""" 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): # pragma: no cover
return 'Undefined'
__repr__ = __str__
undefined = Undefined()
# ____________________________________________________________
class Settings(object):
"``config.Config()``'s configuration options settings"
__slots__ = ('_p_',
'_pp_',
'__weakref__',
'ro_append',
'ro_remove',
'rw_append',
'rw_remove',
'default_properties')
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
self.default_properties = DEFAULT_PROPERTIES
self.ro_append = RO_APPEND
self.ro_remove = RO_REMOVE
self.rw_append = RW_APPEND
self.rw_remove = RW_REMOVE
# ____________________________________________________________
# 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,
self.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,
config_bag.expiration_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_follower():
idx = option_bag.index
if idx is None:
continue
elif option.impl_is_leader() and option_bag.index is None:
continue
elif option.impl_is_multi() and option_bag.index is not None:
is_indexed = True
config_bag = option_bag.config_bag.copy()
soption_bag = OptionBag()
soption_bag.set_option(option,
reqpath,
idx,
config_bag)
if option_bag.option == option:
soption_bag.config_bag.unrestraint()
soption_bag.config_bag.remove_validation()
soption_bag.apply_requires = False
else:
soption_bag.config_bag.properties = soption_bag.config_bag.true_properties
soption_bag.config_bag.set_permissive()
try:
value = context.getattr(reqpath,
soption_bag)
except PropertiesOptionError as err:
properties = err.proptype
# if not transitive, properties must be verify in current requires
# otherwise if same_action, property must be in properties
# otherwise add property in returned properties (if operator is 'and')
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 is_indexed:
value = value[option_bag.index]
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._p_.setproperties(None,
properties)
context.cfgimpl_reset_cache(None)
def setproperties(self,
path,
properties,
option_bag,
context):
"""save properties for specified path
(never save properties if same has option properties)
"""
# should have index !!!
opt = option_bag.option
if opt.impl_getrequires() is not None:
not_allowed_props = properties & \
getattr(opt, '_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),
opt.impl_get_display_name()))
if 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 or 'force_metaconfig_on_freeze' in properties) and \
'frozen' not in properties and \
opt.impl_is_leader():
raise ConfigError(_('a leader ({0}) cannot have '
'"force_default_on_freeze" or "force_metaconfig_on_freeze" property without "frozen"'
'').format(opt.impl_get_display_name()))
self._p_.setproperties(path,
properties)
# values too because of follower values could have a PropertiesOptionError has value
context.cfgimpl_reset_cache(option_bag)
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 - SPECIAL_PROPERTIES
# 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: # pragma: no cover
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,
self.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(self.ro_remove,
self.ro_append,
context)
def read_write(self,
context):
"convenience method to freeze, hide and disable"
self._read(self.rw_remove,
self.rw_append,
context)