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

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",)