Fix ansible-doc and docsite generation for removed modules

* Fix ansible-doc wrt removed modules
  * Fix listing of modules ia ansible-doc to not complain about removed modules
    Removed modules are marked as such in the metadata but nowhere else.
    Need to retrieve the metadata when a module doesn't have a doc so that
    we can tell if it falls under this case.
  * omit removed modules from json dump
  * Print an error that the module has been removed if attempting to run
    ansible-doc on that specific module

* Get plugin_formatter to stop outputting removed modules
This commit is contained in:
Toshio Kuratomi 2018-08-22 19:55:39 -07:00
parent b2932a41b0
commit 0873d46617
4 changed files with 59 additions and 53 deletions

View file

@ -30,6 +30,7 @@ import re
import sys import sys
import warnings import warnings
from collections import defaultdict from collections import defaultdict
from copy import deepcopy
from distutils.version import LooseVersion from distutils.version import LooseVersion
from functools import partial from functools import partial
from pprint import PrettyPrinter from pprint import PrettyPrinter
@ -263,11 +264,18 @@ def get_plugin_info(module_dir, limit_to=None, verbose=False):
# Regular module to process # Regular module to process
# #
# use ansible core library to parse out doc metadata YAML and plaintext examples
doc, examples, returndocs, metadata = plugin_docs.get_docstring(module_path, fragment_loader, verbose=verbose)
if metadata and 'removed' in metadata.get('status'):
continue
category = categories category = categories
# Start at the second directory because we don't want the "vendor" # Start at the second directory because we don't want the "vendor"
mod_path_only = os.path.dirname(module_path[len(module_dir):]) mod_path_only = os.path.dirname(module_path[len(module_dir):])
primary_category = ''
module_categories = [] module_categories = []
# build up the categories that this module belongs to # build up the categories that this module belongs to
for new_cat in mod_path_only.split('/')[1:]: for new_cat in mod_path_only.split('/')[1:]:
@ -283,9 +291,6 @@ def get_plugin_info(module_dir, limit_to=None, verbose=False):
if module_categories: if module_categories:
primary_category = module_categories[0] primary_category = module_categories[0]
# use ansible core library to parse out doc metadata YAML and plaintext examples
doc, examples, returndocs, metadata = plugin_docs.get_docstring(module_path, fragment_loader, verbose=verbose)
if 'options' in doc and doc['options'] is None: if 'options' in doc and doc['options'] is None:
display.error("*** ERROR: DOCUMENTATION.options must be a dictionary/hash when used. ***") display.error("*** ERROR: DOCUMENTATION.options must be a dictionary/hash when used. ***")
pos = getattr(doc, "ansible_pos", None) pos = getattr(doc, "ansible_pos", None)
@ -525,6 +530,11 @@ def process_plugins(module_map, templates, outputname, output_dir, ansible_versi
def process_categories(plugin_info, categories, templates, output_dir, output_name, plugin_type): def process_categories(plugin_info, categories, templates, output_dir, output_name, plugin_type):
# For some reason, this line is changing plugin_info:
# text = templates['list_of_CATEGORY_modules'].render(template_data)
# To avoid that, make a deepcopy of the data.
# We should track that down and fix it at some point in the future.
plugin_info = deepcopy(plugin_info)
for category in sorted(categories.keys()): for category in sorted(categories.keys()):
module_map = categories[category] module_map = categories[category]
category_filename = output_name % category category_filename = output_name % category

View file

@ -30,10 +30,12 @@ from ansible.cli import CLI
from ansible.errors import AnsibleError, AnsibleOptionsError from ansible.errors import AnsibleError, AnsibleOptionsError
from ansible.module_utils._text import to_native from ansible.module_utils._text import to_native
from ansible.module_utils.six import string_types from ansible.module_utils.six import string_types
from ansible.parsing.metadata import extract_metadata
from ansible.parsing.plugin_docs import read_docstub
from ansible.parsing.yaml.dumper import AnsibleDumper from ansible.parsing.yaml.dumper import AnsibleDumper
from ansible.plugins.loader import module_loader, action_loader, lookup_loader, callback_loader, cache_loader, \ from ansible.plugins.loader import module_loader, action_loader, lookup_loader, callback_loader, cache_loader, \
vars_loader, connection_loader, strategy_loader, inventory_loader, shell_loader, fragment_loader vars_loader, connection_loader, strategy_loader, inventory_loader, shell_loader, fragment_loader
from ansible.utils.plugin_docs import BLACKLIST, get_docstring, get_docstub from ansible.utils.plugin_docs import BLACKLIST, get_docstring
try: try:
from __main__ import display from __main__ import display
@ -130,7 +132,7 @@ class DocCLI(CLI):
for path in paths: for path in paths:
self.plugin_list.update(self.find_plugins(path, plugin_type)) self.plugin_list.update(self.find_plugins(path, plugin_type))
self.pager(self.get_plugin_list_text(loader, doc_getter=get_docstub)) self.pager(self.get_plugin_list_text(loader))
return 0 return 0
# process all plugins of type # process all plugins of type
@ -144,7 +146,9 @@ class DocCLI(CLI):
plugin_data[plugin_type] = dict() plugin_data[plugin_type] = dict()
plugin_names = self.get_all_plugins_of_type(plugin_type) plugin_names = self.get_all_plugins_of_type(plugin_type)
for plugin_name in plugin_names: for plugin_name in plugin_names:
plugin_data[plugin_type][plugin_name] = self.get_plugin_metadata(plugin_type, plugin_name) plugin_info = self.get_plugin_metadata(plugin_type, plugin_name)
if plugin_info is not None:
plugin_data[plugin_type][plugin_name] = plugin_info
self.pager(json.dumps(plugin_data, sort_keys=True, indent=4)) self.pager(json.dumps(plugin_data, sort_keys=True, indent=4))
@ -183,12 +187,21 @@ class DocCLI(CLI):
raise AnsibleError("unable to load {0} plugin named {1} ".format(plugin_type, plugin_name)) raise AnsibleError("unable to load {0} plugin named {1} ".format(plugin_type, plugin_name))
try: try:
doc, __, __, __ = get_docstring(filename, fragment_loader, verbose=(self.options.verbosity > 0)) doc, __, __, metadata = get_docstring(filename, fragment_loader, verbose=(self.options.verbosity > 0))
except Exception: except Exception:
display.vvv(traceback.format_exc()) display.vvv(traceback.format_exc())
raise AnsibleError( raise AnsibleError(
"%s %s at %s has a documentation error formatting or is missing documentation." % "%s %s at %s has a documentation error formatting or is missing documentation." %
(plugin_type, plugin_name, filename), wrap_text=False) (plugin_type, plugin_name, filename))
if doc is None:
if 'removed' not in metadata.get('status', []):
raise AnsibleError(
"%s %s at %s has a documentation error formatting or is missing documentation." %
(plugin_type, plugin_name, filename))
# Removed plugins don't have any documentation
return None
return dict( return dict(
name=plugin_name, name=plugin_name,
@ -258,6 +271,10 @@ class DocCLI(CLI):
return text return text
else: else:
if 'removed' in metadata.get('status', []):
display.warning("%s %s has been removed\n" % (plugin_type, plugin))
return
# this typically means we couldn't even parse the docstring, not just that the YAML is busted, # this typically means we couldn't even parse the docstring, not just that the YAML is busted,
# probably a quoting issue. # probably a quoting issue.
raise AnsibleError("Parsing produced an empty object.") raise AnsibleError("Parsing produced an empty object.")
@ -304,7 +321,7 @@ class DocCLI(CLI):
return plugin_list return plugin_list
def get_plugin_list_text(self, loader, doc_getter=get_docstring): def get_plugin_list_text(self, loader):
columns = display.columns columns = display.columns
displace = max(len(x) for x in self.plugin_list) displace = max(len(x) for x in self.plugin_list)
linelimit = columns - displace - 5 linelimit = columns - displace - 5
@ -325,13 +342,19 @@ class DocCLI(CLI):
doc = None doc = None
try: try:
doc, plainexamples, returndocs, metadata = doc_getter(filename, fragment_loader) doc = read_docstub(filename)
except Exception: except Exception:
display.warning("%s has a documentation formatting error" % plugin) display.warning("%s has a documentation formatting error" % plugin)
continue
if not doc or not isinstance(doc, dict): if not doc or not isinstance(doc, dict):
desc = 'UNDOCUMENTED' with open(filename) as f:
display.warning("%s parsing did not produce documentation." % plugin) metadata = extract_metadata(module_data=f.read())
if 'removed' not in metadata[0].get('status', []):
desc = 'UNDOCUMENTED'
display.warning("%s parsing did not produce documentation." % plugin)
else:
continue
else: else:
desc = self.tty_ify(doc.get('short_description', 'INVALID SHORT DESCRIPTION').strip()) desc = self.tty_ify(doc.get('short_description', 'INVALID SHORT DESCRIPTION').strip())

View file

@ -82,41 +82,27 @@ def read_docstring(filename, verbose=True, ignore_errors=True):
return data return data
def read_docstub(filename, verbose=True, ignore_errors=True): def read_docstub(filename):
""" """
Quickly find short_description using string methods instead of node parsing. Quickly find short_description using string methods instead of node parsing.
This does not return a full set of documentation strings and is intended for This does not return a full set of documentation strings and is intended for
operations like ansible-doc -l. operations like ansible-doc -l.
""" """
data = { t_module_data = open(filename, 'r')
'doc': None, capturing = False
'plainexamples': None, doc_stub = []
'returndocs': None,
'metadata': None
}
try: for line in t_module_data:
t_module_data = open(filename, 'r') # start capturing the stub until indentation returns
capturing = False if capturing and line[0] == ' ':
doc_stub = [] doc_stub.append(line)
elif capturing and line[0] != ' ':
break
if 'short_description:' in line:
capturing = True
doc_stub.append(line)
for line in t_module_data: data = AnsibleLoader(r"".join(doc_stub), file_name=filename).get_single_data()
# start capturing the stub until indentation returns
if capturing and line[0] == ' ':
doc_stub.append(line)
elif capturing and line[0] != ' ':
break
if 'short_description:' in line:
capturing = True
doc_stub.append(line)
data['doc'] = AnsibleLoader(r"".join(doc_stub), file_name=filename).get_single_data()
except:
if verbose:
display.error("unable to parse %s" % filename)
if not ignore_errors:
raise
return data return data

View file

@ -120,16 +120,3 @@ def get_docstring(filename, fragment_loader, verbose=False, ignore_errors=False)
add_fragments(data['doc'], filename, fragment_loader=fragment_loader) add_fragments(data['doc'], filename, fragment_loader=fragment_loader)
return data['doc'], data['plainexamples'], data['returndocs'], data['metadata'] return data['doc'], data['plainexamples'], data['returndocs'], data['metadata']
def get_docstub(filename, fragment_loader, verbose=False, ignore_errors=False):
"""
When only short_description is needed, load a stub of the full DOCUMENTATION string to speed up operation.
"""
data = read_docstub(filename, verbose=verbose, ignore_errors=ignore_errors)
if data.get('doc', False):
add_fragments(data['doc'], filename, fragment_loader=fragment_loader)
return data['doc'], data['plainexamples'], data['returndocs'], data['metadata']