pip/docs/pip_sphinxext.py

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

304 lines
9.9 KiB
Python
Raw Permalink Normal View History

2013-01-18 22:25:15 +01:00
"""pip sphinx extensions"""
import optparse
import pathlib
import re
2013-01-18 22:25:15 +01:00
import sys
from textwrap import dedent
2021-04-14 20:19:26 +02:00
from typing import Dict, Iterable, Iterator, List, Optional, Union
from docutils import nodes, statemachine
2013-01-18 22:25:15 +01:00
from docutils.parsers import rst
2021-01-18 23:24:40 +01:00
from docutils.statemachine import StringList, ViewList
2021-02-28 17:32:22 +01:00
from sphinx.application import Sphinx
2018-07-30 08:15:17 +02:00
from pip._internal.cli import cmdoptions
from pip._internal.commands import commands_dict, create_command
from pip._internal.req.req_file import SUPPORTED_OPTIONS
2013-01-18 22:25:15 +01:00
class PipNewsInclude(rst.Directive):
required_arguments = 1
2021-04-04 13:58:59 +02:00
def _is_version_section_title_underline(
self, prev: Optional[str], curr: str
) -> bool:
"""Find a ==== line that marks the version section title."""
if prev is None:
return False
if re.match(r"^=+$", curr) is None:
return False
if len(curr) < len(prev):
return False
return True
2021-04-04 13:58:59 +02:00
def _iter_lines_with_refs(self, lines: Iterable[str]) -> Iterator[str]:
"""Transform the input lines to add a ref before each section title.
This is done by looking one line ahead and locate a title's underline,
and add a ref before the title text.
Dots in the version is converted into dash, and a ``v`` is prefixed.
This makes Sphinx use them as HTML ``id`` verbatim without generating
auto numbering (which would make the the anchors unstable).
"""
prev = None
for line in lines:
# Transform the previous line to include an explicit ref.
if self._is_version_section_title_underline(prev, line):
2021-04-04 13:58:59 +02:00
assert prev is not None
vref = prev.split(None, 1)[0].replace(".", "-")
yield f".. _`v{vref}`:"
yield "" # Empty line between ref and the title.
if prev is not None:
yield prev
prev = line
if prev is not None:
yield prev
2021-04-04 13:58:59 +02:00
def run(self) -> List[nodes.Node]:
source = self.state_machine.input_lines.source(
self.lineno - self.state_machine.input_offset - 1,
)
path = (
2021-04-04 00:20:27 +02:00
pathlib.Path(source).resolve().parent.joinpath(self.arguments[0]).resolve()
)
include_lines = statemachine.string2lines(
path.read_text(encoding="utf-8"),
self.state.document.settings.tab_width,
convert_whitespace=True,
)
include_lines = list(self._iter_lines_with_refs(include_lines))
self.state_machine.insert_input(include_lines, str(path))
return []
2013-01-18 22:25:15 +01:00
class PipCommandUsage(rst.Directive):
required_arguments = 1
optional_arguments = 3
2021-02-28 17:32:22 +01:00
def run(self) -> List[nodes.Node]:
cmd = create_command(self.arguments[0])
2021-01-18 23:24:30 +01:00
cmd_prefix = "python -m pip"
if len(self.arguments) > 1:
cmd_prefix = " ".join(self.arguments[1:])
cmd_prefix = cmd_prefix.strip('"')
cmd_prefix = cmd_prefix.strip("'")
2021-01-18 23:24:30 +01:00
usage = dedent(cmd.usage.replace("%prog", f"{cmd_prefix} {cmd.name}")).strip()
2013-01-18 22:25:15 +01:00
node = nodes.literal_block(usage, usage)
return [node]
2013-01-18 22:25:15 +01:00
class PipCommandDescription(rst.Directive):
required_arguments = 1
2021-02-28 17:32:22 +01:00
def run(self) -> List[nodes.Node]:
2013-01-18 22:25:15 +01:00
node = nodes.paragraph()
node.document = self.state.document
desc = ViewList()
cmd = create_command(self.arguments[0])
2021-02-28 17:32:22 +01:00
assert cmd.__doc__ is not None
description = dedent(cmd.__doc__)
2021-01-18 23:24:30 +01:00
for line in description.split("\n"):
2013-01-18 22:25:15 +01:00
desc.append(line, "")
self.state.nested_parse(desc, 0, node)
return [node]
2013-01-18 22:25:15 +01:00
class PipOptions(rst.Directive):
2021-02-28 17:32:22 +01:00
def _format_option(
self, option: optparse.Option, cmd_name: Optional[str] = None
) -> List[str]:
bookmark_line = (
f".. _`{cmd_name}_{option._long_opts[0]}`:"
2021-01-18 23:24:30 +01:00
if cmd_name
else f".. _`{option._long_opts[0]}`:"
)
2014-02-12 06:55:43 +01:00
line = ".. option:: "
2013-01-18 22:25:15 +01:00
if option._short_opts:
line += option._short_opts[0]
if option._short_opts and option._long_opts:
line += ", " + option._long_opts[0]
2013-01-18 22:25:15 +01:00
elif option._long_opts:
line += option._long_opts[0]
if option.takes_value():
2021-02-28 17:32:22 +01:00
metavar = option.metavar or option.dest
assert metavar is not None
line += f" <{metavar.lower()}>"
2014-03-26 23:24:19 +01:00
# fix defaults
2021-02-28 17:32:22 +01:00
assert option.help is not None
opt_help = option.help.replace("%default", str(option.default))
2014-03-26 23:24:19 +01:00
# fix paths with sys.prefix
2013-01-18 22:25:15 +01:00
opt_help = opt_help.replace(sys.prefix, "<sys.prefix>")
return [bookmark_line, "", line, "", " " + opt_help, ""]
2013-01-18 22:25:15 +01:00
2021-02-28 17:32:22 +01:00
def _format_options(
self, options: Iterable[optparse.Option], cmd_name: Optional[str] = None
) -> None:
2013-01-18 22:25:15 +01:00
for option in options:
if option.help == optparse.SUPPRESS_HELP:
continue
for line in self._format_option(option, cmd_name):
self.view_list.append(line, "")
2021-02-28 17:32:22 +01:00
def run(self) -> List[nodes.Node]:
2013-01-18 22:25:15 +01:00
node = nodes.paragraph()
node.document = self.state.document
self.view_list = ViewList()
self.process_options()
self.state.nested_parse(self.view_list, 0, node)
return [node]
2013-01-18 22:25:15 +01:00
class PipGeneralOptions(PipOptions):
2021-02-28 17:32:22 +01:00
def process_options(self) -> None:
2021-01-18 23:24:30 +01:00
self._format_options([o() for o in cmdoptions.general_group["options"]])
2013-01-18 22:25:15 +01:00
2013-01-18 22:25:15 +01:00
class PipIndexOptions(PipOptions):
required_arguments = 1
2021-02-28 17:32:22 +01:00
def process_options(self) -> None:
cmd_name = self.arguments[0]
self._format_options(
2021-01-18 23:24:30 +01:00
[o() for o in cmdoptions.index_group["options"]],
cmd_name=cmd_name,
)
2013-01-18 22:25:15 +01:00
2013-01-18 22:25:15 +01:00
class PipCommandOptions(PipOptions):
required_arguments = 1
2021-02-28 17:32:22 +01:00
def process_options(self) -> None:
cmd = create_command(self.arguments[0])
self._format_options(
cmd.parser.option_groups[0].option_list,
cmd_name=cmd.name,
)
2013-01-18 22:25:15 +01:00
class PipReqFileOptionsReference(PipOptions):
2021-02-28 17:32:22 +01:00
def determine_opt_prefix(self, opt_name: str) -> str:
for command in commands_dict:
cmd = create_command(command)
if cmd.cmd_opts.has_option(opt_name):
return command
2021-01-18 23:24:30 +01:00
raise KeyError(f"Could not identify prefix of opt {opt_name}")
2021-02-28 17:32:22 +01:00
def process_options(self) -> None:
for option in SUPPORTED_OPTIONS:
2021-01-18 23:24:30 +01:00
if getattr(option, "deprecated", False):
continue
opt = option()
opt_name = opt._long_opts[0]
if opt._short_opts:
2023-11-07 10:14:56 +01:00
short_opt_name = f"{opt._short_opts[0]}, "
else:
2021-01-18 23:24:30 +01:00
short_opt_name = ""
2021-01-18 23:24:30 +01:00
if option in cmdoptions.general_group["options"]:
prefix = ""
else:
2023-11-07 10:14:56 +01:00
prefix = f"{self.determine_opt_prefix(opt_name)}_"
self.view_list.append(
2023-11-07 10:14:56 +01:00
f"* :ref:`{short_opt_name}{opt_name}<{prefix}{opt_name}>`",
2021-01-18 23:24:30 +01:00
"\n",
)
2021-01-18 23:24:40 +01:00
class PipCLIDirective(rst.Directive):
"""
- Only works when used in a MyST document.
- Requires sphinx-inline-tabs' tab directive.
"""
has_content = True
optional_arguments = 1
2021-02-28 17:32:22 +01:00
def run(self) -> List[nodes.Node]:
2021-01-18 23:24:40 +01:00
node = nodes.paragraph()
node.document = self.state.document
os_variants = {
"Linux": {
"highlighter": "console",
"executable": "python",
"prompt": "$",
},
"MacOS": {
"highlighter": "console",
"executable": "python",
"prompt": "$",
},
"Windows": {
"highlighter": "doscon",
"executable": "py",
"prompt": "C:>",
},
}
if self.arguments:
assert self.arguments == ["in-a-venv"]
in_virtual_environment = True
else:
in_virtual_environment = False
lines = []
# Create a tab for each OS
for os, variant in os_variants.items():
# Unpack the values
prompt = variant["prompt"]
highlighter = variant["highlighter"]
if in_virtual_environment:
executable = "python"
pip_spelling = "pip"
else:
executable = variant["executable"]
pip_spelling = f"{executable} -m pip"
# Substitute the various "prompts" into the correct variants
substitution_pipeline = [
(
r"(^|(?<=\n))\$ python",
f"{prompt} {executable}",
),
(
r"(^|(?<=\n))\$ pip",
f"{prompt} {pip_spelling}",
),
]
content = self.block_text
for pattern, substitution in substitution_pipeline:
content = re.sub(pattern, substitution, content)
# Write the tab
lines.append(f"````{{tab}} {os}")
lines.append(f"```{highlighter}")
lines.append(f"{content}")
lines.append("```")
lines.append("````")
string_list = StringList(lines)
self.state.nested_parse(string_list, 0, node)
return [node]
2021-04-14 20:19:26 +02:00
def setup(app: Sphinx) -> Dict[str, Union[bool, str]]:
2021-01-18 23:24:30 +01:00
app.add_directive("pip-command-usage", PipCommandUsage)
app.add_directive("pip-command-description", PipCommandDescription)
app.add_directive("pip-command-options", PipCommandOptions)
app.add_directive("pip-general-options", PipGeneralOptions)
app.add_directive("pip-index-options", PipIndexOptions)
app.add_directive(
2021-01-18 23:24:30 +01:00
"pip-requirements-file-options-ref-list", PipReqFileOptionsReference
)
2021-04-04 00:20:27 +02:00
app.add_directive("pip-news-include", PipNewsInclude)
2021-01-18 23:24:40 +01:00
app.add_directive("pip-cli", PipCLIDirective)
2021-04-14 20:19:26 +02:00
return {
"parallel_read_safe": True,
"parallel_write_safe": True,
}