PageRenderTime 75ms CodeModel.GetById 13ms RepoModel.GetById 0ms app.codeStats 0ms

/v2/hacking/module_formatter.py

https://gitlab.com/18runt88/ansible
Python | 442 lines | 382 code | 25 blank | 35 comment | 2 complexity | e21d71ea7d0144780cdc2fc36ceeee78 MD5 | raw file
  1. #!/usr/bin/env python
  2. # (c) 2012, Jan-Piet Mens <jpmens () gmail.com>
  3. # (c) 2012-2014, Michael DeHaan <michael@ansible.com> and others
  4. #
  5. # This file is part of Ansible
  6. #
  7. # Ansible is free software: you can redistribute it and/or modify
  8. # it under the terms of the GNU General Public License as published by
  9. # the Free Software Foundation, either version 3 of the License, or
  10. # (at your option) any later version.
  11. #
  12. # Ansible is distributed in the hope that it will be useful,
  13. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  14. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  15. # GNU General Public License for more details.
  16. #
  17. # You should have received a copy of the GNU General Public License
  18. # along with Ansible. If not, see <http://www.gnu.org/licenses/>.
  19. #
  20. import os
  21. import glob
  22. import sys
  23. import yaml
  24. import codecs
  25. import json
  26. import ast
  27. import re
  28. import optparse
  29. import time
  30. import datetime
  31. import subprocess
  32. import cgi
  33. from jinja2 import Environment, FileSystemLoader
  34. import ansible.utils
  35. import ansible.utils.module_docs as module_docs
  36. #####################################################################################
  37. # constants and paths
  38. # if a module is added in a version of Ansible older than this, don't print the version added information
  39. # in the module documentation because everyone is assumed to be running something newer than this already.
  40. TO_OLD_TO_BE_NOTABLE = 1.0
  41. # Get parent directory of the directory this script lives in
  42. MODULEDIR=os.path.abspath(os.path.join(
  43. os.path.dirname(os.path.realpath(__file__)), os.pardir, 'lib', 'ansible', 'modules'
  44. ))
  45. # The name of the DOCUMENTATION template
  46. EXAMPLE_YAML=os.path.abspath(os.path.join(
  47. os.path.dirname(os.path.realpath(__file__)), os.pardir, 'examples', 'DOCUMENTATION.yml'
  48. ))
  49. _ITALIC = re.compile(r"I\(([^)]+)\)")
  50. _BOLD = re.compile(r"B\(([^)]+)\)")
  51. _MODULE = re.compile(r"M\(([^)]+)\)")
  52. _URL = re.compile(r"U\(([^)]+)\)")
  53. _CONST = re.compile(r"C\(([^)]+)\)")
  54. DEPRECATED = " (D)"
  55. NOTCORE = " (E)"
  56. #####################################################################################
  57. def rst_ify(text):
  58. ''' convert symbols like I(this is in italics) to valid restructured text '''
  59. t = _ITALIC.sub(r'*' + r"\1" + r"*", text)
  60. t = _BOLD.sub(r'**' + r"\1" + r"**", t)
  61. t = _MODULE.sub(r'``' + r"\1" + r"``", t)
  62. t = _URL.sub(r"\1", t)
  63. t = _CONST.sub(r'``' + r"\1" + r"``", t)
  64. return t
  65. #####################################################################################
  66. def html_ify(text):
  67. ''' convert symbols like I(this is in italics) to valid HTML '''
  68. t = cgi.escape(text)
  69. t = _ITALIC.sub("<em>" + r"\1" + "</em>", t)
  70. t = _BOLD.sub("<b>" + r"\1" + "</b>", t)
  71. t = _MODULE.sub("<span class='module'>" + r"\1" + "</span>", t)
  72. t = _URL.sub("<a href='" + r"\1" + "'>" + r"\1" + "</a>", t)
  73. t = _CONST.sub("<code>" + r"\1" + "</code>", t)
  74. return t
  75. #####################################################################################
  76. def rst_fmt(text, fmt):
  77. ''' helper for Jinja2 to do format strings '''
  78. return fmt % (text)
  79. #####################################################################################
  80. def rst_xline(width, char="="):
  81. ''' return a restructured text line of a given length '''
  82. return char * width
  83. #####################################################################################
  84. def write_data(text, options, outputname, module):
  85. ''' dumps module output to a file or the screen, as requested '''
  86. if options.output_dir is not None:
  87. fname = os.path.join(options.output_dir, outputname % module)
  88. fname = fname.replace(".py","")
  89. f = open(fname, 'w')
  90. f.write(text.encode('utf-8'))
  91. f.close()
  92. else:
  93. print text
  94. #####################################################################################
  95. def list_modules(module_dir, depth=0):
  96. ''' returns a hash of categories, each category being a hash of module names to file paths '''
  97. categories = dict(all=dict(),_aliases=dict())
  98. if depth <= 3: # limit # of subdirs
  99. files = glob.glob("%s/*" % module_dir)
  100. for d in files:
  101. category = os.path.splitext(os.path.basename(d))[0]
  102. if os.path.isdir(d):
  103. res = list_modules(d, depth + 1)
  104. for key in res.keys():
  105. if key in categories:
  106. categories[key] = ansible.utils.merge_hash(categories[key], res[key])
  107. res.pop(key, None)
  108. if depth < 2:
  109. categories.update(res)
  110. else:
  111. category = module_dir.split("/")[-1]
  112. if not category in categories:
  113. categories[category] = res
  114. else:
  115. categories[category].update(res)
  116. else:
  117. module = category
  118. category = os.path.basename(module_dir)
  119. if not d.endswith(".py") or d.endswith('__init__.py'):
  120. # windows powershell modules have documentation stubs in python docstring
  121. # format (they are not executed) so skip the ps1 format files
  122. continue
  123. elif module.startswith("_") and os.path.islink(d):
  124. source = os.path.splitext(os.path.basename(os.path.realpath(d)))[0]
  125. module = module.replace("_","",1)
  126. if not d in categories['_aliases']:
  127. categories['_aliases'][source] = [module]
  128. else:
  129. categories['_aliases'][source].update(module)
  130. continue
  131. if not category in categories:
  132. categories[category] = {}
  133. categories[category][module] = d
  134. categories['all'][module] = d
  135. return categories
  136. #####################################################################################
  137. def generate_parser():
  138. ''' generate an optparse parser '''
  139. p = optparse.OptionParser(
  140. version='%prog 1.0',
  141. usage='usage: %prog [options] arg1 arg2',
  142. description='Generate module documentation from metadata',
  143. )
  144. p.add_option("-A", "--ansible-version", action="store", dest="ansible_version", default="unknown", help="Ansible version number")
  145. p.add_option("-M", "--module-dir", action="store", dest="module_dir", default=MODULEDIR, help="Ansible library path")
  146. p.add_option("-T", "--template-dir", action="store", dest="template_dir", default="hacking/templates", help="directory containing Jinja2 templates")
  147. p.add_option("-t", "--type", action='store', dest='type', choices=['rst'], default='rst', help="Document type")
  148. p.add_option("-v", "--verbose", action='store_true', default=False, help="Verbose")
  149. p.add_option("-o", "--output-dir", action="store", dest="output_dir", default=None, help="Output directory for module files")
  150. p.add_option("-I", "--includes-file", action="store", dest="includes_file", default=None, help="Create a file containing list of processed modules")
  151. p.add_option('-V', action='version', help='Show version number and exit')
  152. return p
  153. #####################################################################################
  154. def jinja2_environment(template_dir, typ):
  155. env = Environment(loader=FileSystemLoader(template_dir),
  156. variable_start_string="@{",
  157. variable_end_string="}@",
  158. trim_blocks=True,
  159. )
  160. env.globals['xline'] = rst_xline
  161. if typ == 'rst':
  162. env.filters['convert_symbols_to_format'] = rst_ify
  163. env.filters['html_ify'] = html_ify
  164. env.filters['fmt'] = rst_fmt
  165. env.filters['xline'] = rst_xline
  166. template = env.get_template('rst.j2')
  167. outputname = "%s_module.rst"
  168. else:
  169. raise Exception("unknown module format type: %s" % typ)
  170. return env, template, outputname
  171. #####################################################################################
  172. def process_module(module, options, env, template, outputname, module_map, aliases):
  173. fname = module_map[module]
  174. if isinstance(fname, dict):
  175. return "SKIPPED"
  176. basename = os.path.basename(fname)
  177. deprecated = False
  178. # ignore files with extensions
  179. if not basename.endswith(".py"):
  180. return
  181. elif module.startswith("_"):
  182. if os.path.islink(fname):
  183. return # ignore, its an alias
  184. deprecated = True
  185. module = module.replace("_","",1)
  186. print "rendering: %s" % module
  187. # use ansible core library to parse out doc metadata YAML and plaintext examples
  188. doc, examples, returndocs = ansible.utils.module_docs.get_docstring(fname, verbose=options.verbose)
  189. # crash if module is missing documentation and not explicitly hidden from docs index
  190. if doc is None:
  191. if module in ansible.utils.module_docs.BLACKLIST_MODULES:
  192. return "SKIPPED"
  193. else:
  194. sys.stderr.write("*** ERROR: MODULE MISSING DOCUMENTATION: %s, %s ***\n" % (fname, module))
  195. sys.exit(1)
  196. if deprecated and 'deprecated' not in doc:
  197. sys.stderr.write("*** ERROR: DEPRECATED MODULE MISSING 'deprecated' DOCUMENTATION: %s, %s ***\n" % (fname, module))
  198. sys.exit(1)
  199. if "/core/" in fname:
  200. doc['core'] = True
  201. else:
  202. doc['core'] = False
  203. if module in aliases:
  204. doc['aliases'] = aliases[module]
  205. all_keys = []
  206. if not 'version_added' in doc:
  207. sys.stderr.write("*** ERROR: missing version_added in: %s ***\n" % module)
  208. sys.exit(1)
  209. added = 0
  210. if doc['version_added'] == 'historical':
  211. del doc['version_added']
  212. else:
  213. added = doc['version_added']
  214. # don't show version added information if it's too old to be called out
  215. if added:
  216. added_tokens = str(added).split(".")
  217. added = added_tokens[0] + "." + added_tokens[1]
  218. added_float = float(added)
  219. if added and added_float < TO_OLD_TO_BE_NOTABLE:
  220. del doc['version_added']
  221. for (k,v) in doc['options'].iteritems():
  222. all_keys.append(k)
  223. all_keys = sorted(all_keys)
  224. doc['option_keys'] = all_keys
  225. doc['filename'] = fname
  226. doc['docuri'] = doc['module'].replace('_', '-')
  227. doc['now_date'] = datetime.date.today().strftime('%Y-%m-%d')
  228. doc['ansible_version'] = options.ansible_version
  229. doc['plainexamples'] = examples #plain text
  230. # here is where we build the table of contents...
  231. text = template.render(doc)
  232. write_data(text, options, outputname, module)
  233. return doc['short_description']
  234. #####################################################################################
  235. def print_modules(module, category_file, deprecated, core, options, env, template, outputname, module_map, aliases):
  236. modstring = module
  237. modname = module
  238. if module in deprecated:
  239. modstring = modstring + DEPRECATED
  240. modname = "_" + module
  241. elif module not in core:
  242. modstring = modstring + NOTCORE
  243. result = process_module(modname, options, env, template, outputname, module_map, aliases)
  244. if result != "SKIPPED":
  245. category_file.write(" %s - %s <%s_module>\n" % (modstring, result, module))
  246. def process_category(category, categories, options, env, template, outputname):
  247. module_map = categories[category]
  248. aliases = {}
  249. if '_aliases' in categories:
  250. aliases = categories['_aliases']
  251. category_file_path = os.path.join(options.output_dir, "list_of_%s_modules.rst" % category)
  252. category_file = open(category_file_path, "w")
  253. print "*** recording category %s in %s ***" % (category, category_file_path)
  254. # TODO: start a new category file
  255. category = category.replace("_"," ")
  256. category = category.title()
  257. modules = []
  258. deprecated = []
  259. core = []
  260. for module in module_map.keys():
  261. if isinstance(module_map[module], dict):
  262. for mod in module_map[module].keys():
  263. if mod.startswith("_"):
  264. mod = mod.replace("_","",1)
  265. deprecated.append(mod)
  266. elif '/core/' in module_map[module][mod]:
  267. core.append(mod)
  268. else:
  269. if module.startswith("_"):
  270. module = module.replace("_","",1)
  271. deprecated.append(module)
  272. elif '/core/' in module_map[module]:
  273. core.append(module)
  274. modules.append(module)
  275. modules.sort()
  276. category_header = "%s Modules" % (category.title())
  277. underscores = "`" * len(category_header)
  278. category_file.write("""\
  279. %s
  280. %s
  281. .. toctree:: :maxdepth: 1
  282. """ % (category_header, underscores))
  283. sections = []
  284. for module in modules:
  285. if module in module_map and isinstance(module_map[module], dict):
  286. sections.append(module)
  287. continue
  288. else:
  289. print_modules(module, category_file, deprecated, core, options, env, template, outputname, module_map, aliases)
  290. sections.sort()
  291. for section in sections:
  292. category_file.write("\n%s\n%s\n\n" % (section.replace("_"," ").title(),'-' * len(section)))
  293. category_file.write(".. toctree:: :maxdepth: 1\n\n")
  294. section_modules = module_map[section].keys()
  295. section_modules.sort()
  296. #for module in module_map[section]:
  297. for module in section_modules:
  298. print_modules(module, category_file, deprecated, core, options, env, template, outputname, module_map[section], aliases)
  299. category_file.write("""\n\n
  300. .. note::
  301. - %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.
  302. - %s: This marks a module as 'extras', which means it ships with ansible but may be a newer module and possibly (but not necessarily) less activity maintained than 'core' modules.
  303. - Tickets filed on modules are filed to different repos than those on the main open source project. Core module tickets should be filed at `ansible/ansible-modules-core on GitHub <http://github.com/ansible/ansible-modules-core>`_, extras tickets to `ansible/ansible-modules-extras on GitHub <http://github.com/ansible/ansible-modules-extras>`_
  304. """ % (DEPRECATED, NOTCORE))
  305. category_file.close()
  306. # TODO: end a new category file
  307. #####################################################################################
  308. def validate_options(options):
  309. ''' validate option parser options '''
  310. if not options.module_dir:
  311. print >>sys.stderr, "--module-dir is required"
  312. sys.exit(1)
  313. if not os.path.exists(options.module_dir):
  314. print >>sys.stderr, "--module-dir does not exist: %s" % options.module_dir
  315. sys.exit(1)
  316. if not options.template_dir:
  317. print "--template-dir must be specified"
  318. sys.exit(1)
  319. #####################################################################################
  320. def main():
  321. p = generate_parser()
  322. (options, args) = p.parse_args()
  323. validate_options(options)
  324. env, template, outputname = jinja2_environment(options.template_dir, options.type)
  325. categories = list_modules(options.module_dir)
  326. last_category = None
  327. category_names = categories.keys()
  328. category_names.sort()
  329. category_list_path = os.path.join(options.output_dir, "modules_by_category.rst")
  330. category_list_file = open(category_list_path, "w")
  331. category_list_file.write("Module Index\n")
  332. category_list_file.write("============\n")
  333. category_list_file.write("\n\n")
  334. category_list_file.write(".. toctree::\n")
  335. category_list_file.write(" :maxdepth: 1\n\n")
  336. for category in category_names:
  337. if category.startswith("_"):
  338. continue
  339. category_list_file.write(" list_of_%s_modules\n" % category)
  340. process_category(category, categories, options, env, template, outputname)
  341. category_list_file.close()
  342. if __name__ == '__main__':
  343. main()