Test command line argument parsing (#12795)

This commit is contained in:
Adam Turner 2024-08-17 02:53:48 +01:00 committed by GitHub
parent 2e1415cf6c
commit 334e69fbb4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 261 additions and 37 deletions

View File

@ -210,6 +210,9 @@ files can be built by specifying individual filenames.
dest='exception_on_warning',
help=__('raise an exception on warnings'))
if parser.prog == '__main__.py':
parser.prog = 'sphinx-build'
return parser
@ -386,7 +389,8 @@ def main(argv: Sequence[str] = (), /) -> int:
if argv[:1] == ['--bug-report']:
return _bug_report_info()
if argv[:1] == ['-M']:
return make_main(argv)
from sphinx.cmd import make_mode
return make_mode.run_make_mode(argv[1:])
else:
return build_main(argv)

View File

@ -58,31 +58,31 @@ BUILDERS = [
class Make:
def __init__(self, srcdir: str, builddir: str, opts: Sequence[str]) -> None:
self.srcdir = srcdir
self.builddir = builddir
def __init__(self, *, source_dir: str, build_dir: str, opts: Sequence[str]) -> None:
self.source_dir = source_dir
self.build_dir = build_dir
self.opts = [*opts]
def builddir_join(self, *comps: str) -> str:
return path.join(self.builddir, *comps)
def build_dir_join(self, *comps: str) -> str:
return path.join(self.build_dir, *comps)
def build_clean(self) -> int:
srcdir = path.abspath(self.srcdir)
builddir = path.abspath(self.builddir)
if not path.exists(self.builddir):
source_dir = path.abspath(self.source_dir)
build_dir = path.abspath(self.build_dir)
if not path.exists(self.build_dir):
return 0
elif not path.isdir(self.builddir):
print("Error: %r is not a directory!" % self.builddir)
elif not path.isdir(self.build_dir):
print("Error: %r is not a directory!" % self.build_dir)
return 1
elif srcdir == builddir:
print("Error: %r is same as source directory!" % self.builddir)
elif source_dir == build_dir:
print("Error: %r is same as source directory!" % self.build_dir)
return 1
elif path.commonpath([srcdir, builddir]) == builddir:
print("Error: %r directory contains source directory!" % self.builddir)
elif path.commonpath([source_dir, build_dir]) == build_dir:
print("Error: %r directory contains source directory!" % self.build_dir)
return 1
print("Removing everything under %r..." % self.builddir)
for item in os.listdir(self.builddir):
rmtree(self.builddir_join(item))
print("Removing everything under %r..." % self.build_dir)
for item in os.listdir(self.build_dir):
rmtree(self.build_dir_join(item))
return 0
def build_help(self) -> None:
@ -105,7 +105,7 @@ class Make:
if not makecmd.lower().startswith('make'):
raise RuntimeError('Invalid $MAKE command: %r' % makecmd)
try:
with chdir(self.builddir_join('latex')):
with chdir(self.build_dir_join('latex')):
if '-Q' in self.opts:
with open('__LATEXSTDOUT__', 'w') as outfile:
returncode = subprocess.call([makecmd,
@ -117,7 +117,7 @@ class Make:
)
if returncode:
print('Latex error: check %s' %
self.builddir_join('latex', '__LATEXSTDOUT__')
self.build_dir_join('latex', '__LATEXSTDOUT__')
)
elif '-q' in self.opts:
returncode = subprocess.call(
@ -129,7 +129,7 @@ class Make:
)
if returncode:
print('Latex error: check .log file in %s' %
self.builddir_join('latex')
self.build_dir_join('latex')
)
else:
returncode = subprocess.call([makecmd, 'all-pdf'])
@ -148,7 +148,7 @@ class Make:
if not makecmd.lower().startswith('make'):
raise RuntimeError('Invalid $MAKE command: %r' % makecmd)
try:
with chdir(self.builddir_join('latex')):
with chdir(self.build_dir_join('latex')):
return subprocess.call([makecmd, 'all-pdf'])
except OSError:
print('Error: Failed to run: %s' % makecmd)
@ -163,32 +163,33 @@ class Make:
if not makecmd.lower().startswith('make'):
raise RuntimeError('Invalid $MAKE command: %r' % makecmd)
try:
with chdir(self.builddir_join('texinfo')):
with chdir(self.build_dir_join('texinfo')):
return subprocess.call([makecmd, 'info'])
except OSError:
print('Error: Failed to run: %s' % makecmd)
return 1
def build_gettext(self) -> int:
dtdir = self.builddir_join('gettext', '.doctrees')
dtdir = self.build_dir_join('gettext', '.doctrees')
if self.run_generic_build('gettext', doctreedir=dtdir) > 0:
return 1
return 0
def run_generic_build(self, builder: str, doctreedir: str | None = None) -> int:
# compatibility with old Makefile
papersize = os.getenv('PAPER', '')
opts = self.opts
if papersize in ('a4', 'letter'):
opts.extend(['-D', 'latex_elements.papersize=' + papersize + 'paper'])
paper_size = os.getenv('PAPER', '')
if paper_size in {'a4', 'letter'}:
self.opts.extend(['-D', f'latex_elements.papersize={paper_size}paper'])
if doctreedir is None:
doctreedir = self.builddir_join('doctrees')
doctreedir = self.build_dir_join('doctrees')
args = ['-b', builder,
'-d', doctreedir,
self.srcdir,
self.builddir_join(builder)]
return build_main(args + opts)
args = [
'--builder', builder,
'--doctree-dir', doctreedir,
self.source_dir,
self.build_dir_join(builder),
]
return build_main(args + self.opts)
def run_make_mode(args: Sequence[str]) -> int:
@ -196,8 +197,10 @@ def run_make_mode(args: Sequence[str]) -> int:
print('Error: at least 3 arguments (builder, source '
'dir, build dir) are required.', file=sys.stderr)
return 1
make = Make(args[1], args[2], args[3:])
run_method = 'build_' + args[0]
builder_name = args[0]
make = Make(source_dir=args[1], build_dir=args[2], opts=args[3:])
run_method = f'build_{builder_name}'
if hasattr(make, run_method):
return getattr(make, run_method)()
return make.run_generic_build(args[0])
return make.run_generic_build(builder_name)

217
tests/test_command_line.py Normal file
View File

@ -0,0 +1,217 @@
from __future__ import annotations
import os.path
from typing import Any
import pytest
from sphinx.cmd import make_mode
from sphinx.cmd.build import get_parser
from sphinx.cmd.make_mode import run_make_mode
DEFAULTS = {
'filenames': [],
'jobs': 1,
'force_all': False,
'freshenv': False,
'doctreedir': None,
'confdir': None,
'noconfig': False,
'define': [],
'htmldefine': [],
'tags': [],
'nitpicky': False,
'verbosity': 0,
'quiet': False,
'really_quiet': False,
'color': 'auto',
'warnfile': None,
'warningiserror': False,
'keep_going': False,
'traceback': False,
'pdb': False,
'exception_on_warning': False,
}
EXPECTED_BUILD_MAIN = {
'builder': 'html',
'sourcedir': 'source_dir',
'outputdir': 'build_dir',
'filenames': ['filename1', 'filename2'],
'freshenv': True,
'noconfig': True,
'quiet': True,
}
EXPECTED_MAKE_MODE = {
'builder': 'html',
'sourcedir': 'source_dir',
'outputdir': os.path.join('build_dir', 'html'),
'doctreedir': os.path.join('build_dir', 'doctrees'),
'filenames': ['filename1', 'filename2'],
'freshenv': True,
'noconfig': True,
'quiet': True,
}
BUILDER_BUILD_MAIN = [
'--builder',
'html',
]
BUILDER_MAKE_MODE = [
'html',
]
POSITIONAL_DIRS = [
'source_dir',
'build_dir',
]
POSITIONAL_FILENAMES = [
'filename1',
'filename2',
]
POSITIONAL = POSITIONAL_DIRS + POSITIONAL_FILENAMES
POSITIONAL_MAKE_MODE = BUILDER_MAKE_MODE + POSITIONAL
EARLY_OPTS = [
'--quiet',
]
LATE_OPTS = [
'-E',
'--isolated',
]
OPTS = EARLY_OPTS + LATE_OPTS
OPTS_BUILD_MAIN = BUILDER_BUILD_MAIN + OPTS
def parse_arguments(args: list[str]) -> dict[str, Any]:
parsed = vars(get_parser().parse_args(args))
return {k: v for k, v in parsed.items() if k not in DEFAULTS or v != DEFAULTS[k]}
def test_build_main_parse_arguments_pos_first() -> None:
# <positional...> <opts>
args = [
*POSITIONAL,
*OPTS,
]
assert parse_arguments(args) == EXPECTED_BUILD_MAIN
def test_build_main_parse_arguments_pos_last() -> None:
# <opts> <positional...>
args = [
*OPTS,
*POSITIONAL,
]
assert parse_arguments(args) == EXPECTED_BUILD_MAIN
def test_build_main_parse_arguments_pos_middle() -> None:
# <opts> <positional...> <opts>
args = [
*EARLY_OPTS,
*BUILDER_BUILD_MAIN,
*POSITIONAL,
*LATE_OPTS,
]
assert parse_arguments(args) == EXPECTED_BUILD_MAIN
@pytest.mark.xfail(reason='sphinx-build does not yet support filenames after options')
def test_build_main_parse_arguments_filenames_last() -> None:
args = [
*POSITIONAL_DIRS,
*OPTS,
*POSITIONAL_FILENAMES,
]
assert parse_arguments(args) == EXPECTED_BUILD_MAIN
def test_build_main_parse_arguments_pos_intermixed(
capsys: pytest.CaptureFixture[str],
) -> None:
args = [
*EARLY_OPTS,
*BUILDER_BUILD_MAIN,
*POSITIONAL_DIRS,
*LATE_OPTS,
*POSITIONAL_FILENAMES,
]
with pytest.raises(SystemExit):
parse_arguments(args)
stderr = capsys.readouterr().err.splitlines()
assert stderr[-1].endswith('error: unrecognized arguments: filename1 filename2')
def test_make_mode_parse_arguments_pos_first(monkeypatch: pytest.MonkeyPatch) -> None:
# -M <positional...> <opts>
monkeypatch.setattr(make_mode, 'build_main', parse_arguments)
args = [
*POSITIONAL_MAKE_MODE,
*OPTS,
]
assert run_make_mode(args) == EXPECTED_MAKE_MODE
def test_make_mode_parse_arguments_pos_last(
monkeypatch: pytest.MonkeyPatch,
capsys: pytest.CaptureFixture[str],
) -> None:
# -M <opts> <positional...>
monkeypatch.setattr(make_mode, 'build_main', parse_arguments)
args = [
*OPTS,
*POSITIONAL_MAKE_MODE,
]
with pytest.raises(SystemExit):
run_make_mode(args)
stderr = capsys.readouterr().err.splitlines()
assert stderr[-1].endswith('error: argument --builder/-b: expected one argument')
def test_make_mode_parse_arguments_pos_middle(
monkeypatch: pytest.MonkeyPatch,
capsys: pytest.CaptureFixture[str],
) -> None:
# -M <opts> <positional...> <opts>
monkeypatch.setattr(make_mode, 'build_main', parse_arguments)
args = [
*EARLY_OPTS,
*POSITIONAL_MAKE_MODE,
*LATE_OPTS,
]
with pytest.raises(SystemExit):
run_make_mode(args)
stderr = capsys.readouterr().err.splitlines()
assert stderr[-1].endswith('error: argument --builder/-b: expected one argument')
@pytest.mark.xfail(reason='sphinx-build does not yet support filenames after options')
def test_make_mode_parse_arguments_filenames_last(
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setattr(make_mode, 'build_main', parse_arguments)
args = [
*BUILDER_MAKE_MODE,
*POSITIONAL_DIRS,
*OPTS,
*POSITIONAL_FILENAMES,
]
assert run_make_mode(args) == EXPECTED_MAKE_MODE
def test_make_mode_parse_arguments_pos_intermixed(
monkeypatch: pytest.MonkeyPatch,
capsys: pytest.CaptureFixture[str],
) -> None:
monkeypatch.setattr(make_mode, 'build_main', parse_arguments)
args = [
*EARLY_OPTS,
*BUILDER_MAKE_MODE,
*POSITIONAL_DIRS,
*LATE_OPTS,
*POSITIONAL_FILENAMES,
]
with pytest.raises(SystemExit):
run_make_mode(args)
stderr = capsys.readouterr().err.splitlines()
assert stderr[-1].endswith('error: argument --builder/-b: expected one argument')