summaryrefslogtreecommitdiff
path: root/docs/bin/plugin_formatter.py
diff options
context:
space:
mode:
authorBrian Coca <brian.coca+git@gmail.com>2017-03-23 01:13:23 -0400
committerBrian Coca <bcoca@users.noreply.github.com>2017-03-24 15:52:36 -0400
commit424e1946f435c5228b450d802a16a585a7798665 (patch)
tree9b93634262a2addc329d2dbda7dbc92483824e39 /docs/bin/plugin_formatter.py
parentb4c47ebf680bd652913387a03aafa6dbc4d66ec0 (diff)
downloadansible-424e1946f435c5228b450d802a16a585a7798665.tar.gz
moved docs generation and templates to docs/
Diffstat (limited to 'docs/bin/plugin_formatter.py')
-rwxr-xr-xdocs/bin/plugin_formatter.py487
1 files changed, 487 insertions, 0 deletions
diff --git a/docs/bin/plugin_formatter.py b/docs/bin/plugin_formatter.py
new file mode 100755
index 0000000000..319b38d1c0
--- /dev/null
+++ b/docs/bin/plugin_formatter.py
@@ -0,0 +1,487 @@
+#!/usr/bin/env python
+# (c) 2012, Jan-Piet Mens <jpmens () gmail.com>
+# (c) 2012-2014, Michael DeHaan <michael@ansible.com> and others
+#
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+#
+
+from __future__ import print_function
+__metaclass__ = type
+
+import os
+import glob
+import sys
+import yaml
+import re
+import optparse
+import datetime
+import cgi
+import warnings
+from collections import defaultdict
+
+from jinja2 import Environment, FileSystemLoader
+from six import iteritems
+
+from ansible.errors import AnsibleError
+from ansible.module_utils._text import to_bytes
+from ansible.utils import plugin_docs
+
+#####################################################################################
+# constants and paths
+
+# if a module is added in a version of Ansible older than this, don't print the version added information
+# in the module documentation because everyone is assumed to be running something newer than this already.
+TO_OLD_TO_BE_NOTABLE = 1.3
+
+# Get parent directory of the directory this script lives in
+MODULEDIR=os.path.abspath(os.path.join(
+ os.path.dirname(os.path.realpath(__file__)), os.pardir, 'lib', 'ansible', 'modules'
+))
+
+# The name of the DOCUMENTATION template
+EXAMPLE_YAML=os.path.abspath(os.path.join(
+ os.path.dirname(os.path.realpath(__file__)), os.pardir, 'examples', 'DOCUMENTATION.yml'
+))
+
+_ITALIC = re.compile(r"I\(([^)]+)\)")
+_BOLD = re.compile(r"B\(([^)]+)\)")
+_MODULE = re.compile(r"M\(([^)]+)\)")
+_URL = re.compile(r"U\(([^)]+)\)")
+_CONST = re.compile(r"C\(([^)]+)\)")
+
+DEPRECATED = " (D)"
+#####################################################################################
+
+def rst_ify(text):
+ ''' convert symbols like I(this is in italics) to valid restructured text '''
+
+ try:
+ t = _ITALIC.sub(r'*' + r"\1" + r"*", text)
+ t = _BOLD.sub(r'**' + r"\1" + r"**", t)
+ t = _MODULE.sub(r':ref:`' + r"\1 <\1>" + r"`", t)
+ t = _URL.sub(r"\1", t)
+ t = _CONST.sub(r'``' + r"\1" + r"``", t)
+ except Exception as e:
+ raise AnsibleError("Could not process (%s) : %s" % (str(text), str(e)))
+
+ return t
+
+#####################################################################################
+
+def html_ify(text):
+ ''' convert symbols like I(this is in italics) to valid HTML '''
+
+ t = cgi.escape(text)
+ t = _ITALIC.sub("<em>" + r"\1" + "</em>", t)
+ t = _BOLD.sub("<b>" + r"\1" + "</b>", t)
+ t = _MODULE.sub("<span class='module'>" + r"\1" + "</span>", t)
+ t = _URL.sub("<a href='" + r"\1" + "'>" + r"\1" + "</a>", t)
+ t = _CONST.sub("<code>" + r"\1" + "</code>", t)
+
+ return t
+
+
+#####################################################################################
+
+def rst_fmt(text, fmt):
+ ''' helper for Jinja2 to do format strings '''
+
+ return fmt % (text)
+
+#####################################################################################
+
+def rst_xline(width, char="="):
+ ''' return a restructured text line of a given length '''
+
+ return char * width
+
+#####################################################################################
+
+def write_data(text, options, outputname, module):
+ ''' dumps module output to a file or the screen, as requested '''
+
+ if options.output_dir is not None:
+ fname = os.path.join(options.output_dir, outputname % module)
+ fname = fname.replace(".py","")
+ f = open(fname, 'w')
+ f.write(text.encode('utf-8'))
+ f.close()
+ else:
+ print(text)
+
+#####################################################################################
+
+
+def list_modules(module_dir, depth=0):
+ ''' returns a hash of categories, each category being a hash of module names to file paths '''
+
+ categories = dict()
+ module_info = dict()
+ aliases = defaultdict(set)
+
+ # * windows powershell modules have documentation stubs in python docstring
+ # format (they are not executed) so skip the ps1 format files
+ # * One glob level for every module level that we're going to traverse
+ files = (
+ glob.glob("%s/*.py" % module_dir) +
+ glob.glob("%s/*/*.py" % module_dir) +
+ glob.glob("%s/*/*/*.py" % module_dir) +
+ glob.glob("%s/*/*/*/*.py" % module_dir)
+ )
+
+ for module_path in files:
+ if module_path.endswith('__init__.py'):
+ continue
+ category = categories
+ mod_path_only = module_path
+ # Start at the second directory because we don't want the "vendor"
+
+ mod_path_only = os.path.dirname(module_path[len(module_dir):])
+
+ # directories (core, extras)
+ for new_cat in mod_path_only.split('/')[1:]:
+ if new_cat not in category:
+ category[new_cat] = dict()
+ category = category[new_cat]
+
+ module = os.path.splitext(os.path.basename(module_path))[0]
+ if module in plugin_docs.BLACKLIST['MODULE']:
+ # Do not list blacklisted modules
+ continue
+ if module.startswith("_") and os.path.islink(module_path):
+ source = os.path.splitext(os.path.basename(os.path.realpath(module_path)))[0]
+ module = module.replace("_","",1)
+ aliases[source].add(module)
+ continue
+
+ category[module] = module_path
+ module_info[module] = module_path
+
+ # keep module tests out of becoming module docs
+ if 'test' in categories:
+ del categories['test']
+
+ return module_info, categories, aliases
+
+#####################################################################################
+
+def generate_parser():
+ ''' generate an optparse parser '''
+
+ p = optparse.OptionParser(
+ version='%prog 1.0',
+ usage='usage: %prog [options] arg1 arg2',
+ description='Generate module documentation from metadata',
+ )
+
+ p.add_option("-A", "--ansible-version", action="store", dest="ansible_version", default="unknown", help="Ansible version number")
+ p.add_option("-M", "--module-dir", action="store", dest="module_dir", default=MODULEDIR, help="Ansible library path")
+ p.add_option("-T", "--template-dir", action="store", dest="template_dir", default="hacking/templates", help="directory containing Jinja2 templates")
+ p.add_option("-t", "--type", action='store', dest='type', choices=['rst'], default='rst', help="Document type")
+ p.add_option("-v", "--verbose", action='store_true', default=False, help="Verbose")
+ p.add_option("-o", "--output-dir", action="store", dest="output_dir", default=None, help="Output directory for module files")
+ p.add_option("-I", "--includes-file", action="store", dest="includes_file", default=None, help="Create a file containing list of processed modules")
+ p.add_option('-V', action='version', help='Show version number and exit')
+ return p
+
+#####################################################################################
+
+def jinja2_environment(template_dir, typ):
+
+ env = Environment(loader=FileSystemLoader(template_dir),
+ variable_start_string="@{",
+ variable_end_string="}@",
+ trim_blocks=True,
+ )
+ env.globals['xline'] = rst_xline
+
+ if typ == 'rst':
+ env.filters['convert_symbols_to_format'] = rst_ify
+ env.filters['html_ify'] = html_ify
+ env.filters['fmt'] = rst_fmt
+ env.filters['xline'] = rst_xline
+ template = env.get_template('plugin.rst.j2')
+ outputname = "%s_module.rst"
+ else:
+ raise Exception("unknown module format type: %s" % typ)
+
+ return env, template, outputname
+
+#####################################################################################
+def too_old(added):
+ if not added:
+ return False
+ try:
+ added_tokens = str(added).split(".")
+ readded = added_tokens[0] + "." + added_tokens[1]
+ added_float = float(readded)
+ except ValueError as e:
+ warnings.warn("Could not parse %s: %s" % (added, str(e)))
+ return False
+ return (added_float < TO_OLD_TO_BE_NOTABLE)
+
+def process_module(module, options, env, template, outputname, module_map, aliases):
+
+ fname = module_map[module]
+ if isinstance(fname, dict):
+ return "SKIPPED"
+
+ basename = os.path.basename(fname)
+ deprecated = False
+
+ # ignore files with extensions
+ if not basename.endswith(".py"):
+ return
+ elif module.startswith("_"):
+ if os.path.islink(fname):
+ return # ignore, its an alias
+ deprecated = True
+ module = module.replace("_","",1)
+
+ print("rendering: %s" % module)
+
+ # use ansible core library to parse out doc metadata YAML and plaintext examples
+ doc, examples, returndocs, metadata = plugin_docs.get_docstring(fname, verbose=options.verbose)
+
+ # crash if module is missing documentation and not explicitly hidden from docs index
+ if doc is None:
+ sys.exit("*** ERROR: MODULE MISSING DOCUMENTATION: %s, %s ***\n" % (fname, module))
+
+ if metadata is None:
+ sys.exit("*** ERROR: MODULE MISSING METADATA: %s, %s ***\n" % (fname, module))
+
+ if deprecated and 'deprecated' not in doc:
+ sys.exit("*** ERROR: DEPRECATED MODULE MISSING 'deprecated' DOCUMENTATION: %s, %s ***\n" % (fname, module))
+
+ if module in aliases:
+ doc['aliases'] = aliases[module]
+
+ all_keys = []
+
+ if not 'version_added' in doc:
+ sys.exit("*** ERROR: missing version_added in: %s ***\n" % module)
+
+ added = 0
+ if doc['version_added'] == 'historical':
+ del doc['version_added']
+ else:
+ added = doc['version_added']
+
+ # don't show version added information if it's too old to be called out
+ if too_old(added):
+ del doc['version_added']
+
+ if 'options' in doc and doc['options']:
+ for (k,v) in iteritems(doc['options']):
+ # don't show version added information if it's too old to be called out
+ if 'version_added' in doc['options'][k] and too_old(doc['options'][k]['version_added']):
+ del doc['options'][k]['version_added']
+ if not 'description' in doc['options'][k]:
+ raise AnsibleError("Missing required description for option %s in %s " % (k, module))
+
+ required_value = doc['options'][k].get('required', False)
+ if not isinstance(required_value, bool):
+ raise AnsibleError("Invalid required value '%s' for option '%s' in '%s' (must be truthy)" % (required_value, k, module))
+ if not isinstance(doc['options'][k]['description'],list):
+ doc['options'][k]['description'] = [doc['options'][k]['description']]
+
+ all_keys.append(k)
+
+ all_keys = sorted(all_keys)
+
+ doc['option_keys'] = all_keys
+ doc['filename'] = fname
+ doc['docuri'] = doc['module'].replace('_', '-')
+ doc['now_date'] = datetime.date.today().strftime('%Y-%m-%d')
+ doc['ansible_version'] = options.ansible_version
+ doc['plainexamples'] = examples #plain text
+ doc['metadata'] = metadata
+
+ if returndocs:
+ try:
+ doc['returndocs'] = yaml.safe_load(returndocs)
+ except:
+ print("could not load yaml: %s" % returndocs)
+ raise
+ else:
+ doc['returndocs'] = None
+
+ # here is where we build the table of contents...
+
+ try:
+ text = template.render(doc)
+ except Exception as e:
+ raise AnsibleError("Failed to render doc for %s: %s" % (fname, str(e)))
+ write_data(text, options, outputname, module)
+ return doc['short_description']
+
+#####################################################################################
+
+def print_modules(module, category_file, deprecated, options, env, template, outputname, module_map, aliases):
+ modstring = module
+ if modstring.startswith('_'):
+ modstring = module[1:]
+ modname = modstring
+ if module in deprecated:
+ modstring = modstring + DEPRECATED
+
+ category_file.write(" %s - %s <%s_module>\n" % (to_bytes(modstring), to_bytes(rst_ify(module_map[module][1])), to_bytes(modname)))
+
+def process_category(category, categories, options, env, template, outputname):
+
+ ### FIXME:
+ # We no longer conceptually deal with a mapping of category names to
+ # modules to file paths. Instead we want several different records:
+ # (1) Mapping of module names to file paths (what's presently used
+ # as categories['all']
+ # (2) Mapping of category names to lists of module names (what you'd
+ # presently get from categories[category_name][subcategory_name].keys()
+ # (3) aliases (what's presently in categories['_aliases']
+ #
+ # list_modules() now returns those. Need to refactor this function and
+ # main to work with them.
+
+ module_map = categories[category]
+ module_info = categories['all']
+
+ aliases = {}
+ if '_aliases' in categories:
+ aliases = categories['_aliases']
+
+ category_file_path = os.path.join(options.output_dir, "list_of_%s_modules.rst" % category)
+ category_file = open(category_file_path, "w")
+ print("*** recording category %s in %s ***" % (category, category_file_path))
+
+ # start a new category file
+
+ category = category.replace("_"," ")
+ category = category.title()
+
+ modules = []
+ deprecated = []
+ for module in module_map.keys():
+ if isinstance(module_map[module], dict):
+ for mod in (m for m in module_map[module].keys() if m in module_info):
+ if mod.startswith("_"):
+ deprecated.append(mod)
+ else:
+ if module not in module_info:
+ continue
+ if module.startswith("_"):
+ deprecated.append(module)
+ modules.append(module)
+
+ modules.sort(key=lambda k: k[1:] if k.startswith('_') else k)
+
+ category_header = "%s Modules" % (category.title())
+ underscores = "`" * len(category_header)
+
+ category_file.write("""\
+%s
+%s
+
+.. toctree:: :maxdepth: 1
+
+""" % (category_header, underscores))
+ sections = []
+ for module in modules:
+ if module in module_map and isinstance(module_map[module], dict):
+ sections.append(module)
+ continue
+ else:
+ print_modules(module, category_file, deprecated, options, env, template, outputname, module_info, aliases)
+
+ sections.sort()
+ for section in sections:
+ category_file.write("\n%s\n%s\n\n" % (section.replace("_"," ").title(),'-' * len(section)))
+ category_file.write(".. toctree:: :maxdepth: 1\n\n")
+
+ section_modules = module_map[section].keys()
+ section_modules.sort(key=lambda k: k[1:] if k.startswith('_') else k)
+ #for module in module_map[section]:
+ for module in (m for m in section_modules if m in module_info):
+ print_modules(module, category_file, deprecated, options, env, template, outputname, module_info, aliases)
+
+ category_file.write("""\n\n
+.. note::
+ - %s: This marks a module as deprecated, which means a module is kept for backwards compatibility but usage is discouraged.
+ The module documentation details page may explain more about this rationale.
+""" % DEPRECATED)
+ category_file.close()
+
+ # TODO: end a new category file
+
+#####################################################################################
+
+def validate_options(options):
+ ''' validate option parser options '''
+
+ if not options.module_dir:
+ sys.exit("--module-dir is required", file=sys.stderr)
+ if not os.path.exists(options.module_dir):
+ sys.exit("--module-dir does not exist: %s" % options.module_dir, file=sys.stderr)
+ if not options.template_dir:
+ sys.exit("--template-dir must be specified")
+
+#####################################################################################
+
+def main():
+
+ p = generate_parser()
+
+ (options, args) = p.parse_args()
+ validate_options(options)
+
+ env, template, outputname = jinja2_environment(options.template_dir, options.type)
+
+ mod_info, categories, aliases = list_modules(options.module_dir)
+ categories['all'] = mod_info
+ categories['_aliases'] = aliases
+ category_names = [c for c in categories.keys() if not c.startswith('_')]
+ category_names.sort()
+
+ # Write master category list
+ category_list_path = os.path.join(options.output_dir, "modules_by_category.rst")
+ with open(category_list_path, "w") as category_list_file:
+ category_list_file.write("Module Index\n")
+ category_list_file.write("============\n")
+ category_list_file.write("\n\n")
+ category_list_file.write(".. toctree::\n")
+ category_list_file.write(" :maxdepth: 1\n\n")
+
+ for category in category_names:
+ category_list_file.write(" list_of_%s_modules\n" % category)
+
+ #
+ # Import all the docs into memory
+ #
+ module_map = mod_info.copy()
+
+ for modname in module_map:
+ result = process_module(modname, options, env, template, outputname, module_map, aliases)
+ if result == 'SKIPPED':
+ del categories['all'][modname]
+ else:
+ categories['all'][modname] = (categories['all'][modname], result)
+
+ #
+ # Render all the docs to rst via category pages
+ #
+ for category in category_names:
+ process_category(category, categories, options, env, template, outputname)
+
+if __name__ == '__main__':
+ main()