feat: better ansible documentation

This commit is contained in:
egarette@silique.fr 2026-06-21 14:42:34 +02:00
parent 5ccd259e2a
commit 94c069ab75
4 changed files with 137 additions and 62 deletions

View file

@ -63,6 +63,10 @@ class Annotator(Walk):
self.default_values = kwargs["force_default_value"]
else:
self.default_values = self.objectspace.rougailconfig["doc.default_values"]
if "force_display_unknown_optional_variable" in kwargs:
self.display_unknown_optional_variable = kwargs["force_display_unknown_optional_variable"]
else:
self.display_unknown_optional_variable = False
self.regexp_description_get_paths = None
self.populate_family()
self.populate_variable()
@ -334,16 +338,20 @@ class Annotator(Walk):
if not variable or (
isinstance(values, VariableCalculation) and values.optional and not variable
):
return None
values_calculation = {"path": variable.path}
if identifiers:
values_calculation["identifiers"] = identifiers
values_calculation["identifier_type"] = "many"
if prop in PROPERTY_ATTRIBUTE:
# get comparative value
self.when_to_condition(values, values_calculation)
if self.display_unknown_optional_variable and values.optional:
values_calculation = {"path": variable_path}
else:
values_calculation = None
else:
values_calculation["type"] = "variable"
values_calculation = {"path": variable.path}
if identifiers:
values_calculation["identifiers"] = identifiers
values_calculation["identifier_type"] = "many"
if prop in PROPERTY_ATTRIBUTE:
# get comparative value
self.when_to_condition(values, values_calculation)
else:
values_calculation["type"] = "variable"
return values_calculation
def when_to_condition(self, values, values_calculation):

View file

@ -153,12 +153,18 @@ class _ToString:
variable_path = condition["path"]
values = []
option = self.true_config.option(variable_path)
if option.isdynamic():
try:
option.get()
except AttributeError:
defined = False
else:
defined = True
if defined and option.isdynamic():
variables = self._get_annotation_variable(
child, option, condition.get("identifiers")
)
else:
option = self.true_config.option(variable_path)
# option = self.true_config.option(variable_path)
try:
is_inaccessible = self.is_inaccessible_user_data(option)
except AttributeError as err:
@ -173,10 +179,10 @@ class _ToString:
for cpath, description, identifiers, identifier_type in variables:
if not cpath:
# we cannot access to this variable, so try with permissive
if condition["type"] == "transitive":
if condition.get("type") == "transitive":
value = None
else:
value = condition["value"]
value = condition.get("value")
option = self.true_config.forcepermissive.option(variable_path)
try:
variable_value = self._get_unmodified_default_value(option)
@ -247,16 +253,25 @@ class _ToString:
def _calculation_variable_to_string_not_property(
self, child, calculation, attribute_type
):
variable = self.true_config.unrestraint.option(calculation["value"]["path"])
path = calculation["value"]["path"]
variable = self.true_config.unrestraint.option(path)
defined = True
if calculation["optional"]:
# option?
try:
variable.get()
except AttributeError:
return None
defined = False
true_msg = _('the value of the variable {0} if it is defined')
else:
true_msg = _('the value of the variable {0}')
return self._calculation_with_variable(child, variable, calculation["value"], true_msg)
if defined:
return self._calculation_with_variable(child, variable, calculation["value"], true_msg)
return {
"message": true_msg,
"path": calculation["value"],
"description": None,
}
def _calculation_with_variable(self, child, variable, calculation, msg):
if not variable.isdynamic():

View file

