hacking/module_formatter.py
d47e15a1
 #!/usr/bin/env python
 # (c) 2012, Jan-Piet Mens <jpmens () gmail.com>
 #
 # 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/>.
 #
 
 import os
391fb98e
 import glob
d47e15a1
 import sys
 import yaml
 import codecs
 import json
 import ast
 import re
2786149b
 import optparse
d47e15a1
 import time
 import datetime
 import subprocess
7681b1ce
 import cgi
fe2d00d9
 from jinja2 import Environment, FileSystemLoader
 
e32f4a05
 import ansible.utils
0ae7f996
 import ansible.utils.module_docs as module_docs
31a4fe41
 
fe2d00d9
 #####################################################################################
 # constants and paths
 
40429ee6
 # 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.0
 
60f06c36
 # 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, 'library'
31d0060d
 ))
 
 # The name of the DOCUMENTATION template
60f06c36
 EXAMPLE_YAML=os.path.abspath(os.path.join(
31d0060d
     os.path.dirname(os.path.realpath(__file__)), os.pardir, 'examples', 'DOCUMENTATION.yml'
 ))
d47e15a1
 
 _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\(([^)]+)\)")
 
fe2d00d9
 #####################################################################################
d47e15a1
 
 def rst_ify(text):
fe2d00d9
     ''' convert symbols like I(this is in italics) to valid restructured text '''
d47e15a1
 
     t = _ITALIC.sub(r'*' + r"\1" + r"*", text)
     t = _BOLD.sub(r'**' + r"\1" + r"**", t)
caf003c8
     t = _MODULE.sub(r'``' + r"\1" + r"``", t)
d47e15a1
     t = _URL.sub(r"\1", t)
caf003c8
     t = _CONST.sub(r'``' + r"\1" + r"``", t)
d47e15a1
 
     return t
 
fe2d00d9
 #####################################################################################
7681b1ce
 
fe2d00d9
 def html_ify(text):
     ''' convert symbols like I(this is in italics) to valid HTML '''
d4f89122
 
7681b1ce
     t = cgi.escape(text)
fe2d00d9
     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)
d4f89122
 
     return t
 
fe2d00d9
 
 #####################################################################################
 
d47e15a1
 def rst_fmt(text, fmt):
fe2d00d9
     ''' helper for Jinja2 to do format strings '''
 
d47e15a1
     return fmt % (text)
 
fe2d00d9
 #####################################################################################
 
d47e15a1
 def rst_xline(width, char="="):
fe2d00d9
     ''' return a restructured text line of a given length '''
 
d47e15a1
     return char * width
 
fe2d00d9
 #####################################################################################
d47e15a1
 
35ec9f81
 def write_data(text, options, outputname, module):
fe2d00d9
     ''' dumps module output to a file or the screen, as requested '''
 
2786149b
     if options.output_dir is not None:
         f = open(os.path.join(options.output_dir, outputname % module), 'w')
29aaa5e6
         f.write(text.encode('utf-8'))
ee679c01
         f.close()
     else:
         print text
 
fe2d00d9
 #####################################################################################
 
391fb98e
 def list_modules(module_dir):
fe2d00d9
     ''' returns a hash of categories, each category being a hash of module names to file paths '''
 
c4a8a6d3
     categories = dict(all=dict())
391fb98e
     files = glob.glob("%s/*" % module_dir)
     for d in files:
         if os.path.isdir(d):
             files2 = glob.glob("%s/*" % d)
             for f in files2:
                 tokens = f.split("/")
                 module = tokens[-1]
                 category = tokens[-2]
                 if not category in categories:
                     categories[category] = {}
                 categories[category][module] = f
c4a8a6d3
                 categories['all'][module] = f
391fb98e
     return categories
ee679c01
 
fe2d00d9
 #####################################################################################
 
 def generate_parser():
     ''' generate an optparse parser '''
d47e15a1
 
2786149b
     p = optparse.OptionParser(
         version='%prog 1.0',
         usage='usage: %prog [options] arg1 arg2',
fe2d00d9
         description='Generate module documentation from metadata',
2786149b
     )
 
fe2d00d9
     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")
95d102f5
     p.add_option("-t", "--type", action='store', dest='type', choices=['rst'], default='rst', help="Document type")
af1f8db5
     p.add_option("-v", "--verbose", action='store_true', default=False, help="Verbose")
fe2d00d9
     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")
60f06c36
     p.add_option('-V', action='version', help='Show version number and exit')
fe2d00d9
     return p
2786149b
 
fe2d00d9
 #####################################################################################
d47e15a1
 
fe2d00d9
 def jinja2_environment(template_dir, typ):
60f06c36
 
fe2d00d9
     env = Environment(loader=FileSystemLoader(template_dir),
62d038dc
         variable_start_string="@{",
         variable_end_string="}@",
e4338d0c
         trim_blocks=True,
626203a7
     )
62d038dc
     env.globals['xline'] = rst_xline
83f277cf
 
fe2d00d9
     if typ == 'rst':
10009b0d
         env.filters['convert_symbols_to_format'] = rst_ify
