Compare commits

..

No commits in common. "16de466eaf3f22b463575e4229967031d00be1b3" and "44afe0a367684b668454f97c2d5119e4a4cd9935" have entirely different histories.

17 changed files with 53 additions and 227 deletions

View file

@ -1,10 +1,3 @@
## 1.2.0a64 (2026-03-02)
### Feat
- identifier could be calculated
- check mandatories is now centralized
## 1.2.0a63 (2026-02-11) ## 1.2.0a63 (2026-02-11)
### Feat ### Feat

View file

@ -1,6 +1,6 @@
[project] [project]
name = "rougail" name = "rougail"
version = "1.2.0a64" version = "1.2.0a63"
[tool.commitizen] [tool.commitizen]
name = "cz_conventional_commits" name = "cz_conventional_commits"

View file

@ -4,7 +4,7 @@ requires = ["flit_core >=3.8.0,<4"]
[project] [project]
name = "rougail-base" name = "rougail-base"
version = "1.2.0a64" version = "1.2.0a63"
authors = [{name = "Emmanuel Garette", email = "gnunux@gnunux.info"}] authors = [{name = "Emmanuel Garette", email = "gnunux@gnunux.info"}]
readme = "README.md" readme = "README.md"
description = "A consistency handling system that was initially designed in the configuration management" description = "A consistency handling system that was initially designed in the configuration management"

View file

