Skip to content

Commit 24db75c

Browse files
committed
Add sphinx._cli
1 parent d81a205 commit 24db75c

File tree

1 file changed

+175
-0
lines changed

1 file changed

+175
-0
lines changed

sphinx/_cli/__init__.py

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

0 commit comments

Comments
 (0)