mirror of
https://github.com/sphinx-doc/sphinx.git
synced 2025-02-25 18:55:22 -06:00
Test command line argument parsing (#12795)
This commit is contained in:
parent
2e1415cf6c
commit
334e69fbb4
@ -210,6 +210,9 @@ files can be built by specifying individual filenames.
|
|||||||
dest='exception_on_warning',
|
dest='exception_on_warning',
|
||||||
help=__('raise an exception on warnings'))
|
help=__('raise an exception on warnings'))
|
||||||
|
|
||||||
|
if parser.prog == '__main__.py':
|
||||||
|
parser.prog = 'sphinx-build'
|
||||||
|
|
||||||
return parser
|
return parser
|
||||||
|
|
||||||
|
|
||||||
@ -386,7 +389,8 @@ def main(argv: Sequence[str] = (), /) -> int:
|
|||||||
if argv[:1] == ['--bug-report']:
|
if argv[:1] == ['--bug-report']:
|
||||||
return _bug_report_info()
|
return _bug_report_info()
|
||||||
if argv[:1] == ['-M']:
|
if argv[:1] == ['-M']:
|
||||||
return make_main(argv)
|
from sphinx.cmd import make_mode
|
||||||
|
return make_mode.run_make_mode(argv[1:])
|
||||||
else:
|
else:
|
||||||
return build_main(argv)
|
return build_main(argv)
|
||||||
|
|
||||||
|
@ -58,31 +58,31 @@ BUILDERS = [
|
|||||||
|
|
||||||
|
|
||||||
class Make:
|
class Make:
|
||||||
def __init__(self, srcdir: str, builddir: str, opts: Sequence[str]) -> None:
|
def __init__(self, *, source_dir: str, build_dir: str, opts: Sequence[str]) -> None:
|
||||||
self.srcdir = srcdir
|
self.source_dir = source_dir
|
||||||
self.builddir = builddir
|
self.build_dir = build_dir
|
||||||
self.opts = [*opts]
|
self.opts = [*opts]
|
||||||
|
|
||||||
def builddir_join(self, *comps: str) -> str:
|
def build_dir_join(self, *comps: str) -> str:
|
||||||
return path.join(self.builddir, *comps)
|
return path.join(self.build_dir, *comps)
|
||||||
|
|
||||||
def build_clean(self) -> int:
|
def build_clean(self) -> int:
|
||||||
srcdir = path.abspath(self.srcdir)
|
source_dir = path.abspath(self.source_dir)
|
||||||
builddir = path.abspath(self.builddir)
|
build_dir = path.abspath(self.build_dir)
|
||||||
if not path.exists(self.builddir):
|
if not path.exists(self.build_dir):
|
||||||
return 0
|
return 0
|
||||||
elif not path.isdir(self.builddir):
|
elif not path.isdir(self.build_dir):
|
||||||
print("Error: %r is not a directory!" % self.builddir)
|
print("Error: %r is not a directory!" % self.build_dir)
|
||||||
return 1
|
return 1
|
||||||
elif srcdir == builddir:
|
elif source_dir == build_dir:
|
||||||
print("Error: %r is same as source directory!" % self.builddir)
|
print("Error: %r is same as source directory!" % self.build_dir)
|
||||||
return 1
|
return 1
|
||||||
elif path.commonpath([srcdir, builddir]) == builddir:
|
elif path.commonpath([source_dir, build_dir]) == build_dir:
|
||||||
print("Error: %r directory contains source directory!" % self.builddir)
|
print("Error: %r directory contains source directory!" % self.build_dir)
|
||||||
return 1
|
return 1
|
||||||
print("Removing everything under %r..." % self.builddir)
|
print("Removing everything under %r..." % self.build_dir)
|
||||||
for item in os.listdir(self.builddir):
|
for item in os.listdir(self.build_dir):
|
||||||
rmtree(self.builddir_join(item))
|
rmtree(self.build_dir_join(item))
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
def build_help(self) -> None:
|
def build_help(self) -> None:
|
||||||
@ -105,7 +105,7 @@ class Make:
|
|||||||
if not makecmd.lower().startswith('make'):
|
if not makecmd.lower().startswith('make'):
|
||||||
raise RuntimeError('Invalid $MAKE command: %r' % makecmd)
|
raise RuntimeError('Invalid $MAKE command: %r' % makecmd)
|
||||||
try:
|
try:
|
||||||
with chdir(self.builddir_join('latex')):
|
with chdir(self.build_dir_join('latex')):
|
||||||
if '-Q' in self.opts:
|
if '-Q' in self.opts:
|
||||||
with open('__LATEXSTDOUT__', 'w') as outfile:
|
with open('__LATEXSTDOUT__', 'w') as outfile:
|
||||||
returncode = subprocess.call([makecmd,
|
returncode = subprocess.call([makecmd,
|
||||||
@ -117,7 +117,7 @@ class Make:
|
|||||||
)
|
)
|
||||||
if returncode:
|
if returncode:
|
||||||
print('Latex error: check %s' %
|
print('Latex error: check %s' %
|
||||||
self.builddir_join('latex', '__LATEXSTDOUT__')
|
self.build_dir_join('latex', '__LATEXSTDOUT__')
|
||||||
)
|
)
|
||||||
elif '-q' in self.opts:
|
elif '-q' in self.opts:
|
||||||
returncode = subprocess.call(
|
returncode = subprocess.call(
|
||||||
@ -129,7 +129,7 @@ class Make:
|
|||||||
)
|
)
|
||||||
if returncode:
|
if returncode:
|
||||||
print('Latex error: check .log file in %s' %
|
print('Latex error: check .log file in %s' %
|
||||||
self.builddir_join('latex')
|
self.build_dir_join('latex')
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
returncode = subprocess.call([makecmd, 'all-pdf'])
|
returncode = subprocess.call([makecmd, 'all-pdf'])
|
||||||
@ -148,7 +148,7 @@ class Make:
|
|||||||
if not makecmd.lower().startswith('make'):
|
if not makecmd.lower().startswith('make'):
|
||||||
raise RuntimeError('Invalid $MAKE command: %r' % makecmd)
|
raise RuntimeError('Invalid $MAKE command: %r' % makecmd)
|
||||||
try:
|
try:
|
||||||
with chdir(self.builddir_join('latex')):
|
with chdir(self.build_dir_join('latex')):
|
||||||
return subprocess.call([makecmd, 'all-pdf'])
|
return subprocess.call([makecmd, 'all-pdf'])
|
||||||
except OSError:
|
except OSError:
|
||||||
print('Error: Failed to run: %s' % makecmd)
|
print('Error: Failed to run: %s' % makecmd)
|
||||||
@ -163,32 +163,33 @@ class Make:
|
|||||||
if not makecmd.lower().startswith('make'):
|
if not makecmd.lower().startswith('make'):
|
||||||
raise RuntimeError('Invalid $MAKE command: %r' % makecmd)
|
raise RuntimeError('Invalid $MAKE command: %r' % makecmd)
|
||||||
try:
|
try:
|
||||||
with chdir(self.builddir_join('texinfo')):
|
with chdir(self.build_dir_join('texinfo')):
|
||||||
return subprocess.call([makecmd, 'info'])
|
return subprocess.call([makecmd, 'info'])
|
||||||
except OSError:
|
except OSError:
|
||||||
print('Error: Failed to run: %s' % makecmd)
|
print('Error: Failed to run: %s' % makecmd)
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
def build_gettext(self) -> int:
|
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:
|
if self.run_generic_build('gettext', doctreedir=dtdir) > 0:
|
||||||
return 1
|
return 1
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
def run_generic_build(self, builder: str, doctreedir: str | None = None) -> int:
|
def run_generic_build(self, builder: str, doctreedir: str | None = None) -> int:
|
||||||
# compatibility with old Makefile
|
# compatibility with old Makefile
|
||||||
papersize = os.getenv('PAPER', '')
|
paper_size = os.getenv('PAPER', '')
|
||||||
opts = self.opts
|
if paper_size in {'a4', 'letter'}:
|
||||||
if papersize in ('a4', 'letter'):
|
self.opts.extend(['-D', f'latex_elements.papersize={paper_size}paper'])
|
||||||
opts.extend(['-D', 'latex_elements.papersize=' + papersize + 'paper'])
|
|
||||||
if doctreedir is None:
|
if doctreedir is None:
|
||||||
doctreedir = self.builddir_join('doctrees')
|
doctreedir = self.build_dir_join('doctrees')
|
||||||
|
|
||||||
args = ['-b', builder,
|
args = [
|
||||||
'-d', doctreedir,
|
'--builder', builder,
|
||||||
self.srcdir,
|
'--doctree-dir', doctreedir,
|
||||||
self.builddir_join(builder)]
|
self.source_dir,
|
||||||
return build_main(args + opts)
|
self.build_dir_join(builder),
|
||||||
|
]
|
||||||
|
return build_main(args + self.opts)
|
||||||
|
|
||||||
|
|
||||||
def run_make_mode(args: Sequence[str]) -> int:
|
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 '
|
print('Error: at least 3 arguments (builder, source '
|
||||||
'dir, build dir) are required.', file=sys.stderr)
|
'dir, build dir) are required.', file=sys.stderr)
|
||||||
return 1
|
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):
|
if hasattr(make, run_method):
|
||||||
return getattr(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
217
tests/test_command_line.py
Normal 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')
|
Loading…
Reference in New Issue
Block a user