1"""Convert a argparse parser to option directives. 2 3Inspired by sphinxcontrib.autoprogram but with a few differences: 4 5- Instead of relying on private argparse structures uses hooking 6 to extract information from a argparse parser. 7 8- Contains some simple pre-processing on the help messages to make 9 the Sphinx version a bit prettier. 10 11""" 12 13import argparse 14import re 15 16from collections import namedtuple 17from textwrap import dedent 18 19from docutils import nodes 20from docutils.parsers.rst.directives import unchanged 21from docutils.statemachine import ViewList 22from sphinx.util.nodes import nested_parse_with_titles 23from sphinx.util.compat import Directive 24 25 26_ArgumentParser = argparse.ArgumentParser 27_Argument = namedtuple("Argument", ["args", "options"]) 28 29_block_re = re.compile(r":\n{2}\s{2}") 30_default_re = re.compile(r"Default is (.+)\.\n") 31_note_re = re.compile(r"Note: (.*)\n\n", re.DOTALL) 32_option_re = re.compile(r"(--[\w-]+)") 33 34 35class ArgumentParser(object): 36 def __init__(self, *args, **kwargs): 37 self.args = args 38 self.kwargs = kwargs 39 self.groups = [] 40 self.arguments = [] 41 42 def add_argument(self, *args, **options): 43 if not options.get("help") == argparse.SUPPRESS: 44 self.arguments.append(_Argument(args, options)) 45 46 def add_argument_group(self, *args, **options): 47 group = ArgumentParser(*args, **options) 48 self.groups.append(group) 49 return group 50 51 52def get_parser(module_name, attr): 53 argparse.ArgumentParser = ArgumentParser 54 module = __import__(module_name, globals(), locals(), [attr]) 55 argparse.ArgumentParser = _ArgumentParser 56 return getattr(module, attr) 57 58 59def indent(value, length=4): 60 space = " " * length 61 return "\n".join(space + line for line in value.splitlines()) 62 63 64class ArgparseDirective(Directive): 65 has_content = True 66 option_spec = { 67 "module": unchanged, 68 "attr": unchanged, 69 } 70 71 def process_help(self, help): 72 # Dedent the help to make sure we are always dealing with 73 # non-indented text. 74 help = dedent(help) 75 76 # Create simple blocks. 77 help = _block_re.sub("::\n\n ", help) 78 79 # Boldify the default value. 80 help = _default_re.sub(r"Default is: **\1**.\n", help) 81 82 # Create note directives from "Note: " paragraphs. 83 help = _note_re.sub( 84 lambda m: ".. note::\n\n" + indent(m.group(1)) + "\n\n", 85 help 86 ) 87 88 # Replace option references with links. 89 help = _option_re.sub( 90 lambda m: ( 91 ":option:`{0}`".format(m.group(1)) 92 if m.group(1) in self._available_options 93 else m.group(1) 94 ), 95 help 96 ) 97 98 return indent(help) 99 100 def generate_group_rst(self, group): 101 for arg in group.arguments: 102 help = arg.options.get("help") 103 metavar = arg.options.get("metavar") 104 105 if isinstance(metavar, tuple): 106 metavar = " ".join(metavar) 107 108 if metavar: 109 optional = arg.options.get("nargs") == "?" 110 if optional: 111 metavar = "[{0}]".format(metavar) 112 113 options = [] 114 for a in arg.args: 115 if a.startswith("-"): 116 options.append("{0} {1}".format(a, metavar)) 117 else: 118 options.append(metavar) 119 else: 120 options = arg.args 121 122 yield ".. option:: {0}".format(", ".join(options)) 123 yield "" 124 for line in self.process_help(help).split("\n"): 125 yield line 126 yield "" 127 128 def generate_parser_rst(self, parser): 129 for group in parser.groups: 130 title = group.args[0] 131 yield "" 132 yield title 133 yield "^" * len(title) 134 for line in self.generate_group_rst(group): 135 yield line 136 137 def run(self): 138 module = self.options.get("module") 139 attr = self.options.get("attr") 140 parser = get_parser(module, attr) 141 142 self._available_options = [] 143 for group in parser.groups: 144 for arg in group.arguments: 145 self._available_options += arg.args 146 147 node = nodes.section() 148 node.document = self.state.document 149 result = ViewList() 150 for line in self.generate_parser_rst(parser): 151 result.append(line, "argparse") 152 153 nested_parse_with_titles(self.state, result, node) 154 return node.children 155 156 157def setup(app): 158 app.add_directive("argparse", ArgparseDirective) 159