Skip to content

Commit d87f507

Browse files
committed
Add sphinx._cli
1 parent 5169f0f commit d87f507

File tree

1 file changed

+180
-0
lines changed

1 file changed

+180
-0
lines changed

sphinx/_cli/__init__.py

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
"""Base 'sphinx' command.
2+
3+
Subcommands are loaded lazily from the ``_COMMANDS`` table for performance.
4+
5+
All subcommand modules must define three attributes:
6+
7+
- ``parser_description``, a description of the subcommand. The first paragraph
8+
is taken as the short description for the command.
9+
- ``set_up_parser``, a callable taking and returning an ``ArgumentParser``. This
10+
function is responsible for adding options and arguments to the subcommand's
11+
parser..
12+
- ``run``, a callable taking parsed arguments and returning an exit code. This
13+
function is responsible for running the main body of the subcommand and
14+
returning the exit status.
15+
16+
The entire ``sphinx._cli`` namespace is private, only the command line interface
17+
has backwards-compatability guarantees.
18+
"""
19+
20+
from __future__ import annotations
21+
22+
import argparse
23+
import locale
24+
import sys
25+
from typing import TYPE_CHECKING, Callable
26+
27+
from sphinx._cli.util.colour import color_terminal, nocolor
28+
from sphinx.locale import __, init_console
29+
30+
if TYPE_CHECKING:
31+
from typing import Iterator, Sequence
32+
33+
_PARSER_SETUP = Callable[[argparse.ArgumentParser], argparse.ArgumentParser]
34+
_RUNNER = Callable[[argparse.Namespace], int]
35+
36+
if sys.version_info[:2] > (3, 8):
37+
from typing import Protocol
38+
39+
class _SubcommandModule(Protocol):
40+
parser_description: str
41+
set_up_parser: _PARSER_SETUP # takes and returns argument parser
42+
run: _RUNNER # takes parsed args, returns exit code
43+
else:
44+
from typing import Any as _SubcommandModule
45+
46+
47+
# Command name -> import path
48+
_COMMANDS: dict[str, str] = {
49+
}
50+
51+
52+
class _HelpFormatter(argparse.RawDescriptionHelpFormatter):
53+
def _format_usage(self, usage, actions, groups, prefix):
54+
if prefix is None:
55+
prefix = __('Usage: ')
56+
return super()._format_usage(usage, actions, groups, prefix)
57+
58+
def _format_args(self, action, default_metavar):
59+
if action.nargs == argparse.REMAINDER:
60+
return __('<command> [<args>]')
61+
return super()._format_args(action, default_metavar)
62+
63+
64+
class _RootArgumentParser(argparse.ArgumentParser):
65+
@staticmethod
66+
def _load_subcommands() -> Iterator[tuple[str, str]]:
67+
import importlib
68+
69+
for command, module_name in _COMMANDS.items():
70+
module: _SubcommandModule = importlib.import_module(module_name)
71+
try:
72+
yield command, module.parser_description.partition('\n\n')[0]
73+
except AttributeError:
74+
continue
75+
76+
def format_help(self):
77+
formatter = self._get_formatter()
78+
formatter.add_usage(self.usage, self._actions, [])
79+
formatter.add_text(self.description)
80+
81+
formatter.start_section(__('Commands'))
82+
for command_name, command_desc in self._load_subcommands():
83+
formatter.add_argument(argparse.Action((), command_name, help=command_desc))
84+
formatter.end_section()
85+
86+
formatter.start_section(__('Options'))
87+
formatter.add_arguments(self._optionals._group_actions)
88+
formatter.end_section()
89+
90+
formatter.add_text(self.epilog)
91+
return formatter.format_help()
92+
93+
94+
def _create_parser() -> _RootArgumentParser:
95+
parser = _RootArgumentParser(
96+
prog='sphinx',
97+
description=__(' Manage documentation with Sphinx.'),
98+
epilog=__('For more information, visit <https://www.sphinx-doc.org/en/master/man/>.'),
99+
formatter_class=_HelpFormatter,
100+
add_help=False,
101+
allow_abbrev=False,
102+
)
103+
parser.add_argument('--version', '-V', action='store_true',
104+
default=argparse.SUPPRESS,
105+
help=__('Show the version and exit.'))
106+
parser.add_argument('--help', '-h', '-?', action='store_true',
107+
default=argparse.SUPPRESS,
108+
help=__('Show this message and exit.'))
109+
parser.add_argument('--quiet', '-q', action='store_true',
110+
help=__('Only print errors and warnings.'))
111+
parser.add_argument('COMMAND', nargs='...', metavar=__('<command>'))
112+
return parser
113+
114+
115+
def _parse_command(argv: Sequence[str] = ()) -> tuple[str, Sequence[str]]:
116+
parser = _create_parser()
117+
args = parser.parse_args(argv)
118+
command_name, *command_argv = args.COMMAND or ['help']
119+
command_name = command_name.lower()
120+
121+
if 'version' in args or {'-V', '--version'}.intersection(command_argv):
122+
from sphinx import __display_version__
123+
sys.stdout.write(f'sphinx {__display_version__}\n')
124+
raise SystemExit(0)
125+
126+
if 'help' in args or command_name == 'help':
127+
sys.stdout.write(parser.format_help())
128+
raise SystemExit(0)
129+
130+
if command_name not in _COMMANDS:
131+
sys.stderr.write(__(f'sphinx: {command_name!r} is not a sphinx command. '
132+
"See 'sphinx --help'.\n"))
133+
raise SystemExit(2)
134+
135+
if not color_terminal() or not args.colour:
136+
nocolor()
137+
138+
return command_name, command_argv
139+
140+
141+
def _load_subcommand(command_name: str) -> tuple[str, _PARSER_SETUP, _RUNNER]:
142+
import importlib
143+
144+
module: _SubcommandModule = importlib.import_module(_COMMANDS[command_name])
145+
return module.parser_description, module.set_up_parser, module.run
146+
147+
148+
def _create_sub_parser(
149+
command_name: str,
150+
description: str,
151+
parser_setup: _PARSER_SETUP,
152+
) -> argparse.ArgumentParser:
153+
parser = argparse.ArgumentParser(
154+
prog=f'sphinx {command_name}',
155+
description=description,
156+
formatter_class=argparse.RawDescriptionHelpFormatter,
157+
allow_abbrev=False,
158+
)
159+
return parser_setup(parser)
160+
161+
162+
def run(__argv: Sequence[str] = ()) -> int:
163+
locale.setlocale(locale.LC_ALL, '')
164+
init_console()
165+
166+
argv = __argv or sys.argv[1:]
167+
try:
168+
cmd_name, cmd_argv = _parse_command(argv)
169+
cmd_description, set_up_parser, runner = _load_subcommand(cmd_name)
170+
cmd_parser = _create_sub_parser(cmd_name, cmd_description, set_up_parser)
171+
cmd_args = cmd_parser.parse_args(cmd_argv)
172+
return runner(cmd_args)
173+
except SystemExit as exc:
174+
return exc.code # type: ignore[return-value]
175+
except Exception:
176+
return 2
177+
178+
179+
if __name__ == '__main__':
180+
raise SystemExit(run())

0 commit comments

Comments
 (0)