@ -82,7 +82,7 @@ class Examples: # pylint: disable=no-member,too-few-public-methods
self._set_mandatories(config)
return config
def _gen_doc_examples(self, config, only_modified: bool):
def _gen_doc_examples(self, config, only_modified: bool, with_secret_manager: bool=True, with_calculated_value: bool=True) -> dict:
if not only_modified:
self._set_examples(config)
results = CommentedMap()
@ -106,16 +106,19 @@ class Examples: # pylint: disable=no-member,too-few-public-methods
n_results[name] = new_results
n_results = n_results[name]
if only_modified:
dump_type = 'modified'
if with_calculated_value:
dump_type = 'modified'
else:
dump_type = "empty"
else:
dump_type = 'all'
if root_config.isoptiondescription():
self._example_parse_family(
root_config.value.get(), results, dump_type
root_config.value.get(), results, dump_type, with_secret_manager=with_secret_manager
)
else:
self._set_example_value(
results, root_config, root_config.value.get(), dump_type
results, root_config, root_config.value.get(), dump_type, with_secret_manager, False
)
if true_results and results:
n_results.update(results)
@ -200,31 +203,33 @@ class Examples: # pylint: disable=no-member,too-few-public-methods
return option.issubmulti()
return option.ismulti()
def _example_parse_family(self, config, results, dump_type):
def _example_parse_family(self, config, results, dump_type, *, with_secret_manager: bool=True, with_true_path: bool=False) -> None:
for option, values in config.items():
if option.isoptiondescription():
if option.isleadership():
subresults = self._example_parse_sequence(values, dump_type)
subresults = self._example_parse_sequence(values, dump_type, with_secret_manager, with_true_path)
if subresults:
name = option.name()
name = option.name(uncalculated=with_true_path)
results[name] = subresults
self._set_description(results, name, option)
else:
subresults = CommentedMap()
self._example_parse_family(values, subresults, dump_type)
self._example_parse_family(values, subresults, dump_type, with_secret_manager=with_secret_manager, with_true_path=with_true_path)
if subresults:
name = option.name()
name = option.name(uncalculated=with_true_path)
results[name] = subresults
self._set_description(results, name, option)
else:
self._set_example_value(results, option, values, dump_type)
self._set_example_value(results, option, values, dump_type, with_secret_manager, with_true_path)
def _set_example_value(self, results, option, values, dump_type):
def _set_example_value(self, results, option, values, dump_type, with_secret_manager, with_true_path):
if not self._is_valid_owner(option, dump_type):
return
if dump_type == "hidden" and values is None:
if not with_secret_manager and option.information.get("secret_manager", False):
return
if dump_type in ["hidden", "empty"] and values is None:
values = self._get_an_example(option)
name = option.name()
name = option.name(uncalculated=with_true_path)
results[name] = values
self._set_description(results, name, option)
@ -238,20 +243,21 @@ class Examples: # pylint: disable=no-member,too-few-public-methods
is_default = option.owner.isdefault()
return (
(dump_type == 'modified' and not is_default and option.owner.get() != owners.forced)
or (dump_type == "empty" and "hidden" in option.property.get() and not option.information.get("default_calculation", None) and (option.value.default(uncalculated=True) in [None, []] or not option.information.get("default_value_makes_sense", True)))
or (dump_type == 'default' and is_default)
)
def _example_parse_sequence(self, values, dump_type):
def _example_parse_sequence(self, values, dump_type, with_secret_manager, with_true_path):
sequence_iter = iter(values.items())
leader, leader_values = next(sequence_iter)
if not self._is_valid_owner(leader, dump_type):
return None
sequence = [CommentedMap() for idx in range(len(leader_values))]
for idx, value in enumerate(leader_values):
self._set_example_value(sequence[idx], leader, value, dump_type)
self._set_example_value(sequence[idx], leader, value, dump_type, with_secret_manager, with_true_path)
for option, value in sequence_iter:
idx = option.index()
self._set_example_value(sequence[idx], option, value, dump_type)
self._set_example_value(sequence[idx], option, value, dump_type, with_secret_manager, with_true_path)
return sequence
def _set_description(self, results, name, option):

View file

