489 lines
16 KiB
Python
489 lines
16 KiB
Python
"""
|
|
Silique (https://www.silique.fr)
|
|
Copyright (C) 2022-2025
|
|
|
|
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 Any, List, Optional
|
|
from io import BytesIO
|
|
|
|
from rich.tree import Tree
|
|
from rich.console import Console
|
|
from rich.table import Table
|
|
from rich.panel import Panel
|
|
from ruamel.yaml import YAML
|
|
|
|
from tiramisu.error import PropertiesOptionError, ConfigError
|
|
from rougail.utils import undefined
|
|
|
|
from .i18n import _
|
|
from .__version__ import __version__
|
|
|
|
|
|
class RougailOutputConsole:
|
|
variable_hidden_color = "orange1"
|
|
variable_advanced_color = "bright_blue"
|
|
variable_advanced_and_modified_color = "red1"
|
|
value_unmodified_color = "gold1"
|
|
value_default_color = "green"
|
|
|
|
def __init__(
|
|
self,
|
|
config: "Config",
|
|
rougailconfig: "RougailConfig" = None,
|
|
user_data_errors: Optional[list] = None,
|
|
user_data_warnings: Optional[list] = None,
|
|
) -> None:
|
|
if rougailconfig is None:
|
|
from rougail import RougailConfig
|
|
|
|
rougailconfig = RougailConfig
|
|
self.rougailconfig = rougailconfig
|
|
self.config = config
|
|
self.is_mandatory = self.rougailconfig["console.mandatory"]
|
|
self.show_secrets = self.rougailconfig["console.show_secrets"]
|
|
self.key_is_description = self.rougailconfig["console.key_is_description"]
|
|
self.variable_default_enable = False
|
|
self.variable_hidden_enable = False
|
|
self.variable_advanced_enable = False
|
|
self.variable_advanced_and_modified_enable = False
|
|
self.value_modified_enable = False
|
|
self.value_unmodified_enable = False
|
|
self.value_default_enable = False
|
|
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
|
|
self.console = Console(force_terminal=True)
|
|
self.out = []
|
|
self.root = self.get_root()
|
|
|
|
def mandatory(self):
|
|
try:
|
|
mandatories = self.config.value.mandatory()
|
|
except (ConfigError, PropertiesOptionError) as err:
|
|
self.errors.append(_("Error in config: {0}").format(err))
|
|
return
|
|
except ValueError as err:
|
|
self.errors.append(str(err))
|
|
return
|
|
options_with_error = []
|
|
options = []
|
|
if mandatories:
|
|
for option in mandatories:
|
|
try:
|
|
option.value.get()
|
|
options.append(option.description())
|
|
except PropertiesOptionError:
|
|
options_with_error.append(option)
|
|
if options:
|
|
self.errors.append(
|
|
_("The following variables are mandatory but have no value:")
|
|
)
|
|
self.errors.append(options)
|
|
elif options_with_error:
|
|
self.errors.append(
|
|
_(
|
|
"The following variables are inaccessible but are empty and mandatory:"
|
|
)
|
|
)
|
|
self.errors.append([option.description() for option in options_with_error])
|
|
|
|
def exporter(self) -> bool:
|
|
if self.is_mandatory:
|
|
ori_properties = self.config.property.exportation()
|
|
self.config.property.read_write()
|
|
self.mandatory()
|
|
self.config.property.importation(ori_properties)
|
|
warnings = self.user_data_warnings + self.warnings
|
|
if warnings:
|
|
self.display_warnings(warnings)
|
|
errors = self.user_data_errors + self.errors
|
|
if errors:
|
|
self.display_errors(errors)
|
|
return False
|
|
old_path_in_description = self.config.information.get(
|
|
"path_in_description", True
|
|
)
|
|
self.config.information.set("path_in_description", False)
|
|
self.parse_options(
|
|
self.config,
|
|
self.root,
|
|
)
|
|
self.config.information.set("path_in_description", old_path_in_description)
|
|
self.header()
|
|
self.end()
|
|
return True
|
|
|
|
def run(self) -> str:
|
|
with self.console.capture() as capture:
|
|
ret = self.print()
|
|
return ret, capture.get()
|
|
|
|
def print(self) -> None:
|
|
ret = self.exporter()
|
|
for out in self.out:
|
|
self.console.print(out)
|
|
return ret
|
|
|
|
def parse_options(
|
|
self,
|
|
conf,
|
|
parent,
|
|
):
|
|
for option in conf:
|
|
if option.isoptiondescription():
|
|
family = parent.add_family(option)
|
|
if option.isleadership():
|
|
self.parse_leadership(
|
|
option,
|
|
family,
|
|
)
|
|
else:
|
|
self.parse_options(
|
|
option,
|
|
family,
|
|
)
|
|
else:
|
|
parent.add_variable(option)
|
|
|
|
def parse_leadership(
|
|
self,
|
|
conf,
|
|
parent,
|
|
):
|
|
leader, *followers = list(conf)
|
|
leader_values = leader.value.get()
|
|
for idx, leader_value in enumerate(leader_values):
|
|
leader_obj = parent.add_family(leader)
|
|
leader_obj.add_variable(
|
|
leader,
|
|
value=leader_value,
|
|
leader_index=idx,
|
|
)
|
|
for follower in followers:
|
|
if follower.index() != idx:
|
|
continue
|
|
leader_obj.add_variable(follower)
|
|
|
|
def header(self):
|
|
header_variable = ""
|
|
if self.variable_default_enable:
|
|
header_variable += _("Variable") + "\n"
|
|
if self.variable_advanced_enable:
|
|
header_variable += f'[{self.variable_advanced_color}]{_("Undocumented variable")}[/{self.variable_advanced_color}]\n'
|
|
if self.variable_advanced_and_modified_enable:
|
|
header_variable += f'[{self.variable_advanced_and_modified_color}]{_("Undocumented but modified variable")}[/{self.variable_advanced_and_modified_color}]\n'
|
|
if self.variable_hidden_enable:
|
|
header_variable += f'[{self.variable_hidden_color}]{_("Unmodifiable variable")}[/{self.variable_hidden_color}]\n'
|
|
header_value = ""
|
|
if self.value_unmodified_enable:
|
|
header_value = f'[{self.value_unmodified_color}]{_("Default value")}[/{self.value_unmodified_color}]\n'
|
|
if self.value_modified_enable:
|
|
header_value += _("Modified value") + "\n"
|
|
if self.value_default_enable:
|
|
header_value += f'([{self.value_default_color}]{_("Original default value")}[/{self.value_default_color}])\n'
|
|
header = Table.grid(padding=1, collapse_padding=True)
|
|
header.pad_edge = False
|
|
header.add_row(header_variable[:-1], header_value[:-1])
|
|
self.out.append(Panel.fit(header, title=_("Caption")))
|
|
|
|
def display_errors(
|
|
self,
|
|
errors,
|
|
) -> None:
|
|
tree = Tree(
|
|
f"[bold][bright_red]:stop_sign: {_('ERRORS')}[/bright_red][/bold]",
|
|
guide_style="bold bright_red",
|
|
)
|
|
for error in errors:
|
|
if isinstance(error, list):
|
|
for err in error:
|
|
previous_tree.add(err)
|
|
else:
|
|
previous_tree = tree.add(error)
|
|
self.out.append(tree)
|
|
|
|
def display_warnings(
|
|
self,
|
|
warnings: list,
|
|
) -> None:
|
|
tree = Tree(
|
|
f"[bold][bright_yellow]:bell: {_('WARNINGS')}[/bright_yellow][/bold]",
|
|
guide_style="bold bright_yellow",
|
|
)
|
|
for warning in warnings:
|
|
tree.add(warning)
|
|
self.out.append(tree)
|
|
|
|
def get_root(self) -> None:
|
|
yaml = YAML()
|
|
yaml.indent(mapping=2, sequence=4, offset=2)
|
|
self.output = OutputFamily(
|
|
_("Variables:"),
|
|
None,
|
|
self,
|
|
yaml,
|
|
self.key_is_description,
|
|
no_icon=True,
|
|
)
|
|
return self.output
|
|
|
|
def end(self):
|
|
self.out.append(self.output.tree)
|
|
|
|
|
|
class OutputFamily:
|
|
def __init__(
|
|
self,
|
|
family,
|
|
parent,
|
|
root,
|
|
_yaml,
|
|
key_is_description,
|
|
*,
|
|
is_leader: bool = False,
|
|
no_icon: bool = False,
|
|
) -> None:
|
|
if parent is None:
|
|
tree = Tree
|
|
else:
|
|
tree = parent.add
|
|
if is_leader:
|
|
self.tree = tree(
|
|
":notebook: " + _("{0}:").format(family),
|
|
guide_style="bold bright_blue",
|
|
)
|
|
elif no_icon:
|
|
self.tree = tree(
|
|
f"{family}",
|
|
guide_style="bold bright_blue",
|
|
)
|
|
else:
|
|
self.tree = tree(
|
|
f":open_file_folder: {family}",
|
|
guide_style="bold bright_blue",
|
|
)
|
|
self.root = root
|
|
self._yaml = _yaml
|
|
self.key_is_description = key_is_description
|
|
|
|
def add_family(
|
|
self,
|
|
option,
|
|
) -> None:
|
|
properties = option.property.get()
|
|
if "hidden" in properties:
|
|
self.root.variable_hidden_enable = True
|
|
color = self.root.variable_hidden_color
|
|
elif "advanced" in properties:
|
|
self.root.variable_advanced_enable = True
|
|
color = self.root.variable_advanced_color
|
|
else:
|
|
self.root.variable_default_enable = True
|
|
color = None
|
|
if self.key_is_description:
|
|
key_name = option.description()
|
|
else:
|
|
key_name = option.name()
|
|
return OutputFamily(
|
|
self.colorize(
|
|
None,
|
|
key_name,
|
|
color,
|
|
None,
|
|
None,
|
|
),
|
|
self.tree,
|
|
self.root,
|
|
self._yaml,
|
|
self.key_is_description,
|
|
)
|
|
|
|
def add_variable(
|
|
self, option, value: Any = undefined, leader_index: Optional[int] = None
|
|
):
|
|
properties = option.property.get()
|
|
variable_color = None
|
|
if option.owner.isdefault():
|
|
if "hidden" in properties:
|
|
self.root.variable_hidden_enable = True
|
|
variable_color = self.root.variable_hidden_color
|
|
elif "advanced" in properties:
|
|
self.root.variable_advanced_enable = True
|
|
variable_color = self.root.variable_advanced_color
|
|
self.root.value_unmodified_enable = True
|
|
color = self.root.value_unmodified_color
|
|
default_value = None
|
|
loaded_from = None
|
|
else:
|
|
if "hidden" in properties:
|
|
self.root.variable_hidden_enable = True
|
|
variable_color = self.root.variable_hidden_color
|
|
elif "advanced" in properties:
|
|
self.root.variable_advanced_and_modified_enable = True
|
|
variable_color = self.root.variable_advanced_and_modified_color
|
|
color = None
|
|
self.root.value_modified_enable = True
|
|
if option.information.get("default_value_makes_sense", True):
|
|
try:
|
|
default_value = option.value.default()
|
|
except ConfigError:
|
|
if option.ismulti():
|
|
default_value = []
|
|
else:
|
|
default_value = None
|
|
if leader_index is not None:
|
|
if len(default_value) > leader_index:
|
|
default_value = default_value[leader_index]
|
|
else:
|
|
default_value = None
|
|
else:
|
|
default_value = None
|
|
follower_index = option.index()
|
|
if follower_index is not None:
|
|
loaded_from = option.information.get(
|
|
f"loaded_from_{follower_index}", None
|
|
)
|
|
else:
|
|
loaded_from = option.information.get("loaded_from", None)
|
|
if variable_color is None:
|
|
self.root.variable_default_enable = True
|
|
if value is undefined:
|
|
value = option.value.get()
|
|
if self.key_is_description:
|
|
key_name = option.description()
|
|
else:
|
|
key_name = option.name()
|
|
key = self.colorize(
|
|
None,
|
|
key_name,
|
|
variable_color,
|
|
None,
|
|
None,
|
|
)
|
|
value = self.colorize(
|
|
option,
|
|
value,
|
|
color,
|
|
default_value,
|
|
loaded_from,
|
|
)
|
|
if isinstance(value, list):
|
|
subtree = self.tree.add(
|
|
":notebook: " + _("{0}:").format(key),
|
|
guide_style="bold bright_blue",
|
|
)
|
|
for val in value:
|
|
subtree.add(str(val))
|
|
else:
|
|
self.tree.add(":notebook: " + _("{0}: {1}").format(key, value))
|
|
|
|
def colorize(
|
|
self,
|
|
option,
|
|
value,
|
|
color: str,
|
|
default_value,
|
|
loaded_from,
|
|
) -> str:
|
|
if isinstance(value, list):
|
|
if default_value is None:
|
|
default_value = []
|
|
# default_value = [self.convert_value(option, val) for val in default_value]
|
|
len_value = len(value)
|
|
len_default_value = len(default_value)
|
|
len_values = max(len_value, len_default_value)
|
|
ret = []
|
|
for idx in range(len_values):
|
|
if idx < len_value:
|
|
val = value[idx]
|
|
else:
|
|
val = ""
|
|
if idx < len_default_value:
|
|
default = default_value[idx]
|
|
else:
|
|
default = None
|
|
ret.append(
|
|
self.colorize(
|
|
option,
|
|
val,
|
|
color,
|
|
default,
|
|
loaded_from,
|
|
)
|
|
)
|
|
return ret
|
|
if value is None:
|
|
ret = ""
|
|
else:
|
|
if option:
|
|
value = self.convert_value(
|
|
option,
|
|
value,
|
|
)
|
|
if color is not None:
|
|
ret = f"[{color}]{value}[/{color}]"
|
|
else:
|
|
ret = value
|
|
if (
|
|
default_value is not None
|
|
and "force_store_value" not in option.property.get()
|
|
):
|
|
self.root.value_default_enable = True
|
|
default_value_color = self.root.value_default_color
|
|
default_value = self.convert_value(option, default_value)
|
|
if ret:
|
|
if loaded_from:
|
|
ret += f" ([{default_value_color}]{default_value}[/{default_value_color}] - {loaded_from})"
|
|
else:
|
|
ret += f" ([{default_value_color}]{default_value}[/{default_value_color}])"
|
|
else:
|
|
ret = f"[{default_value_color}]{default_value}[/{default_value_color}]"
|
|
if loaded_from:
|
|
ret += f" ({loaded_from})"
|
|
elif loaded_from:
|
|
ret += f" ({loaded_from})"
|
|
return ret
|
|
|
|
def convert_value(
|
|
self,
|
|
option,
|
|
value: Any,
|
|
) -> str:
|
|
if isinstance(value, list):
|
|
print(value)
|
|
raise Exception("pfff")
|
|
"""Dump variable, means transform bool, ... to yaml string"""
|
|
if not self.root.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
|
|
|
|
|
|
RougailOutput = RougailOutputConsole
|
|
|
|
|
|
__all__ = ("RougailOutputConsole",)
|