This commit is contained in:
egarette@silique.fr 2025-12-03 21:49:02 +01:00
parent 62fb0eab60
commit 65ffa84538
8 changed files with 155 additions and 58 deletions

View file

@ -17,18 +17,71 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
""" """
from pathlib import Path from pathlib import Path
from rougail.utils import load_modules
from .i18n import _ from .i18n import _
OUTPUTS = None
def get_outputs() -> None:
"""Load all outputs"""
module_name = f"rougail.output_display.output"
outputs = {}
names = []
for path in (Path(__file__).parent / "output").iterdir():
name = path.name
if not name.endswith(".py") or name.endswith("__.py"):
continue
module = load_modules(module_name + "." + name[:-3], str(path))
if "OutputFamily" not in dir(module):
continue
obj_class = module.OutputFamily
level = obj_class.level
if level in outputs:
raise ImportError(
_('duplicated level rougail-output-display for output "{0}": {1} and {2}').format(
level, obj_class.name, outputs[level].name
)
)
if obj_class.name in names:
raise ImportError(
_('duplicated name "{0}" in rougail-output-doc').format(
obj_class.name
)
)
names.append(obj_class.name)
outputs[level] = obj_class
return {outputs[level].name: outputs[level] for level in sorted(outputs)}
class OutPuts: # pylint: disable=R0903
"""Transformations applied on a object instance"""
def __init__(
self,
) -> None:
global OUTPUTS
if OUTPUTS is None:
OUTPUTS = get_outputs()
def get(self) -> dict:
"""Get all outputs"""
return OUTPUTS
def get_rougail_config( def get_rougail_config(
*, *,
backward_compatibility=True, backward_compatibility=True,
) -> dict: ) -> dict:
outputs = list(OutPuts().get())
output_format_default = outputs[0]
options = f""" options = f"""
display: display:
description: {_('Display variables and values')} description: {_('Display variables and values')}
help: {_('Find all the variables and their values in your configuration (structural and user datas). Additional informations are available, such as the default value, the location where the value is loaded, etc.')} help: |-
{_('Find all the variables and their values in your configuration (structural and user datas). Additional informations are available, such as the default value, the location where the value is loaded, etc.')}
disabled: disabled:
jinja: |- jinja: |-
{{% if step.output is propertyerror or step.output != 'display' %}} {{% if step.output is propertyerror or step.output != 'display' %}}
@ -39,6 +92,14 @@ display:
return_type: boolean return_type: boolean
description: {_('if display is not set in "step.output"')} description: {_('if display is not set in "step.output"')}
output_format:
description: {_('The output format for displaying variables')}
default: { output_format_default }
choices:
"""
for output in outputs:
options += f" - {output}\n"
options += f"""
show_secrets: false # {_('Show secrets instead of obscuring them')} show_secrets: false # {_('Show secrets instead of obscuring them')}
mandatory: mandatory:
@ -53,6 +114,12 @@ display:
{{% endif %}} {{% endif %}}
description: {_('do not test if "cli.read_write" is true')} description: {_('do not test if "cli.read_write" is true')}
console:
description: {_("Specific configuration when variables are displayed")}
disabled:
variable: _.output_format
when_not: console
max_width: max_width:
description: {_("Maximum number of characters per line")} description: {_("Maximum number of characters per line")}
help: {_('null means unlimited')} help: {_('null means unlimited')}

View file

