Skip to content

Commit 9f35d1b

Browse files
committed
Add sphinx._cli
1 parent 50cc78d commit 9f35d1b

File tree

1 file changed

+196
-0
lines changed

1 file changed

+196
-0
lines changed

sphinx/_cli/__init__.py

Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
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
26+
27+
from sphinx._cli.util.colour import disable_colour, enable_colour, terminal_supports_colour
28+
from sphinx.locale import __, init_console
29+
30+
if TYPE_CHECKING:
31+
from collections.abc import Callable, 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+
description = module.parser_description
73+
except AttributeError:
74+
continue
75+
else:
76+
yield command, description.split('\n\n', 1)[0]
77+
78+
def format_help(self):
79+
formatter = self._get_formatter()
80+
formatter.add_usage(self.usage, self._actions, [])
81+
formatter.add_text(self.description)
82+
83+
formatter.start_section(__('Commands'))
84+
for command_name, command_desc in self._load_subcommands():
85+
formatter.add_argument(argparse.Action((), command_name, help=command_desc))
86+
formatter.end_section()
87+
88+
formatter.start_section(__('Options'))
89+
formatter.add_arguments(self._optionals._group_actions)
90+
formatter.end_section()
91+
92+
formatter.add_text(self.epilog)
93+
return formatter.format_help()
94+
95+
96+
def _create_parser() -> _RootArgumentParser:
97+
parser = _RootArgumentParser(
98+
prog='sphinx',
99+
description=__(' Manage documentation with Sphinx.'),
100+
epilog=__('For more information, visit <https://www.sphinx-doc.org/en/master/man/>.'),
101+
formatter_class=_HelpFormatter,
102+
add_help=False,
103+
allow_abbrev=False,
104+
)
105+
parser.add_argument(
106+
'--version', '-V',
107+
action='store_true',
108+
default=argparse.SUPPRESS,
109+
help=__('Show the version and exit.'),
110+
)
111+
parser.add_argument(
112+
'--help', '-h', '-?',
113+
action='store_true',
114+
default=argparse.SUPPRESS,
115+
help=__('Show this message and exit.'),
116+
)
117+
118+
parser.add_argument(
119+
'COMMAND',
120+
nargs='...',
121+
metavar=__('<command>'),
122+
)
123+
return parser
124+
125+
126+
def _parse_command(argv: Sequence[str] = ()) -> tuple[str, Sequence[str]]:
127+
parser = _create_parser()
128+
args = parser.parse_args(argv)
129+
command_name, *command_argv = args.COMMAND or ('help',)
130+
command_name = command_name.lower()
131+
132+
# Handle '--version' or '-V' passed to the main command or any subcommand
133+
if 'version' in args or {'-V', '--version'}.intersection(command_argv):
134+
from sphinx import __display_version__
135+
sys.stdout.write(f'sphinx {__display_version__}\n')
136+
raise SystemExit(0)
137+
138+
# Handle '--help' or '-h' passed to the main command (subcommands may have
139+
# their own help text)
140+
if 'help' in args or command_name == 'help':
141+
sys.stdout.write(parser.format_help())
142+
raise SystemExit(0)
143+
144+
if command_name not in _COMMANDS:
145+
sys.stderr.write(__(f'sphinx: {command_name!r} is not a sphinx command. '
146+
"See 'sphinx --help'.\n"))
147+
raise SystemExit(2)
148+
149+
if args.colour == 'yes' or (args.colour == 'auto' and terminal_supports_colour()):
150+
enable_colour()
151+
else:
152+
disable_colour()
153+
154+
return command_name, command_argv
155+
156+
157+
def _load_subcommand(command_name: str) -> tuple[str, _PARSER_SETUP, _RUNNER]:
158+
import importlib
159+
160+
module: _SubcommandModule = importlib.import_module(_COMMANDS[command_name])
161+
return module.parser_description, module.set_up_parser, module.run
162+
163+
164+
def _create_sub_parser(
165+
command_name: str,
166+
description: str,
167+
parser_setup: _PARSER_SETUP,
168+
) -> argparse.ArgumentParser:
169+
parser = argparse.ArgumentParser(
170+
prog=f'sphinx {command_name}',
171+
description=description,
172+
formatter_class=argparse.RawDescriptionHelpFormatter,
173+
allow_abbrev=False,
174+
)
175+
return parser_setup(parser)
176+
177+
178+
def run(argv: Sequence[str] = (), /) -> int:
179+
locale.setlocale(locale.LC_ALL, '')
180+
init_console()
181+
182+
argv = argv or sys.argv[1:]
183+
try:
184+
cmd_name, cmd_argv = _parse_command(argv)
185+
cmd_description, set_up_parser, runner = _load_subcommand(cmd_name)
186+
cmd_parser = _create_sub_parser(cmd_name, cmd_description, set_up_parser)
187+
cmd_args = cmd_parser.parse_args(cmd_argv)
188+
return runner(cmd_args)
189+
except SystemExit as exc:
190+
return exc.code # type: ignore[return-value]
191+
except (Exception, KeyboardInterrupt):
192+
return 2
193+
194+
195+
if __name__ == '__main__':
196+
raise SystemExit(run())

0 commit comments

Comments
 (0)