module_formatter.py 11.2 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
#!/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
import sys
import yaml
import codecs
import json
import ast
from jinja2 import Environment, FileSystemLoader
import re
28
import optparse
29 30 31
import time
import datetime
import subprocess
32
import traceback
33

34 35 36 37 38
# modules that are ok that they do not have documentation strings
BLACKLIST_MODULES = [
   'async_wrapper'
]

39 40 41 42 43 44 45
# 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'
    ))
EXAMPLE_YAML=os.path.abspath(os.path.join(
    os.path.dirname(os.path.realpath(__file__)), os.pardir, 'examples', 'DOCUMENTATION.yaml'
    ))
46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71

# There is a better way of doing this!
# TODO: somebody add U(text, http://foo.bar/) as described by Tim in #991

_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\(([^)]+)\)")

def latex_ify(text):

    t = _ITALIC.sub("\\I{" + r"\1" + "}", text)
    t = _BOLD.sub("\\B{" + r"\1" + "}", t)
    t = _MODULE.sub("\\M{" + r"\1" + "}", t)
    t = _URL.sub("\\url{" + r"\1" + "}", t)
    t = _CONST.sub("\\C{" + r"\1" + "}", t)

    return t

def html_ify(text):

    t = _ITALIC.sub("<em>" + r"\1" + "</em>", text)
    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)
72 73
    #t = _CONST.sub("<code>" + r"\1" + "</code>", t)
    t = _CONST.sub(r"\1", t)
74 75
    return t

76 77 78 79 80 81 82 83 84 85
def json_ify(text):

    t = _ITALIC.sub("<em>" + r"\1" + "</em>", text)
    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

86 87 88 89 90 91

def js_ify(text):

    return text


92 93 94 95 96 97 98 99 100 101 102 103 104 105
def man_ify(text):

    t = _ITALIC.sub(r'\\fI' + r"\1" + r"\\fR", text)
    t = _BOLD.sub(r'\\fB' + r"\1" + r"\\fR", t)
    t = _MODULE.sub(r'\\fI' + r"\1" + r"\\fR", t)
    t = _URL.sub(r'\\fI' + r"\1" + r"\\fR", t)
    t = _CONST.sub(r'\\fC' + r"\1" + r"\\fR", t)

    return t

def rst_ify(text):

    t = _ITALIC.sub(r'*' + r"\1" + r"*", text)
    t = _BOLD.sub(r'**' + r"\1" + r"**", t)
106 107 108
    # mdehaan is disabling because he finds all the Sphinx orange distracting
    #t = _MODULE.sub(r'``' + r"\1" + r"``", t)
    t = _MODULE.sub(r"\1", t)
109
    t = _URL.sub(r"\1", t)
110 111 112
    # ditto
    # t = _CONST.sub(r'``' + r"\1" + r"``", t)
    t = _CONST.sub(r"\1", t)
113 114 115

    return t

116 117 118 119 120 121 122 123 124 125
def markdown_ify(text):

    t = _ITALIC.sub("_" + r"\1" + "_", text)
    t = _BOLD.sub("**" + r"\1" + "**", t)
    t = _MODULE.sub("*" + r"\1" + "*", t)
    t = _URL.sub("[" + r"\1" + "](" + r"\1" + ")", t)
    t = _CONST.sub("`" + r"\1" + "`", t)

    return t

126 127 128 129 130 131 132
# Helper for Jinja2 (format() doesn't work here...)
def rst_fmt(text, fmt):
    return fmt % (text)

def rst_xline(width, char="="):
    return char * width

133 134
def load_examples_section(text):
    return text.split('***BREAK***')
135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150

def get_docstring(filename, verbose=False):
    """
    Search for assignment of the DOCUMENTATION variable in the given file.
    Parse that from YAML and return the YAML doc or None.
    """

    doc = None

    try:
        # Thank you, Habbie, for this bit of code :-)
        M = ast.parse(''.join(open(filename)))
        for child in M.body:
            if isinstance(child, ast.Assign):
                if 'DOCUMENTATION' in (t.id for t in child.targets):
                    doc = yaml.load(child.value.s)
151

152
    except:
153 154
        traceback.print_exc()
        print "unable to parse %s" % filename
155 156
    return doc

157

158 159 160
def return_data(text, options, outputname, module):
    if options.output_dir is not None:
        f = open(os.path.join(options.output_dir, outputname % module), 'w')
161 162 163 164 165
        f.write(text)
        f.close()
    else:
        print text

166 167 168 169 170
def boilerplate():
    if not os.path.exists(EXAMPLE_YAML):
        print >>sys.stderr, "Missing example boiler plate: %S" % EXAMPLE_YAML
    print file(EXAMPLE_YAML).read()

171

172 173
def main():

174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197
    p = optparse.OptionParser(
        version='%prog 1.0',
        usage='usage: %prog [options] arg1 arg2',
        description='Convert Ansible module DOCUMENTATION strings to other formats',
    )

    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 modules/ directory")
    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',
198
            choices=['html', 'latex', 'man', 'rst', 'json', 'markdown'],
199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224
            default='latex',
            help="Output type")
    p.add_option("-m", "--module",
            action='append',
            default=[],
            dest='module_list',
            help="Add modules to process in module_dir")
    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("-G", "--generate",
            action="store_true",
            dest="do_boilerplate",
            default=False,
            help="generate boilerplate DOCUMENTATION to stdout")
