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