Skip to content

Commit 2f2c2ce

Browse files
gh-155263: Add the --list option to Argument Clinic (GH-155264)
It prints the modules, classes and functions which Argument Clinic defines in the specified files, each function with its signature.
1 parent 9b05395 commit 2f2c2ce

5 files changed

Lines changed: 430 additions & 186 deletions

File tree

Lib/test/test_clinic.py

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3681,6 +3681,163 @@ def test_cli_converters_no_converters(self):
36813681
f.write("/*[clinic input]\n[clinic start generated code]*/\n")
36823682
self.assertEqual(self.expect_success("--converters", fn), "")
36833683

3684+
LIST_CODE = dedent("""
3685+
/*[clinic input]
3686+
func
3687+
a: int
3688+
/
3689+
3690+
Docstring.
3691+
[clinic start generated code]*/
3692+
3693+
/*[clinic input]
3694+
cloned = func
3695+
[clinic start generated code]*/
3696+
3697+
/*[clinic input]
3698+
module m
3699+
class m.C "void *" ""
3700+
class m.C.D "void *" ""
3701+
[clinic start generated code]*/
3702+
3703+
/*[clinic input]
3704+
m.C.meth
3705+
self: self(type="void *")
3706+
a: object
3707+
[
3708+
b: object
3709+
]
3710+
/
3711+
3712+
Docstring.
3713+
[clinic start generated code]*/
3714+
3715+
/*[clinic input]
3716+
@classmethod
3717+
m.C.__new__
3718+
a: object
3719+
3720+
Docstring.
3721+
[clinic start generated code]*/
3722+
3723+
/*[clinic input]
3724+
@getter
3725+
m.C.prop
3726+
[clinic start generated code]*/
3727+
3728+
/*[clinic input]
3729+
@setter
3730+
m.C.prop
3731+
[clinic start generated code]*/
3732+
3733+
/*[clinic input]
3734+
m.C.D.meth
3735+
self: self(type="void *")
3736+
3737+
Docstring.
3738+
[clinic start generated code]*/
3739+
""")
3740+
3741+
def make_list_file(self, tmp_dir):
3742+
fn = os.path.join(tmp_dir, "test.c")
3743+
with open(fn, "w", encoding="utf-8") as f:
3744+
f.write(self.LIST_CODE)
3745+
return fn
3746+
3747+
LIST_OUTPUT = [
3748+
" func($module, a, /)",
3749+
" cloned($module, a, /)",
3750+
" module m",
3751+
" class m.C",
3752+
# A signature with an option group is only for the docstring.
3753+
" m.C.meth(a, [b])",
3754+
" m.C(a)",
3755+
" getter m.C.prop",
3756+
" setter m.C.prop",
3757+
" class m.C.D",
3758+
" m.C.D.meth($self, /)",
3759+
]
3760+
3761+
def test_cli_list(self):
3762+
with os_helper.temp_dir() as tmp_dir:
3763+
fn = self.make_list_file(tmp_dir)
3764+
pre_mtime = os.stat(fn).st_mtime_ns
3765+
out = self.expect_success("--list", fn)
3766+
self.assertEqual(out.splitlines(), [fn] + self.LIST_OUTPUT)
3767+
# Nothing is written.
3768+
with open(fn, encoding="utf-8") as f:
3769+
self.assertEqual(f.read(), self.LIST_CODE)
3770+
self.assertEqual(os.stat(fn).st_mtime_ns, pre_mtime)
3771+
self.assertEqual(os.listdir(tmp_dir), ["test.c"])
3772+
3773+
def test_cli_list_no_clinic_block(self):
3774+
with os_helper.temp_dir() as tmp_dir:
3775+
fn = os.path.join(tmp_dir, "test.c")
3776+
with open(fn, "w", encoding="utf-8") as f:
3777+
f.write("int x;\n")
3778+
self.assertEqual(self.expect_success("--list", fn), "")
3779+
3780+
def test_cli_list_no_definitions(self):
3781+
with os_helper.temp_dir() as tmp_dir:
3782+
fn = os.path.join(tmp_dir, "test.c")
3783+
with open(fn, "w", encoding="utf-8") as f:
3784+
f.write("/*[clinic input]\n[clinic start generated code]*/\n")
3785+
self.assertEqual(self.expect_success("--list", fn), "")
3786+
3787+
def test_cli_list_make(self):
3788+
with os_helper.temp_dir() as tmp_dir:
3789+
fn = self.make_list_file(tmp_dir)
3790+
out = self.expect_success("--list", "--make", "--srcdir", tmp_dir)
3791+
self.assertEqual(out.splitlines(), [fn] + self.LIST_OUTPUT)
3792+
self.assertEqual(os.listdir(tmp_dir), ["test.c"])
3793+
3794+
def test_cli_list_verbose(self):
3795+
with os_helper.temp_dir() as tmp_dir:
3796+
fn = self.make_list_file(tmp_dir)
3797+
# The progress does not mix with the report.
3798+
out, err, code = self.run_clinic("-v", "--list", fn)
3799+
self.assertEqual(code, 0)
3800+
self.assertEqual(err.splitlines(), [fn])
3801+
self.assertEqual(out.splitlines(), [fn] + self.LIST_OUTPUT)
3802+
3803+
def test_cli_list_checksum_mismatch(self):
3804+
with os_helper.temp_dir() as tmp_dir:
3805+
fn = self.make_list_file(tmp_dir)
3806+
with open(fn, "a", encoding="utf-8") as f:
3807+
f.write("/*[clinic end generated code: "
3808+
"output=0123456789abcdef input=fedcba9876543210]*/\n")
3809+
_, err = self.expect_failure("--list", fn)
3810+
self.assertIn("Checksum mismatch!", err)
3811+
# The check is skipped with --force.
3812+
out = self.expect_success("-f", "--list", fn)
3813+
self.assertEqual(out.splitlines(), [fn] + self.LIST_OUTPUT)
3814+
self.assertEqual(os.listdir(tmp_dir), ["test.c"])
3815+
3816+
def test_cli_list_external(self):
3817+
# A file which uses getters, setters and nested classes.
3818+
source = support.findfile('clinic.test.c')
3819+
out = self.expect_success("--list", source)
3820+
lines = out.splitlines()
3821+
self.assertEqual(lines[0], source)
3822+
for line in (" class Test",
3823+
" getter Test.property",
3824+
" setter Test.property",
3825+
" Test.class_method($type, /)",
3826+
" module m",
3827+
" class m.T"):
3828+
with self.subTest(line=line):
3829+
self.assertIn(line, lines)
3830+
3831+
def test_cli_fail_list_and_dry_run(self):
3832+
for opt in "--dry-run", "--diff":
3833+
with self.subTest(opt=opt):
3834+
_, err = self.expect_failure("--list", opt, "test.c")
3835+
self.assertIn("can't use --dry-run or --diff with --list", err)
3836+
3837+
def test_cli_fail_list_and_converters(self):
3838+
_, err = self.expect_failure("--list", "--converters", "test.c")
3839+
self.assertIn("can't use --converters with --list", err)
3840+
36843841
def test_cli_fail_directory(self):
36853842
with os_helper.temp_dir() as tmp_dir:
36863843
subdir = os.path.join(tmp_dir, "test.c")
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Add the ``--list`` option to Argument Clinic.
2+
It prints the modules, classes and functions which Argument Clinic defines in
3+
the specified files, each function with its signature.

