2022-06-23 14:46:29 +02:00
|
|
|
"""
|
|
|
|
Convert an argparse parser to option directives.
|
2014-07-06 23:58:01 +02:00
|
|
|
|
|
|
|
Inspired by sphinxcontrib.autoprogram but with a few differences:
|
|
|
|
|
|
|
|
- Contains some simple pre-processing on the help messages to make
|
|
|
|
the Sphinx version a bit prettier.
|
|
|
|
"""
|
|
|
|
|
|
|
|
import argparse
|
|
|
|
import re
|
|
|
|
from textwrap import dedent
|
|
|
|
|
|
|
|
from docutils import nodes
|
2017-10-07 19:58:34 +02:00
|
|
|
from docutils.parsers.rst import Directive
|
2014-07-06 23:58:01 +02:00
|
|
|
from docutils.parsers.rst.directives import unchanged
|
|
|
|
from docutils.statemachine import ViewList
|
|
|
|
from sphinx.util.nodes import nested_parse_with_titles
|
|
|
|
|
|
|
|
|
|
|
|
_block_re = re.compile(r":\n{2}\s{2}")
|
|
|
|
_default_re = re.compile(r"Default is (.+)\.\n")
|
2023-04-25 18:05:28 +02:00
|
|
|
_note_re = re.compile(r"Note: (.*?)(?:\n\n|\n*$)", re.DOTALL)
|
2022-07-17 18:07:09 +02:00
|
|
|
_option_line_re = re.compile(r"^(?!\s{2,}%\(prog\)s|\s{2,}--\w[\w-]*\w\b|Example: )(.+)$", re.MULTILINE)
|
2023-03-31 11:12:55 +02:00
|
|
|
_option_re = re.compile(r"(?:^|(?<=\s))(?P<arg>--\w[\w-]*\w)(?P<val>=\w+)?\b")
|
2018-07-26 21:33:59 +02:00
|
|
|
_prog_re = re.compile(r"%\(prog\)s")
|
2021-05-28 20:54:42 +02:00
|
|
|
_percent_re = re.compile(r"%%")
|
2022-07-18 17:19:35 +02:00
|
|
|
_inline_code_block_re = re.compile(r"(?<!`)`([^`]+?)`")
|
2022-07-18 17:19:41 +02:00
|
|
|
_example_inline_code_block_re = re.compile(r"(?<=^Example: )(.+)$", re.MULTILINE)
|
2014-07-06 23:58:01 +02:00
|
|
|
|
|
|
|
|
|
|
|
def get_parser(module_name, attr):
|
|
|
|
module = __import__(module_name, globals(), locals(), [attr])
|
2018-05-25 23:25:15 +02:00
|
|
|
parser = getattr(module, attr)
|
2022-07-31 17:40:18 +02:00
|
|
|
return parser if not callable(parser) else parser()
|
2014-07-06 23:58:01 +02:00
|
|
|
|
|
|
|
|
|
|
|
def indent(value, length=4):
|
|
|
|
space = " " * length
|
|
|
|
return "\n".join(space + line for line in value.splitlines())
|
|
|
|
|
|
|
|
|
|
|
|
class ArgparseDirective(Directive):
|
|
|
|
has_content = True
|
|
|
|
option_spec = {
|
|
|
|
"module": unchanged,
|
|
|
|
"attr": unchanged,
|
|
|
|
}
|
|
|
|
|
2020-11-14 11:16:12 +01:00
|
|
|
_headlines = ["^", "~"]
|
|
|
|
|
2023-02-20 22:30:07 +01:00
|
|
|
def process_help(self, helptext):
|
2014-07-06 23:58:01 +02:00
|
|
|
# Dedent the help to make sure we are always dealing with
|
|
|
|
# non-indented text.
|
2023-02-20 22:30:07 +01:00
|
|
|
helptext = dedent(helptext)
|
2014-07-06 23:58:01 +02:00
|
|
|
|
2023-02-20 22:30:07 +01:00
|
|
|
helptext = _inline_code_block_re.sub(
|
2022-07-18 17:19:35 +02:00
|
|
|
lambda m: (
|
|
|
|
":code:`{0}`".format(m.group(1).replace("\\", "\\\\"))
|
|
|
|
),
|
2023-02-20 22:30:07 +01:00
|
|
|
helptext,
|
2022-07-18 17:19:35 +02:00
|
|
|
)
|
|
|
|
|
2023-02-20 22:30:07 +01:00
|
|
|
helptext = _example_inline_code_block_re.sub(r":code:`\1`", helptext)
|
2022-07-18 17:19:41 +02:00
|
|
|
|
2017-05-23 12:51:42 +02:00
|
|
|
# Replace option references with links.
|
|
|
|
# Do this before indenting blocks and notes.
|
2023-02-20 22:30:07 +01:00
|
|
|
helptext = _option_line_re.sub(
|
2023-03-31 11:12:55 +02:00
|
|
|
lambda m: _option_re.sub(
|
|
|
|
lambda m2: f":option:`{m2['arg']}{m2['val'] or ''}`" if m2["arg"] in self._available_options else m2[0],
|
|
|
|
m[1],
|
2017-05-23 12:51:42 +02:00
|
|
|
),
|
2023-02-20 22:30:07 +01:00
|
|
|
helptext,
|
2017-05-23 12:51:42 +02:00
|
|
|
)
|
|
|
|
|
2014-07-06 23:58:01 +02:00
|
|
|
# Create simple blocks.
|
2023-02-20 22:30:07 +01:00
|
|
|
helptext = _block_re.sub("::\n\n ", helptext)
|
2014-07-06 23:58:01 +02:00
|
|
|
|
|
|
|
# Boldify the default value.
|
2023-02-20 22:30:07 +01:00
|
|
|
helptext = _default_re.sub(r"Default is: **\1**.\n", helptext)
|
2014-07-06 23:58:01 +02:00
|
|
|
|
|
|
|
# Create note directives from "Note: " paragraphs.
|
2023-02-20 22:30:07 +01:00
|
|
|
helptext = _note_re.sub(
|
2014-07-06 23:58:01 +02:00
|
|
|
lambda m: ".. note::\n\n" + indent(m.group(1)) + "\n\n",
|
2023-02-20 22:30:07 +01:00
|
|
|
helptext,
|
2014-07-06 23:58:01 +02:00
|
|
|
)
|
|
|
|
|
2018-07-26 21:33:59 +02:00
|
|
|
# workaround to replace %(prog)s with streamlink
|
2023-02-20 22:30:07 +01:00
|
|
|
helptext = _prog_re.sub("streamlink", helptext)
|
2018-07-26 21:33:59 +02:00
|
|
|
|
2021-05-28 20:54:42 +02:00
|
|
|
# fix escaped chars for percent-formatted argparse help strings
|
2023-02-20 22:30:07 +01:00
|
|
|
helptext = _percent_re.sub("%", helptext)
|
2021-05-28 20:54:42 +02:00
|
|
|
|
2023-08-26 00:28:19 +02:00
|
|
|
# create cross-links for the "Metadata variables" and "Plugins" sections
|
|
|
|
helptext = re.sub(
|
|
|
|
r"the \"Metadata variables\" section",
|
2022-06-21 17:19:31 +02:00
|
|
|
"the \":ref:`Metadata variables <cli/metadata:Variables>`\" section",
|
2023-02-20 22:30:07 +01:00
|
|
|
helptext,
|
2022-06-21 17:19:31 +02:00
|
|
|
)
|
2023-08-26 00:28:19 +02:00
|
|
|
helptext = re.sub(
|
|
|
|
r"the \"Plugins\" section",
|
|
|
|
"the \":ref:`Plugins <plugins:Plugins>`\" section",
|
|
|
|
helptext,
|
|
|
|
)
|
2022-06-21 17:19:31 +02:00
|
|
|
|
2023-02-20 22:30:07 +01:00
|
|
|
return indent(helptext)
|
2014-07-06 23:58:01 +02:00
|
|
|
|
|
|
|
def generate_group_rst(self, group):
|
2020-11-08 05:44:23 +01:00
|
|
|
for action in group._group_actions:
|
|
|
|
# don't document suppressed parameters
|
|
|
|
if action.help == argparse.SUPPRESS:
|
|
|
|
continue
|
2014-07-06 23:58:01 +02:00
|
|
|
|
2020-11-08 05:44:23 +01:00
|
|
|
metavar = action.metavar
|
2014-07-06 23:58:01 +02:00
|
|
|
if isinstance(metavar, tuple):
|
|
|
|
metavar = " ".join(metavar)
|
|
|
|
|
2020-11-08 05:44:23 +01:00
|
|
|
options = []
|
|
|
|
# parameter(s) with metavar
|
|
|
|
if action.option_strings and metavar:
|
|
|
|
for arg in action.option_strings:
|
|
|
|
# optional parameter value
|
|
|
|
if action.nargs == "?":
|
|
|
|
metavar = f"[{metavar}]"
|
|
|
|
options.append(f"{arg} {metavar}")
|
|
|
|
# positional parameter
|
|
|
|
elif metavar:
|
|
|
|
options.append(metavar)
|
|
|
|
# parameter(s) without metavar
|
2014-07-06 23:58:01 +02:00
|
|
|
else:
|
2020-11-08 05:44:23 +01:00
|
|
|
options += action.option_strings
|
2014-07-06 23:58:01 +02:00
|
|
|
|
2020-11-29 16:57:12 +01:00
|
|
|
directive = ".. option:: "
|
|
|
|
options = f"\n{' ' * len(directive)}".join(options)
|
|
|
|
yield f"{directive}{options}"
|
2014-07-06 23:58:01 +02:00
|
|
|
yield ""
|
2020-11-08 05:44:23 +01:00
|
|
|
for line in self.process_help(action.help).split("\n"):
|
2014-07-06 23:58:01 +02:00
|
|
|
yield line
|
|
|
|
yield ""
|
|
|
|
|
2022-07-12 18:01:25 +02:00
|
|
|
def generate_parser_rst(self, parser, parent=None, depth=0):
|
2020-11-14 11:16:12 +01:00
|
|
|
if depth >= len(self._headlines):
|
|
|
|
return
|
2022-07-12 18:01:25 +02:00
|
|
|
for group in parser.NESTED_ARGUMENT_GROUPS[parent]:
|
|
|
|
is_parent = group in parser.NESTED_ARGUMENT_GROUPS
|
2020-11-08 05:44:23 +01:00
|
|
|
# Exclude empty groups
|
2022-07-12 18:01:25 +02:00
|
|
|
if not group._group_actions and not is_parent:
|
2020-11-08 05:44:23 +01:00
|
|
|
continue
|
|
|
|
title = group.title
|
2014-07-06 23:58:01 +02:00
|
|
|
yield ""
|
|
|
|
yield title
|
2020-11-14 11:16:12 +01:00
|
|
|
yield self._headlines[depth] * len(title)
|
|
|
|
yield from self.generate_group_rst(group)
|
2022-07-12 18:01:25 +02:00
|
|
|
if is_parent:
|
2020-11-14 11:16:12 +01:00
|
|
|
yield ""
|
2022-07-12 18:01:25 +02:00
|
|
|
yield from self.generate_parser_rst(parser, group, depth + 1)
|
2014-07-06 23:58:01 +02:00
|
|
|
|
|
|
|
def run(self):
|
|
|
|
module = self.options.get("module")
|
|
|
|
attr = self.options.get("attr")
|
|
|
|
parser = get_parser(module, attr)
|
|
|
|
|
|
|
|
self._available_options = []
|
2020-11-08 05:44:23 +01:00
|
|
|
for action in parser._actions:
|
|
|
|
# positional parameters have an empty option_strings list
|
|
|
|
self._available_options += action.option_strings or [action.dest]
|
2014-07-06 23:58:01 +02:00
|
|
|
|
|
|
|
node = nodes.section()
|
|
|
|
node.document = self.state.document
|
|
|
|
result = ViewList()
|
|
|
|
for line in self.generate_parser_rst(parser):
|
|
|
|
result.append(line, "argparse")
|
|
|
|
|
|
|
|
nested_parse_with_titles(self.state, result, node)
|
|
|
|
return node.children
|
|
|
|
|
|
|
|
|
|
|
|
def setup(app):
|
|
|
|
app.add_directive("argparse", ArgparseDirective)
|