Skip to content

Commit ac2a54d

Browse files
committed
Add sphinx._cli
1 parent 18394a4 commit ac2a54d

File tree

1 file changed

+179
-0
lines changed

1 file changed

+179
-0
lines changed

sphinx/_cli/__init__.py

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

0 commit comments

Comments
 (0)