Tools/clinic/libclinic/cli.py

Lines changed: 48 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@
2222
return_converters, ReturnConverterType)
2323
from libclinic.clanguage import CLanguage
2424
from libclinic.app import Clinic
25+
from libclinic.dsl_parser import render_text_signature
26+
from libclinic.function import (
27+
Class, Definition, Module, GETTER, SETTER, walk_definitions)
2528

2629

2730
# TODO:
@@ -54,7 +57,7 @@ def parse_file(
5457
output: str | None = None,
5558
verify: bool = True,
5659
writer: libclinic.FileWriter | None = None,
57-
) -> None:
60+
) -> Clinic | None:
5861
if not output:
5962
output = filename
6063
if writer is None:
@@ -78,7 +81,7 @@ def parse_file(
7881
# exit quickly if there are no clinic markers in the file
7982
find_start_re = BlockParser("", language).find_start_re
8083
if not find_start_re.search(raw):
81-
return
84+
return None
8285

8386
if LIMITED_CAPI_REGEX.search(raw):
8487
limited_capi = True
@@ -97,6 +100,31 @@ def parse_file(
97100
writer.update_times(output,
98101
[fn for fn, _ in files if fn != output],
99102
any(changed for _, changed in files))
103+
return clinic
104+
105+
106+
def format_definition(depth: int, name: str, definition: Definition) -> str:
107+
indent = " " * (depth + 1)
108+
if isinstance(definition, Module):
109+
return f"{indent}module {name}"
110+
if isinstance(definition, Class):
111+
return f"{indent}class {name}"
112+
if definition.kind is GETTER:
113+
return f"{indent}getter {name}"
114+
if definition.kind is SETTER:
115+
return f"{indent}setter {name}"
116+
signature = render_text_signature(definition, definition.render_parameters,
117+
name=name, line_width=None)
118+
return indent + signature
119+
120+
121+
def print_definitions(clinic: Clinic) -> None:
122+
"""Print the modules, classes and functions defined in the parsed file."""
123+
lines = [format_definition(depth, name, definition)
124+
for depth, name, definition in walk_definitions(clinic)]
125+
if lines:
126+
print(clinic.filename)
127+
print("\n".join(lines))
100128

101129

102130
def create_cli() -> argparse.ArgumentParser:
@@ -126,6 +154,10 @@ def create_cli() -> argparse.ArgumentParser:
126154
"and return converters; if files are "
127155
"specified, print only the converters "
128156
"which they define"))
157+
cmdline.add_argument("--list", action='store_true',
158+
help=("don't write any file, only list the modules, "
159+
"classes and functions which the specified "
160+
"files define, with their signatures"))
129161
cmdline.add_argument("--make", action='store_true',
130162
help="walk --srcdir to run over all relevant files")
131163
cmdline.add_argument("--srcdir", type=str, default=os.curdir,
@@ -252,7 +284,7 @@ def run_clinic(parser: argparse.ArgumentParser, ns: argparse.Namespace) -> None:
252284
dry_run = ns.dry_run or ns.diff
253285
# The report is written to the standard output, so the progress
254286
# is written to the standard error stream to not mix them.
255-
verbose_file = sys.stderr if dry_run else sys.stdout
287+
verbose_file = sys.stderr if dry_run or ns.list else sys.stdout
256288

257289
filenames: Iterable[str]
258290
if ns.make:
@@ -268,6 +300,12 @@ def run_clinic(parser: argparse.ArgumentParser, ns: argparse.Namespace) -> None:
268300
parser.error("can't use -o with multiple filenames")
269301
filenames = ns.filename
270302

303+
if ns.list:
304+
if dry_run:
305+
parser.error("can't use --dry-run or --diff with --list")
306+
if ns.converters:
307+
parser.error("can't use --converters with --list")
308+
271309
if ns.converters:
272310
if dry_run:
273311
parser.error("can't use --dry-run or --diff with --converters")
@@ -280,20 +318,22 @@ def run_clinic(parser: argparse.ArgumentParser, ns: argparse.Namespace) -> None:
280318
builtin_legacy_converters = dict(legacy_converters)
281319
builtin_return_converters = dict(return_converters)
282320

283-
writer = libclinic.FileWriter(dry_run=dry_run or ns.converters)
321+
writer = libclinic.FileWriter(dry_run=dry_run or ns.converters or ns.list)
284322
for filename in filenames:
285323
if ns.verbose:
286324
print(filename, file=verbose_file)
287-
parse_file(filename, output=ns.output,
288-
verify=not ns.force, limited_capi=ns.limited_capi,
289-
writer=writer)
325+
clinic = parse_file(filename, output=ns.output,
326+
verify=not ns.force, limited_capi=ns.limited_capi,
327+
writer=writer)
328+
if ns.list and clinic is not None:
329+
print_definitions(clinic)
290330

291331
if ns.converters:
292332
print_converters(
293333
defined_in_files(converters, builtin_converters),
294334
defined_in_files(legacy_converters, builtin_legacy_converters),
295335
defined_in_files(return_converters, builtin_return_converters))
296-
else:
336+
elif not ns.list:
297337
report_changes(writer, diff=ns.diff)
298338

299339

0 commit comments

Comments
 (0)