mirror of https://github.com/python/cpython
232 lines
8.1 KiB
Python
232 lines
8.1 KiB
Python
from __future__ import annotations
|
|
|
|
import argparse
|
|
import inspect
|
|
import os
|
|
import re
|
|
import sys
|
|
from collections.abc import Callable
|
|
from typing import NoReturn
|
|
|
|
|
|
# Local imports.
|
|
import libclinic
|
|
import libclinic.cpp
|
|
from libclinic import ClinicError
|
|
from libclinic.language import Language, PythonLanguage
|
|
from libclinic.block_parser import BlockParser
|
|
from libclinic.converter import (
|
|
ConverterType, converters, legacy_converters)
|
|
from libclinic.return_converters import (
|
|
return_converters, ReturnConverterType)
|
|
from libclinic.clanguage import CLanguage
|
|
from libclinic.app import Clinic
|
|
|
|
|
|
# TODO:
|
|
#
|
|
# soon:
|
|
#
|
|
# * allow mixing any two of {positional-only, positional-or-keyword,
|
|
# keyword-only}
|
|
# * dict constructor uses positional-only and keyword-only
|
|
# * max and min use positional only with an optional group
|
|
# and keyword-only
|
|
#
|
|
|
|
|
|
# Match '#define Py_LIMITED_API'.
|
|
# Match '# define Py_LIMITED_API 0x030d0000' (without the version).
|
|
LIMITED_CAPI_REGEX = re.compile(r'# *define +Py_LIMITED_API')
|
|
|
|
|
|
# "extensions" maps the file extension ("c", "py") to Language classes.
|
|
LangDict = dict[str, Callable[[str], Language]]
|
|
extensions: LangDict = { name: CLanguage for name in "c cc cpp cxx h hh hpp hxx".split() }
|
|
extensions['py'] = PythonLanguage
|
|
|
|
|
|
def parse_file(
|
|
filename: str,
|
|
*,
|
|
limited_capi: bool,
|
|
output: str | None = None,
|
|
verify: bool = True,
|
|
) -> None:
|
|
if not output:
|
|
output = filename
|
|
|
|
extension = os.path.splitext(filename)[1][1:]
|
|
if not extension:
|
|
raise ClinicError(f"Can't extract file type for file {filename!r}")
|
|
|
|
try:
|
|
language = extensions[extension](filename)
|
|
except KeyError:
|
|
raise ClinicError(f"Can't identify file type for file {filename!r}")
|
|
|
|
with open(filename, encoding="utf-8") as f:
|
|
raw = f.read()
|
|
|
|
# exit quickly if there are no clinic markers in the file
|
|
find_start_re = BlockParser("", language).find_start_re
|
|
if not find_start_re.search(raw):
|
|
return
|
|
|
|
if LIMITED_CAPI_REGEX.search(raw):
|
|
limited_capi = True
|
|
|
|
assert isinstance(language, CLanguage)
|
|
clinic = Clinic(language,
|
|
verify=verify,
|
|
filename=filename,
|
|
limited_capi=limited_capi)
|
|
cooked = clinic.parse(raw)
|
|
|
|
libclinic.write_file(output, cooked)
|
|
|
|
|
|
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"
|
|
)
|
|
AnyConverterType = ConverterType | ReturnConverterType
|
|
converter_list: list[tuple[str, AnyConverterType]] = []
|
|
return_converter_list: list[tuple[str, AnyConverterType]] = []
|
|
|
|
for name, converter in converters.items():
|
|
converter_list.append((
|
|
name,
|
|
converter,
|
|
))
|
|
for name, return_converter in return_converters.items():
|
|
return_converter_list.append((
|
|
name,
|
|
return_converter
|
|
))
|
|
|
|
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', converter_list),
|
|
("Return converters", 'return_converter_init', return_converter_list),
|
|
):
|
|
print(title + ":")
|
|
|
|
ids.sort(key=lambda item: item[0].lower())
|
|
longest = -1
|
|
for name, _ in ids:
|
|
longest = max(longest, len(name))
|
|
|
|
for name, cls in ids:
|
|
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(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)
|