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)