rougail-output-display/src/rougail/output_console/__init__.py

441 lines
15 KiB
Python
Raw Normal View History

2024-07-28 17:52:11 +02:00
"""
Silique (https://www.silique.fr)
2025-02-10 09:54:59 +01:00
Copyright (C) 2022-2025
2024-07-28 17:52:11 +02:00
2024-10-31 18:55:00 +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/>.
2024-07-28 17:52:11 +02:00
"""
2024-09-04 09:04:08 +02:00
from typing import Any, List, Optional
2024-12-02 20:22:50 +01:00
from io import BytesIO
2024-07-28 17:52:11 +02:00
from rich.tree import Tree
from rich.console import Console
from rich.table import Table
from rich.panel import Panel
2024-12-02 20:22:50 +01:00
from ruamel.yaml import YAML
2024-07-28 17:52:11 +02:00
from tiramisu import undefined
2024-11-27 16:50:48 +01:00
from tiramisu.error import PropertiesOptionError, ConfigError
from .i18n import _
2024-07-28 17:52:11 +02:00
2024-11-27 16:50:48 +01:00
class RougailOutputConsole:
2024-11-01 10:58:53 +01:00
variable_hidden_color = "orange1"
variable_advanced_color = "bright_blue"
variable_advanced_and_modified_color = "red1"
value_unmodified_color = "gold1"
value_default_color = "green"
2024-07-28 17:52:11 +02:00
2024-11-01 10:58:53 +01:00
def __init__(
self,
2024-11-27 16:50:48 +01:00
config: "Config",
rougailconfig: "RougailConfig" = None,
user_data_errors: Optional[list] = None,
user_data_warnings: Optional[list] = None,
2024-11-01 10:58:53 +01:00
) -> None:
2024-11-27 16:50:48 +01:00
if rougailconfig is None:
from rougail import RougailConfig
rougailconfig = RougailConfig
2024-07-28 17:52:11 +02:00
self.rougailconfig = rougailconfig
2024-11-27 16:50:48 +01:00
self.config = config
self.read_write = self.rougailconfig["console.read_write"]
self.is_mandatory = self.rougailconfig["console.mandatory"]
self.show_secrets = self.rougailconfig["console.show_secrets"]
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
2024-11-27 16:50:48 +01:00
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)
2024-07-28 17:52:11 +02:00
self.out = []
2024-11-27 16:50:48 +01:00
self.root = self.get_root()
def mandatory(self):
if not self.is_mandatory:
return
title = False
options_with_error = []
try:
mandatories = self.config.value.mandatory()
except (ConfigError, PropertiesOptionError) as err:
2025-04-01 21:08:44 +02:00
self.errors.append(_("Error in config: {0}").format(err))
2024-11-27 16:50:48 +01:00
return
2025-02-15 18:42:02 +01:00
except ValueError as err:
self.errors.append(str(err))
return
2024-11-27 16:50:48 +01:00
for option in mandatories:
try:
option.value.get()
if not title:
self.errors.append(
_("The following variables are mandatory but have no value:")
)
title = True
self.errors.append(f" - {option.description()}")
except PropertiesOptionError:
options_with_error.append(option)
if not title:
for idx, option in enumerate(options_with_error):
if not idx:
self.errors.append(
_(
"The following variables are inaccessible but are empty and mandatory:"
2024-11-27 16:50:48 +01:00
)
)
self.errors.append(f" - {option.description()}")
def exporter(self) -> bool:
self.config.property.read_write()
self.mandatory()
if self.read_write:
self.config.property.read_write()
else:
self.config.property.read_only()
warnings = self.user_data_warnings + self.warnings
if warnings:
self.display_warnings(warnings)
2024-11-27 16:50:48 +01:00
errors = self.user_data_errors + self.errors
if errors:
self.display_errors(errors)
return False
self.parse_options(
self.config,
self.root,
)
self.header()
2024-11-27 16:50:48 +01:00
self.end()
return True
def run(self) -> str:
with self.console.capture() as capture:
2025-02-10 09:54:59 +01:00
ret = self.print()
return ret, capture.get()
def print(self) -> None:
2025-02-10 09:54:59 +01:00
ret = self.exporter()
for out in self.out:
self.console.print(out)
2025-02-10 09:54:59 +01:00
return ret
2024-11-27 16:50:48 +01:00
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)
2024-07-28 17:52:11 +02:00
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'
2024-07-28 17:52:11 +02:00
header = Table.grid(padding=1, collapse_padding=True)
header.pad_edge = False
header.add_row(header_variable[:-1], header_value[:-1])
2024-07-28 17:52:11 +02:00
self.out.append(Panel.fit(header, title=_("Caption")))
2024-11-27 16:50:48 +01:00
def display_errors(
2024-11-01 10:58:53 +01:00
self,
errors,
) -> None:
tree = Tree(
f"[bold][bright_red]:stop_sign: {_('ERRORS')}[/bright_red][/bold]",
2024-11-01 10:58:53 +01:00
guide_style="bold bright_red",
)
2024-07-28 17:52:11 +02:00
for error in errors:
tree.add(error)
self.out.append(tree)
2024-11-27 16:50:48 +01:00
def display_warnings(
2024-11-01 10:58:53 +01:00
self,
warnings: list,
) -> None:
tree = Tree(f"[bold][bright_yellow]:bell: {_('WARNINGS')}[/bright_yellow][/bold]",
guide_style="bold bright_yellow",
)
2024-07-28 17:52:11 +02:00
for warning in warnings:
tree.add(warning)
self.out.append(tree)
2024-11-27 16:50:48 +01:00
def get_root(self) -> None:
2024-12-02 20:22:50 +01:00
yaml = YAML()
yaml.indent(mapping=2, sequence=4, offset=2)
2024-11-01 10:58:53 +01:00
self.output = OutputFamily(
_("Variables:"),
None,
self,
2024-12-02 20:22:50 +01:00
yaml,
2024-11-01 10:58:53 +01:00
no_icon=True,
)
2024-07-28 17:52:11 +02:00
return self.output
def end(self):
self.out.append(self.output.tree)
class OutputFamily:
2024-11-01 10:58:53 +01:00
def __init__(
2024-12-02 20:22:50 +01:00
self, family, parent, root, _yaml, *, is_leader: bool = False, no_icon: bool = False
2024-11-01 10:58:53 +01:00
) -> None:
2024-07-28 17:52:11 +02:00
if parent is None:
tree = Tree
else:
tree = parent.add
if is_leader:
2024-11-01 10:58:53 +01:00
self.tree = tree(
":notebook: " + _('{0}:').format(family),
2024-11-01 10:58:53 +01:00
guide_style="bold bright_blue",
)
2024-07-28 17:52:11 +02:00
elif no_icon:
2024-11-01 10:58:53 +01:00
self.tree = tree(
f"{family}",
guide_style="bold bright_blue",
)
2024-07-28 17:52:11 +02:00
else:
2024-11-01 10:58:53 +01:00
self.tree = tree(
f":open_file_folder: {family}",
guide_style="bold bright_blue",
)
2024-07-28 17:52:11 +02:00
self.root = root
2024-12-02 20:22:50 +01:00
self._yaml = _yaml
2024-11-01 10:58:53 +01:00
def add_family(
self,
option,
) -> None:
2024-07-28 17:52:11 +02:00
properties = option.property.get()
2024-11-01 10:58:53 +01:00
if "hidden" in properties:
self.root.variable_hidden_enable = True
2024-07-28 17:52:11 +02:00
color = self.root.variable_hidden_color
2024-11-01 10:58:53 +01:00
elif "advanced" in properties:
self.root.variable_advanced_enable = True
2024-07-28 17:52:11 +02:00
color = self.root.variable_advanced_color
else:
self.root.variable_default_enable = True
2024-07-28 17:52:11 +02:00
color = None
2024-11-01 10:58:53 +01:00
return OutputFamily(
self.colorize(
None,
option.name(),
color,
None,
),
self.tree,
self.root,
2024-12-02 20:22:50 +01:00
self._yaml,
2024-11-01 10:58:53 +01:00
)
2024-07-28 17:52:11 +02:00
2024-11-01 10:58:53 +01:00
def add_variable(
self, option, value: Any = undefined, leader_index: Optional[int] = None
):
2024-07-28 17:52:11 +02:00
properties = option.property.get()
variable_color = None
if option.owner.isdefault():
2024-11-01 10:58:53 +01:00
if "hidden" in properties:
self.root.variable_hidden_enable = True
2024-07-28 17:52:11 +02:00
variable_color = self.root.variable_hidden_color
2024-11-01 10:58:53 +01:00
elif "advanced" in properties:
self.root.variable_advanced_enable = True
2024-07-28 17:52:11 +02:00
variable_color = self.root.variable_advanced_color
self.root.value_unmodified_enable = True
2024-07-28 17:52:11 +02:00
color = self.root.value_unmodified_color
default_value = None
else:
2024-11-01 10:58:53 +01:00
if "hidden" in properties:
self.root.variable_hidden_enable = True
2024-07-28 17:52:11 +02:00
variable_color = self.root.variable_hidden_color
2024-11-01 10:58:53 +01:00
elif "advanced" in properties:
self.root.variable_advanced_and_modified_enable = True
2024-07-28 17:52:11 +02:00
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
if variable_color is None:
self.root.variable_default_enable = True
2024-07-28 17:52:11 +02:00
if value is undefined:
value = option.value.get()
2024-11-01 10:58:53 +01:00
key = self.colorize(
None,
option.name(),
variable_color,
None,
)
value = self.colorize(
option,
value,
color,
default_value,
)
2024-07-28 17:52:11 +02:00
if isinstance(value, list):
2024-11-01 10:58:53 +01:00
subtree = self.tree.add(
":notebook: " + _('{0}:').format(key),
2024-11-01 10:58:53 +01:00
guide_style="bold bright_blue",
)
2024-07-28 17:52:11 +02:00
for val in value:
subtree.add(str(val))
else:
self.tree.add(f":notebook: {key}: {value}")
2024-11-01 10:58:53 +01:00
def colorize(
self,
option,
value,
color: str,
default_value,
) -> str:
2024-07-28 17:52:11 +02:00
if isinstance(value, list):
if default_value is None:
default_value = []
2024-12-02 20:22:50 +01:00
# default_value = [self.convert_value(option, val) for val in default_value]
2024-07-28 17:52:11 +02:00
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:
2024-11-01 10:58:53 +01:00
val = ""
2024-07-28 17:52:11 +02:00
if idx < len_default_value:
default = default_value[idx]
else:
default = None
2024-11-01 10:58:53 +01:00
ret.append(
self.colorize(
option,
val,
color,
default,
)
)
2024-07-28 17:52:11 +02:00
return ret
if value is None:
ret = ''
2024-07-28 17:52:11 +02:00
else:
if option:
value = self.convert_value(
option,
value,
)
if color is not None:
ret = f"[{color}]{value}[/{color}]"
else:
ret = value
2024-12-02 20:22:50 +01:00
if default_value is not None and "force_store_value" not in option.property.get():
self.root.value_default_enable = True
2024-07-28 17:52:11 +02:00
default_value_color = self.root.value_default_color
2024-12-02 20:22:50 +01:00
default_value = self.convert_value(option, default_value)
if ret:
ret += f" ([{default_value_color}]{default_value}[/{default_value_color}])"
else:
ret = f"[{default_value_color}]{default_value}[/{default_value_color}]"
2024-07-28 17:52:11 +02:00
return ret
2024-11-01 10:58:53 +01:00
def convert_value(
self,
option,
2024-12-02 20:22:50 +01:00
value: Any,
) -> str:
if isinstance(value, list):
print(value)
raise Exception('pfff')
"""Dump variable, means transform bool, ... to yaml string"""
2024-11-01 10:58:53 +01:00
if not self.root.show_secrets and option.type() == "password":
2024-07-28 17:52:11 +02:00
return "*" * 10
2024-12-02 20:22:50 +01:00
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
2024-11-27 16:50:48 +01:00
RougailOutput = RougailOutputConsole
__all__ = ("RougailOutputConsole",)