diff --git a/CHANGELOG.md b/CHANGELOG.md
index de7c638ee..a11e85e93 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,9 @@
+## 1.2.0a26 (2025-05-26)
+
+### Fix
+
+- user_data better support for follower variable
+
## 1.2.0a25 (2025-05-14)
### Feat
diff --git a/pyproject.toml b/pyproject.toml
index 3549f057e..1b9ebb78b 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,7 +4,7 @@ requires = ["flit_core >=3.8.0,<4"]
[project]
name = "rougail"
-version = "1.2.0a25"
+version = "1.2.0a26"
authors = [{name = "Emmanuel Garette", email = "gnunux@gnunux.info"}]
readme = "README.md"
description = "A consistency handling system that was initially designed in the configuration management"
diff --git a/src/rougail/__init__.py b/src/rougail/__init__.py
index e7b1fd50c..60ba4d432 100644
--- a/src/rougail/__init__.py
+++ b/src/rougail/__init__.py
@@ -33,44 +33,10 @@ from .config import RougailConfig
from .utils import normalize_family
from .object_model import CONVERT_OPTION
from .user_datas import UserDatas
+from .tiramisu import tiramisu_display_name
from .__version__ import __version__
-def tiramisu_display_name(
- kls,
- subconfig,
- with_quote: bool = False,
-) -> str:
- """Replace the Tiramisu display_name function to display path + description"""
- config_bag = subconfig.config_bag
- context = config_bag.context
- values = context.get_values()
- context_subconfig = context.get_root(config_bag)
- doc = values.get_information(subconfig, "doc", None)
- comment = doc if doc and doc != kls.impl_getname() else ""
- if "{{ identifier }}" in comment and subconfig.identifiers:
- comment = comment.replace("{{ identifier }}", str(subconfig.identifiers[-1]))
- path_in_description = values.get_information(
- context_subconfig, "path_in_description", True
- )
- if path_in_description or not comment:
- comment = f" ({comment})" if comment else ""
- if path_in_description is False:
- path = kls.impl_getname()
- else:
- path = kls.impl_getpath()
- if "{{ identifier }}" in path and subconfig.identifiers:
- path = path.replace(
- "{{ identifier }}", normalize_family(str(subconfig.identifiers[-1]))
- )
- else:
- path = comment
- comment = ""
- if with_quote:
- return f'"{path}"{comment}'
- return f"{path}{comment}"
-
-
class Rougail(UserDatas):
"""Main Rougail object"""
diff --git a/src/rougail/__version__.py b/src/rougail/__version__.py
index eeba43e76..b79497be7 100644
--- a/src/rougail/__version__.py
+++ b/src/rougail/__version__.py
@@ -1 +1 @@
-__version__ = "1.2.0a25"
+__version__ = "1.2.0a26"
diff --git a/src/rougail/error.py b/src/rougail/error.py
index c39e21b69..ab0161a40 100644
--- a/src/rougail/error.py
+++ b/src/rougail/error.py
@@ -28,13 +28,6 @@ along with this program. If not, see .
from .i18n import _
-def display_xmlfiles(xmlfiles: list) -> str:
- """The function format xmlfiles informations to generate errors"""
- if len(xmlfiles) == 1:
- return '"' + xmlfiles[0] + '"'
- return '"' + '", "'.join(xmlfiles[:-1]) + '"' + " and " + '"' + xmlfiles[-1] + '"'
-
-
class ConfigError(Exception):
"""Standard error for templating"""
diff --git a/src/rougail/object_model.py b/src/rougail/object_model.py
index db4779b9d..98a275039 100644
--- a/src/rougail/object_model.py
+++ b/src/rougail/object_model.py
@@ -31,82 +31,12 @@ from tiramisu.config import get_common_path
from .utils import get_jinja_variable_to_param, calc_multi_for_type_variable, undefined
from .i18n import _
from .error import DictConsistencyError, VariableCalculationDependencyError
+from .tiramisu import CONVERT_OPTION
BASETYPE = Union[StrictBool, StrictInt, StrictFloat, StrictStr, None]
PROPERTY_ATTRIBUTE = ["frozen", "hidden", "disabled", "mandatory"]
-def convert_boolean(value: str) -> bool:
- """Boolean coercion. The Rougail XML may contain srings like `True` or `False`"""
- if isinstance(value, bool):
- return value
- value = value.lower()
- if value == "true":
- return True
- elif value == "false":
- return False
- elif value in ["", None]:
- return None
- raise Exception(_('unknown boolean value "{0}"').format(value))
-
-
-CONVERT_OPTION = {
- "string": dict(opttype="StrOption", example="example"),
- "number": dict(opttype="IntOption", func=int, example=42),
- "float": dict(opttype="FloatOption", func=float, example=1.42),
- "boolean": dict(opttype="BoolOption", func=convert_boolean),
- "secret": dict(opttype="PasswordOption", example="secrets"),
- "mail": dict(opttype="EmailOption", example="user@example.net"),
- "unix_filename": dict(opttype="FilenameOption", example="/tmp/myfile.txt"),
- "date": dict(opttype="DateOption", example="2000-01-01"),
- "unix_user": dict(opttype="UsernameOption", example="username"),
- "ip": dict(
- opttype="IPOption", initkwargs={"allow_reserved": True}, example="1.1.1.1"
- ),
- "cidr": dict(opttype="IPOption", initkwargs={"cidr": True}, example="1.1.1.0/24"),
- "netmask": dict(opttype="NetmaskOption", example="255.255.255.0"),
- "network": dict(opttype="NetworkOption", example="1.1.1.0"),
- "network_cidr": dict(
- opttype="NetworkOption", initkwargs={"cidr": True}, example="1.1.1.0/24"
- ),
- "broadcast": dict(opttype="BroadcastOption", example="1.1.1.255"),
- "netbios": dict(
- opttype="DomainnameOption",
- initkwargs={"type": "netbios", "warnings_only": True},
- example="example",
- ),
- "domainname": dict(
- opttype="DomainnameOption",
- initkwargs={"type": "domainname", "allow_ip": False},
- example="example.net",
- ),
- "hostname": dict(
- opttype="DomainnameOption",
- initkwargs={"type": "hostname", "allow_ip": False},
- example="example",
- ),
- "web_address": dict(
- opttype="URLOption",
- initkwargs={"allow_ip": False, "allow_without_dot": True},
- example="https://example.net",
- ),
- "port": dict(
- opttype="PortOption", initkwargs={"allow_private": True}, example="111"
- ),
- "mac": dict(opttype="MACOption", example="00:00:00:00:00"),
- "unix_permissions": dict(
- opttype="PermissionsOption",
- initkwargs={"warnings_only": True},
- func=int,
- example="644",
- ),
- "choice": dict(opttype="ChoiceOption", example="a_choice"),
- "regexp": dict(opttype="RegexpOption"),
- #
- "symlink": dict(opttype="SymLinkOption"),
-}
-
-
def get_convert_option_types():
for typ, datas in CONVERT_OPTION.items():
obj = getattr(tiramisu, datas["opttype"])
diff --git a/src/rougail/tiramisu.py b/src/rougail/tiramisu.py
index 29f68fb93..024ca6c9a 100644
--- a/src/rougail/tiramisu.py
+++ b/src/rougail/tiramisu.py
@@ -32,10 +32,9 @@ from importlib.util import (
spec_from_loader as _spec_from_loader,
module_from_spec as _module_from_spec,
)
+from unicodedata import normalize, combining
from jinja2 import StrictUndefined, DictLoader
from jinja2.sandbox import SandboxedEnvironment
-from rougail.object_model import CONVERT_OPTION
-from rougail.error import display_xmlfiles
from tiramisu import DynOptionDescription, calc_value, function_waiting_for_error
from tiramisu.error import (
ValueWarning,
@@ -44,12 +43,97 @@ from tiramisu.error import (
CancelParam,
errors,
)
-from .utils import normalize_family
-from .i18n import _
+
ori_raise_carry_out_calculation_error = errors.raise_carry_out_calculation_error
+try:
+ from .i18n import _
+except ModuleNotFoundError:
+ # FIXME
+ def _(msg):
+ return msg
+
+
+def display_xmlfiles(xmlfiles: list) -> str:
+ """The function format xmlfiles informations to generate errors"""
+ if len(xmlfiles) == 1:
+ return '"' + xmlfiles[0] + '"'
+ return '"' + '", "'.join(xmlfiles[:-1]) + '"' + " and " + '"' + xmlfiles[-1] + '"'
+
+
+def convert_boolean(value: str) -> bool:
+ """Boolean coercion. The Rougail XML may contain srings like `True` or `False`"""
+ if isinstance(value, bool):
+ return value
+ value = value.lower()
+ if value == "true":
+ return True
+ elif value == "false":
+ return False
+ elif value in ["", None]:
+ return None
+ raise Exception(_('unknown boolean value "{0}"').format(value))
+
+
+CONVERT_OPTION = {
+ "string": dict(opttype="StrOption", example="example"),
+ "number": dict(opttype="IntOption", func=int, example=42),
+ "float": dict(opttype="FloatOption", func=float, example=1.42),
+ "boolean": dict(opttype="BoolOption", func=convert_boolean),
+ "secret": dict(opttype="PasswordOption", example="secrets"),
+ "mail": dict(opttype="EmailOption", example="user@example.net"),
+ "unix_filename": dict(opttype="FilenameOption", example="/tmp/myfile.txt"),
+ "date": dict(opttype="DateOption", example="2000-01-01"),
+ "unix_user": dict(opttype="UsernameOption", example="username"),
+ "ip": dict(
+ opttype="IPOption", initkwargs={"allow_reserved": True}, example="1.1.1.1"
+ ),
+ "cidr": dict(opttype="IPOption", initkwargs={"cidr": True}, example="1.1.1.0/24"),
+ "netmask": dict(opttype="NetmaskOption", example="255.255.255.0"),
+ "network": dict(opttype="NetworkOption", example="1.1.1.0"),
+ "network_cidr": dict(
+ opttype="NetworkOption", initkwargs={"cidr": True}, example="1.1.1.0/24"
+ ),
+ "broadcast": dict(opttype="BroadcastOption", example="1.1.1.255"),
+ "netbios": dict(
+ opttype="DomainnameOption",
+ initkwargs={"type": "netbios", "warnings_only": True},
+ example="example",
+ ),
+ "domainname": dict(
+ opttype="DomainnameOption",
+ initkwargs={"type": "domainname", "allow_ip": False},
+ example="example.net",
+ ),
+ "hostname": dict(
+ opttype="DomainnameOption",
+ initkwargs={"type": "hostname", "allow_ip": False},
+ example="example",
+ ),
+ "web_address": dict(
+ opttype="URLOption",
+ initkwargs={"allow_ip": False, "allow_without_dot": True},
+ example="https://example.net",
+ ),
+ "port": dict(
+ opttype="PortOption", initkwargs={"allow_private": True}, example="111"
+ ),
+ "mac": dict(opttype="MACOption", example="00:00:00:00:00"),
+ "unix_permissions": dict(
+ opttype="PermissionsOption",
+ initkwargs={"warnings_only": True},
+ func=int,
+ example="644",
+ ),
+ "choice": dict(opttype="ChoiceOption", example="a_choice"),
+ "regexp": dict(opttype="RegexpOption"),
+ #
+ "symlink": dict(opttype="SymLinkOption"),
+}
+
+
def raise_carry_out_calculation_error(subconfig, *args, **kwargs):
try:
ori_raise_carry_out_calculation_error(subconfig, *args, **kwargs)
@@ -122,6 +206,52 @@ def load_functions(path, dict_func=None):
dict_func[function] = getattr(func_, function)
+def normalize_family(family_name: str) -> str:
+ """replace space, accent, uppercase, ... by valid character"""
+ if not family_name:
+ return
+ family_name = family_name.lower()
+ family_name = family_name.replace("-", "_").replace(" ", "_").replace(".", "_")
+ nfkd_form = normalize("NFKD", family_name)
+ family_name = "".join([c for c in nfkd_form if not combining(c)])
+ return family_name.lower()
+
+
+def tiramisu_display_name(
+ kls,
+ subconfig,
+ with_quote: bool = False,
+) -> str:
+ """Replace the Tiramisu display_name function to display path + description"""
+ config_bag = subconfig.config_bag
+ context = config_bag.context
+ values = context.get_values()
+ context_subconfig = context.get_root(config_bag)
+ doc = values.get_information(subconfig, "doc", None)
+ comment = doc if doc and doc != kls.impl_getname() else ""
+ if "{{ identifier }}" in comment and subconfig.identifiers:
+ comment = comment.replace("{{ identifier }}", str(subconfig.identifiers[-1]))
+ path_in_description = values.get_information(
+ context_subconfig, "path_in_description", True
+ )
+ if path_in_description or not comment:
+ comment = f" ({comment})" if comment else ""
+ if path_in_description is False:
+ path = kls.impl_getname()
+ else:
+ path = kls.impl_getpath()
+ if "{{ identifier }}" in path and subconfig.identifiers:
+ path = path.replace(
+ "{{ identifier }}", normalize_family(str(subconfig.identifiers[-1]))
+ )
+ else:
+ path = comment
+ comment = ""
+ if with_quote:
+ return f'"{path}"{comment}'
+ return f"{path}{comment}"
+
+
def rougail_calc_value(*args, __default_value=None, __internal_multi=False, **kwargs):
values = calc_value(*args, **kwargs)
if values is None and __internal_multi:
diff --git a/src/rougail/utils.py b/src/rougail/utils.py
index 71140e64d..8ed16d099 100644
--- a/src/rougail/utils.py
+++ b/src/rougail/utils.py
@@ -26,7 +26,6 @@ along with this program. If not, see .
"""
from typing import List, Union
-from unicodedata import normalize, combining
import re
from itertools import chain
@@ -58,17 +57,6 @@ def valid_variable_family_name(
raise DictConsistencyError(msg, 76, xmlfiles)
-def normalize_family(family_name: str) -> str:
- """replace space, accent, uppercase, ... by valid character"""
- if not family_name:
- return
- family_name = family_name.lower()
- family_name = family_name.replace("-", "_").replace(" ", "_").replace(".", "_")
- nfkd_form = normalize("NFKD", family_name)
- family_name = "".join([c for c in nfkd_form if not combining(c)])
- return family_name.lower()
-
-
def load_modules(name, module) -> List[str]:
"""list all functions in a module"""
loader = SourceFileLoader(name, module)