225
    p.add_option('-V', action='version', help='Show version number and exit')
226 227 228 229 230 231 232 233 234

    (options, args) = p.parse_args()

#    print "M: %s" % options.module_dir
#    print "t: %s" % options.type
#    print "m: %s" % options.module_list
#    print "v: %s" % options.verbose

    if options.do_boilerplate:
235 236 237
        boilerplate()
        sys.exit(0)

238
    if not options.module_dir:
239 240
        print "Need module_dir"
        sys.exit(1)
241 242 243 244
    if not os.path.exists(options.module_dir):
        print >>sys.stderr, "Module directory does not exist: %s" % options.module_dir
        sys.exit(1)

245

246
    if not options.template_dir:
Jan-Piet Mens committed
247 248 249
        print "Need template_dir"
        sys.exit(1)

250
    env = Environment(loader=FileSystemLoader(options.template_dir),
Jan-Piet Mens committed
251 252
        variable_start_string="@{",
        variable_end_string="}@",
253
        trim_blocks=True,
Jan-Piet Mens committed
254 255 256
        )

    env.globals['xline'] = rst_xline
257

258
    if options.type == 'latex':
259 260 261
        env.filters['jpfunc'] = latex_ify
        template = env.get_template('latex.j2')
        outputname = "%s.tex"
262 263
        includecmt = "% generated code\n"
        includefmt = "\\input %s\n"
264
    if options.type == 'html':
265 266 267
        env.filters['jpfunc'] = html_ify
        template = env.get_template('html.j2')
        outputname = "%s.html"
268 269
        includecmt = ""
        includefmt = ""
270
    if options.type == 'man':
271 272
        env.filters['jpfunc'] = man_ify
        template = env.get_template('man.j2')
273
        outputname = "ansible.%s.3"
274 275
        includecmt = ""
        includefmt = ""
276
    if options.type == 'rst':
277
        env.filters['jpfunc'] = rst_ify
278
        env.filters['html_ify'] = html_ify
279 280 281 282
        env.filters['fmt'] = rst_fmt
        env.filters['xline'] = rst_xline
        template = env.get_template('rst.j2')
        outputname = "%s.rst"
283
        includecmt = ".. Generated by module_formatter\n"
284
        includefmt = ".. include:: modules/%s.rst\n"
285
    if options.type == 'json':
286 287
        env.filters['jpfunc'] = json_ify
        outputname = "%s.json"
288 289
        includecmt = ""
        includefmt = ""
290
    if options.type == 'js':
291 292 293
        env.filters['jpfunc'] = js_ify
        template = env.get_template('js.j2')
        outputname = "%s.js"
294 295 296 297 298 299 300
    if options.type == 'markdown':
        env.filters['jpfunc'] = markdown_ify
        env.filters['html_ify'] = html_ify
        template = env.get_template('markdown.j2')
        outputname = "%s.md"
        includecmt = ""
        includefmt = ""
301

302 303
    if options.includes_file is not None and includefmt != "":
        incfile = open(options.includes_file, "w")
304
        incfile.write(includecmt)
305

306 307
    # Temporary variable required to genrate aggregated content in 'js' format.
    js_data = []
308 309 310
    for module in sorted(os.listdir(options.module_dir)):
        if len(options.module_list):
            if not module in options.module_list:
311 312
                continue

313
        fname = os.path.join(options.module_dir, module)
314 315
        extra = os.path.join("inc", "%s.tex" % module)

316 317 318 319
        if fname.endswith(".swp"):
            continue

        print " processing module source ---> %s" % fname
320

321
        if options.type == 'js':
322 323 324 325 326 327 328
            if fname.endswith(".json"):
                f = open(fname)
                j = json.load(f)
                f.close()
                js_data.append(j)
            continue

329
        doc = get_docstring(fname, verbose=options.verbose)
330 331 332 333 334

        if doc is None and module not in BLACKLIST_MODULES:
            sys.stderr.write("*** ERROR: CORE MODULE MISSING DOCUMENTATION: %s ***\n" % module)
            #sys.exit(1)

335
        if not doc is None:
336 337 338 339

            doc['filename']         = fname
            doc['docuri']           = doc['module'].replace('_', '-')
            doc['now_date']         = datetime.date.today().strftime('%Y-%m-%d')
340
            doc['ansible_version']  = options.ansible_version
341

342
            if options.includes_file is not None and includefmt != "":
343 344
                incfile.write(includefmt % module)

345
            if options.verbose:
346 347 348
                print json.dumps(doc, indent=4)


349
            if options.type == 'latex':
350 351 352 353 354 355
                if os.path.exists(extra):
                    f = open(extra)
                    extradata = f.read()
                    f.close()
                    doc['extradata'] = extradata

356
            if options.type == 'json':
357 358 359 360
                text = json.dumps(doc, indent=2)
            else:
                text = template.render(doc)

361
            return_data(text, options, outputname, module)
362

363
    if options.type == 'js':
364 365 366
        docs = {}
        docs['json'] = json.dumps(js_data, indent=2)
        text = template.render(docs)
367
        return_data(text, options, outputname, 'modules')
368 369 370

if __name__ == '__main__':
    main()