83f277cf
         env.filters['html_ify'] = html_ify
d47e15a1
         env.filters['fmt'] = rst_fmt
         env.filters['xline'] = rst_xline
         template = env.get_template('rst.j2')
35ec9f81
         outputname = "%s_module.rst"
fe2d00d9
     else:
         raise Exception("unknown module format type: %s" % typ)
eb8a1123
 
fe2d00d9
     return env, template, outputname
d47e15a1
 
fe2d00d9
 #####################################################################################
391fb98e
 
fe2d00d9
 def process_module(module, options, env, template, outputname, module_map):
 
     print "rendering: %s" % module
 
     fname = module_map[module]
 
     # ignore files with extensions
07491122
     if "." in os.path.basename(fname):
fe2d00d9
         return
 
     # use ansible core library to parse out doc metadata YAML and plaintext examples
     doc, examples = ansible.utils.module_docs.get_docstring(fname, verbose=options.verbose)
 
     # crash if module is missing documentation and not explicitly hidden from docs index
     if doc is None and module not in ansible.utils.module_docs.BLACKLIST_MODULES:
         sys.stderr.write("*** ERROR: CORE MODULE MISSING DOCUMENTATION: %s, %s ***\n" % (fname, module))
         sys.exit(1)
     if doc is None:
35ec9f81
         return "SKIPPED"
fe2d00d9
 
     all_keys = []
391fb98e
 
fe2d00d9
     if not 'version_added' in doc:
         sys.stderr.write("*** ERROR: missing version_added in: %s ***\n" % module)
         sys.exit(1)
 
     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 added:
         added_tokens = str(added).split(".")
         added = added_tokens[0] + "." + added_tokens[1]
         added_float = float(added)
         if added and added_float < TO_OLD_TO_BE_NOTABLE:
             del doc['version_added']
 
     for (k,v) in doc['options'].iteritems():
         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
 
     # here is where we build the table of contents...
 
     text = template.render(doc)
35ec9f81
     write_data(text, options, outputname, module)
391fb98e
 
fe2d00d9
 #####################################################################################
5f18a535
 
fe2d00d9
 def process_category(category, categories, options, env, template, outputname):
5f18a535
 
fe2d00d9
     module_map = categories[category]
391fb98e
 
35ec9f81
     category_file_path = os.path.join(options.output_dir, "list_of_%s_modules.rst" % category)
     category_file = open(category_file_path, "w")
af1f8db5
     print "*** recording category %s in %s ***" % (category, category_file_path)
35ec9f81
 
fe2d00d9
     # TODO: start a new category file
391fb98e
 
fe2d00d9
     category = category.replace("_"," ")
     category = category.title()
 
     modules = module_map.keys()
     modules.sort()
 
35ec9f81
     category_header = "%s Modules" % (category.title())
     underscores = "`" * len(category_header)
 
7965d331
     category_file.write("""\
 %s
 %s
 
 .. toctree::
    :maxdepth: 1
 
 """ % (category_header, underscores))
35ec9f81
 
fe2d00d9
     for module in modules:
35ec9f81
         result = process_module(module, options, env, template, outputname, module_map)
         if result != "SKIPPED":
7965d331
             category_file.write("   %s_module\n" % module)
35ec9f81
 
 
     category_file.close()
fe2d00d9
 
     # TODO: end a new category file
 
 #####################################################################################
 
 def validate_options(options):
     ''' validate option parser options '''
 
     if not options.module_dir:
         print >>sys.stderr, "--module-dir is required"
         sys.exit(1)
     if not os.path.exists(options.module_dir):
         print >>sys.stderr, "--module-dir does not exist: %s" % options.module_dir
         sys.exit(1)
     if not options.template_dir:
         print "--template-dir must be specified"
         sys.exit(1)
391fb98e
 
fe2d00d9
 #####################################################################################
d47e15a1
 
fe2d00d9
 def main():
d47e15a1
 
fe2d00d9
     p = generate_parser()
0c855a85
 
fe2d00d9
     (options, args) = p.parse_args()
     validate_options(options)
d47e15a1
 
fe2d00d9
     env, template, outputname = jinja2_environment(options.template_dir, options.type)
ee679c01
 
fe2d00d9
     categories = list_modules(options.module_dir)
     last_category = None
     category_names = categories.keys()
     category_names.sort()
af1f8db5
 
35ec9f81
     category_list_path = os.path.join(options.output_dir, "modules_by_category.rst")
     category_list_file = open(category_list_path, "w")
     category_list_file.write("Module Index\n")
     category_list_file.write("============\n")
     category_list_file.write("\n\n")
     category_list_file.write(".. toctree::\n")
83d298ac
     category_list_file.write("   :maxdepth: 1\n\n")
af1f8db5
 
fe2d00d9
     for category in category_names:
83d298ac
         category_list_file.write("   list_of_%s_modules\n" % category)
fe2d00d9
         process_category(category, categories, options, env, template, outputname)
d47e15a1
 
35ec9f81
     category_list_file.close()
 
d47e15a1
 if __name__ == '__main__':
     main()