@ -21,8 +21,8 @@ from ruamel.yaml import YAML
from tiramisu.error import PropertiesOptionError, ConfigError from tiramisu.error import PropertiesOptionError, ConfigError
from .config import OutPuts
from .i18n import _ from .i18n import _
from .output.console import OutputFamily
class RougailOutputDisplay: class RougailOutputDisplay:
@ -66,14 +66,13 @@ class RougailOutputDisplay:
return self.output.run() return self.output.run()
def print(self) -> None: def print(self) -> None:
ret = self.exporter() return self.output.print()
self.output.print()
return ret
def get_root(self) -> None: def get_root(self) -> None:
yaml = YAML() yaml = YAML()
yaml.indent(mapping=2, sequence=4, offset=2) yaml.indent(mapping=2, sequence=4, offset=2)
self.output = OutputFamily( output_format = self.rougailconfig["display.output_format"]
self.output = OutPuts().get()[output_format](
_("Variables:"), _("Variables:"),
None, None,
self, self,

View file

@ -26,23 +26,28 @@ from ..util import CommonOutput
class OutputFamily(CommonOutput): class OutputFamily(CommonOutput):
level = 10
name = "console"
variable_hidden_color = "orange1" variable_hidden_color = "orange1"
variable_normal_color = None variable_normal_color = None
value_unmodified_color = "gold1" value_unmodified_color = "gold1"
value_modified_color = "green" value_modified_color = "green"
value_default_color = None value_default_color = None
error_color = 'bright_red'
def run(self): def run(self):
max_width = self.root.rougailconfig["display.max_width"] max_width = self.root.rougailconfig["display.console.max_width"]
self.console = Console(force_terminal=True, width=max_width) self.console = Console(force_terminal=True, width=max_width)
with self.console.capture() as capture: with self.console.capture() as capture:
ret = self.root.print() ret = self.root.print()
return ret, capture.get() return ret, capture.get()
def print(self) -> None: def print(self) -> None:
ret = self.root.exporter()
for out in self.out: for out in self.out:
self.console.print(out) self.console.print(out)
return ret
def header(self): def header(self):
caption_line = "" caption_line = ""
@ -52,7 +57,7 @@ class OutputFamily(CommonOutput):
caption_line += f'[{self.variable_hidden_color}]{_("Unmodifiable variable")}[/{self.variable_hidden_color}]\n' caption_line += f'[{self.variable_hidden_color}]{_("Unmodifiable variable")}[/{self.variable_hidden_color}]\n'
header_value = "" header_value = ""
if self.root_family.value_unmodified_enable: if self.root_family.value_unmodified_enable:
header_value = f'[{self.value_unmodified_color}]{_("Default value")}[/{self.value_unmodified_color}]\n' header_value += f'[{self.value_unmodified_color}]{_("Default value")}[/{self.value_unmodified_color}]\n'
if self.root_family.value_modified_enable: if self.root_family.value_modified_enable:
header_value += f'[{self.value_modified_color}]{_("Modified value")}[/{self.value_modified_color}]\n' header_value += f'[{self.value_modified_color}]{_("Modified value")}[/{self.value_modified_color}]\n'
if self.root_family.value_default_enable: if self.root_family.value_default_enable:
@ -82,12 +87,14 @@ class OutputFamily(CommonOutput):
self.out.append(Panel.fit(layers, title=_("Layers"))) self.out.append(Panel.fit(layers, title=_("Layers")))
def error_header(self): def error_header(self):
return Tree( tree = Tree(
f"[bold][bright_red]:stop_sign: {_('ERRORS')}[/bright_red][/bold]", f"[bold][{self.error_color}]:stop_sign: {_('ERRORS')}[/{self.error_color}][/bold]",
guide_style="bold bright_red", guide_style=f"bold {self.error_color}",
) )
self.out.append(tree)
return tree
def display_error(self, tree, error): def display_error(self, tree, error, level):
return tree.add(error) return tree.add(error)
def display_warnings( def display_warnings(

View file

@ -66,23 +66,22 @@ class CommonOutput:
tree = self.error_header() tree = self.error_header()
for error in errors: for error in errors:
self.parse_error(tree, error) self.parse_error(tree, error)
self.out.append(tree)
def parse_error(self, tree, error): def parse_error(self, tree, error, level=-1):
if isinstance(error, list): if isinstance(error, list):
for err in error: for err in error:
self.parse_error(tree, err) self.parse_error(tree, err, level+1)
return tree return tree
elif isinstance(error, dict): elif isinstance(error, dict):
for key, value in error.items(): for key, value in error.items():
if key is None: if key is None:
# it's variables, no more families # it's variables, no more families
self.parse_error(tree, value) self.parse_error(tree, value, level+1)
else: else:
sub_tree = self.parse_error(tree, key) sub_tree = self.parse_error(tree, key, level+1)
self.parse_error(sub_tree, value) self.parse_error(sub_tree, value, level+1)
else: else:
return self.display_error(tree, error) return self.display_error(tree, error, level)
def add_family( def add_family(
self, self,

21
test.md Normal file
View file

@ -0,0 +1,21 @@
<pre>
Variables:
┗━ 📂 A family
┣━ 📓 The first variable: string1 ◀ loaded from rougail-test
┗━ 📓 The second variable: string1 ◀ loaded from rougail-test
</pre>
<pre>
Variables:
<span style="color: #5c5cff">┣━━ </span>📓 <span style="color: #ff0000">Configure Proxy Access to the Internet</span>: <span style="color: #ffd700">No proxy</span>
<span style="color: #5c5cff">┗━━ </span>📂 Manual proxy configuration
<span style="color: #5c5cff"> </span><span style="color: #5c5cff">┗━━ </span>📂 HTTP Proxy
<span style="color: #5c5cff"> </span><span style="color: #5c5cff"> </span><span style="color: #5c5cff">┗━━ </span>📓 <span style="color: #ff0000">HTTP address</span>: <span style="color: #00aa00">example.net</span> ◀ loaded from the YAML file
<span style="color: #5c5cff"> </span><span style="color: #5c5cff"> </span><span style="color: #5c5cff"> </span>"config/03/config.yml"
</pre>
Variables:
- 📂 A family
- 📓 The first variable: string1 ◀ loaded from rougail-test
- 📓 The second variable: <span style="color: #5c5cff">string1</span> ◀ loaded from rougail-test

View file

@ -1,3 +0,0 @@
🛑 ERRORS
┗━━ The following variables are inaccessible but are empty and mandatory:
 ┗━━ mandatory_variable (please set a value)

View file

@ -0,0 +1,3 @@
<span style="color: bright_red">:stop_sign: ERRORS:</span>
- <span style="color: bright_red">The following variables are inaccessible but are empty and mandatory:</span>
- please set a value

View file

@ -4,6 +4,8 @@ from rougail.output_display import RougailOutputDisplay as RougailOutput
from rougail_tests.utils import get_rougail_config from rougail_tests.utils import get_rougail_config
EXT = {'console': 'sh', 'github': 'md'}
def test_error_mandatory_hidden(): def test_error_mandatory_hidden():
rougailconfig = get_rougail_config(Path("tests/errors/"), namespace=False) rougailconfig = get_rougail_config(Path("tests/errors/"), namespace=False)
@ -12,9 +14,11 @@ def test_error_mandatory_hidden():
config = rougail.run() config = rougail.run()
config.information.set("description_type", "description") config.information.set("description_type", "description")
config.property.read_only() config.property.read_only()
for output_format, ext in EXT.items():
rougailconfig['display.output_format'] = output_format
no_pb, generated_output = RougailOutput(config, rougailconfig=rougailconfig).run() no_pb, generated_output = RougailOutput(config, rougailconfig=rougailconfig).run()
assert no_pb == False assert no_pb == False
output_file = Path(__file__).parent / 'errors-results' / 'display.sh' output_file = Path(__file__).parent / 'errors-results' / f'display.{ext}'
if not output_file.is_file(): if not output_file.is_file():
if not output_file.parent.is_dir(): if not output_file.parent.is_dir():
output_file.parent.mkdir() output_file.parent.mkdir()
@ -25,7 +29,7 @@ def test_error_mandatory_hidden():
assert generated_output == attented_output, f'filename {output_file}' assert generated_output == attented_output, f'filename {output_file}'
def test_error_mandatory_family_hidden(): def test_error_mandatory_family_hidden():
rougailconfig = get_rougail_config(Path("tests/errors2/"), namespace=False) rougailconfig = get_rougail_config(Path("tests/errors2/"), namespace=False)
rougailconfig['step.output'] = 'display' rougailconfig['step.output'] = 'display'
rougail = Rougail(rougailconfig) rougail = Rougail(rougailconfig)