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"] self.default_values = kwargs["force_default_value"]
else: else:
self.default_values = self.objectspace.rougailconfig["doc.default_values"] 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.regexp_description_get_paths = None
self.populate_family() self.populate_family()
self.populate_variable() self.populate_variable()
@ -334,7 +338,11 @@ class Annotator(Walk):
if not variable or ( if not variable or (
isinstance(values, VariableCalculation) and values.optional and not variable isinstance(values, VariableCalculation) and values.optional and not variable
): ):
return None if self.display_unknown_optional_variable and values.optional:
values_calculation = {"path": variable_path}
else:
values_calculation = None
else:
values_calculation = {"path": variable.path} values_calculation = {"path": variable.path}
if identifiers: if identifiers:
values_calculation["identifiers"] = identifiers values_calculation["identifiers"] = identifiers

View file

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

View file

@ -82,7 +82,7 @@ class Examples: # pylint: disable=no-member,too-few-public-methods
self._set_mandatories(config) self._set_mandatories(config)
return 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: if not only_modified:
self._set_examples(config) self._set_examples(config)
results = CommentedMap() results = CommentedMap()
@ -106,16 +106,19 @@ class Examples: # pylint: disable=no-member,too-few-public-methods
n_results[name] = new_results n_results[name] = new_results
n_results = n_results[name] n_results = n_results[name]
if only_modified: if only_modified:
if with_calculated_value:
dump_type = 'modified' dump_type = 'modified'
else:
dump_type = "empty"
else: else:
dump_type = 'all' dump_type = 'all'
if root_config.isoptiondescription(): if root_config.isoptiondescription():
self._example_parse_family( 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: else:
self._set_example_value( 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: if true_results and results:
n_results.update(results) n_results.update(results)
@ -200,31 +203,33 @@ class Examples: # pylint: disable=no-member,too-few-public-methods
return option.issubmulti() return option.issubmulti()
return option.ismulti() 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(): for option, values in config.items():
if option.isoptiondescription(): if option.isoptiondescription():
if option.isleadership(): 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: if subresults:
name = option.name() name = option.name(uncalculated=with_true_path)
results[name] = subresults results[name] = subresults
self._set_description(results, name, option) self._set_description(results, name, option)
else: else:
subresults = CommentedMap() 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: if subresults:
name = option.name() name = option.name(uncalculated=with_true_path)
results[name] = subresults results[name] = subresults
self._set_description(results, name, option) self._set_description(results, name, option)
else: 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): if not self._is_valid_owner(option, dump_type):
return 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) values = self._get_an_example(option)
name = option.name() name = option.name(uncalculated=with_true_path)
results[name] = values results[name] = values
self._set_description(results, name, option) 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() is_default = option.owner.isdefault()
return ( return (
(dump_type == 'modified' and not is_default and option.owner.get() != owners.forced) (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) 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()) sequence_iter = iter(values.items())
leader, leader_values = next(sequence_iter) leader, leader_values = next(sequence_iter)
if not self._is_valid_owner(leader, dump_type): if not self._is_valid_owner(leader, dump_type):
return None return None
sequence = [CommentedMap() for idx in range(len(leader_values))] sequence = [CommentedMap() for idx in range(len(leader_values))]
for idx, value in enumerate(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: for option, value in sequence_iter:
idx = option.index() 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 return sequence
def _set_description(self, results, name, option): def _set_description(self, results, name, option):

View file

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