""" 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 . """ 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 import undefined from tiramisu.error import PropertiesOptionError, ConfigError from .i18n import _ 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.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 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): if not self.is_mandatory: return 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: 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) errors = self.user_data_errors + self.errors if errors: self.display_errors(errors) return False self.parse_options( self.config, self.root, ) 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, no_icon=True, ) return self.output def end(self): self.out.append(self.output.tree) class OutputFamily: def __init__( self, family, parent, root, _yaml, *, 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 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 return OutputFamily( self.colorize( None, option.name(), color, None, ), self.tree, self.root, self._yaml, ) 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 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 if variable_color is None: self.root.variable_default_enable = True if value is undefined: value = option.value.get() key = self.colorize( None, option.name(), variable_color, None, ) value = self.colorize( option, value, color, default_value, ) 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(f":notebook: {key}: {value}") def colorize( self, option, value, color: str, default_value, ) -> 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, ) ) 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: ret += f" ([{default_value_color}]{default_value}[/{default_value_color}])" else: ret = f"[{default_value_color}]{default_value}[/{default_value_color}]" 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",)