Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 49f238e

Browse filesBrowse files
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>
1 parent 557b05c commit 49f238e
Copy full SHA for 49f238e

File tree

Expand file treeCollapse file tree

3 files changed

+57
-66
lines changed
Filter options
Expand file treeCollapse file tree

3 files changed

+57
-66
lines changed

‎Lib/test/test_clinic.py

Copy file name to clipboardExpand all lines: Lib/test/test_clinic.py
+33-41Lines changed: 33 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,12 @@
44

55
from test import support, test_tools
66
from test.support import os_helper
7-
from test.support import SHORT_TIMEOUT, requires_subprocess
87
from test.support.os_helper import TESTFN, unlink
98
from textwrap import dedent
109
from unittest import TestCase
1110
import collections
1211
import inspect
1312
import os.path
14-
import subprocess
1513
import sys
1614
import unittest
1715

@@ -1411,30 +1409,26 @@ def test_scaffolding(self):
14111409

14121410
class ClinicExternalTest(TestCase):
14131411
maxDiff = None
1414-
clinic_py = os.path.join(test_tools.toolsdir, "clinic", "clinic.py")
1415-
1416-
def _do_test(self, *args, expect_success=True):
1417-
with subprocess.Popen(
1418-
[sys.executable, "-Xutf8", self.clinic_py, *args],
1419-
encoding="utf-8",
1420-
bufsize=0,
1421-
stdout=subprocess.PIPE,
1422-
stderr=subprocess.PIPE,
1423-
) as proc:
1424-
proc.wait()
1425-
if expect_success and proc.returncode:
1426-
self.fail("".join([*proc.stdout, *proc.stderr]))
1427-
stdout = proc.stdout.read()
1428-
stderr = proc.stderr.read()
1429-
# Clinic never writes to stderr.
1430-
self.assertEqual(stderr, "")
1431-
return stdout
1412+
1413+
def run_clinic(self, *args):
1414+
with (
1415+
support.captured_stdout() as out,
1416+
support.captured_stderr() as err,
1417+
self.assertRaises(SystemExit) as cm
1418+
):
1419+
clinic.main(args)
1420+
return out.getvalue(), err.getvalue(), cm.exception.code
14321421

14331422
def expect_success(self, *args):
1434-
return self._do_test(*args)
1423+
out, err, code = self.run_clinic(*args)
1424+
self.assertEqual(code, 0, f"Unexpected failure: {args=}")
1425+
self.assertEqual(err, "")
1426+
return out
14351427

14361428
def expect_failure(self, *args):
1437-
return self._do_test(*args, expect_success=False)
1429+
out, err, code = self.run_clinic(*args)
1430+
self.assertNotEqual(code, 0, f"Unexpected success: {args=}")
1431+
return out, err
14381432

14391433
def test_external(self):
14401434
CLINIC_TEST = 'clinic.test.c'
@@ -1498,8 +1492,9 @@ def test_cli_force(self):
14981492
# First, run the CLI without -f and expect failure.
14991493
# Note, we cannot check the entire fail msg, because the path to
15001494
# the tmp file will change for every run.
1501-
out = self.expect_failure(fn)
1502-
self.assertTrue(out.endswith(fail_msg))
1495+
out, _ = self.expect_failure(fn)
1496+
self.assertTrue(out.endswith(fail_msg),
1497+
f"{out!r} does not end with {fail_msg!r}")
15031498
# Then, force regeneration; success expected.
15041499
out = self.expect_success("-f", fn)
15051500
self.assertEqual(out, "")
@@ -1641,33 +1636,30 @@ def test_cli_converters(self):
16411636
)
16421637

16431638
def test_cli_fail_converters_and_filename(self):
1644-
out = self.expect_failure("--converters", "test.c")
1645-
msg = (
1646-
"Usage error: can't specify --converters "
1647-
"and a filename at the same time"
1648-
)
1649-
self.assertIn(msg, out)
1639+
_, err = self.expect_failure("--converters", "test.c")
1640+
msg = "can't specify --converters and a filename at the same time"
1641+
self.assertIn(msg, err)
16501642

16511643
def test_cli_fail_no_filename(self):
1652-
out = self.expect_failure()
1653-
self.assertIn("usage: clinic.py", out)
1644+
_, err = self.expect_failure()
1645+
self.assertIn("no input files", err)
16541646

16551647
def test_cli_fail_output_and_multiple_files(self):
1656-
out = self.expect_failure("-o", "out.c", "input.c", "moreinput.c")
1657-
msg = "Usage error: can't use -o with multiple filenames"
1658-
self.assertIn(msg, out)
1648+
_, err = self.expect_failure("-o", "out.c", "input.c", "moreinput.c")
1649+
msg = "error: can't use -o with multiple filenames"
1650+
self.assertIn(msg, err)
16591651

