tiramisu/tiramisu/setting.py

902 lines
28 KiB
Python

# -*- coding: utf-8 -*-
"sets the options of the configuration objects Config object itself"
# Copyright (C) 2012-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/>.
# ____________________________________________________________
from typing import Union, Set
from itertools import chain
from .error import (
PropertiesOptionError,
ConstError,
ConfigError,
LeadershipError,
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
#
# * 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
#
# mandatory
# should set value for option with this properties if 'mandatory' is set in
# config
# example: 'a', ['a'], [None] are valid
# None, [] are not valid
#
# empty
# raise mandatory PropertiesOptionError if multi or leader have empty value
# example: ['a'] is valid
# [None] is not valid
#
# unique
# raise ValueError if a value is set twice or more in a multi Option
#
# * 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)
#
# 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", "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 = 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",
"validator",
]
)
ALLOWED_LEADER_PROPERTIES = {
"empty",
"notempty",
"notunique",
"unique",
"force_store_value",
"mandatory",
"validator",
"novalidator",
"force_default_on_freeze",
"force_metaconfig_on_freeze",
"frozen",
}
static_set = frozenset()
# ____________________________________________________________
class Undefined:
"""Object undefined, means that there is not value"""
# pylint: disable=too-few-public-methods
def __str__(self): # pragma: no cover
return "Undefined"
__repr__ = __str__
undefined = Undefined()
class ConfigBag:
"""Object to store information for context"""
__slots__ = (
"context", # link to the current context
"properties", # properties for current context
"true_properties", # properties for current context
"is_unrestraint",
"permissives", # permissives for current context
"expiration_time", # EXPIRATION_TIME
)
def __init__(self, context, properties: set, permissives: frozenset, **kwargs):
self.context = context
self.properties = properties
self.permissives = permissives
for key, value in kwargs.items():
setattr(self, key, value)
def __getattr__(self, key):
if key == "true_properties":
return self.properties
if key == "expiration_time":
self.expiration_time = (
EXPIRATION_TIME # pylint: disable=attribute-defined-outside-init
)
return self.expiration_time
if key == "is_unrestraint":
return False
raise KeyError(f'unknown key "{key}" for ConfigBag') # pragma: no cover
def nowarnings(self):
"""do not warnings"""
self.properties = frozenset(self.properties - {"warnings"})
def remove_validation(self):
"""do not validate option"""
self.properties = frozenset(self.properties - {"validator"})
def unrestraint(self):
"""do not restraint access to option"""
self.is_unrestraint = True # pylint: disable=attribute-defined-outside-init
self.true_properties = (
self.properties
) # pylint: disable=attribute-defined-outside-init
self.properties = frozenset(["cache"])
def set_permissive(self):
"""set permissive"""
self.properties = frozenset(self.properties | {"permissive"})
def copy(self):
"""copy the config"""
kwargs = {}
for key in self.__slots__:
kwargs[key] = getattr(self, key)
return ConfigBag(**kwargs)
# ____________________________________________________________
class _NameSpace:
"""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"
# pylint: disable=too-few-public-methods
class GroupType(str):
"""allowed normal group (OptionDescription) names
*normal* means : groups that are not leader
"""
class DefaultGroupType(GroupType):
"""groups that are default (typically 'default')"""
class LeadershipGroupType(GroupType):
"""allowed normal group (OptionDescription) names
*leadership* means : groups that have the 'leadership' attribute set
"""
class RootGroupType(GroupType):
"""root means this is the root optiondescription of whole config"""
def addgroup(self, name):
"""add a new group type"""
setattr(groups, name, groups.GroupType(name))
class OwnerModule(_NameSpace):
"""emulates a module to manage unique owner names.
owners are living in `Config._value_owners`
"""
# pylint: disable=too-few-public-methods
class Owner(str):
"""allowed owner names"""
class DefaultOwner(Owner):
"""groups that are default (typically 'default')"""
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"
) # pylint: disable=attribute-defined-outside-init
# 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"
) # pylint: disable=attribute-defined-outside-init
# groups.root: this group is the root optiondescription of whole config
groups.root = groups.RootGroupType(
"root"
) # pylint: disable=attribute-defined-outside-init
# ____________________________________________________________
# populate owners with default attributes
owners = OwnerModule()
# default: is the config owner after init time
owners.default = owners.DefaultOwner(
"default"
) # pylint: disable=attribute-defined-outside-init
# user: is the generic is the generic owner
owners.addowner("user")
# forced: special owner when value is forced
owners.addowner("forced")
forbidden_owners = (owners.default, owners.forced) # pylint: disable=no-member
# ____________________________________________________________
class Settings:
"``config.Config()``'s configuration options settings"
__slots__ = (
"_properties",
"_permissives",
"_permissives",
"__weakref__",
"ro_append",
"ro_remove",
"rw_append",
"rw_remove",
)
def __init__(self):
"""
initializer
:param context: the root config
:param storage: the storage type
"""
# generic owner
self._properties = {None: {None: DEFAULT_PROPERTIES}}
self._permissives = {}
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):
"""get context properties"""
return self.get_personalize_properties()
def get_personalize_properties(
self,
path: Union[None, str] = None,
index: Union[None, int] = None,
) -> Set[str]:
"""Get the properties modified by user for a path or index"""
if path not in self._properties or index not in self._properties[path]:
return frozenset()
return self._properties[path][index]
def getproperties(
self,
subconfig: "SubConfig",
*,
apply_requires=True,
uncalculated=False,
help_property=False,
transitive_raise=True,
):
"""get properties"""
# pylint: disable=too-many-branches
option = subconfig.option
if option.impl_is_symlinkoption():
option = option.impl_getopt()
if apply_requires and not uncalculated and not help_property:
cache = subconfig.config_bag.context.properties_cache
is_cached, props, validated = cache.getcache(
subconfig, # pylint: disable=unused-variable
"self_props",
)
else:
is_cached = False
if not is_cached:
props = set()
# if index, get option's properties (without index) too
p_props = [option.impl_getproperties()]
props_config = self.get_personalize_properties(subconfig.path)
if props_config:
p_props.append(props_config)
if subconfig.index is not None:
props_config = self.get_personalize_properties(
subconfig.path,
subconfig.index,
)
if props_config:
p_props.append(props_config)
for prop in chain(*p_props):
if uncalculated or isinstance(prop, str):
if not help_property:
props.add(prop)
else:
props.add((prop, prop))
elif apply_requires:
try:
if not help_property:
new_prop = prop.execute(
subconfig,
for_settings=True,
)
else:
new_prop = prop.help(
subconfig,
for_settings=True,
)
if isinstance(new_prop, str):
new_prop = (new_prop, new_prop)
if new_prop is None:
continue
except ConfigError as err:
if transitive_raise:
raise err from err
continue
if (not help_property and not isinstance(new_prop, str)) or (
help_property and not isinstance(new_prop, tuple)
):
raise ValueError(
_(
"invalid property type {type(new_prop)} for "
"{subconfig.option.impl_getname()} with "
"{prop.function.__name__} function"
)
)
if (
not option.impl_is_optiondescription()
and option.impl_is_leader()
and new_prop not in ALLOWED_LEADER_PROPERTIES
):
raise LeadershipError(
_('leader cannot have "{new_prop}" property')
)
props.add(new_prop)
props -= self.getpermissives(subconfig)
if (
not uncalculated
and apply_requires
and not subconfig.config_bag.is_unrestraint
and not help_property
and transitive_raise
):
cache.setcache(
subconfig,
props,
type_="properties",
)
if (
not uncalculated
and subconfig.parent
and subconfig.parent.transitive_properties
):
parent_properties = subconfig.parent.transitive_properties
parent_properties -= self.getpermissives(subconfig)
if help_property:
parent_properties = {(prop, prop) for prop in parent_properties}
return props | parent_properties
return props
def get_context_permissives(self):
"""get context permissives"""
return self.getpermissives(None)
def _getpermissives(
self,
path,
index,
):
if not path in self._permissives:
ret = frozenset()
else:
ret = self._permissives[path].get(index, frozenset())
return ret
def getpermissives(
self,
subconfig: "SubConfig",
):
"""get permissive"""
if subconfig is None:
path = None
index = None
else:
opt = subconfig.option
if opt.impl_is_symlinkoption():
opt = opt.impl_getopt()
path = opt.impl_getpath()
else:
path = subconfig.path
index = subconfig.index
permissives = self._getpermissives(
path,
None,
)
if index is not None:
option_permissives = self._permissives.get(path, {}).get(index, set())
permissives = frozenset(option_permissives | permissives)
return permissives
# ____________________________________________________________
# set methods
def set_context_properties(self, properties, context):
"""set context properties"""
self._properties[None][None] = properties
context.reset_cache(None)
def setproperties(
self,
subconfig,
properties,
):
"""save properties for specified path
(never save properties if same has option properties)
"""
opt = subconfig.option
if not opt.impl_is_optiondescription() and opt.impl_is_leader():
not_allowed_properties = properties - ALLOWED_LEADER_PROPERTIES
if not_allowed_properties:
raise LeadershipError(
_('leader cannot have "{0}" property').format(
display_list(not_allowed_properties)
)
)
if (
"force_default_on_freeze" in properties
or "force_metaconfig_on_freeze" in properties
) and "frozen" not in properties:
raise LeadershipError(
_(
'a leader ({0}) cannot have "force_default_on_freeze" or "force_metaconfig_on_freeze" property without "frozen"'
).format(opt.impl_get_display_name())
)
self._properties.setdefault(subconfig.path, {})[subconfig.index] = properties
# values too because of follower values could have a PropertiesOptionError has value
subconfig.config_bag.context.reset_cache(subconfig)
subconfig.properties = properties
def set_context_permissives(
self,
permissives,
):
"""set context permissive"""
self.setpermissives(
None,
permissives,
)
def setpermissives(
self,
subconfig,
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 subconfig is not None:
path = subconfig.path
index = subconfig.index
else:
path = None
index = None
forbidden_permissives = FORBIDDEN_SET_PERMISSIVES & permissives
if forbidden_permissives:
raise ConfigError(
_("cannot add those permissives: {0}").format(
" ".join(forbidden_permissives)
)
)
self._permissives.setdefault(path, {})[index] = permissives
if subconfig is not None:
subconfig.config_bag.context.reset_cache(subconfig)
# ____________________________________________________________
# reset methods
def _get_path_index_config_option(
self,
bag: Union["SubConfig", ConfigBag],
msg: str,
):
if isinstance(bag, ConfigBag):
path = None
index = None
config_bag = bag
subconfig = None
else:
assert not bag.option.impl_is_symlinkoption(), msg.format(
bag.option.impl_get_display_name()
)
path = bag.path
index = bag.index
config_bag = bag.config_bag
subconfig = bag
return path, index, config_bag, subconfig
def reset(
self,
bag: Union["SubConfig", ConfigBag],
):
"""reset property"""
path, index, config_bag, subconfig = self._get_path_index_config_option(
bag,
_("can't reset properties to " 'the symlinkoption "{}"'),
)
if path in self._properties and index in self._properties[path]:
del self._properties[path][index]
config_bag.context.reset_cache(subconfig)
def reset_permissives(
self,
bag: Union["SubConfig", ConfigBag],
):
"""reset permission"""
path, index, config_bag, subconfig = self._get_path_index_config_option(
bag,
_("can't reset permissives to " 'the symlinkoption "{}"'),
)
if path in self._permissives and index in self._permissives[path]:
del self._permissives[path][index]
config_bag.context.reset_cache(subconfig)
# ____________________________________________________________
# validate properties
def calc_raises_properties(
self,
subconfig,
*,
apply_requires=True,
uncalculated=False,
transitive_raise=True,
not_unrestraint: bool = False,
):
"""raise if needed"""
if not uncalculated and apply_requires and subconfig.properties is not None:
option_properties = subconfig.properties
else:
option_properties = self.getproperties(
subconfig,
apply_requires=apply_requires,
uncalculated=uncalculated,
transitive_raise=transitive_raise,
)
return self._calc_raises_properties(
subconfig,
option_properties,
not_unrestraint,
)
def calc_transitive_properties(
self,
subconfig,
option_properties,
):
config_bag = subconfig.config_bag
modified, context_properties = self.calc_read(
self.rw_remove,
self.rw_append,
config_bag,
)
raises_properties = context_properties - SPECIAL_PROPERTIES
# remove global permissive properties
if raises_properties and "permissive" in raises_properties:
raises_properties -= config_bag.permissives
properties = option_properties & raises_properties
# at this point it should not remain any property for the option
return properties
def _calc_raises_properties(
self,
subconfig,
option_properties,
not_unrestraint: bool,
):
config_bag = subconfig.config_bag
if not_unrestraint and config_bag.is_unrestraint:
context_properties = config_bag.true_properties
else:
context_properties = config_bag.properties
raises_properties = context_properties - SPECIAL_PROPERTIES
# remove global permissive properties
if raises_properties and "permissive" in raises_properties:
raises_properties -= config_bag.permissives
properties = option_properties & raises_properties
# at this point it should not remain any property for the option
return properties
def validate_properties(
self,
subconfig,
*,
need_help=True,
):
"""check properties"""
config_properties = subconfig.config_bag.properties
if not config_properties or config_properties == frozenset(["cache"]):
# if no global property
return
for transitive_raise in [False, True]:
properties = self.calc_raises_properties(
subconfig,
transitive_raise=transitive_raise,
)
if properties != frozenset():
if need_help:
help_properties = dict(
self.getproperties(
subconfig,
help_property=True,
transitive_raise=transitive_raise,
)
)
calc_properties = []
for property_ in self._calc_raises_properties(
subconfig,
set(help_properties.keys()),
False,
):
calc_properties.append(help_properties[property_])
calc_properties = frozenset(calc_properties)
else:
calc_properties = properties
raise PropertiesOptionError(
subconfig,
properties,
self,
help_properties=calc_properties,
)
def validate_mandatory(
self,
subconfig,
value,
):
"""verify if option is mandatory without value"""
if "mandatory" not in subconfig.config_bag.properties:
return
values = subconfig.config_bag.context.get_values()
if (
not (
"permissive" in subconfig.config_bag.properties
and "mandatory" in subconfig.config_bag.permissives
)
and "mandatory" in subconfig.properties
and values.isempty(
subconfig,
value,
False,
)
):
raise PropertiesOptionError(
subconfig,
["mandatory"],
self,
)
if "empty" in subconfig.properties and values.isempty(
subconfig,
value,
True,
):
raise PropertiesOptionError(
subconfig,
["empty"],
self,
)
def validate_frozen(
self,
subconfig,
):
"""verify if option is frozen"""
if (
subconfig.config_bag.properties
and (
"everything_frozen" in subconfig.config_bag.properties
or (
"frozen" in subconfig.config_bag.properties
and "frozen" in subconfig.properties
)
)
and not (
("permissive" in subconfig.config_bag.properties)
and "frozen" in subconfig.config_bag.permissives
)
):
raise PropertiesOptionError(
subconfig,
["frozen"],
self,
)
return False
# ____________________________________________________________
# read only/read write
def calc_read(
self,
remove,
append,
config_bag,
):
props = self.get_personalize_properties()
modified = False
if remove & props:
props = props - remove
modified = True
if append & props != append:
props = props | append
modified = True
return modified, frozenset(props)
def _read(
self,
remove,
append,
config_bag,
):
modified, props = self.calc_read(
remove,
append,
config_bag,
)
if modified:
self.set_context_properties(
props,
config_bag.context,
)
def read_only(
self,
config_bag,
):
"convenience method to freeze, hide and disable"
self._read(
self.ro_remove,
self.ro_append,
config_bag,
)
def read_write(
self,
config_bag,
):
"convenience method to freeze, hide and disable"
self._read(
self.rw_remove,
self.rw_append,
config_bag,
)