tiramisu/tiramisu/setting.py
2016-03-17 23:16:21 +01:00

679 lines
26 KiB
Python

# -*- 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 <http://www.gnu.org/licenses/>.
# ____________________________________________________________
from time import time
from copy import copy
from logging import getLogger
import weakref
from .error import (RequirementError, PropertiesOptionError,
ConstError, ConfigError)
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', 'expire', '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 = set(['force_store_value'])
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__: # pragma: optional cover
raise ConstError(_("can't rebind {0}").format(name))
self.__dict__[name] = value
def __delattr__(self, name): # pragma: optional cover
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'))
setattr(owners, 'forced', owners.Owner('forced'))
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(object):
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
"""
self._append(propname)
def _append(self, propname, save=True):
if self._opt is not None and self._opt.impl_getrequires() is not None \
and propname in self._opt.impl_get_calc_properties(): # pragma: optional cover
raise ValueError(_('cannot append {0} property for option {1}: '
'this property is calculated').format(
propname, self._opt.impl_getname()))
if propname in forbidden_set_properties:
raise ConfigError(_('cannot add those properties: {0}').format(propname))
self._properties.add(propname)
if save:
self._setting._setproperties(self._properties, self._path, force=True)
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._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, save=False)
self._setting._setproperties(self._properties, self._path)
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: # pragma: optional cover
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(read_write=False)
def __repr__(self):
return str(list(self._getproperties(read_write=False)))
def __getitem__(self, opt):
path = opt.impl_getpath(self._getcontext())
return self.getproperties(opt, path)
def getproperties(self, opt, path, setting_properties=undefined):
return Property(self,
self._getproperties(opt, path,
setting_properties=setting_properties),
opt, path)
def __setitem__(self, opt, value): # pragma: optional cover
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): # pragma: optional cover
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()
def _getproperties(self, opt=None, path=None,
setting_properties=undefined, read_write=True,
apply_requires=True, index=None):
"""
"""
if opt is None:
props = self._p_.getproperties(path, default_properties)
else:
if setting_properties is undefined:
setting_properties = self._getproperties(read_write=False)
if path is None: # pragma: optional cover
raise ValueError(_('if opt is not None, path should not be'
' None in _getproperties'))
is_cached = False
if apply_requires:
if 'cache' in setting_properties and 'expire' in setting_properties:
ntime = int(time())
else:
ntime = None
if 'cache' in setting_properties and self._p_.hascache(path, index):
is_cached, props = self._p_.getcache(path, ntime, index)
if not is_cached:
props = self._p_.getproperties(path, opt.impl_getproperties())
if apply_requires:
props = copy(props)
props |= self.apply_requires(opt, path, setting_properties, index)
if 'cache' in setting_properties:
if 'expire' in setting_properties:
ntime = ntime + expires_time
self._p_.setcache(path, props, ntime, index)
if read_write:
props = copy(props)
return props
def append(self, propname):
"puts property propname in the Config's properties attribute"
props = self._p_.getproperties(None, default_properties)
if propname not in props:
props.add(propname)
self._setproperties(props, 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)
def extend(self, propnames):
for propname in propnames:
self.append(propname)
def _setproperties(self, properties, path, force=False):
"""save properties for specified path
(never save properties if same has option properties)
"""
if not force:
forbidden_properties = forbidden_set_properties & properties
if forbidden_properties:
raise ConfigError(_('cannot add those properties: {0}').format(
' '.join(forbidden_properties)))
self._p_.setproperties(path, properties)
self._getcontext().cfgimpl_reset_cache()
#____________________________________________________________
def validate_properties(self, opt_or_descr, is_descr, check_frozen, path,
value=None, force_permissive=False,
setting_properties=undefined,
self_properties=undefined,
index=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 is_descr: we have to know if we are in an option description,
just because the mandatory property
doesn't exist here
:param check_frozen: in the validation process, an option is to be modified,
the behavior can be different
(typically with the `frozen` property)
"""
# opt properties
if setting_properties is undefined:
setting_properties = self._getproperties(read_write=False)
if self_properties is not undefined:
properties = copy(self_properties)
else:
properties = self._getproperties(opt_or_descr, path,
setting_properties=setting_properties,
index=index)
# 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 setting_properties:
properties -= self._p_.getpermissive()
# calc properties
properties &= setting_properties
if not is_descr:
#mandatory
if 'mandatory' in properties and \
not self._getcontext().cfgimpl_get_values()._isempty(
opt_or_descr, value, index=index):
properties.remove('mandatory')
elif opt_or_descr.impl_is_multi() and \
'empty' in setting_properties and \
not opt_or_descr.impl_is_master_slaves('slave') and \
self._getcontext().cfgimpl_get_values()._isempty(
opt_or_descr, value, force_allow_empty_list=True):
properties.add('mandatory')
# should return 'frozen' only when tried to modify a value
if check_frozen and 'everything_frozen' in setting_properties:
properties.add('frozen')
elif 'frozen' in properties and not check_frozen:
properties.remove('frozen')
# at this point an option should not remain in properties
if properties != frozenset():
props = list(properties)
if 'frozen' in properties:
return PropertiesOptionError(_('cannot change the value for '
'option {0} this option is'
' frozen').format(
opt_or_descr.impl_getname()),
props)
else:
if is_descr:
opt_type = 'optiondescription'
else:
opt_type = 'option'
return PropertiesOptionError(_("trying to access to an {0} "
"named: {1} with properties {2}"
"").format(opt_type,
opt_or_descr._name,
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 = opt.impl_getpath(self._getcontext())
if not isinstance(permissive, tuple): # pragma: optional cover
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): # pragma: optional cover
raise TypeError(_("invalid generic owner {0}").format(str(owner)))
self._owner = owner
def getowner(self):
return self._owner
#____________________________________________________________
def _read(self, remove, append):
props = self._p_.getproperties(None, default_properties)
modified = False
if remove & props != set([]):
props = props - remove
modified = True
if append & props != append:
props = props | append
modified = True
if modified:
self._setproperties(props, None)
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, setting_properties, index):
"""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.impl_getrequires() == []:
return frozenset()
# filters the callbacks
calc_properties = set()
context = self._getcontext()
for requires in opt.impl_getrequires():
for require in requires:
option, expected, action, inverse, \
transitive, same_action = require
reqpath = option.impl_getpath(context)
if reqpath == path or reqpath.startswith(path + '.'): # pragma: optional cover
raise RequirementError(_("malformed requirements "
"imbrication detected for option:"
" '{0}' with requirement on: "
"'{1}'").format(path, reqpath))
if option.impl_is_multi():
idx = index
else:
idx = None
value = context.getattr(reqpath, force_permissive=True,
_setting_properties=setting_properties,
index=idx, returns_raise=True)
if isinstance(value, Exception):
if isinstance(value, PropertiesOptionError):
if not transitive:
continue
properties = value.proptype
if same_action and action not in properties: # pragma: optional cover
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
else:
raise value
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_modified_properties(self):
return self._p_.get_modified_properties()
def get_modified_permissives(self):
return self._p_.get_modified_permissives()
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'])