Plugin/module docs: parse return values, add collection names in them (version_added_collection), and format them nicely in ansible-doc (#69796)
* Tag return value docs if they are a dict (and not str/None). * Try to parse return docs as YAML. * Properly dump return values in ansible-doc. * Adjust plugin formatter. * Add changelog fragment. * Don't add 'default' for return values. * Fix plugin_formatter. * Only try to parse return docs if they are still a string. * Add tests. * Warn if RETURN cannot be parsed. * Adjust tests. Also test for warning. * if -> elif (otherwise EXAMPLE will be parsed too). * Always parse return documentation, and fail if it is invalid YAML. * Polishing. * Mostly re-enable ansible-doc tests. Listing from the local collection seems to be somewhat broken. I assume this is why the test was disabled. * Lint and make tests work with Python 2. * Keep FQCNs in plugins (not modules), i.e. restore previous state.
This commit is contained in:
parent
f509a22f9d
commit
8d93ba9120
13 changed files with 203 additions and 38 deletions
4
changelogs/fragments/69796-ansible-doc-return-values.yml
Normal file
4
changelogs/fragments/69796-ansible-doc-return-values.yml
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
minor_changes:
|
||||||
|
- "ansible-doc - return values will be properly formatted (https://github.com/ansible/ansible/pull/69796)."
|
||||||
|
major_changes:
|
||||||
|
- "Both ansible-doc and ansible-console's help command will error for modules and plugins whose return documentation cannot be parsed as YAML. All modules and plugins passing ``ansible-test sanity --test yamllint`` will not be affected by this."
|
|
@ -527,12 +527,8 @@ def process_plugins(module_map, templates, outputname, output_dir, ansible_versi
|
||||||
|
|
||||||
display.vvvvv(pp.pformat(module_map[module]))
|
display.vvvvv(pp.pformat(module_map[module]))
|
||||||
if module_map[module]['returndocs']:
|
if module_map[module]['returndocs']:
|
||||||
try:
|
doc['returndocs'] = module_map[module]['returndocs']
|
||||||
doc['returndocs'] = yaml.safe_load(module_map[module]['returndocs'])
|
process_returndocs(doc['returndocs'])
|
||||||
process_returndocs(doc['returndocs'])
|
|
||||||
except Exception as e:
|
|
||||||
print("%s:%s:yaml error:%s:returndocs=%s" % (fname, module, e, module_map[module]['returndocs']))
|
|
||||||
doc['returndocs'] = None
|
|
||||||
else:
|
else:
|
||||||
doc['returndocs'] = None
|
doc['returndocs'] = None
|
||||||
|
|
||||||
|
|
|
@ -235,13 +235,6 @@ class DocCLI(CLI):
|
||||||
plugin_docs[plugin] = {'doc': doc, 'examples': plainexamples, 'return': returndocs, 'metadata': metadata}
|
plugin_docs[plugin] = {'doc': doc, 'examples': plainexamples, 'return': returndocs, 'metadata': metadata}
|
||||||
|
|
||||||
if do_json:
|
if do_json:
|
||||||
# Some changes to how json docs are formatted
|
|
||||||
for plugin, doc_data in plugin_docs.items():
|
|
||||||
try:
|
|
||||||
doc_data['return'] = yaml.safe_load(doc_data['return'])
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
jdump(plugin_docs)
|
jdump(plugin_docs)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
@ -346,6 +339,8 @@ class DocCLI(CLI):
|
||||||
# TODO: do we really want this?
|
# TODO: do we really want this?
|
||||||
# add_collection_to_versions_and_dates(doc, '(unknown)', is_module=(plugin_type == 'module'))
|
# add_collection_to_versions_and_dates(doc, '(unknown)', is_module=(plugin_type == 'module'))
|
||||||
# remove_current_collection_from_versions_and_dates(doc, collection_name, is_module=(plugin_type == 'module'))
|
# remove_current_collection_from_versions_and_dates(doc, collection_name, is_module=(plugin_type == 'module'))
|
||||||
|
# remove_current_collection_from_versions_and_dates(
|
||||||
|
# returndocs, collection_name, is_module=(plugin_type == 'module'), return_docs=True)
|
||||||
|
|
||||||
# assign from other sections
|
# assign from other sections
|
||||||
doc['plainexamples'] = plainexamples
|
doc['plainexamples'] = plainexamples
|
||||||
|
@ -515,7 +510,7 @@ class DocCLI(CLI):
|
||||||
Dumper=AnsibleDumper).split('\n')]))
|
Dumper=AnsibleDumper).split('\n')]))
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def add_fields(text, fields, limit, opt_indent, base_indent=''):
|
def add_fields(text, fields, limit, opt_indent, return_values=False, base_indent=''):
|
||||||
|
|
||||||
for o in sorted(fields):
|
for o in sorted(fields):
|
||||||
opt = fields[o]
|
opt = fields[o]
|
||||||
|
@ -552,8 +547,9 @@ class DocCLI(CLI):
|
||||||
choices = "(Choices: " + ", ".join(to_text(i) for i in opt['choices']) + ")"
|
choices = "(Choices: " + ", ".join(to_text(i) for i in opt['choices']) + ")"
|
||||||
del opt['choices']
|
del opt['choices']
|
||||||
default = ''
|
default = ''
|
||||||
if 'default' in opt or not required:
|
if not return_values:
|
||||||
default = "[Default: %s" % to_text(opt.pop('default', '(null)')) + "]"
|
if 'default' in opt or not required:
|
||||||
|
default = "[Default: %s" % to_text(opt.pop('default', '(null)')) + "]"
|
||||||
|
|
||||||
text.append(textwrap.fill(DocCLI.tty_ify(aliases + choices + default), limit,
|
text.append(textwrap.fill(DocCLI.tty_ify(aliases + choices + default), limit,
|
||||||
initial_indent=opt_indent, subsequent_indent=opt_indent))
|
initial_indent=opt_indent, subsequent_indent=opt_indent))
|
||||||
|
@ -591,7 +587,7 @@ class DocCLI(CLI):
|
||||||
for subkey, subdata in suboptions:
|
for subkey, subdata in suboptions:
|
||||||
text.append('')
|
text.append('')
|
||||||
text.append("%s%s:\n" % (opt_indent, subkey.upper()))
|
text.append("%s%s:\n" % (opt_indent, subkey.upper()))
|
||||||
DocCLI.add_fields(text, subdata, limit, opt_indent + ' ', opt_indent)
|
DocCLI.add_fields(text, subdata, limit, opt_indent + ' ', return_values, opt_indent)
|
||||||
if not suboptions:
|
if not suboptions:
|
||||||
text.append('')
|
text.append('')
|
||||||
|
|
||||||
|
@ -705,9 +701,6 @@ class DocCLI(CLI):
|
||||||
|
|
||||||
if doc.get('returndocs', False):
|
if doc.get('returndocs', False):
|
||||||
text.append("RETURN VALUES:")
|
text.append("RETURN VALUES:")
|
||||||
if isinstance(doc['returndocs'], string_types):
|
DocCLI.add_fields(text, doc.pop('returndocs'), limit, opt_indent, return_values=True)
|
||||||
text.append(doc.pop('returndocs'))
|
|
||||||
else:
|
|
||||||
text.append(yaml.dump(doc.pop('returndocs'), indent=2, default_flow_style=False))
|
|
||||||
|
|
||||||
return "\n".join(text)
|
return "\n".join(text)
|
||||||
|
|
|
@ -56,8 +56,8 @@ def read_docstring(filename, verbose=True, ignore_errors=True):
|
||||||
if isinstance(child.value, ast.Dict):
|
if isinstance(child.value, ast.Dict):
|
||||||
data[varkey] = ast.literal_eval(child.value)
|
data[varkey] = ast.literal_eval(child.value)
|
||||||
else:
|
else:
|
||||||
if theid in ['EXAMPLES', 'RETURN']:
|
if theid == 'EXAMPLES':
|
||||||
# examples 'can' be yaml, return must be, but even if so, we dont want to parse as such here
|
# examples 'can' be yaml, but even if so, we dont want to parse as such here
|
||||||
# as it can create undesired 'objects' that don't display well as docs.
|
# as it can create undesired 'objects' that don't display well as docs.
|
||||||
data[varkey] = to_text(child.value.s)
|
data[varkey] = to_text(child.value.s)
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -85,6 +85,9 @@ def _process_versions_and_dates(fragment, is_module, return_docs, callback):
|
||||||
if isinstance(return_value.get('contains'), MutableMapping):
|
if isinstance(return_value.get('contains'), MutableMapping):
|
||||||
process_return_values(return_value['contains'])
|
process_return_values(return_value['contains'])
|
||||||
|
|
||||||
|
if not fragment:
|
||||||
|
return
|
||||||
|
|
||||||
if return_docs:
|
if return_docs:
|
||||||
process_return_values(fragment)
|
process_return_values(fragment)
|
||||||
return
|
return
|
||||||
|
@ -203,13 +206,18 @@ def get_docstring(filename, fragment_loader, verbose=False, ignore_errors=False,
|
||||||
data = read_docstring(filename, verbose=verbose, ignore_errors=ignore_errors)
|
data = read_docstring(filename, verbose=verbose, ignore_errors=ignore_errors)
|
||||||
|
|
||||||
if data.get('doc', False):
|
if data.get('doc', False):
|
||||||
# tag version_added
|
# add collection name to versions and dates
|
||||||
if collection_name is not None:
|
if collection_name is not None:
|
||||||
add_collection_to_versions_and_dates(data['doc'], collection_name, is_module=is_module)
|
add_collection_to_versions_and_dates(data['doc'], collection_name, is_module=is_module)
|
||||||
|
|
||||||
# add fragments to documentation
|
# add fragments to documentation
|
||||||
add_fragments(data['doc'], filename, fragment_loader=fragment_loader, is_module=is_module)
|
add_fragments(data['doc'], filename, fragment_loader=fragment_loader, is_module=is_module)
|
||||||
|
|
||||||
|
if data.get('returndocs', False):
|
||||||
|
# add collection name to versions and dates
|
||||||
|
if collection_name is not None:
|
||||||
|
add_collection_to_versions_and_dates(data['returndocs'], collection_name, is_module=is_module, return_docs=True)
|
||||||
|
|
||||||
return data['doc'], data['plainexamples'], data['returndocs'], data['metadata']
|
return data['doc'], data['plainexamples'], data['returndocs'], data['metadata']
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,2 +1 @@
|
||||||
shippable/posix/group1
|
shippable/posix/group1
|
||||||
disabled
|
|
||||||
|
|
|
@ -2,7 +2,7 @@ from __future__ import (absolute_import, division, print_function)
|
||||||
__metaclass__ = type
|
__metaclass__ = type
|
||||||
|
|
||||||
DOCUMENTATION = '''
|
DOCUMENTATION = '''
|
||||||
vars: noop_vars_plugin
|
vars: testns.testcol.noop_vars_plugin
|
||||||
short_description: Do NOT load host and group vars
|
short_description: Do NOT load host and group vars
|
||||||
description: don't test loading host and group vars from a collection
|
description: don't test loading host and group vars from a collection
|
||||||
options:
|
options:
|
||||||
|
|
|
@ -0,0 +1,56 @@
|
||||||
|
#!/usr/bin/python
|
||||||
|
from __future__ import absolute_import, division, print_function
|
||||||
|
__metaclass__ = type
|
||||||
|
|
||||||
|
|
||||||
|
DOCUMENTATION = '''
|
||||||
|
---
|
||||||
|
module: test_docs_returns
|
||||||
|
short_description: Test module
|
||||||
|
description:
|
||||||
|
- Test module
|
||||||
|
author:
|
||||||
|
- Ansible Core Team
|
||||||
|
'''
|
||||||
|
|
||||||
|
EXAMPLES = '''
|
||||||
|
'''
|
||||||
|
|
||||||
|
RETURN = '''
|
||||||
|
z_last:
|
||||||
|
description: A last result.
|
||||||
|
type: str
|
||||||
|
returned: success
|
||||||
|
|
||||||
|
m_middle:
|
||||||
|
description:
|
||||||
|
- This should be in the middle.
|
||||||
|
- Has some more data
|
||||||
|
type: dict
|
||||||
|
returned: success and 1st of month
|
||||||
|
contains:
|
||||||
|
suboption:
|
||||||
|
description: A suboption.
|
||||||
|
type: str
|
||||||
|
choices: [ARF, BARN, c_without_capital_first_letter]
|
||||||
|
|
||||||
|
a_first:
|
||||||
|
description: A first result.
|
||||||
|
type: str
|
||||||
|
returned: success
|
||||||
|
'''
|
||||||
|
|
||||||
|
|
||||||
|
from ansible.module_utils.basic import AnsibleModule
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
module = AnsibleModule(
|
||||||
|
argument_spec=dict(),
|
||||||
|
)
|
||||||
|
|
||||||
|
module.exit_json()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
|
@ -0,0 +1,40 @@
|
||||||
|
#!/usr/bin/python
|
||||||
|
from __future__ import absolute_import, division, print_function
|
||||||
|
__metaclass__ = type
|
||||||
|
|
||||||
|
|
||||||
|
DOCUMENTATION = '''
|
||||||
|
---
|
||||||
|
module: test_docs_returns_broken
|
||||||
|
short_description: Test module
|
||||||
|
description:
|
||||||
|
- Test module
|
||||||
|
author:
|
||||||
|
- Ansible Core Team
|
||||||
|
'''
|
||||||
|
|
||||||
|
EXAMPLES = '''
|
||||||
|
'''
|
||||||
|
|
||||||
|
RETURN = '''
|
||||||
|
test:
|
||||||
|
description: A test return value.
|
||||||
|
type: str
|
||||||
|
|
||||||
|
broken_key: [
|
||||||
|
'''
|
||||||
|
|
||||||
|
|
||||||
|
from ansible.module_utils.basic import AnsibleModule
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
module = AnsibleModule(
|
||||||
|
argument_spec=dict(),
|
||||||
|
)
|
||||||
|
|
||||||
|
module.exit_json()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
|
@ -12,18 +12,13 @@ current_out="$(ansible-doc --playbook-dir ./ testns.testcol.fakemodule)"
|
||||||
expected_out="$(cat fakemodule.output)"
|
expected_out="$(cat fakemodule.output)"
|
||||||
test "$current_out" == "$expected_out"
|
test "$current_out" == "$expected_out"
|
||||||
|
|
||||||
# test module docs from collection
|
|
||||||
current_out="$(ansible-doc --playbook-dir ./ test_docs_suboptions)"
|
|
||||||
expected_out="$(cat test_docs_suboptions.output)"
|
|
||||||
test "$current_out" == "$expected_out"
|
|
||||||
|
|
||||||
# test listing diff plugin types from collection
|
# test listing diff plugin types from collection
|
||||||
for ptype in cache inventory lookup vars
|
for ptype in cache inventory lookup vars
|
||||||
do
|
do
|
||||||
# each plugin type adds 1 from collection
|
# each plugin type adds 1 from collection
|
||||||
pre=$(ansible-doc -l -t ${ptype}|wc -l)
|
# FIXME pre=$(ansible-doc -l -t ${ptype}|wc -l)
|
||||||
post=$(ansible-doc -l -t ${ptype} --playbook-dir ./|wc -l)
|
# FIXME post=$(ansible-doc -l -t ${ptype} --playbook-dir ./|wc -l)
|
||||||
test "$pre" -eq $((post - 1))
|
# FIXME test "$pre" -eq $((post - 1))
|
||||||
|
|
||||||
# ensure we ONLY list from the collection
|
# ensure we ONLY list from the collection
|
||||||
justcol=$(ansible-doc -l -t ${ptype} --playbook-dir ./ testns.testcol|wc -l)
|
justcol=$(ansible-doc -l -t ${ptype} --playbook-dir ./ testns.testcol|wc -l)
|
||||||
|
|
|
@ -3,6 +3,46 @@
|
||||||
environment:
|
environment:
|
||||||
ANSIBLE_LIBRARY: "{{ playbook_dir }}/library"
|
ANSIBLE_LIBRARY: "{{ playbook_dir }}/library"
|
||||||
tasks:
|
tasks:
|
||||||
|
- name: module with suboptions
|
||||||
|
command: ansible-doc test_docs_suboptions
|
||||||
|
register: result
|
||||||
|
ignore_errors: true
|
||||||
|
|
||||||
|
- set_fact:
|
||||||
|
actual_output: >-
|
||||||
|
{{ result.stdout | regex_replace('^(> [A-Z_]+ +\().+library/([a-z_]+.py)\)$', '\1library/\2)', multiline=true) }}
|
||||||
|
expected_output: "{{ lookup('file', 'test_docs_suboptions.output') }}"
|
||||||
|
|
||||||
|
- assert:
|
||||||
|
that:
|
||||||
|
- result is succeeded
|
||||||
|
- actual_output == expected_output
|
||||||
|
|
||||||
|
- name: module with return docs
|
||||||
|
command: ansible-doc test_docs_returns
|
||||||
|
register: result
|
||||||
|
ignore_errors: true
|
||||||
|
|
||||||
|
- set_fact:
|
||||||
|
actual_output: >-
|
||||||
|
{{ result.stdout | regex_replace('^(> [A-Z_]+ +\().+library/([a-z_]+.py)\)$', '\1library/\2)', multiline=true) }}
|
||||||
|
expected_output: "{{ lookup('file', 'test_docs_returns.output') }}"
|
||||||
|
|
||||||
|
- assert:
|
||||||
|
that:
|
||||||
|
- result is succeeded
|
||||||
|
- actual_output == expected_output
|
||||||
|
|
||||||
|
- name: module with broken return docs
|
||||||
|
command: ansible-doc test_docs_returns_broken
|
||||||
|
register: result
|
||||||
|
ignore_errors: true
|
||||||
|
|
||||||
|
- assert:
|
||||||
|
that:
|
||||||
|
- result is failed
|
||||||
|
- '"ERROR! module test_docs_returns_broken missing documentation (or could not parse documentation)" in result.stderr'
|
||||||
|
|
||||||
- name: non-existent module
|
- name: non-existent module
|
||||||
command: ansible-doc test_does_not_exist
|
command: ansible-doc test_does_not_exist
|
||||||
register: result
|
register: result
|
||||||
|
|
|
@ -0,0 +1,37 @@
|
||||||
|
> TEST_DOCS_RETURNS (library/test_docs_returns.py)
|
||||||
|
|
||||||
|
Test module
|
||||||
|
|
||||||
|
AUTHOR: Ansible Core Team
|
||||||
|
|
||||||
|
EXAMPLES:
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
RETURN VALUES:
|
||||||
|
- a_first
|
||||||
|
A first result.
|
||||||
|
|
||||||
|
returned: success
|
||||||
|
type: str
|
||||||
|
|
||||||
|
- m_middle
|
||||||
|
This should be in the middle.
|
||||||
|
Has some more data
|
||||||
|
|
||||||
|
returned: success and 1st of month
|
||||||
|
type: dict
|
||||||
|
|
||||||
|
CONTAINS:
|
||||||
|
|
||||||
|
- suboption
|
||||||
|
A suboption.
|
||||||
|
(Choices: ARF, BARN, c_without_capital_first_letter)
|
||||||
|
type: str
|
||||||
|
|
||||||
|
- z_last
|
||||||
|
A last result.
|
||||||
|
|
||||||
|
returned: success
|
||||||
|
type: str
|
|
@ -1,4 +1,4 @@
|
||||||
> TEST_DOCS_SUBOPTIONS (/home/felix/projects/code/github-cloned/ansible/test/integration/targets/ansible-doc/library/test_docs_suboptions.py)
|
> TEST_DOCS_SUBOPTIONS (library/test_docs_suboptions.py)
|
||||||
|
|
||||||
Test module
|
Test module
|
||||||
|
|
||||||
|
@ -41,6 +41,3 @@ EXAMPLES:
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
RETURN VALUES:
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue