Skip to content
Next Next commit
Reorganise the Tools/clinic directory
  • Loading branch information
AlexWaygood authored and erlend-aasland committed Feb 15, 2024
commit f064435c2ea7d3ae2b70c74b0ea097c98a2e3deb
7 changes: 4 additions & 3 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
6 changes: 3 additions & 3 deletions Makefile.pre.in
Original file line number Diff line number Diff line change
Expand Up @@ -817,11 +817,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 @@ -849,7 +849,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
2 changes: 1 addition & 1 deletion 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 Down
157 changes: 157 additions & 0 deletions Tools/clinic/libclinic/cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
"""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


def create_cli() -> argparse.ArgumentParser:
cmdline = argparse.ArgumentParser(
prog="Argument Clinic",
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 clinic.ClinicError as exc:
sys.stderr.write(exc.report())
sys.exit(1)
else:
sys.exit(0)
155 changes: 0 additions & 155 deletions Tools/clinic/clinic.py → Tools/clinic/libclinic/clinic.py
100755 → 100644
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
from __future__ import annotations

import abc
import argparse
import ast
import builtins as bltns
import collections
Expand All @@ -25,7 +24,6 @@
import shlex
import sys
import textwrap

from collections.abc import (
Callable,
Iterable,
Expand All @@ -47,8 +45,6 @@
overload,
)


# Local imports.
import libclinic
import libclinic.cpp
from libclinic import ClinicError
Expand Down Expand Up @@ -6145,154 +6141,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()
4 changes: 4 additions & 0 deletions Tools/clinic/run_clinic.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from libclinic.cli import main

if __name__ == "__main__":
main()