tiramisu/tiramisu/option/optiondescription.py

372 lines
16 KiB
Python

# -*- coding: utf-8 -*-
# Copyright (C) 2014 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/>.
#
# The original `Config` design model is unproudly borrowed from
# the rough pypy's guys: http://codespeak.net/svn/pypy/dist/pypy/config/
# the whole pypy projet is under MIT licence
# ____________________________________________________________
from copy import copy
import re
from ..i18n import _
from ..setting import groups, undefined # , log
from .baseoption import BaseOption, SymLinkOption
from . import MasterSlaves
from ..error import ConfigError, ConflictError
from ..storage import get_storages_option
from ..autolib import carry_out_calculation
StorageOptionDescription = get_storages_option('optiondescription')
name_regexp = re.compile(r'^[a-zA-Z\d\-_]*$')
class OptionDescription(BaseOption, StorageOptionDescription):
"""Config's schema (organisation, group) and container of Options
The `OptionsDescription` objects lives in the `tiramisu.config.Config`.
"""
__slots__ = tuple()
def __init__(self, name, doc, children, requires=None, properties=None):
"""
:param children: a list of options (including optiondescriptions)
"""
super(OptionDescription, self).__init__(name, doc=doc,
requires=requires,
properties=properties,
callback=False)
child_names = []
dynopt_names = []
for child in children:
name = child.impl_getname()
child_names.append(name)
if isinstance(child, DynOptionDescription):
dynopt_names.append(name)
#better performance like this
valid_child = copy(child_names)
valid_child.sort()
old = None
for child in valid_child:
if child == old: # pragma: optional cover
raise ConflictError(_('duplicate option name: '
'{0}').format(child))
if dynopt_names:
for dynopt in dynopt_names:
if child != dynopt and child.startswith(dynopt):
raise ConflictError(_('option must not start as '
'dynoptiondescription'))
old = child
self._add_children(child_names, children)
_setattr = object.__setattr__
_setattr(self, '_cache_consistencies', None)
# the group_type is useful for filtering OptionDescriptions in a config
_setattr(self, '_group_type', groups.default)
def impl_getdoc(self):
return self.impl_get_information('doc')
def impl_validate(self, *args, **kwargs):
"""usefull for OptionDescription"""
pass
def impl_getpaths(self, include_groups=False, _currpath=None):
"""returns a list of all paths in self, recursively
_currpath should not be provided (helps with recursion)
"""
return _impl_getpaths(self, include_groups, _currpath)
def impl_build_cache(self, _consistencies=None, cache_option=None):
"""validate duplicate option and set option has readonly option
"""
if cache_option is None:
init = True
_consistencies = {}
cache_option = []
else:
init = False
for option in self._impl_getchildren(dyn=False):
#FIXME specifique id for sqlalchemy?
#FIXME avec sqlalchemy ca marche le multi parent ? (dans des configs différentes)
cache_option.append(option._get_id())
if isinstance(option, OptionDescription):
option._set_readonly(False)
option.impl_build_cache(_consistencies, cache_option)
#cannot set multi option as OptionDescription requires
else:
option._set_readonly(True)
for func, all_cons_opts, params in option._get_consistencies():
all_cons_opts[0]._valid_consistencies(all_cons_opts[1:])
for opt in all_cons_opts:
_consistencies.setdefault(opt,
[]).append((func,
all_cons_opts,
params))
is_slave = None
if option.impl_is_multi():
all_requires = option.impl_getrequires()
if all_requires != tuple():
for requires in all_requires:
for require in requires:
#if option in require is a multi:
# * option in require must be a master or a slave
# * current option must be a slave (and only a slave)
# * option in require and current option must be in same master/slaves
require_opt = require[0]
if require_opt.impl_is_multi():
if is_slave is None:
is_slave = option.impl_is_master_slaves('slave')
if is_slave:
masterslaves = option.impl_get_master_slaves()
if is_slave and require_opt.impl_is_master_slaves():
if masterslaves != require_opt.impl_get_master_slaves():
raise ValueError(_('malformed requirements option {0} '
'must be in same master/slaves for {1}').format(
require_opt.impl_getname(), option.impl_getname()))
else:
raise ValueError(_('malformed requirements option {0} '
'must not be a multi for {1}').format(
require_opt.impl_getname(), option.impl_getname()))
if init:
if len(cache_option) != len(set(cache_option)):
for idx in xrange(1, len(cache_option) + 1):
opt = cache_option.pop(0)
if opt in cache_option:
raise ConflictError(_('duplicate option: {0}').format(opt))
if _consistencies != {}:
self._cache_consistencies = {}
for opt, cons in _consistencies.items():
if opt._get_id() not in cache_option: # pragma: optional cover
raise ConfigError(_('consistency with option {0} '
'which is not in Config').format(
opt.impl_getname()))
self._cache_consistencies[opt] = tuple(cons)
self._set_readonly(False)
# ____________________________________________________________
def impl_set_group_type(self, group_type):
"""sets a given group object to an OptionDescription
:param group_type: an instance of `GroupType` or `MasterGroupType`
that lives in `setting.groups`
"""
if self._group_type != groups.default: # pragma: optional cover
raise TypeError(_('cannot change group_type if already set '
'(old {0}, new {1})').format(self._group_type,
group_type))
if isinstance(group_type, groups.GroupType):
self._group_type = group_type
if isinstance(group_type, groups.MasterGroupType):
MasterSlaves(self.impl_getname(), self.impl_getchildren())
else: # pragma: optional cover
raise ValueError(_('group_type: {0}'
' not allowed').format(group_type))
def _impl_getstate(self, descr=None):
"""enables us to export into a dict
:param descr: parent :class:`tiramisu.option.OptionDescription`
"""
if descr is None:
self.impl_build_cache_option()
descr = self
super(OptionDescription, self)._impl_getstate(descr)
self._state_group_type = str(self._group_type)
for option in self._impl_getchildren():
option._impl_getstate(descr)
def __getstate__(self):
"""special method to enable the serialization with pickle
"""
stated = True
try:
# the `_state` attribute is a flag that which tells us if
# the serialization can be performed
self._stated
except AttributeError:
# if cannot delete, _impl_getstate never launch
# launch it recursivement
# _stated prevent __getstate__ launch more than one time
# _stated is delete, if re-serialize, re-lauch _impl_getstate
self._impl_getstate()
stated = False
return super(OptionDescription, self).__getstate__(stated)
def _impl_setstate(self, descr=None):
"""enables us to import from a dict
:param descr: parent :class:`tiramisu.option.OptionDescription`
"""
if descr is None:
self._cache_consistencies = None
self.impl_build_cache_option()
descr = self
self._group_type = getattr(groups, self._state_group_type)
if isinstance(self._group_type, groups.MasterGroupType):
MasterSlaves(self.impl_getname(), self.impl_getchildren(),
validate=False)
del(self._state_group_type)
super(OptionDescription, self)._impl_setstate(descr)
for option in self._impl_getchildren(dyn=False):
option._impl_setstate(descr)
def __setstate__(self, state):
super(OptionDescription, self).__setstate__(state)
try:
self._stated
except AttributeError:
self._impl_setstate()
def _impl_get_suffixes(self, context):
callback, callback_params = self.impl_get_callback()
values = carry_out_calculation(self, context=context,
callback=callback,
callback_params=callback_params)
if len(values) > len(set(values)):
raise ConfigError(_('DynOptionDescription callback return not uniq value'))
for val in values:
if not isinstance(val, str) or re.match(name_regexp, val) is None:
raise ValueError(_("invalid suffix: {0} for option").format(val))
return values
def _impl_search_dynchild(self, name=undefined, context=undefined):
ret = []
for child in self._impl_st_getchildren(context, only_dyn=True):
cname = child.impl_getname()
if name is undefined or name.startswith(cname):
path = cname
for value in child._impl_get_suffixes(context):
if name is undefined:
ret.append(SynDynOptionDescription(child, cname + value, path + value, value))
elif name == cname + value:
return SynDynOptionDescription(child, name, path + value, value)
return ret
def _impl_get_dynchild(self, child, suffix):
name = child.impl_getname() + suffix
path = self.impl_getname() + suffix + '.' + name
if isinstance(child, OptionDescription):
return SynDynOptionDescription(child, name, path, suffix)
else:
return child._impl_to_dyn(name, path)
def _impl_getchildren(self, dyn=True, context=undefined):
for child in self._impl_st_getchildren(context):
cname = child.impl_getname()
if dyn and child.impl_is_dynoptiondescription():
path = cname
for value in child._impl_get_suffixes(context):
yield SynDynOptionDescription(child,
cname + value,
path + value, value)
else:
yield child
def impl_getchildren(self):
return list(self._impl_getchildren())
def __getattr__(self, name, context=undefined):
if name.startswith('_'): # or name.startswith('impl_'):
return object.__getattribute__(self, name)
if '.' in name:
path = name.split('.')[0]
subpath = '.'.join(name.split('.')[1:])
return self.__getattr__(path, context=context).__getattr__(subpath, context=context)
return self._getattr(name, context=context)
class DynOptionDescription(OptionDescription):
def __init__(self, name, doc, children, requires=None, properties=None,
callback=None, callback_params=None):
super(DynOptionDescription, self).__init__(name, doc, children,
requires, properties)
for child in children:
if isinstance(child, OptionDescription):
if child.impl_get_group_type() != groups.master:
raise ConfigError(_('cannot set optiondescription in a '
'dynoptiondescription'))
for chld in child._impl_getchildren():
chld._impl_setsubdyn(self)
if isinstance(child, SymLinkOption):
raise ConfigError(_('cannot set symlinkoption in a '
'dynoptiondescription'))
if isinstance(child, SymLinkOption):
raise ConfigError(_('cannot set symlinkoption in a '
'dynoptiondescription'))
child._impl_setsubdyn(self)
self.impl_set_callback(callback, callback_params)
self.commit()
def _validate_callback(self, callback, callback_params):
if callback is None:
raise ConfigError(_('callback is mandatory for dynoptiondescription'))
class SynDynOptionDescription(object):
__slots__ = ('_opt', '_name', '_path', '_suffix')
def __init__(self, opt, name, path, suffix):
self._opt = opt
self._name = name
self._path = path
self._suffix = suffix
def __getattr__(self, name, context=undefined):
if name in dir(self._opt):
return getattr(self._opt, name)
return self._opt._getattr(name, suffix=self._suffix, context=context)
def impl_getname(self):
return self._name
def _impl_getchildren(self, dyn=True, context=undefined):
children = []
for child in self._opt._impl_getchildren():
children.append(self._opt._impl_get_dynchild(child, self._suffix))
return children
def impl_getchildren(self):
return self._impl_getchildren()
def impl_getpath(self, context):
return self._path
def impl_getpaths(self, include_groups=False, _currpath=None):
return _impl_getpaths(self, include_groups, _currpath)
def _impl_getopt(self):
return self._opt
def _impl_getpaths(klass, include_groups, _currpath):
"""returns a list of all paths in klass, recursively
_currpath should not be provided (helps with recursion)
"""
if _currpath is None:
_currpath = []
paths = []
for option in klass._impl_getchildren():
attr = option.impl_getname()
if option.impl_is_optiondescription():
if include_groups:
paths.append('.'.join(_currpath + [attr]))
paths += option.impl_getpaths(include_groups=include_groups,
_currpath=_currpath + [attr])
else:
paths.append('.'.join(_currpath + [attr]))
return paths