tiramisu/tiramisu/option/optiondescription.py

426 lines
15 KiB
Python

# -*- coding: utf-8 -*-
# Copyright (C) 2014-2024 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
# ____________________________________________________________
"""OptionDescription
"""
import weakref
from typing import Optional, Iterator, Union, List, Dict
from ..i18n import _
from ..setting import ConfigBag, groups, undefined, owners, Undefined
from .baseoption import BaseOption
# from .syndynoption import SubDynOptionDescription, SynDynOptionDescription
from ..error import ConfigError, ConflictError, AttributeOptionError
class CacheOptionDescription(BaseOption):
"""manage cache for option description"""
__slots__ = (
"_cache_force_store_values",
"_cache_dependencies_information",
)
def impl_already_build_caches(self) -> bool:
"""is a readonly option?"""
return self.impl_is_readonly()
def _build_cache(
self,
display_name,
_consistencies=None,
_consistencies_id=0,
currpath: List[str] = None,
cache_option=None,
force_store_values=None,
dependencies_information=None,
) -> None:
"""validate options and set option has readonly option"""
# pylint: disable=too-many-branches,too-many-arguments
# _consistencies is None only when we start to build cache
if _consistencies is None:
init = True
_consistencies = {}
if __debug__:
cache_option = []
force_store_values = []
dependencies_information = {}
currpath = []
else:
init = False
if self.impl_is_readonly():
# cache already set
raise ConfigError(
_("option description seems to be part of an other " "config")
)
for option in self.get_children():
if __debug__:
cache_option.append(option)
sub_currpath = currpath + [option.impl_getname()]
subpath = ".".join(sub_currpath)
if isinstance(option, OptionDescription):
# pylint: disable=protected-access
option._build_cache(
display_name,
_consistencies,
_consistencies_id,
sub_currpath,
cache_option,
force_store_values,
dependencies_information,
)
elif not option.impl_is_symlinkoption():
informations = option.get_dependencies_information()
if informations:
for param in informations.pop(None):
del param.self_option
for (
information,
options,
) in option.get_dependencies_information().items():
if None in options:
dependencies_information.setdefault(information, []).append(
option
)
properties = option.impl_getproperties()
if "force_store_value" in properties:
force_store_values.append(option)
if option.impl_is_readonly():
previous_path = option.impl_getpath()
if "." in previous_path:
previous_path = _('"{0}" option description').format(
previous_path.rsplit(".", 1)[0]
)
else:
previous_path = _("root option description")
if currpath:
current_path = _('"{0}" option description').format(
".".join(currpath)
)
else:
current_path = _("root option description")
raise ConflictError(
_('option "{0}" is include in {1} but is also in {2}').format(
option.impl_get_display_name(None), current_path, previous_path
)
)
if not self.impl_is_readonly() and display_name:
option._display_name_function = (
display_name # pylint: disable=protected-access
)
option._path = subpath # pylint: disable=protected-access
option._set_readonly() # pylint: disable=protected-access
if init:
self._cache_force_store_values = (
force_store_values # pylint: disable=attribute-defined-outside-init
)
self._cache_dependencies_information = dependencies_information # pylint: disable=attribute-defined-outside-init
self._path = (
None # pylint: disable=attribute-defined-outside-init,no-member
)
self._set_readonly()
def impl_build_force_store_values(
self,
config_bag: ConfigBag,
) -> None:
"""set value to force_store_values option"""
# pylint: disable=too-many-branches
context = config_bag.context
if "force_store_value" not in config_bag.properties:
return
values = config_bag.context.get_values()
for option in self._cache_force_store_values:
if option.issubdyn():
paths = option.impl_getpath().split(".")
parents = [config_bag.context.get_root(config_bag)]
for name in paths:
new_parents = []
for parent in parents:
doption = parent.option.get_child(
name,
config_bag,
parent,
allow_dynoption=True,
)
if doption.impl_is_dynoptiondescription():
new_parents.extend(
parent.dyn_to_subconfig(
doption,
True,
)
)
else:
new_parents.append(
parent.get_child(
doption,
None,
True,
name=name,
)
)
parents = new_parents
subconfigs = new_parents
else:
subconfigs = [
context.get_sub_config(
config_bag,
option.impl_getpath(),
None,
properties=None,
validate_properties=False,
)
]
if option.impl_is_follower():
for follower_subconfig in subconfigs:
parent = follower_subconfig.parent
follower_len = parent.get_length_leadership()
for index in range(follower_len):
if values.hasvalue(
follower_subconfig.path,
index=index,
):
continue
idx_follower_subconfig = parent.get_child(
follower_subconfig.option,
index,
validate_properties=False,
)
value = values.get_value(idx_follower_subconfig)[0]
if value is None:
continue
values.set_storage_value(
follower_subconfig.path,
index,
value,
owners.forced,
)
else:
for subconfig in subconfigs:
subconfig.properties = frozenset()
value = values.get_value(subconfig)[0]
if value is None:
continue
if values.hasvalue(subconfig.path):
continue
values.set_storage_value(
subconfig.path,
None,
value,
owners.forced,
)
class OptionDescriptionWalk(CacheOptionDescription):
"""get child of option description"""
__slots__ = ("_children",)
def get_path(
self,
config_bag,
):
if config_bag is undefined or config_bag.context.get_description() == self:
return ""
return self.impl_getpath()
def get_child_not_dynamic(
self,
name: str,
allow_dynoption: bool,
parent: "SubConfig",
):
if name in self._children[0]: # pylint: disable=no-member
option = self._children[1][
self._children[0].index(name)
] # pylint: disable=no-member
if option.impl_is_dynoptiondescription() and not allow_dynoption:
if parent.path:
path = parent.path + "." + name
else:
path = name
raise AttributeOptionError(path, "option-dynamic")
return option
def get_child(
self,
name: str,
config_bag: ConfigBag,
parent: "SubConfig",
*,
with_identifier: bool = False,
allow_dynoption: bool = False,
) -> Union[BaseOption]:
"""get a child"""
# if not dyn
option = self.get_child_not_dynamic(
name,
allow_dynoption,
parent,
)
if option:
return option
# if dyn
for child in self._children[1]: # pylint: disable=no-member
if not child.impl_is_dynoptiondescription():
continue
for identifier in child.get_identifiers(parent):
if name != child.impl_getname(identifier):
continue
if not with_identifier:
return child
return identifier, child
if parent.path is None:
path = name
else:
path = parent.path + "." + name
raise AttributeOptionError(path, "option-not-found")
def get_children(self) -> List[BaseOption]:
"""get children"""
return self._children[1]
class OptionDescription(OptionDescriptionWalk):
"""Config's schema (organisation, group) and container of Options
The `OptionsDescription` objects lives in the `tiramisu.config.Config`.
"""
__slots__ = ("_group_type",)
def __init__(
self,
name: str,
doc: str,
children: List[BaseOption],
*,
properties=None,
informations: Optional[Dict] = None,
group_type: Optional[groups.GroupType] = groups.default,
) -> None:
"""
:param children: a list of options (including optiondescriptions)
"""
assert isinstance(children, list), _(
'children in optiondescription "{}" ' "must be a list"
).format(name)
super().__init__(
name,
doc,
informations,
properties=properties,
)
child_names = []
fix_child_names = []
dynopts = []
for child in children:
name = child.impl_getname()
child_names.append(name)
if child.impl_is_dynoptiondescription():
dynopts.append(child)
else:
fix_child_names.append(name)
# before sorting
children_ = (tuple(child_names), tuple(children))
# better performance like this
fix_child_names.sort()
old = None
for child_name in fix_child_names:
if child_name == old:
raise ConflictError(
_('the option name "{0}" is duplicate in "{1}"').format(
child_name, self.impl_get_display_name(None)
)
)
old = child_name
for dynopt in dynopts:
if dynopt.could_conflict:
continue
for child in children:
if child != dynopt and dynopt.name_could_conflict(dynopt, child):
dynopt.could_conflict.append(weakref.ref(child))
child.could_conflict.append(weakref.ref(dynopt))
break
self._children = children_
# the group_type is useful for filtering OptionDescriptions in a config
self._group_type = None
self.impl_set_group_type(group_type)
def _setsubdyn(
self,
subdyn,
) -> None:
for child in self._children[1]:
child._setsubdyn(subdyn)
super()._setsubdyn(subdyn)
def impl_is_optiondescription(self) -> bool:
"""the option is an option description"""
return True
def impl_is_dynoptiondescription(self) -> bool:
"""the option is not dynamic"""
return False
def impl_is_leadership(self) -> bool:
"""the option is not a leadership"""
return False
# ____________________________________________________________
def impl_set_group_type(
self,
group_type: groups.GroupType,
) -> None:
"""sets a given group object to an OptionDescription
:param group_type: an instance of `GroupType` or `LeadershipGroupType`
that lives in `setting.groups`
"""
if __debug__:
if self._group_type is not None and self._group_type != groups.default:
raise ValueError(
_(
"cannot change group_type if already set " "(old {0}, new {1})"
).format(self._group_type, group_type)
)
if not isinstance(group_type, groups.GroupType):
raise ValueError(_("group_type: {0}" " not allowed").format(group_type))
if isinstance(group_type, groups.LeadershipGroupType):
raise ConfigError(
"please use Leadership object instead of OptionDescription"
)
self._group_type = group_type
def impl_get_group_type(self) -> groups.GroupType:
"""get the group type of option description"""
return self._group_type
def impl_is_dynsymlinkoption(self) -> bool:
"""option is not a dyn symlink option"""
return False