import re from pathlib import Path import sys import _colorize import textwrap SIMPLE_FUNCTION_REGEX = re.compile(r"PyAPI_FUNC(.+) (\w+)\(") SIMPLE_MACRO_REGEX = re.compile(r"# *define *(\w+)(\(.+\))? ") SIMPLE_INLINE_REGEX = re.compile(r"static inline .+( |\n)(\w+)") SIMPLE_DATA_REGEX = re.compile(r"PyAPI_DATA\(.+\) (\w+)") CPYTHON = Path(__file__).parent.parent.parent INCLUDE = CPYTHON / "Include" C_API_DOCS = CPYTHON / "Doc" / "c-api" IGNORED = ( (CPYTHON / "Tools" / "check-c-api-docs" / "ignored_c_api.txt") .read_text() .split("\n") ) for index, line in enumerate(IGNORED): if line.startswith("#"): IGNORED.pop(index) MISTAKE = """ If this is a mistake and this script should not be failing, create an issue and tag Peter (@ZeroIntensity) on it.\ """ def found_undocumented(singular: bool) -> str: some = "an" if singular else "some" s = "" if singular else "s" these = "this" if singular else "these" them = "it" if singular else "them" were = "was" if singular else "were" return ( textwrap.dedent( f""" Found {some} undocumented C API{s}! Python requires documentation on all public C API symbols, macros, and types. If {these} API{s} {were} not meant to be public, prefix {them} with a leading underscore (_PySomething_API) or move {them} to the internal C API (pycore_*.h files). In exceptional cases, certain APIs can be ignored by adding them to Tools/check-c-api-docs/ignored_c_api.txt """ ) + MISTAKE ) def found_ignored_documented(singular: bool) -> str: some = "a" if singular else "some" s = "" if singular else "s" them = "it" if singular else "them" were = "was" if singular else "were" they = "it" if singular else "they" return ( textwrap.dedent( f""" Found {some} C API{s} listed in Tools/c-api-docs-check/ignored_c_api.txt, but {they} {were} found in the documentation. To fix this, remove {them} from ignored_c_api.txt. """ ) + MISTAKE ) def is_documented(name: str) -> bool: """ Is a name present in the C API documentation? """ for path in C_API_DOCS.iterdir(): if path.is_dir(): continue if path.suffix != ".rst": continue text = path.read_text(encoding="utf-8") if name in text: return True return False def scan_file_for_docs(filename: str, text: str) -> tuple[list[str], list[str]]: """ Scan a header file for C API functions. """ undocumented: list[str] = [] documented_ignored: list[str] = [] colors = _colorize.get_colors() def check_for_name(name: str) -> None: documented = is_documented(name) if documented and (name in IGNORED): documented_ignored.append(name) elif not documented and (name not in IGNORED): undocumented.append(name) for function in SIMPLE_FUNCTION_REGEX.finditer(text): name = function.group(2) if not name.startswith("Py"): continue check_for_name(name) for macro in SIMPLE_MACRO_REGEX.finditer(text): name = macro.group(1) if not name.startswith("Py"): continue if "(" in name: name = name[: name.index("(")] check_for_name(name) for inline in SIMPLE_INLINE_REGEX.finditer(text): name = inline.group(2) if not name.startswith("Py"): continue check_for_name(name) for data in SIMPLE_DATA_REGEX.finditer(text): name = data.group(1) if not name.startswith("Py"): continue check_for_name(name) # Remove duplicates and sort alphabetically to keep the output deterministic undocumented = list(set(undocumented)) undocumented.sort() if undocumented or documented_ignored: print(f"{filename} {colors.RED}BAD{colors.RESET}") for name in undocumented: print(f"{colors.BOLD_RED}UNDOCUMENTED:{colors.RESET} {name}") for name in documented_ignored: print(f"{colors.BOLD_YELLOW}DOCUMENTED BUT IGNORED:{colors.RESET} {name}") else: print(f"{filename} {colors.GREEN}OK{colors.RESET}") return undocumented, documented_ignored def main() -> None: print("Scanning for undocumented C API functions...") files = [*INCLUDE.iterdir(), *(INCLUDE / "cpython").iterdir()] all_missing: list[str] = [] all_found_ignored: list[str] = [] for file in files: if file.is_dir(): continue assert file.exists() text = file.read_text(encoding="utf-8") missing, ignored = scan_file_for_docs(str(file.relative_to(INCLUDE)), text) all_found_ignored += ignored all_missing += missing fail = False to_check = [ (all_missing, "missing", found_undocumented(len(all_missing) == 1)), ( all_found_ignored, "documented but ignored", found_ignored_documented(len(all_found_ignored) == 1), ), ] for name_list, what, message in to_check: if not name_list: continue s = "s" if len(name_list) != 1 else "" print(f"-- {len(name_list)} {what} C API{s} --") for name in name_list: print(f" - {name}") print(message) fail = True sys.exit(1 if fail else 0) if __name__ == "__main__": main()