feat: better ansible documentation
This commit is contained in:
parent
5ccd259e2a
commit
94c069ab75
4 changed files with 137 additions and 62 deletions
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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():
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue