Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

gh-113299: Move Argument Clinic CLI into libclinic #115542

Closed
wants to merge 12 commits into from
9 changes: 5 additions & 4 deletions Lib/test/test_clinic.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,9 @@
test_tools.skip_if_missing('clinic')
with test_tools.imports_under_tool('clinic'):
import libclinic
import clinic
from clinic import DSLParser
import libclinic.cli
from libclinic import clinic
from libclinic.clinic import DSLParser


def _make_clinic(*, filename='clinic_tests'):
Expand Down Expand Up @@ -2429,7 +2430,7 @@ def run_clinic(self, *args):
support.captured_stderr() as err,
self.assertRaises(SystemExit) as cm
):
clinic.main(args)
libclinic.cli.main(args)
return out.getvalue(), err.getvalue(), cm.exception.code

def expect_success(self, *args):
Expand Down Expand Up @@ -2602,7 +2603,7 @@ def test_cli_verbose(self):

def test_cli_help(self):
out = self.expect_success("-h")
self.assertIn("usage: clinic.py", out)
self.assertIn("Preprocessor for CPython C files.", out)

def test_cli_converters(self):
prelude = dedent("""
Expand Down
6 changes: 3 additions & 3 deletions Makefile.pre.in
Original file line number Diff line number Diff line change
Expand Up @@ -818,11 +818,11 @@ coverage-report: regen-token regen-frozen
# Run "Argument Clinic" over all source files
.PHONY: clinic
clinic: check-clean-src $(srcdir)/Modules/_blake2/blake2s_impl.c
$(PYTHON_FOR_REGEN) $(srcdir)/Tools/clinic/clinic.py --make --exclude Lib/test/clinic.test.c --srcdir $(srcdir)
$(PYTHON_FOR_REGEN) $(srcdir)/Tools/clinic/run_clinic.py --make --exclude Lib/test/clinic.test.c --srcdir $(srcdir)

.PHONY: clinic-tests
clinic-tests: check-clean-src $(srcdir)/Lib/test/clinic.test.c
$(PYTHON_FOR_REGEN) $(srcdir)/Tools/clinic/clinic.py -f $(srcdir)/Lib/test/clinic.test.c
$(PYTHON_FOR_REGEN) $(srcdir)/Tools/clinic/run_clinic.py -f $(srcdir)/Lib/test/clinic.test.c

# Build the interpreter
$(BUILDPYTHON): Programs/python.o $(LINK_PYTHON_DEPS)
Expand Down Expand Up @@ -850,7 +850,7 @@ pybuilddir.txt: $(PYTHON_FOR_BUILD_DEPS)
# blake2s is auto-generated from blake2b
$(srcdir)/Modules/_blake2/blake2s_impl.c: $(srcdir)/Modules/_blake2/blake2b_impl.c $(srcdir)/Modules/_blake2/blake2b2s.py
$(PYTHON_FOR_REGEN) $(srcdir)/Modules/_blake2/blake2b2s.py
$(PYTHON_FOR_REGEN) $(srcdir)/Tools/clinic/clinic.py -f $@
$(PYTHON_FOR_REGEN) $(srcdir)/Tools/clinic/run_clinic.py -f $@

# Build static library
$(LIBRARY): $(LIBRARY_OBJS)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
The Argument Clinic CLI is now invoked as either :program:`python3
Tools/clinic` or :program:`python3 Tools/clinic/run_clinic.py`, as
:file:`Tools/clinic/clinic.py` has been refactored into ``libclinic``.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The NEWS entry is outdated, there is no __main__.py script.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ohh, thanks; the NEWS entry was not part of the commit I reverted. Good catch.

Comment on lines +1 to +3
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
The Argument Clinic CLI is now invoked as either :program:`python3
Tools/clinic` or :program:`python3 Tools/clinic/run_clinic.py`, as
:file:`Tools/clinic/clinic.py` has been refactored into ``libclinic``.
The Argument Clinic CLI is now invoked as
:program:`python3 Tools/clinic/run_clinic.py`,
since :file:`Tools/clinic/clinic.py` has been refactored into ``libclinic``.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, I like this NEWS entry.

4 changes: 2 additions & 2 deletions PC/winreg.c
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,7 @@ class HKEY_return_converter(CReturnConverter):
'return_value = PyHKEY_FromHKEY(_PyModule_GetState(module), _return_value);\n')

# HACK: this only works for PyHKEYObjects, nothing else.
# Should this be generalized and enshrined in clinic.py,
# Should this be generalized and enshrined in Argument Clinic,
# destroy this converter with prejudice.
class self_return_converter(CReturnConverter):
type = 'PyHKEYObject *'
Expand All @@ -252,7 +252,7 @@ class self_return_converter(CReturnConverter):
data.return_conversion.append(
'return_value = (PyObject *)_return_value;\n')
[python start generated code]*/
/*[python end generated code: output=da39a3ee5e6b4b0d input=4979f33998ffb6f8]*/
/*[python end generated code: output=da39a3ee5e6b4b0d input=9c01a5ec9b2e88a1]*/

#include "clinic/winreg.c.h"

Expand Down
188 changes: 188 additions & 0 deletions Tools/clinic/libclinic/cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
"""Parse arguments passed to the code generator for parsing arguments passed."""

import argparse
import inspect
import os
import sys
from typing import NoReturn

from libclinic import clinic, ClinicError

__all__ = ["main"]


def create_cli() -> argparse.ArgumentParser:
cmdline = argparse.ArgumentParser(
description="""Preprocessor for CPython C files.

The purpose of the Argument Clinic is automating all the boilerplate involved
with writing argument parsing code for builtins and providing introspection
signatures ("docstrings") for CPython builtins.

For more information see https://devguide.python.org/development-tools/clinic/""",
)
cmdline.add_argument(
"-f", "--force", action="store_true", help="force output regeneration"
)
cmdline.add_argument(
"-o", "--output", type=str, help="redirect file output to OUTPUT"
)
cmdline.add_argument(
"-v", "--verbose", action="store_true", help="enable verbose mode"
)
cmdline.add_argument(
"--converters",
action="store_true",
help=("print a list of all supported converters " "and return converters"),
)
cmdline.add_argument(
"--make",
action="store_true",
help="walk --srcdir to run over all relevant files",
)
cmdline.add_argument(
"--srcdir",
type=str,
default=os.curdir,
help="the directory tree to walk in --make mode",
)
cmdline.add_argument(
"--exclude",
type=str,
action="append",
help=("a file to exclude in --make mode; " "can be given multiple times"),
)
cmdline.add_argument(
"--limited",
dest="limited_capi",
action="store_true",
help="use the Limited C API",
)
cmdline.add_argument(
"filename",
metavar="FILE",
type=str,
nargs="*",
help="the list of files to process",
)
return cmdline