16601652
def test_cli_fail_filename_or_output_and_make(self):
1653+
msg = "can't use -o or filenames with --make"
16611654
for opts in ("-o", "out.c"), ("filename.c",):
16621655
with self.subTest(opts=opts):
1663-
out = self.expect_failure("--make", *opts)
1664-
msg = "Usage error: can't use -o or filenames with --make"
1665-
self.assertIn(msg, out)
1656+
_, err = self.expect_failure("--make", *opts)
1657+
self.assertIn(msg, err)
16661658

16671659
def test_cli_fail_make_without_srcdir(self):
1668-
out = self.expect_failure("--make", "--srcdir", "")
1669-
msg = "Usage error: --srcdir must not be empty with --make"
1670-
self.assertIn(msg, out)
1660+
_, err = self.expect_failure("--make", "--srcdir", "")
1661+
msg = "error: --srcdir must not be empty with --make"
1662+
self.assertIn(msg, err)
16711663

16721664

16731665
try:
+2Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
The Argument Clinic command-line tool now prints to stderr instead of stdout
2+
on failure.

‎Tools/clinic/clinic.py

Copy file name to clipboardExpand all lines: Tools/clinic/clinic.py
+22-25Lines changed: 22 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from __future__ import annotations
88

99
import abc
10+
import argparse
1011
import ast
1112
import builtins as bltns
1213
import collections
@@ -5620,10 +5621,9 @@ def state_terminal(self, line: str | None) -> None:
56205621
clinic = None
56215622

56225623

5623-
def main(argv: list[str]) -> None:
5624-
import sys
5625-
import argparse
5624+
def create_cli() -> argparse.ArgumentParser:
56265625
cmdline = argparse.ArgumentParser(
5626+
prog="clinic.py",
56275627
description="""Preprocessor for CPython C files.
56285628
56295629
The purpose of the Argument Clinic is automating all the boilerplate involved
@@ -5646,14 +5646,15 @@ def main(argv: list[str]) -> None:
56465646
help="the directory tree to walk in --make mode")
56475647
cmdline.add_argument("filename", metavar="FILE", type=str, nargs="*",
56485648
help="the list of files to process")
5649-
ns = cmdline.parse_args(argv)
5649+
return cmdline
56505650

5651+
5652+
def run_clinic(parser: argparse.ArgumentParser, ns: argparse.Namespace) -> None:
56515653
if ns.converters:
56525654
if ns.filename:
5653-
print("Usage error: can't specify --converters and a filename at the same time.")
5654-
print()
5655-
cmdline.print_usage()
5656-
sys.exit(-1)
5655+
parser.error(
5656+
"can't specify --converters and a filename at the same time"
5657+
)
56575658
converters: list[tuple[str, str]] = []
56585659
return_converters: list[tuple[str, str]] = []
56595660
ignored = set("""
@@ -5707,19 +5708,13 @@ def main(argv: list[str]) -> None:
57075708
print()
57085709
print("All converters also accept (c_default=None, py_default=None, annotation=None).")
57095710
print("All return converters also accept (py_default=None).")
5710-
sys.exit(0)
5711+
return
57115712

57125713
if ns.make:
57135714
if ns.output or ns.filename:
5714-
print("Usage error: can't use -o or filenames with --make.")
5715-
print()
5716-
cmdline.print_usage()
5717-
sys.exit(-1)
5715+
parser.error("can't use -o or filenames with --make")
57185716
if not ns.srcdir:
5719-
print("Usage error: --srcdir must not be empty with --make.")
5720-
print()
5721-
cmdline.print_usage()
5722-
sys.exit(-1)
5717+
parser.error("--srcdir must not be empty with --make")
57235718
for root, dirs, files in os.walk(ns.srcdir):
57245719
for rcs_dir in ('.svn', '.git', '.hg', 'build', 'externals'):
57255720
if rcs_dir in dirs:
@@ -5735,21 +5730,23 @@ def main(argv: list[str]) -> None:
57355730
return
57365731

57375732
if not ns.filename:
5738-
cmdline.print_usage()
5739-
sys.exit(-1)
5733+
parser.error("no input files")
57405734

57415735
if ns.output and len(ns.filename) > 1:
5742-
print("Usage error: can't use -o with multiple filenames.")
5743-
print()
5744-
cmdline.print_usage()
5745-
sys.exit(-1)
5736+
parser.error("can't use -o with multiple filenames")
57465737

57475738
for filename in ns.filename:
57485739
if ns.verbose:
57495740
print(filename)
57505741
parse_file(filename, output=ns.output, verify=not ns.force)
57515742

57525743

5753-
if __name__ == "__main__":
5754-
main(sys.argv[1:])
5744+
def main(argv: list[str] | None = None) -> NoReturn:
5745+
parser = create_cli()
5746+
args = parser.parse_args(argv)
5747+
run_clinic(parser, args)
57555748
sys.exit(0)
5749+
5750+
5751+
if __name__ == "__main__":
5752+
main()

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.