This commit is contained in:
egarette@silique.fr 2025-12-03 21:49:02 +01:00
parent 62fb0eab60
commit 422597ade5
10 changed files with 349 additions and 62 deletions

View file

@ -17,18 +17,71 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
from pathlib import Path
from rougail.utils import load_modules
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(
*,
backward_compatibility=True,
) -> dict:
outputs = list(OutPuts().get())
output_format_default = outputs[0]
options = f"""
display:
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:
jinja: |-
{{% if step.output is propertyerror or step.output != 'display' %}}
@ -39,6 +92,14 @@ display:
return_type: boolean
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')}
mandatory:
@ -53,13 +114,19 @@ display:
{{% endif %}}
description: {_('do not test if "cli.read_write" is true')}
max_width:
description: {_("Maximum number of characters per line")}
help: {_('null means unlimited')}
type: integer
mandatory: false
params:
min_integer: 50
console:
description: {_("Specific configuration when variables are displayed")}
disabled:
variable: _.output_format
when_not: console
max_width:
description: {_("Maximum number of characters per line")}
help: {_('null means unlimited')}
type: integer
mandatory: false
params:
min_integer: 50
"""
return {
"name": "display",

View file

@ -21,8 +21,8 @@ from ruamel.yaml import YAML
from tiramisu.error import PropertiesOptionError, ConfigError
from .config import OutPuts
from .i18n import _
from .output.console import OutputFamily
class RougailOutputDisplay:
@ -66,14 +66,13 @@ class RougailOutputDisplay:
return self.output.run()
def print(self) -> None:
ret = self.exporter()
self.output.print()
return ret
return self.output.print()
def get_root(self) -> None:
yaml = YAML()
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:"),
None,
self,

View file

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

View file

@ -0,0 +1,194 @@
"""
Silique (https://www.silique.fr)
Copyright (C) 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 ..i18n import _
from ..util import CommonOutput
class OutputFamily(CommonOutput):
level = 20
name = "github"
variable_hidden_color = "brown" # #A52A2A
variable_normal_color = None
value_unmodified_color = "darkgoldenrod" # #B8860B
value_modified_color = "darkgreen" # #006400
value_default_color = None
error_color = 'bright_red'
def run(self):
ret = self.root.exporter()
print(ret, self.out)
return ret, "\n".join(self.out) + "\n"
def print(self) -> None:
ret, export = self.run()
print(export)
return ret
def header(self):
variables = []
if self.root_family.variable_default_enable:
variables.append(_("Variable"))
if self.root_family.variable_hidden_enable:
variables.append(f'<span style="color: {self.variable_hidden_color}">_("Unmodifiable variable")</span>')
values = []
if self.root_family.value_unmodified_enable:
values.append(f'<span style="color: {self.value_unmodified_color}">_("Default value")</span>')
if self.root_family.value_modified_enable:
values.append(f'<span style="color: {self.value_modified_color}">_("Modified value")</span>')
if self.root_family.value_default_enable:
values.append(f'(:hourglass_flowing_sand: {_("Original default value")})')
if not variables and not values:
return ""
caption = "> [!NOTE]" + "\n>\n"
caption += f'> _("Caption")\n'
if variables:
caption += "> " + "\\\n> ".join(variables) + "\n"
if values:
caption += "> + " "\\\n> ".join(values) + "\n"
if self.root.layer_datas:
raise Exception('unsupported layers')
# max_len = 0
# for datas in self.root.layer_datas.values():
# for data in datas.values():
# max_len = max(max_len, len(data))
# display_layers = ["" for i in range(max_len)]
# for datas in self.root.layer_datas.values():
# for data in datas.values():
# last_index = len(data) - 1
# for idx in range(max_len):
# if last_index < idx:
# display_layers[idx] += "\n"
# else:
# display_layers[idx] += data[idx] + "\n"
# layers.add_row(*[layer[:-1] for layer in display_layers])
# self.out.append(Panel.fit(layers, title=_("Layers")))
print('1', caption)
self.out.append(caption)
def error_header(self):
self.out.append('> [!CAUTION]\n> ')
def display_error(self, tree, error, level):
if not level:
self.out.append("> - :stop_sign: " + error)
else:
self.out.append("> " + " " * level + "- " + error)
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)
print('ppp')
self.out.append(tree)
def add_variable(
self, *args, **kwargs,
):
key, value = super().add_variable(*args, **kwargs)
if isinstance(value, list):
subtree = self.get_tree().add(
":notebook: " + _("{0}:").format(key),
guide_style="bold bright_blue",
)
for val in value:
subtree.add(str(val))
else:
self.get_tree().add(":notebook: " + _("{0}: {1}").format(key, value))
def colorize(
self,
values,
option=None,
) -> str:
multi = False
ret = []
default = []
for idx, data in enumerate(values):
value = data["value"]
if isinstance(value, list):
multi = True
else:
value = [value]
for vidx, val in enumerate(value):
if len(ret) == vidx:
ret.append("")
default.append(False)
if idx:
if not default[vidx]:
if ret[vidx]:
ret[vidx] += " "
ret[vidx] += "("
default[vidx] = True
else:
ret[vidx] += " "
ret[vidx] += ":hourglass_flowing_sand: "
if option:
val = self.convert_value(
option,
val,
)
color = data["color"]
if color is not None:
ret[vidx] += f"[{color}]{val}[/{color}]"
else:
ret[vidx] += val
loaded_from = data["loaded_from"]
if loaded_from:
ret[vidx] += f" :arrow_backward: {loaded_from}"
for idx in range(len(ret)):
if default[idx]:
ret[idx] += ")"
if not multi:
if not ret:
return ""
return ret[0]
return ret
def get_tree(self):
if self.tree is None:
if self.parent is None:
tree = Tree
else:
tree = self.parent.add
# if self.is_leader:
# self.tree = tree(
# ":notebook: " + _("{0}:").format(self.family),
# guide_style="bold bright_blue",
# )
# elif self.no_icon:
if self.no_icon:
self.tree = tree(
self.family,
guide_style="bold bright_blue",
)
else:
self.tree = tree(
f":open_file_folder: {self.family}",
guide_style="bold bright_blue",
)
return self.tree

View file

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

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,4 @@
> [!CAUTION]
>
> - :stop_sign: The following variables are inaccessible but are empty and mandatory:
> - please set a value

View file

@ -0,0 +1,6 @@
> [!CAUTION]
>
> - :stop_sign: The following variables are inaccessible but are empty and mandatory:
> - family1
> - family2
> - please set a value

View file

@ -0,0 +1,6 @@
> [!CAUTION]
>
> - :stop_sign: The following variables are mandatory but have no value:
> - family1
> - family2
> - 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
EXT = {'console': 'sh', 'github': 'md'}
def test_error_mandatory_hidden():
rougailconfig = get_rougail_config(Path("tests/errors/"), namespace=False)
@ -12,17 +14,19 @@ def test_error_mandatory_hidden():
config = rougail.run()
config.information.set("description_type", "description")
config.property.read_only()
no_pb, generated_output = RougailOutput(config, rougailconfig=rougailconfig).run()
assert no_pb == False
output_file = Path(__file__).parent / 'errors-results' / 'display.sh'
if not output_file.is_file():
if not output_file.parent.is_dir():
output_file.parent.mkdir()
with output_file.open('w') as outfh:
outfh.write(generated_output)
with output_file.open() as outfh:
attented_output = outfh.read()
assert generated_output == attented_output, f'filename {output_file}'
for output_format, ext in EXT.items():
rougailconfig['display.output_format'] = output_format
no_pb, generated_output = RougailOutput(config, rougailconfig=rougailconfig).run()
assert no_pb == False
output_file = Path(__file__).parent / 'errors-results' / f'display.{ext}'
if not output_file.is_file():
if not output_file.parent.is_dir():
output_file.parent.mkdir()
with output_file.open('w') as outfh:
outfh.write(generated_output)
with output_file.open() as outfh:
attented_output = outfh.read()
assert generated_output == attented_output, f'filename {output_file}'
def test_error_mandatory_family_hidden():
@ -32,17 +36,19 @@ def test_error_mandatory_family_hidden():
config = rougail.run()
config.information.set("description_type", "description")
config.property.read_only()
no_pb, generated_output = RougailOutput(config, rougailconfig=rougailconfig).run()
assert no_pb == False
output_file = Path(__file__).parent / 'errors2-results' / 'display.sh'
if not output_file.is_file():
if not output_file.parent.is_dir():
output_file.parent.mkdir()
with output_file.open('w') as outfh:
outfh.write(generated_output)
with output_file.open() as outfh:
attented_output = outfh.read()
assert generated_output == attented_output, f'filename {output_file}'
for output_format, ext in EXT.items():
rougailconfig['display.output_format'] = output_format
no_pb, generated_output = RougailOutput(config, rougailconfig=rougailconfig).run()
assert no_pb == False
output_file = Path(__file__).parent / 'errors2-results' / f'display.{ext}'
if not output_file.is_file():
if not output_file.parent.is_dir():
output_file.parent.mkdir()
with output_file.open('w') as outfh:
outfh.write(generated_output)
with output_file.open() as outfh:
attented_output = outfh.read()
assert generated_output == attented_output, f'filename {output_file}'
def test_error_mandatory_family():
@ -52,14 +58,16 @@ def test_error_mandatory_family():
config = rougail.run()
config.information.set("description_type", "description")
config.property.read_only()
no_pb, generated_output = RougailOutput(config, rougailconfig=rougailconfig).run()
assert no_pb == False
output_file = Path(__file__).parent / 'errors3-results' / 'display.sh'
if not output_file.is_file():
if not output_file.parent.is_dir():
output_file.parent.mkdir()
with output_file.open('w') as outfh:
outfh.write(generated_output)
with output_file.open() as outfh:
attented_output = outfh.read()
assert generated_output == attented_output, f'filename {output_file}'
for output_format, ext in EXT.items():
rougailconfig['display.output_format'] = output_format
no_pb, generated_output = RougailOutput(config, rougailconfig=rougailconfig).run()
assert no_pb == False
output_file = Path(__file__).parent / 'errors3-results' / f'display.{ext}'
if not output_file.is_file():
if not output_file.parent.is_dir():
output_file.parent.mkdir()
with output_file.open('w') as outfh:
outfh.write(generated_output)
with output_file.open() as outfh:
attented_output = outfh.read()
assert generated_output == attented_output, f'filename {output_file}'