@ -200,8 +200,12 @@ class CommonTabular:
+ gen_argument_name(alternative_name, True, True, False)
+ f", {commandlines[1]}"
)
if "full_path" in self.informations:
full_path = self.informations["full_path"]
else:
full_path = self.informations["path"]
self.commandlines = self.formatter.section(
_("Command line"), commandlines, force_enter=True
full_path, _("Command line"), commandlines, force_enter=True
)
else:
self.commandlines = None
@ -218,8 +222,12 @@ class CommonTabular:
variable_prefix=self.formatter.prefix,
with_anchor=False,
)
if "full_path" in self.informations:
full_path = self.informations["full_path"]
else:
full_path = self.informations["path"]
self.environments = self.formatter.section(
_("Environment variable"), environments
full_path, _("Environment variable"), environments
)
else:
self.environments = None
@ -336,8 +344,8 @@ class CommonFormatter:
filename: Optional[str],
) -> str:
"""Set a text link to variable anchor"""
# return f'"{description}"'
# FIXME OPTION POUR METTRE LE PATH
if not description:
return f'"{path}"'
return f'"{description}" ({path})'
def stripped(
@ -449,7 +457,7 @@ class CommonFormatter:
if "full_path" in informations:
full_path = informations["full_path"]
else:
full_path = path
full_path = informations["path"]
if "identifiers" in informations:
for idx, identifier in enumerate(informations["identifiers"]):
if force_identifiers and identifier != force_identifiers:
@ -541,7 +549,11 @@ class CommonFormatter:
informations, {}, None, with_anchor=False, is_bold=False
)
if path:
msg.append(self.section(_("Path"), path, type_="family"))
if "full_path" in informations:
full_path = informations["full_path"]
else:
full_path = informations["path"]
msg.append(self.section(full_path, _("Path"), path, type_="family"))
calculated_properties = []
property_str = self.property_to_string(informations, calculated_properties, {})
if property_str:
@ -549,9 +561,13 @@ class CommonFormatter:
if calculated_properties:
msg.append(self.join(calculated_properties))
if "identifier" in informations:
if "full_path" in informations:
full_path = informations["full_path"]
else:
full_path = informations["path"]
msg.append(
self.section(
_("Identifiers"), informations["identifier"], type_="family"
full_path, _("Identifiers"), informations["identifier"], type_="family"
)
)
if msg:
@ -617,8 +633,6 @@ class CommonFormatter:
) -> str:
add_new_description = True
def _get_description(description, identifiers, delete=False, new=[], previous_identifiers=[], new_identifiers=[], its_a_name=False):
# if new_identifiers:
# new_identifiers = new_identifiers[0]
if identifiers and "{{ identifier }}" in description:
if its_a_name:
information_type = "name"
@ -727,14 +741,18 @@ class CommonFormatter:
) -> str():
values = []
submessage = ""
if "full_path" in informations:
full_path = informations["full_path"]
else:
full_path = informations["path"]
if modified_attributes and attribute in modified_attributes:
name, previous, new = modified_attributes[attribute]
if isinstance(previous, list):
for p in previous:
submessage, m = self.message_to_string(p, submessage)
submessage, m = self.message_to_string(full_path, p, submessage)
values.append(self.delete(m))
else:
submessage, old_values = self.message_to_string(previous, submessage)
submessage, old_values = self.message_to_string(full_path, previous, submessage)
values.append(self.delete(old_values))
else:
new = []
@ -743,7 +761,11 @@ class CommonFormatter:
name = old["name"]
if isinstance(old["values"], list):
for value in old["values"]:
submessage, old_value = self.message_to_string(value, submessage)
if "identifiers" in informations and (not isinstance(value, dict) or "identifiers" not in value):
identifiers = informations["identifiers"]
else:
identifiers = []
submessage, old_value = self.message_to_string(full_path, value, submessage, force_identifiers=identifiers)
if value in new:
old_value = self.underline(old_value)
values.append(old_value)
@ -753,17 +775,23 @@ class CommonFormatter:
values = self.join(values)
elif values:
old_values = old["values"]
submessage, old_values = self.message_to_string(old_values, submessage)
submessage, old_values = self.message_to_string(full_path, old_values, submessage)
if old["values"] in new:
old_values = self.underline(old_values)
values.append(old_values)
values = self.join(values)
else:
submessage, values = self.message_to_string(old["values"], submessage)
old_values = old["values"]
if "identifiers" in informations and (not isinstance(old_values, dict) or "identifiers" not in old_values):
identifiers = informations["identifiers"]
else:
identifiers = []
submessage, values = self.message_to_string(full_path, old_values, submessage, force_identifiers=identifiers)
if old["values"] in new:
values = self.underline(values)
if values != []:
return self.section(
full_path,
name,
values,
submessage=submessage,
@ -786,9 +814,13 @@ class CommonFormatter:
default_is_a_list = False
else:
default_is_a_list = True
if "full_path" in informations:
full_path = informations["full_path"]
else:
full_path = informations["path"]
for idx, choice in enumerate(choices_values.copy()):
if isinstance(choice, dict):
choices_values[idx] = self.message_to_string(choice, None)[1]
choices_values[idx] = self.message_to_string(full_path, choice, None)[1]
if "default" in modified_attributes:
name, old_default, new_default = modified_attributes["default"]
if not old_default:
@ -814,7 +846,7 @@ class CommonFormatter:
default = [default]
for idx, value in enumerate(default.copy()):
if isinstance(value, dict):
default[idx] = self.message_to_string(value, None)[1]
default[idx] = self.message_to_string(full_path, value, None)[1]
default_value_not_in_choices = set(default) - set(choices_values)
if default_value_not_in_choices:
default_is_changed = False
@ -842,7 +874,7 @@ class CommonFormatter:
name, previous, new = modified_attributes["choices"]
for choice in reversed(previous):
if isinstance(choice, dict):
choice = self.message_to_string(choice, None)[1]
choice = self.message_to_string(full_path, choice, None)[1]
if with_default and choice in old_default:
choices_values.insert(
0, self.delete(dump(choice) + "" + _("(default)"))
@ -877,7 +909,7 @@ class CommonFormatter:
# if old value and new value is a list, display a list
if not default_is_a_list and len(choices_values) == 1:
choices_values = choices_values[0]
return default_is_already_set, self.section(choices["name"], choices_values)
return default_is_already_set, self.section(full_path, choices["name"], choices_values)
return default_is_already_set, None
# OTHERs
@ -1044,13 +1076,17 @@ class CommonFormatter:
)
)
if local_calculated_properties:
if "full_path" in informations:
full_path = informations["full_path"]
else:
full_path = informations["path"]
for (
calculated_property_name,
calculated_property,
) in local_calculated_properties.items():
data = []
for calc in calculated_property:
annotation = self.message_to_string(calc["annotation"], None)[1]
annotation = self.message_to_string(full_path, calc["annotation"], None)[1]
if calc.get("underline", False):
annotation = self.underline(annotation)
if calc.get("delete", False):
@ -1062,7 +1098,7 @@ class CommonFormatter:
calculated_property = data[0]
calculated_properties.append(
self.section(
calculated_property_name.capitalize(), calculated_property
full_path, calculated_property_name.capitalize(), calculated_property
)
)
if not properties:
@ -1099,7 +1135,7 @@ class CommonFormatter:
)
return msg
def message_to_string(self, msg, ret, *, identifiers=[]):
def message_to_string(self, full_path, msg, ret, *, force_identifiers=[]):
if isinstance(msg, dict):
if "submessage" in msg:
ret += msg["submessage"]
@ -1117,10 +1153,14 @@ class CommonFormatter:
filename = self.other_root_filenames["."]
if "identifiers" in msg["path"]:
msg["identifiers"] = msg["path"]["identifiers"]
calculated_paths = calc_path(msg["path"], formatter=self, identifiers=identifiers)
calculated_paths = calc_path(msg["path"], formatter=self, identifiers=force_identifiers)
if self.support_namespace and self.document_a_type:
namespace = full_path.split(".", 1)[0]
else:
namespace = None
if isinstance(calculated_paths, list):
msgs = [msg["message"].format(self.link_variable(
doc_path(calculated_path, self.document_a_type),
doc_path(calculated_path, self.document_a_type, namespace),
msg["path"]["path"],
self.get_description(msg, {}, force_identifiers=[msg["path"]["identifiers"][idx]], with_to_phrase=False),
filename=filename,
@ -1128,7 +1168,7 @@ class CommonFormatter:
msg = self.list(msgs)
else:
path = self.link_variable(
doc_path(calculated_paths, self.document_a_type),
doc_path(calculated_paths, self.document_a_type, namespace),
msg["path"]["path"],
self.get_description(msg, {}, with_to_phrase=False),
filename=filename,
@ -1149,6 +1189,8 @@ class CommonFormatter:
if "." in self.other_root_filenames:
filename = self.other_root_filenames["."]
identifiers = variable.get("identifiers")
if identifiers is None and force_identifiers:
identifiers = force_identifiers
if self.format_in_title:
formatter = self
else:
@ -1176,6 +1218,7 @@ class CommonFormatter:
def section(
self,
full_path: str,
name: str,
msg: str,
submessage: str = "",
@ -1185,10 +1228,10 @@ class CommonFormatter:
force_enter=False,
) -> str:
"""Return something like Name: msg"""
submessage, msg = self.message_to_string(msg, submessage)
submessage, msg = self.message_to_string(full_path, msg, submessage)
if isinstance(msg, list):
if len(msg) == 1:
submessage, elt = self.message_to_string(msg[0], submessage)
submessage, elt = self.message_to_string(full_path, msg[0], submessage)
if isinstance(elt, list):
submessage += self.list(elt, type_=type_, with_enter=section_name)
elif force_enter:
@ -1198,7 +1241,7 @@ class CommonFormatter:
else:
lst = []
for p in msg:
submessage, elt = self.message_to_string(p, submessage)
submessage, elt = self.message_to_string(full_path, p, submessage)
lst.append(elt)
submessage += self.list(lst, type_=type_, with_enter=section_name)
msg = ""
@ -1222,11 +1265,13 @@ def calc_path(path, *, formatter=None, identifiers: List[str] = None) -> str:
if formatter:
identifier = formatter.italic(identifier)
return path.replace("{{ identifier }}", identifier, 1)
if isinstance(path, dict):
path_ = path["path"]
if "identifiers" in path:
path_ = get_path_from_identifiers(path["path"], path["identifiers"], [], [], path["identifier_type"], formatter)
elif identifiers:
for identifier in identifiers[0]:
path_ = _path_with_identifier(path_, identifier)
elif identifiers:
path_ = path
for identifier in identifiers:
@ -1237,11 +1282,12 @@ def calc_path(path, *, formatter=None, identifiers: List[str] = None) -> str:
return path_
def doc_path(path, document_a_type):
def doc_path(path, document_a_type, namespace: Optional[str]=None) -> str:
if document_a_type:
if "." not in path:
return None
return path.split(".", 1)[-1]
if not namespace or path.startswith(namespace + '.'):
return path.split(".", 1)[-1]
return path