mirror of https://github.com/python/cpython
gh-107467: Restructure Argument Clinic command-line interface (#107469)
- Use ArgumentParser.error() to handle CLI errors - Put the entire CLI in main() - Rework ClinicExternalTest to call main() instead of using subprocesses Co-authored-by: AlexWaygood <alex.waygood@gmail.com>
This commit is contained in:
parent
557b05c7a5
commit
49f238e78c
|
@ -4,14 +4,12 @@
|
||||||
|
|
||||||
from test import support, test_tools
|
from test import support, test_tools
|
||||||
from test.support import os_helper
|
from test.support import os_helper
|
||||||
from test.support import SHORT_TIMEOUT, requires_subprocess
|
|
||||||
from test.support.os_helper import TESTFN, unlink
|
from test.support.os_helper import TESTFN, unlink
|
||||||
from textwrap import dedent
|
from textwrap import dedent
|
||||||
from unittest import TestCase
|
from unittest import TestCase
|
||||||
import collections
|
import collections
|
||||||
import inspect
|
import inspect
|
||||||
import os.path
|
import os.path
|
||||||
import subprocess
|
|
||||||
import sys
|
import sys
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
|
@ -1411,30 +1409,26 @@ Couldn't find existing function 'fooooooooooooooooooooooo'!
|
||||||
|
|
||||||
class ClinicExternalTest(TestCase):
|
class ClinicExternalTest(TestCase):
|
||||||
maxDiff = None
|
maxDiff = None
|
||||||
clinic_py = os.path.join(test_tools.toolsdir, "clinic", "clinic.py")
|
|
||||||
|
|
||||||
def _do_test(self, *args, expect_success=True):
|
def run_clinic(self, *args):
|
||||||
with subprocess.Popen(
|
with (
|
||||||
[sys.executable, "-Xutf8", self.clinic_py, *args],
|
support.captured_stdout() as out,
|
||||||
encoding="utf-8",
|
support.captured_stderr() as err,
|
||||||
bufsize=0,
|
self.assertRaises(SystemExit) as cm
|
||||||
stdout=subprocess.PIPE,
|
):
|
||||||
stderr=subprocess.PIPE,
|
clinic.main(args)
|
||||||
) as proc:
|
return out.getvalue(), err.getvalue(), cm.exception.code
|
||||||
proc.wait()
|
|
||||||
if expect_success and proc.returncode:
|
|
||||||
self.fail("".join([*proc.stdout, *proc.stderr]))
|
|
||||||
stdout = proc.stdout.read()
|
|
||||||
stderr = proc.stderr.read()
|
|
||||||
# Clinic never writes to stderr.
|
|
||||||
self.assertEqual(stderr, "")
|
|
||||||
return stdout
|
|
||||||
|
|
||||||
def expect_success(self, *args):
|
def expect_success(self, *args):
|
||||||
return self._do_test(*args)
|
out, err, code = self.run_clinic(*args)
|
||||||
|
self.assertEqual(code, 0, f"Unexpected failure: {args=}")
|
||||||
|
self.assertEqual(err, "")
|
||||||
|
return out
|
||||||
|
|
||||||
def expect_failure(self, *args):
|
def expect_failure(self, *args):
|
||||||
return self._do_test(*args, expect_success=False)
|
out, err, code = self.run_clinic(*args)
|
||||||
|
self.assertNotEqual(code, 0, f"Unexpected success: {args=}")
|
||||||
|
return out, err
|
||||||
|
|
||||||
def test_external(self):
|
def test_external(self):
|
||||||
CLINIC_TEST = 'clinic.test.c'
|
CLINIC_TEST = 'clinic.test.c'
|
||||||
|
@ -1498,8 +1492,9 @@ class ClinicExternalTest(TestCase):
|
||||||
# First, run the CLI without -f and expect failure.
|
# First, run the CLI without -f and expect failure.
|
||||||
# Note, we cannot check the entire fail msg, because the path to
|
# Note, we cannot check the entire fail msg, because the path to
|
||||||
# the tmp file will change for every run.
|
# the tmp file will change for every run.
|
||||||
out = self.expect_failure(fn)
|
out, _ = self.expect_failure(fn)
|
||||||
self.assertTrue(out.endswith(fail_msg))
|
self.assertTrue(out.endswith(fail_msg),
|
||||||
|
f"{out!r} does not end with {fail_msg!r}")
|
||||||
# Then, force regeneration; success expected.
|
# Then, force regeneration; success expected.
|
||||||
out = self.expect_success("-f", fn)
|
out = self.expect_success("-f", fn)
|
||||||
self.assertEqual(out, "")
|
self.assertEqual(out, "")
|
||||||
|
@ -1641,33 +1636,30 @@ class ClinicExternalTest(TestCase):
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_cli_fail_converters_and_filename(self):
|
def test_cli_fail_converters_and_filename(self):
|
||||||
out = self.expect_failure("--converters", "test.c")
|
_, err = self.expect_failure("--converters", "test.c")
|
||||||
msg = (
|
msg = "can't specify --converters and a filename at the same time"
|
||||||
"Usage error: can't specify --converters "
|
self.assertIn(msg, err)
|
||||||
"and a filename at the same time"
|
|
||||||
)
|
|
||||||
self.assertIn(msg, out)
|
|
||||||
|
|
||||||
def test_cli_fail_no_filename(self):
|
def test_cli_fail_no_filename(self):
|
||||||
out = self.expect_failure()
|
_, err = self.expect_failure()
|
||||||
self.assertIn("usage: clinic.py", out)
|
self.assertIn("no input files", err)
|
||||||
|
|
||||||
def test_cli_fail_output_and_multiple_files(self):
|
def test_cli_fail_output_and_multiple_files(self):
|
||||||
out = self.expect_failure("-o", "out.c", "input.c", "moreinput.c")
|
_, err = self.expect_failure("-o", "out.c", "input.c", "moreinput.c")
|
||||||
msg = "Usage error: can't use -o with multiple filenames"
|
msg = "error: can't use -o with multiple filenames"
|
||||||
self.assertIn(msg, out)
|
self.assertIn(msg, err)
|
||||||
|
|
||||||
def test_cli_fail_filename_or_output_and_make(self):
|
def test_cli_fail_filename_or_output_and_make(self):
|
||||||
|
msg = "can't use -o or filenames with --make"
|
||||||
for opts in ("-o", "out.c"), ("filename.c",):
|
for opts in ("-o", "out.c"), ("filename.c",):
|
||||||
with self.subTest(opts=opts):
|
with self.subTest(opts=opts):
|
||||||
out = self.expect_failure("--make", *opts)
|
_, err = self.expect_failure("--make", *opts)
|
||||||
msg = "Usage error: can't use -o or filenames with --make"
|
self.assertIn(msg, err)
|
||||||
self.assertIn(msg, out)
|
|
||||||
|
|
||||||
def test_cli_fail_make_without_srcdir(self):
|
def test_cli_fail_make_without_srcdir(self):
|
||||||
out = self.expect_failure("--make", "--srcdir", "")
|
_, err = self.expect_failure("--make", "--srcdir", "")
|
||||||
msg = "Usage error: --srcdir must not be empty with --make"
|
msg = "error: --srcdir must not be empty with --make"
|
||||||
self.assertIn(msg, out)
|
self.assertIn(msg, err)
|
||||||
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
The Argument Clinic command-line tool now prints to stderr instead of stdout
|
||||||
|
on failure.
|
|
@ -7,6 +7,7 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import abc
|
import abc
|
||||||
|
import argparse
|
||||||
import ast
|
import ast
|
||||||
import builtins as bltns
|
import builtins as bltns
|
||||||
import collections
|
import collections
|
||||||
|
@ -5620,10 +5621,9 @@ parsers: dict[str, Callable[[Clinic], Parser]] = {
|
||||||
clinic = None
|
clinic = None
|
||||||
|
|
||||||
|
|
||||||
def main(argv: list[str]) -> None:
|
def create_cli() -> argparse.ArgumentParser:
|
||||||
import sys
|
|
||||||
import argparse
|
|
||||||
cmdline = argparse.ArgumentParser(
|
cmdline = argparse.ArgumentParser(
|
||||||
|
prog="clinic.py",
|
||||||
description="""Preprocessor for CPython C files.
|
description="""Preprocessor for CPython C files.
|
||||||
|
|
||||||
The purpose of the Argument Clinic is automating all the boilerplate involved
|
The purpose of the Argument Clinic is automating all the boilerplate involved
|
||||||
|
@ -5646,14 +5646,15 @@ For more information see https://docs.python.org/3/howto/clinic.html""")
|
||||||
help="the directory tree to walk in --make mode")
|
help="the directory tree to walk in --make mode")
|
||||||
cmdline.add_argument("filename", metavar="FILE", type=str, nargs="*",
|
cmdline.add_argument("filename", metavar="FILE", type=str, nargs="*",
|
||||||
help="the list of files to process")
|
help="the list of files to process")
|
||||||
ns = cmdline.parse_args(argv)
|
return cmdline
|
||||||
|
|
||||||
|
|
||||||
|
def run_clinic(parser: argparse.ArgumentParser, ns: argparse.Namespace) -> None:
|
||||||
if ns.converters:
|
if ns.converters:
|
||||||
if ns.filename:
|
if ns.filename:
|
||||||
print("Usage error: can't specify --converters and a filename at the same time.")
|
parser.error(
|
||||||
print()
|
"can't specify --converters and a filename at the same time"
|
||||||
cmdline.print_usage()
|
)
|
||||||
sys.exit(-1)
|
|
||||||
converters: list[tuple[str, str]] = []
|
converters: list[tuple[str, str]] = []
|
||||||
return_converters: list[tuple[str, str]] = []
|
return_converters: list[tuple[str, str]] = []
|
||||||
ignored = set("""
|
ignored = set("""
|
||||||
|
@ -5707,19 +5708,13 @@ For more information see https://docs.python.org/3/howto/clinic.html""")
|
||||||
print()
|
print()
|
||||||
print("All converters also accept (c_default=None, py_default=None, annotation=None).")
|
print("All converters also accept (c_default=None, py_default=None, annotation=None).")
|
||||||
print("All return converters also accept (py_default=None).")
|
print("All return converters also accept (py_default=None).")
|
||||||
sys.exit(0)
|
return
|
||||||
|
|
||||||
if ns.make:
|
if ns.make:
|
||||||
if ns.output or ns.filename:
|
if ns.output or ns.filename:
|
||||||
print("Usage error: can't use -o or filenames with --make.")
|
parser.error("can't use -o or filenames with --make")
|
||||||
print()
|
|
||||||
cmdline.print_usage()
|
|
||||||
sys.exit(-1)
|
|
||||||
if not ns.srcdir:
|
if not ns.srcdir:
|
||||||
print("Usage error: --srcdir must not be empty with --make.")
|
parser.error("--srcdir must not be empty with --make")
|
||||||
print()
|
|
||||||
cmdline.print_usage()
|
|
||||||
sys.exit(-1)
|
|
||||||
for root, dirs, files in os.walk(ns.srcdir):
|
for root, dirs, files in os.walk(ns.srcdir):
|
||||||
for rcs_dir in ('.svn', '.git', '.hg', 'build', 'externals'):
|
for rcs_dir in ('.svn', '.git', '.hg', 'build', 'externals'):
|
||||||
if rcs_dir in dirs:
|
if rcs_dir in dirs:
|
||||||
|
@ -5735,14 +5730,10 @@ For more information see https://docs.python.org/3/howto/clinic.html""")
|
||||||
return
|
return
|
||||||
|
|
||||||
if not ns.filename:
|
if not ns.filename:
|
||||||
cmdline.print_usage()
|
parser.error("no input files")
|
||||||
sys.exit(-1)
|
|
||||||
|
|
||||||
if ns.output and len(ns.filename) > 1:
|
if ns.output and len(ns.filename) > 1:
|
||||||
print("Usage error: can't use -o with multiple filenames.")
|
parser.error("can't use -o with multiple filenames")
|
||||||
print()
|
|
||||||
cmdline.print_usage()
|
|
||||||
sys.exit(-1)
|
|
||||||
|
|
||||||
for filename in ns.filename:
|
for filename in ns.filename:
|
||||||
if ns.verbose:
|
if ns.verbose:
|
||||||
|
@ -5750,6 +5741,12 @@ For more information see https://docs.python.org/3/howto/clinic.html""")
|
||||||
parse_file(filename, output=ns.output, verify=not ns.force)
|
parse_file(filename, output=ns.output, verify=not ns.force)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
def main(argv: list[str] | None = None) -> NoReturn:
|
||||||
main(sys.argv[1:])
|
parser = create_cli()
|
||||||
|
args = parser.parse_args(argv)
|
||||||
|
run_clinic(parser, args)
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
|
Loading…
Reference in New Issue