diff --git a/Lib/test/test_clinic.py b/Lib/test/test_clinic.py index f5e9b11ad1cc8a..5117f5d90801a1 100644 --- a/Lib/test/test_clinic.py +++ b/Lib/test/test_clinic.py @@ -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'): @@ -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): @@ -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(""" diff --git a/Makefile.pre.in b/Makefile.pre.in index 66c4266b2f8f97..93bdeaea0ae36a 100644 --- a/Makefile.pre.in +++ b/Makefile.pre.in @@ -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) @@ -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) diff --git a/Misc/NEWS.d/next/Tools-Demos/2024-02-16-00-33-14.gh-issue-113299.lqPMAV.rst b/Misc/NEWS.d/next/Tools-Demos/2024-02-16-00-33-14.gh-issue-113299.lqPMAV.rst new file mode 100644 index 00000000000000..a56a692cfa56dd --- /dev/null +++ b/Misc/NEWS.d/next/Tools-Demos/2024-02-16-00-33-14.gh-issue-113299.lqPMAV.rst @@ -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``. diff --git a/PC/winreg.c b/PC/winreg.c index 77b80217ac0ab1..07db0516b05c15 100644 --- a/PC/winreg.c +++ b/PC/winreg.c @@ -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 *' @@ -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" diff --git a/Tools/clinic/libclinic/cli.py b/Tools/clinic/libclinic/cli.py new file mode 100644 index 00000000000000..8ab6e25011bc2e --- /dev/null +++ b/Tools/clinic/libclinic/cli.py @@ -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) diff --git a/Tools/clinic/clinic.py b/Tools/clinic/libclinic/clinic.py old mode 100755 new mode 100644 similarity index 97% rename from Tools/clinic/clinic.py rename to Tools/clinic/libclinic/clinic.py index 5d2617b3bd579f..2559a0dad63353 --- a/Tools/clinic/clinic.py +++ b/Tools/clinic/libclinic/clinic.py @@ -7,7 +7,6 @@ from __future__ import annotations import abc -import argparse import ast import builtins as bltns import collections @@ -25,7 +24,6 @@ import shlex import sys import textwrap - from collections.abc import ( Callable, Iterable, @@ -47,8 +45,6 @@ overload, ) - -# Local imports. import libclinic import libclinic.cpp from libclinic import ClinicError @@ -6120,154 +6116,3 @@ def do_post_block_processing_cleanup(self, lineno: int) -> None: 'clinic': DSLParser, 'python': PythonParser, } - - -def create_cli() -> argparse.ArgumentParser: - cmdline = argparse.ArgumentParser( - prog="clinic.py", - 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 = globals() - 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(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) - 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) - 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) - - -if __name__ == "__main__": - main() diff --git a/Tools/clinic/run_clinic.py b/Tools/clinic/run_clinic.py new file mode 100644 index 00000000000000..c786ebad31df67 --- /dev/null +++ b/Tools/clinic/run_clinic.py @@ -0,0 +1,4 @@ +from libclinic.cli import main + +if __name__ == "__main__": + main()