def run_clinic(parser: argparse.ArgumentParser, ns: argparse.Namespace) -> None:
if ns.converters:
if ns.filename:
parser.error("can't specify --converters and a filename at the same time")
converters: list[tuple[str, str]] = []
return_converters: list[tuple[str, str]] = []
ignored = set(
"""
add_c_converter
add_c_return_converter
add_default_legacy_c_converter
add_legacy_c_converter
""".strip().split()
)

module = vars(clinic)
for name in module:
for suffix, ids in (
("_return_converter", return_converters),
("_converter", converters),
):
if name in ignored:
continue
if name.endswith(suffix):
ids.append((name, name.removesuffix(suffix)))
break
print()

print("Legacy converters:")
legacy = sorted(clinic.legacy_converters)
print(" " + " ".join(c for c in legacy if c[0].isupper()))
print(" " + " ".join(c for c in legacy if c[0].islower()))
print()

for title, attribute, ids in (
("Converters", "converter_init", converters),
("Return converters", "return_converter_init", return_converters),
):
print(title + ":")
longest = -1
for name, short_name in ids:
longest = max(longest, len(short_name))
for name, short_name in sorted(ids, key=lambda x: x[1].lower()):
cls = module[name]
callable = getattr(cls, attribute, None)
if not callable:
continue
signature = inspect.signature(callable)
parameters = []
for parameter_name, parameter in signature.parameters.items():
if parameter.kind == inspect.Parameter.KEYWORD_ONLY:
if parameter.default != inspect.Parameter.empty:
s = f"{parameter_name}={parameter.default!r}"
else:
s = parameter_name
parameters.append(s)
print(" {}({})".format(short_name, ", ".join(parameters)))
print()
print(
"All converters also accept (c_default=None, py_default=None, annotation=None)."
)
print("All return converters also accept (py_default=None).")
return

if ns.make:
if ns.output or ns.filename:
parser.error("can't use -o or filenames with --make")
if not ns.srcdir:
parser.error("--srcdir must not be empty with --make")
if ns.exclude:
excludes = [os.path.join(ns.srcdir, f) for f in ns.exclude]
excludes = [os.path.normpath(f) for f in excludes]
else:
excludes = []
for root, dirs, files in os.walk(ns.srcdir):
for rcs_dir in (".svn", ".git", ".hg", "build", "externals"):
if rcs_dir in dirs:
dirs.remove(rcs_dir)
for filename in files:
# handle .c, .cpp and .h files
if not filename.endswith((".c", ".cpp", ".h")):
continue
path = os.path.join(root, filename)
path = os.path.normpath(path)
if path in excludes:
continue
if ns.verbose:
print(path)
clinic.parse_file(path, verify=not ns.force, limited_capi=ns.limited_capi)
return

if not ns.filename:
parser.error("no input files")

if ns.output and len(ns.filename) > 1:
parser.error("can't use -o with multiple filenames")

for filename in ns.filename:
if ns.verbose:
print(filename)
clinic.parse_file(
filename,
output=ns.output,
verify=not ns.force,
limited_capi=ns.limited_capi,
)


def main(argv: list[str] | None = None) -> NoReturn:
parser = create_cli()
args = parser.parse_args(argv)
try:
run_clinic(parser, args)
except ClinicError as exc:
sys.stderr.write(exc.report())
sys.exit(1)
else:
sys.exit(0)
Loading
Loading