From 94c069ab758f9e4810df09635efd201130580bcd Mon Sep 17 00:00:00 2001 From: Emmanuel Garette Date: Sun, 21 Jun 2026 14:42:34 +0200 Subject: [PATCH] feat: better ansible documentation --- src/rougail/output_doc/annotator.py | 26 ++++--- src/rougail/output_doc/collect.py | 29 ++++++-- src/rougail/output_doc/example.py | 38 +++++----- src/rougail/output_doc/utils.py | 106 ++++++++++++++++++++-------- 4 files changed, 137 insertions(+), 62 deletions(-) diff --git a/src/rougail/output_doc/annotator.py b/src/rougail/output_doc/annotator.py index 70ef90742..14bf20921 100644 --- a/src/rougail/output_doc/annotator.py +++ b/src/rougail/output_doc/annotator.py @@ -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): diff --git a/src/rougail/output_doc/collect.py b/src/rougail/output_doc/collect.py index e1c84c821..585d80a8f 100644 --- a/src/rougail/output_doc/collect.py +++ b/src/rougail/output_doc/collect.py @@ -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(): diff --git a/src/rougail/output_doc/example.py b/src/rougail/output_doc/example.py index 0c7a13786..e6659143f 100644 --- a/src/rougail/output_doc/example.py +++ b/src/rougail/output_doc/example.py @@ -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): diff --git a/src/rougail/output_doc/utils.py b/src/rougail/output_doc/utils.py index eb51890a2..4a64d871b 100644 --- a/src/rougail/output_doc/utils.py +++ b/src/rougail/output_doc/utils.py @@ -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