rougail-output-display/src/rougail/output_display/display.py

389 lines
15 KiB
Python
Raw Normal View History

2025-12-02 13:59:33 +01:00
"""
Silique (https://www.silique.fr)
2026-01-02 17:20:34 +01:00
Copyright (C) 2022-2026
2025-12-02 13:59:33 +01:00
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/>.
"""
2025-12-03 21:49:02 +01:00
from typing import Optional, Any
2025-12-02 13:59:33 +01:00
from ruamel.yaml import YAML
2025-12-03 21:49:02 +01:00
from io import BytesIO
2025-12-02 13:59:33 +01:00
from tiramisu.error import PropertiesOptionError, ConfigError
2025-12-03 21:49:02 +01:00
from tiramisu import owners, groups
2025-12-02 13:59:33 +01:00
2025-12-03 21:49:02 +01:00
from .config import OutPuts
2025-12-02 13:59:33 +01:00
from .i18n import _
class RougailOutputDisplay:
def __init__(
self,
config: "Config",
*,
rougailconfig: "RougailConfig" = None,
2025-12-03 21:49:02 +01:00
root_config=None,
2025-12-02 13:59:33 +01:00
user_data_errors: Optional[list] = None,
user_data_warnings: Optional[list] = None,
config_owner_is_path: bool = False,
layer_datas=None,
2026-01-15 21:20:14 +01:00
true_config=None,
2025-12-02 13:59:33 +01:00
**kwargs,
) -> None:
if rougailconfig is None:
from rougail import RougailConfig
rougailconfig = RougailConfig
self.rougailconfig = rougailconfig
self.config = config
2026-01-15 21:20:14 +01:00
if true_config is None:
self.true_config = config
else:
self.true_config = true_config
2025-12-03 21:49:02 +01:00
self.root_config = root_config
2025-12-02 13:59:33 +01:00
self.layer_datas = layer_datas
self.config_owner_is_path = config_owner_is_path
self.errors = []
self.warnings = []
if user_data_errors is None:
user_data_errors = []
self.user_data_errors = user_data_errors
if user_data_warnings is None:
user_data_warnings = []
self.user_data_warnings = user_data_warnings
2025-12-03 21:49:02 +01:00
# self.out = []
self.nodes = None
self.yaml = YAML()
self.yaml.indent(mapping=2, sequence=4, offset=2)
2025-12-02 13:59:33 +01:00
def run(self) -> str:
self.is_mandatory = self.rougailconfig["display.mandatory"]
if self.is_mandatory:
2026-01-15 21:20:14 +01:00
ori_properties = self.true_config.property.exportation()
self.true_config.property.read_write()
2025-12-02 13:59:33 +01:00
if not self.user_data_errors and not self.errors:
self.mandatories()
2026-01-15 21:20:14 +01:00
self.true_config.property.importation(ori_properties)
2025-12-03 21:49:02 +01:00
output_format = self.rougailconfig["display.output_format"]
output = OutPuts().get()[output_format](self.rougailconfig)
2025-12-02 13:59:33 +01:00
warnings = self.user_data_warnings + self.warnings
errors = self.user_data_errors + self.errors
2025-12-03 21:49:02 +01:00
level = None
errors_warnings_dict = {}
2026-01-01 09:22:58 +01:00
if not errors and self.nodes is None:
show_secrets = self.rougailconfig["display.show_secrets"]
self.nodes = Node(self.yaml, show_secrets, self.config, self.root_config, self.config_owner_is_path, errors)
2025-12-03 21:49:02 +01:00
if warnings:
level = "warnings"
#output.display_warnings(errors_warnings_dict, warnings)
for warning in warnings:
output.error_warn_to_dict(warning, errors_warnings_dict, 'warning')
2025-12-02 13:59:33 +01:00
if errors:
2025-12-03 21:49:02 +01:00
level = "errors"
for error in errors:
output.error_warn_to_dict(error, errors_warnings_dict, 'error')
if level:
if level == "errors":
tree = output.error_header()
else:
tree = output.warning_header()
ret = output.parse_error_warning(tree, errors_warnings_dict, output.display_error, level)
if errors:
return False, ret
ret += "\n"
else:
ret = ''
2026-01-01 09:22:58 +01:00
code, run = output.run(self.nodes, self.layer_datas)
2025-12-03 21:49:02 +01:00
return code, ret + run
def print(self) -> None:
status, ret = self.run()
print(ret)
return status
#
# def exporter(self) -> bool:
# if self.is_mandatory:
# ori_properties = self.config.property.exportation()
# self.config.property.read_write()
# if not self.user_data_errors and not self.errors:
# self.mandatories()
# self.config.property.importation(ori_properties)
# warnings = self.user_data_warnings + self.warnings
# if warnings:
# self.output.display_warnings(warnings)
#
# errors = self.user_data_errors + self.errors
# if errors:
# self.output.display_errors(errors)
# return False
# self.output.out = []
# if self.output.has_variable():
# self.output.header()
# self.output.end()
# return True
2025-12-02 13:59:33 +01:00
def mandatories(self) -> None:
try:
mandatories = self.config.value.mandatory()
2025-12-03 21:49:02 +01:00
except (ConfigError, PropertiesOptionError, ValueError) as err:
try:
subconfig = err.subconfig
except AttributeError:
subconfig = None
if subconfig:
err.prefix = ""
self.errors.append({str(err): subconfig})
else:
self.errors.append(str(err))
2025-12-02 13:59:33 +01:00
else:
2025-12-03 21:49:02 +01:00
self.populate_mandatories(mandatories)
2025-12-02 13:59:33 +01:00
2025-12-03 21:49:02 +01:00
def populate_mandatories(self, mandatories: list) -> None:
2025-12-02 13:59:33 +01:00
for option in mandatories:
try:
option.value.get()
except PropertiesOptionError:
2025-12-03 21:49:02 +01:00
self.errors.append(
{
_(
"mandatory variable but is inaccessible and has no value"
): option._subconfig,
}
)
2025-12-02 13:59:33 +01:00
else:
2025-12-03 21:49:02 +01:00
self.errors.append({_("mandatory variable but has no value"): option._subconfig})
2025-12-02 13:59:33 +01:00
2025-12-03 21:49:02 +01:00
class Node:
def __init__(
self,
yaml,
show_secrets,
config,
root_config,
config_owner_is_path,
2026-01-01 09:22:58 +01:00
errors,
2025-12-03 21:49:02 +01:00
*,
node=None,
values: Optional[dict]=None,
level: int=0,
leader_index: Optional[int]=None,
) -> None:
self.yaml = yaml
self.show_secrets = show_secrets
self.config = config
self.root_config = root_config
self.config_owner_is_path = config_owner_is_path
2026-01-01 09:22:58 +01:00
self.errors = errors
2025-12-03 21:49:02 +01:00
if node is None:
values = self.config.value.get()
self.level = level
if node:
self.hidden = "hidden" in node.property.get()
self.description = node.description()
else:
self.hidden = None
self.description = _("Variables:")
self.level = level
self.children = []
if node and node.isoptiondescription() and node.isleadership():
values_iter = iter(values.items())
leader, leader_values = next(values_iter)
followers_values = {idx: {leader: value} for idx, value in enumerate(leader_values)}
for follower, follower_value in values_iter:
followers_values[follower.index()][follower] = follower_value
for idx, fvalues in enumerate(followers_values.values()):
self.add_node(leader, fvalues, leader_index=idx)
else:
for option, value in values.items():
if option.isoptiondescription():
if option.group_type() == groups.namespace and not value:
continue
if option.isleadership():
leader, leader_values = next(iter(value.items()))
if not leader_values:
self.add_leaf(leader, [], description=option.description())
else:
self.add_node(option, value)
else:
if value == {}:
self.add_leaf(option, "{}")
else:
self.add_node(option, value)
2025-12-02 13:59:33 +01:00
else:
2025-12-03 21:49:02 +01:00
if option.isleader():
index = leader_index
else:
index = None
self.add_leaf(option, value, leader_index=index)
2025-12-02 13:59:33 +01:00
2025-12-03 21:49:02 +01:00
def add_node(
2025-12-02 13:59:33 +01:00
self,
2025-12-03 21:49:02 +01:00
option,
values,
*,
leader_index: Optional[int]=None,
) -> 'Node':
self.children.append({"type": "node",
2026-01-01 09:22:58 +01:00
"node": Node(
2025-12-03 21:49:02 +01:00
self.yaml,
self.show_secrets,
self.config,
self.root_config,
self.config_owner_is_path,
2026-01-01 09:22:58 +01:00
self.errors,
2025-12-03 21:49:02 +01:00
node=option,
values=values,
level=self.level + 1,
leader_index=leader_index,
),
})
def add_leaf(
self, option, value: Any, *, leader_index: Optional[int] = None, description: Optional[str] = None
2025-12-02 13:59:33 +01:00
):
2025-12-03 21:49:02 +01:00
properties = option.property.get()
if description is None:
description = option.description()
icon = "leaf"
else:
icon = "node"
self.children.append({"type": "leaf",
"description": description,
"values": self.get_values(option, value, leader_index, properties),
"icon": icon,
"hidden": "hidden" in properties,
},
)
def get_values(self, option: "Option", value: Any, leader_index: Optional[int], properties: list[str]) -> None:
if option.isoptiondescription():
return [{"is_default": True, "value": value, "loaded_from": None}]
values = []
meta_config = self.config
meta_option = option
index = option.index()
if index is not None:
loaded_from_key = f"loaded_from_{index}"
else:
loaded_from_key = "loaded_from"
#default = option.owner.isdefault() or not option.information.get("default_value_makes_sense", True)
force_store_value = "force_store_value" in properties
is_default = option.owner.isdefault()
option_path = option.path()
2026-01-01 21:39:06 +01:00
default_owner = owners.default
2026-01-13 10:12:31 +01:00
if self.root_config == meta_config:
added = True
true_default = is_default
else:
true_default = is_default and meta_option.owner.get() == default_owner
added = not is_default or true_default
secret_manager = option.information.get('secret_manager', False)
2025-12-03 21:49:02 +01:00
while True:
if values and true_default and (value in [None, []] or secret_manager):
2025-12-03 21:49:02 +01:00
break
if isinstance(value, list):
value = [self.convert_value(meta_option, val) for val in value]
else:
value = self.convert_value(meta_option, value)
2026-01-01 21:39:06 +01:00
if true_default:
loaded_from = None
else:
loaded_from = meta_option.information.get(loaded_from_key, None)
2026-01-13 10:12:31 +01:00
if added or force_store_value:
2026-01-01 21:39:06 +01:00
values.append(
{
"value": value,
"is_default": true_default,
"loaded_from": loaded_from,
}
)
if true_default or force_store_value:
break
2025-12-03 21:49:02 +01:00
meta_config_path = meta_config.path()
2026-01-01 21:39:06 +01:00
if is_default and (not meta_config_path or "." not in meta_config_path):
# we are in root metaconfig and we have default value
2025-12-03 21:49:02 +01:00
break
new_meta_config = self.get_metaconfig_with_default_value(meta_config, meta_option)
2026-01-01 21:39:06 +01:00
if not new_meta_config:
2025-12-03 21:49:02 +01:00
break
meta_option = new_meta_config.option(option_path, index)
if new_meta_config == meta_config:
2026-01-01 21:39:06 +01:00
# we already have current value, so we search default's one
2025-12-03 21:49:02 +01:00
is_default = True
2026-01-01 21:39:06 +01:00
true_default = True
2025-12-03 21:49:02 +01:00
value = meta_option.value.default()
else:
is_default = meta_option.owner.isdefault()
2026-01-01 09:22:58 +01:00
try:
value = meta_option.value.get()
except ValueError as err:
2026-01-01 21:39:06 +01:00
if not is_default:
2026-01-01 09:28:58 +01:00
meta_option._set_subconfig()
self.errors.append({str(err): meta_option._subconfig})
2026-01-01 09:22:58 +01:00
break
except Exception as err:
break
2026-01-01 21:39:06 +01:00
true_default = is_default and meta_option.owner.get() == default_owner
2026-01-13 10:12:31 +01:00
added = True
2025-12-03 21:49:02 +01:00
meta_config = new_meta_config
if leader_index is not None:
if len(value) > leader_index:
value = value[leader_index]
else:
value = None
return values
2025-12-02 13:59:33 +01:00
2025-12-03 21:49:02 +01:00
def convert_value(
2025-12-02 13:59:33 +01:00
self,
2025-12-03 21:49:02 +01:00
option,
value: Any,
) -> str:
"""Dump variable, means transform bool, ... to yaml string"""
if (
value is not None
and not self.show_secrets
and option.type() == "password"
):
return "*" * 10
if isinstance(value, str):
return value
with BytesIO() as ymlfh:
self.yaml.dump(value, ymlfh)
ret = ymlfh.getvalue().decode("utf-8").strip()
if ret.endswith("..."):
ret = ret[:-3].strip()
return ret
2025-12-02 13:59:33 +01:00
2025-12-03 21:49:02 +01:00
def get_metaconfig_with_default_value(self, meta_config, option):
default_owner = option.owner.default()
if default_owner == owners.default:
2026-01-01 21:39:06 +01:00
if option.owner.get() == default_owner:
return None
2025-12-03 21:49:02 +01:00
if self.root_config:
return self.root_config
return self.config
if not self.config_owner_is_path:
while True:
2026-01-01 21:39:06 +01:00
if meta_config.type() != 'metaconfig':
return None
2025-12-03 21:49:02 +01:00
meta_config = meta_config.parent()
if not meta_config.owner.isdefault():
break
else:
meta_config = self.root_config
for child in default_owner.split(".")[1:]:
meta_config = meta_config.config(child)
return meta_config