tiramisu/tiramisu/setting.py

656 lines
24 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
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'])
# ____________________________________________________________
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)
def populate_multitypes():
"""all multi option should have a type, this type is automaticly set do
not touch this
default
default's multi option set if not master or slave
master
master's option in a group with master's type, name of this option
should be the same name of the optiondescription
slave
slave's option in a group with master's type
"""
setattr(multitypes, 'default', multitypes.DefaultMultiType('default'))
setattr(multitypes, 'master', multitypes.MasterMultiType('master'))
setattr(multitypes, 'slave', multitypes.SlaveMultiType('slave'))
# ____________________________________________________________
# populate groups, owners and multitypes with default attributes
groups = GroupModule()
populate_groups()
owners = OwnerModule()
populate_owners()
multitypes = MultiTypeModule()
populate_multitypes()
# ____________________________________________________________
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._calc_properties 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._name))
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):
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 = copy(self._p_.getproperties(path, opt._properties))
if is_apply_req:
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._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._name),
props)
else:
raise PropertiesOptionError(_("trying to access to an option "
"named: {0} with properties {1}"
"").format(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 = 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 __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'])