@ -4,7 +4,7 @@ requires = ["flit_core >=3.8.0,<4"]
[project] [project]
name = "rougail" name = "rougail"
version = "1.2.0a64" version = "1.2.0a63"
authors = [{name = "Emmanuel Garette", email = "gnunux@gnunux.info"}] authors = [{name = "Emmanuel Garette", email = "gnunux@gnunux.info"}]
description = "A consistency handling system that was initially designed in the configuration management" description = "A consistency handling system that was initially designed in the configuration management"
classifiers = [ classifiers = [
@ -18,7 +18,7 @@ classifiers = [
dependencies = [ dependencies = [
"ruamel.yaml ~= 0.18.6", "ruamel.yaml ~= 0.18.6",
"pydantic ~= 2.9.2", "pydantic ~= 2.9.2",
"rougail-base == 1.2.0a64", "rougail-base == 1.2.0a63",
] ]
[tool.flit.sdist] [tool.flit.sdist]

View file

@ -1 +1 @@
__version__ = "1.2.0a64" __version__ = "1.2.0a63"

View file

@ -1000,17 +1000,19 @@ class ParserVariable:
calculation_object["namespace"] = self.namespace calculation_object["namespace"] = self.namespace
calculation_object["xmlfiles"] = xmlfiles calculation_object["xmlfiles"] = xmlfiles
# #
if attribute == "default" and "identifier" in calculation_object:
identifier = calculation_object["identifier"]
if isinstance(identifier, dict):
self.set_calculation(calculation_object, "identifier", identifier, path, family_is_dynamic, xmlfiles, inside_list=inside_list, index=index)
if "params" in calculation_object: if "params" in calculation_object:
if not isinstance(calculation_object["params"], dict): if not isinstance(calculation_object["params"], dict):
raise Exception("params must be a dict") raise Exception("params must be a dict")
params = [] params = []
for key, val in calculation_object["params"].items(): for key, val in calculation_object["params"].items():
if isinstance(val, dict) and "type" not in val: if isinstance(val, dict) and "type" not in val:
self.set_param_type(val) # auto set type
param_typ = set(CALCULATION_TYPES) & set(val)
# XXX variable is also set to information
if param_typ == {"variable", "information"}:
param_typ = {"information"}
if len(param_typ) == 1:
val["type"] = list(param_typ)[0]
if not isinstance(val, dict) or "type" not in val: if not isinstance(val, dict) or "type" not in val:
param_typ = "any" param_typ = "any"
val = { val = {
@ -1062,15 +1064,6 @@ class ParserVariable:
else: else:
obj[attribute][index] = calc obj[attribute][index] = calc
def set_param_type(self, val):
# auto set type
param_typ = set(CALCULATION_TYPES) & set(val)
# XXX variable is also set to information
if param_typ == {"variable", "information"}:
param_typ = {"information"}
if len(param_typ) == 1:
val["type"] = list(param_typ)[0]
class RougailConvert(ParserVariable): class RougailConvert(ParserVariable):
"""Main Rougail conversion""" """Main Rougail conversion"""

View file

@ -81,8 +81,8 @@ def get_convert_option_types():
class Param(BaseModel): class Param(BaseModel):
key: StrictStr key: str
namespace: Optional[StrictStr] namespace: Optional[str]
model_config = ConfigDict(extra="forbid") model_config = ConfigDict(extra="forbid")
def __init__( def __init__(
@ -102,13 +102,13 @@ class Param(BaseModel):
class AnyParam(Param): class AnyParam(Param):
type: StrictStr type: str
value: Union[BASETYPE, List[BASETYPE]] value: Union[BASETYPE, List[BASETYPE]]
class VariableParam(Param): class VariableParam(Param):
type: StrictStr type: str
variable: StrictStr variable: str
propertyerror: bool = True propertyerror: bool = True
whole: bool = False whole: bool = False
# dynamic: bool = True # dynamic: bool = True
@ -151,7 +151,7 @@ class VariableParam(Param):
class IdentifierParam(Param): class IdentifierParam(Param):
type: StrictStr type: str
identifier: Optional[int] = None identifier: Optional[int] = None
def __init__( def __init__(
@ -167,9 +167,9 @@ class IdentifierParam(Param):
class InformationParam(Param): class InformationParam(Param):
type: StrictStr type: str
information: StrictStr information: str
variable: Optional[StrictStr] = None variable: Optional[str] = None
def to_param( def to_param(
self, attribute_name, objectspace, path, version, namespace, xmlfiles self, attribute_name, objectspace, path, version, namespace, xmlfiles
@ -202,7 +202,7 @@ class InformationParam(Param):
class IndexParam(Param): class IndexParam(Param):
type: StrictStr type: str
def to_param( def to_param(
self, attribute_name, objectspace, path, version, namespace, xmlfiles self, attribute_name, objectspace, path, version, namespace, xmlfiles
@ -220,8 +220,8 @@ class IndexParam(Param):
class NamespaceParam(Param): class NamespaceParam(Param):
type: StrictStr type: str
namespace: StrictStr namespace: str
def to_param( def to_param(
self, attribute_name, objectspace, path, version, namespace, xmlfiles self, attribute_name, objectspace, path, version, namespace, xmlfiles
@ -247,15 +247,15 @@ PARAM_TYPES = {
class Calculation(BaseModel): class Calculation(BaseModel):
# path: StrictStr # path: str
inside_list: bool inside_list: bool
version: StrictStr version: str
ori_path: Optional[StrictStr] = None ori_path: Optional[str] = None
default_values: Any = None default_values: Any = None
namespace: Optional[StrictStr] namespace: Optional[str]
warnings: Optional[bool] = None warnings: Optional[bool] = None
description: Optional[StrictStr] = None description: Optional[StrictStr] = None
xmlfiles: List[StrictStr] xmlfiles: List[str]
model_config = ConfigDict(extra="forbid") model_config = ConfigDict(extra="forbid")
@ -492,7 +492,6 @@ class _VariableCalculation(Calculation):
propertyerror: bool = True, propertyerror: bool = True,
allow_none: bool = False allow_none: bool = False
optional: bool = False optional: bool = False
identifier: Optional[Calculation] = None
def get_variable( def get_variable(
self, self,
@ -541,15 +540,6 @@ class _VariableCalculation(Calculation):
variable_in_calculation_identifier: Optional[str], variable_in_calculation_identifier: Optional[str],
key: str = None, key: str = None,
): ):
identifier_is_a_calculation = False
if self.identifier:
if variable_in_calculation_identifier:
msg = _(
'variable "{0}" has an attribute "{1}" with an identifier "{2}" but the path has also the identifier "{3}"'
).format(path, self.attribute_name, self.variable, self.identifier, variable_in_calculation_identifier)
raise DictConsistencyError(msg, 89, self.xmlfiles)
variable_in_calculation_identifier = self.identifier
identifier_is_a_calculation = True
if not variable_in_calculation: if not variable_in_calculation:
if not objectspace.force_optional: if not objectspace.force_optional:
msg = _( msg = _(
@ -574,10 +564,9 @@ class _VariableCalculation(Calculation):
params["__default_value"] = self.default_values params["__default_value"] = self.default_values
if self.allow_none: if self.allow_none:
params["allow_none"] = True params["allow_none"] = True
if not identifier_is_a_calculation: self.check_multi(
self.check_multi( objectspace, path, variable_in_calculation_path, variable_in_calculation
objectspace, path, variable_in_calculation_path, variable_in_calculation, )
)
if path in objectspace.followers: if path in objectspace.followers:
multi = objectspace.multis[path] == "submulti" multi = objectspace.multis[path] == "submulti"
else: else:
@ -729,7 +718,7 @@ class _VariableCalculation(Calculation):
class VariableCalculation(_VariableCalculation): class VariableCalculation(_VariableCalculation):
attribute_name: Literal["default", "choices", "dynamic", "identifier"] attribute_name: Literal["default", "choices", "dynamic"]
default: Any = undefined default: Any = undefined
def to_function( def to_function(
@ -1050,7 +1039,7 @@ SECRET_BASETYPE_CALC = Union[StrictStr, JinjaCalculation]
class Family(BaseModel): class Family(BaseModel):
name: StrictStr name: str
# informations # informations
description: Optional[StrictStr] = None description: Optional[StrictStr] = None
help: Optional[StrictStr] = None help: Optional[StrictStr] = None
@ -1071,12 +1060,12 @@ class Family(BaseModel):
class Dynamic(Family): class Dynamic(Family):
# None only for format 1.0 # None only for format 1.0
variable: StrictStr = None variable: str = None
dynamic: Union[List[Union[StrictStr, Calculation]], Calculation] dynamic: Union[List[Union[StrictStr, Calculation]], Calculation]
class Variable(BaseModel): class Variable(BaseModel):
name: StrictStr name: str
# user informations # user informations
description: Optional[StrictStr] = None description: Optional[StrictStr] = None
help: Optional[StrictStr] = None help: Optional[StrictStr] = None
@ -1086,7 +1075,7 @@ class Variable(BaseModel):
test: Optional[list] = None test: Optional[list] = None
# validations # validations
## type will be set dynamically in `annotator/value.py`, default is None ## type will be set dynamically in `annotator/value.py`, default is None
type: StrictStr = None type: str = None
params: Optional[List[Param]] = None params: Optional[List[Param]] = None
regexp: Optional[StrictStr] = None regexp: Optional[StrictStr] = None
choices: Optional[Union[List[BASETYPE_CALC], Calculation]] = None choices: Optional[Union[List[BASETYPE_CALC], Calculation]] = None
@ -1105,21 +1094,21 @@ class Variable(BaseModel):
disabled: Union[bool, Calculation] = False disabled: Union[bool, Calculation] = False
frozen: Union[bool, Calculation] = False frozen: Union[bool, Calculation] = False
# others # others
path: StrictStr path: str
namespace: Optional[StrictStr] namespace: Optional[str]
version: StrictStr version: str
xmlfiles: List[StrictStr] = [] xmlfiles: List[str] = []
model_config = ConfigDict(extra="forbid", arbitrary_types_allowed=True) model_config = ConfigDict(extra="forbid", arbitrary_types_allowed=True)
class SymLink(BaseModel): class SymLink(BaseModel):
type: Literal["symlink"] = "symlink" type: Literal["symlink"] = "symlink"
name: StrictStr name: str
path: StrictStr path: str
opt: Variable opt: Variable
namespace: Optional[StrictStr] namespace: Optional[str]
version: StrictStr version: str
xmlfiles: List[StrictStr] = [] xmlfiles: List[str] = []
model_config = ConfigDict(extra="forbid") model_config = ConfigDict(extra="forbid")

View file

@ -362,16 +362,12 @@ class Common:
) )
raise DictConsistencyError(msg, 49, self.elt.xmlfiles) raise DictConsistencyError(msg, 49, self.elt.xmlfiles)
param_type = "ParamDynOption" param_type = "ParamDynOption"
if isinstance(identifier, Calculation): identifiers = []
identifiers = self.populate_calculation(identifier) for ident in identifier:
else: if isinstance(ident, str):
identifiers = [] ident = self.convert_str(ident)
for ident in identifier: identifiers.append(str(ident))
if isinstance(ident, str): params.append("[" + ", ".join(identifiers) + "]")
ident = self.convert_str(ident)
identifiers.append(str(ident))
identifiers = "[" + ", ".join(identifiers) + "]"
params.append(identifiers)
if param.get("optional", False): if param.get("optional", False):
params.append("optional=True") params.append("optional=True")
else: else:

View file

@ -611,40 +611,3 @@ def convert_value(option, value, needs_convert):
except: except:
pass pass
return value return value
def mandatories(config, errors: list) -> None:
try:
mandatories = config.value.mandatory()
except (ConfigError, PropertiesOptionError, ValueError) as err:
try:
subconfig = err.subconfig
except AttributeError:
subconfig = None
if subconfig:
err.prefix = ""
errors.append({str(err): subconfig})
else:
errors.append(str(err))
else:
for option in mandatories:
_populate_mandatory(option, errors)
def _populate_mandatory(option, errors: list) -> None:
try:
option.value.get()
except PropertiesOptionError as err:
if "empty" in err.proptype:
msg = _("null is not a valid value for a multi")
elif "mandatory" in err.proptype:
msg = _("mandatory variable but has no value")
else:
msg = _("mandatory variable but is inaccessible and has no value or has null in value")
else:
proptype = option.value.mandatory(return_type=True)
if proptype == "empty":
msg = _("null is not a valid value for a multi")
elif proptype == "mandatory":
msg = _("mandatory variable but has no value")
errors.append({msg: option._subconfig})

View file

@ -1 +1 @@
[] ["rougail.var1"]

View file

@ -1,25 +0,0 @@
{
"rougail.var1": {
"owner": "default",
"value": [
"val1",
"val2"
]
},
"rougail.var2": {
"owner": "default",
"value": "val1"
},
"rougail.dynval1.var": {
"owner": "default",
"value": "val1"
},
"rougail.dynval2.var": {
"owner": "default",
"value": "val2"
},
"rougail.var3": {
"owner": "default",
"value": "val1"
}
}

View file

@ -1,10 +0,0 @@
{
"rougail.var1": [
"val1",
"val2"
],
"rougail.var2": "val1",
"rougail.dynval1.var": "val1",
"rougail.dynval2.var": "val2",
"rougail.var3": "val1"
}

View file

@ -1,25 +0,0 @@
{
"rougail.var1": {
"owner": "default",
"value": [
"val1",
"val2"
]
},
"rougail.var2": {
"owner": "default",
"value": "val1"
},
"rougail.dynval1.var": {
"owner": "default",
"value": "val1"
},
"rougail.dynval2.var": {
"owner": "default",
"value": "val2"
},
"rougail.var3": {
"owner": "default",
"value": "val1"
}
}

View file

@ -1,10 +0,0 @@
{
"rougail.var1": [
"val1",
"val2"
],
"rougail.var2": "val1",
"rougail.dynval1.var": "val1",
"rougail.dynval2.var": "val2",
"rougail.var3": "val1"
}

View file

@ -1,19 +0,0 @@
from tiramisu import *
from tiramisu.setting import ALLOWED_LEADER_PROPERTIES
from re import compile as re_compile
from rougail.tiramisu import func, dict_env, load_functions, ConvertDynOptionDescription
load_functions('../rougail-tests/funcs/test.py')
try:
groups.namespace
except:
groups.addgroup('namespace')
ALLOWED_LEADER_PROPERTIES.add("basic")
ALLOWED_LEADER_PROPERTIES.add("standard")
ALLOWED_LEADER_PROPERTIES.add("advanced")
option_2 = StrOption(name="var1", doc="A suffix variable", multi=True, default=["val1", "val2"], default_multi="val1", properties=frozenset({"mandatory", "standard"}), informations={'ymlfiles': ['../rougail-tests/structures/60_5family_dynamic_calc_identifier/rougail/00-base.yml'], 'type': 'string'})
option_3 = StrOption(name="var2", doc="A suffix variable2", default="val1", properties=frozenset({"mandatory", "standard"}), informations={'ymlfiles': ['../rougail-tests/structures/60_5family_dynamic_calc_identifier/rougail/00-base.yml'], 'type': 'string'})
option_5 = StrOption(name="var", doc="A dynamic variable", default=Calculation(func['calc_value'], Params((ParamIdentifier()))), properties=frozenset({"mandatory", "standard"}), informations={'ymlfiles': ['../rougail-tests/structures/60_5family_dynamic_calc_identifier/rougail/00-base.yml'], 'type': 'string'})
optiondescription_4 = ConvertDynOptionDescription(name="dyn{{ identifier }}", doc="dyn{{ identifier }}", identifiers=Calculation(func['calc_value'], Params((ParamOption(option_2)))), children=[option_5], properties=frozenset({"standard"}), informations={'dynamic_variable': 'rougail.var1', 'ymlfiles': ['../rougail-tests/structures/60_5family_dynamic_calc_identifier/rougail/00-base.yml']})
option_6 = StrOption(name="var3", doc="A variable calculated", default=Calculation(func['calc_value'], Params((ParamDynOption(option_5, Calculation(func['calc_value'], Params((ParamOption(option_3)))))))), properties=frozenset({"mandatory", "standard"}), informations={'ymlfiles': ['../rougail-tests/structures/60_5family_dynamic_calc_identifier/rougail/00-base.yml'], 'type': 'string'})
optiondescription_1 = OptionDescription(name="rougail", doc="Rougail", group_type=groups.namespace, children=[option_2, option_3, optiondescription_4, option_6], properties=frozenset({"standard"}))
option_0 = OptionDescription(name="baseoption", doc="baseoption", children=[optiondescription_1])

View file

@ -1,18 +0,0 @@
from tiramisu import *
from tiramisu.setting import ALLOWED_LEADER_PROPERTIES
from re import compile as re_compile
from rougail.tiramisu import func, dict_env, load_functions, ConvertDynOptionDescription
load_functions('../rougail-tests/funcs/test.py')
try:
groups.namespace
except:
groups.addgroup('namespace')
ALLOWED_LEADER_PROPERTIES.add("basic")
ALLOWED_LEADER_PROPERTIES.add("standard")
ALLOWED_LEADER_PROPERTIES.add("advanced")
option_1 = StrOption(name="var1", doc="A suffix variable", multi=True, default=["val1", "val2"], default_multi="val1", properties=frozenset({"mandatory", "standard"}), informations={'ymlfiles': ['../rougail-tests/structures/60_5family_dynamic_calc_identifier/rougail/00-base.yml'], 'type': 'string'})
option_2 = StrOption(name="var2", doc="A suffix variable2", default="val1", properties=frozenset({"mandatory", "standard"}), informations={'ymlfiles': ['../rougail-tests/structures/60_5family_dynamic_calc_identifier/rougail/00-base.yml'], 'type': 'string'})
option_4 = StrOption(name="var", doc="A dynamic variable", default=Calculation(func['calc_value'], Params((ParamIdentifier()))), properties=frozenset({"mandatory", "standard"}), informations={'ymlfiles': ['../rougail-tests/structures/60_5family_dynamic_calc_identifier/rougail/00-base.yml'], 'type': 'string'})
optiondescription_3 = ConvertDynOptionDescription(name="dyn{{ identifier }}", doc="dyn{{ identifier }}", identifiers=Calculation(func['calc_value'], Params((ParamOption(option_1)))), children=[option_4], properties=frozenset({"standard"}), informations={'dynamic_variable': 'var1', 'ymlfiles': ['../rougail-tests/structures/60_5family_dynamic_calc_identifier/rougail/00-base.yml']})
option_5 = StrOption(name="var3", doc="A variable calculated", default=Calculation(func['calc_value'], Params((ParamDynOption(option_4, Calculation(func['calc_value'], Params((ParamOption(option_2)))))))), properties=frozenset({"mandatory", "standard"}), informations={'ymlfiles': ['../rougail-tests/structures/60_5family_dynamic_calc_identifier/rougail/00-base.yml'], 'type': 'string'})
option_0 = OptionDescription(name="baseoption", doc="baseoption", children=[option_1, option_2, optiondescription_3, option_5])