|
| 1 | +import re |
| 2 | +from pathlib import Path |
| 3 | +import sys |
| 4 | +import _colorize |
| 5 | + |
| 6 | +SIMPLE_FUNCTION_REGEX = re.compile(r"PyAPI_FUNC(.+) (\w+)\(") |
| 7 | +SIMPLE_MACRO_REGEX = re.compile(r"# *define *(\w+)(\(.+\))? ") |
| 8 | +SIMPLE_INLINE_REGEX = re.compile(r"static inline .+( |\n)(\w+)") |
| 9 | +SIMPLE_DATA_REGEX = re.compile(r"PyAPI_DATA\(.+\) (\w+)") |
| 10 | + |
| 11 | +_CPYTHON = Path(__file__).parent.parent.parent |
| 12 | +INCLUDE = _CPYTHON / "Include" |
| 13 | +C_API_DOCS = _CPYTHON / "Doc" / "c-api" |
| 14 | +IGNORED = (_CPYTHON / "Tools" / "c-api-docs-check" / "ignored_c_api.txt").read_text().split("\n") |
| 15 | + |
| 16 | +for index, line in enumerate(IGNORED): |
| 17 | + if line.startswith("#"): |
| 18 | + IGNORED.pop(index) |
| 19 | + |
| 20 | + |
| 21 | +def is_documented(name: str) -> bool: |
| 22 | + """ |
| 23 | + Is a name present in the C API documentation? |
| 24 | + """ |
| 25 | + if name in IGNORED: |
| 26 | + return True |
| 27 | + |
| 28 | + for path in C_API_DOCS.iterdir(): |
| 29 | + if path.is_dir(): |
| 30 | + continue |
| 31 | + if path.suffix != ".rst": |
| 32 | + continue |
| 33 | + |
| 34 | + text = path.read_text(encoding="utf-8") |
| 35 | + if name in text: |
| 36 | + return True |
| 37 | + |
| 38 | + return False |
| 39 | + |
| 40 | + |
| 41 | +def scan_file_for_missing_docs(filename: str, text: str) -> list[str]: |
| 42 | + """ |
| 43 | + Scan a header file for undocumented C API functions. |
| 44 | + """ |
| 45 | + undocumented: list[str] = [] |
| 46 | + colors = _colorize.get_colors() |
| 47 | + |
| 48 | + for function in SIMPLE_FUNCTION_REGEX.finditer(text): |
| 49 | + name = function.group(2) |
| 50 | + if not name.startswith("Py"): |
| 51 | + continue |
| 52 | + |
| 53 | + if not is_documented(name): |
| 54 | + undocumented.append(name) |
| 55 | + |
| 56 | + for macro in SIMPLE_MACRO_REGEX.finditer(text): |
| 57 | + name = macro.group(1) |
| 58 | + if not name.startswith("Py"): |
| 59 | + continue |
| 60 | + |
| 61 | + if "(" in name: |
| 62 | + name = name[: name.index("(")] |
| 63 | + |
| 64 | + if not is_documented(name): |
| 65 | + undocumented.append(name) |
| 66 | + |
| 67 | + for inline in SIMPLE_INLINE_REGEX.finditer(text): |
| 68 | + name = inline.group(2) |
| 69 | + if not name.startswith("Py"): |
| 70 | + continue |
| 71 | + |
| 72 | + if not is_documented(name): |
| 73 | + undocumented.append(name) |
| 74 | + |
| 75 | + for data in SIMPLE_DATA_REGEX.finditer(text): |
| 76 | + name = data.group(1) |
| 77 | + if not name.startswith("Py"): |
| 78 | + continue |
| 79 | + |
| 80 | + if not is_documented(name): |
| 81 | + undocumented.append(name) |
| 82 | + |
| 83 | + # Remove duplicates and sort alphabetically to keep the output non-deterministic |
| 84 | + undocumented = list(set(undocumented)) |
| 85 | + undocumented.sort() |
| 86 | + |
| 87 | + if undocumented: |
| 88 | + print(f"{filename} {colors.RED}BAD{colors.RESET}") |
| 89 | + for name in undocumented: |
| 90 | + print(f"{colors.BOLD_RED}UNDOCUMENTED:{colors.RESET} {name}") |
| 91 | + |
| 92 | + return undocumented |
| 93 | + else: |
| 94 | + print(f"{filename} {colors.GREEN}OK{colors.RESET}") |
| 95 | + |
| 96 | + return [] |
| 97 | + |
| 98 | + |
| 99 | +def main() -> None: |
| 100 | + print("Scanning for undocumented C API functions...") |
| 101 | + files = [*INCLUDE.iterdir(), *(INCLUDE / "cpython").iterdir()] |
| 102 | + all_missing: list[str] = [] |
| 103 | + for file in files: |
| 104 | + if file.is_dir(): |
| 105 | + continue |
| 106 | + assert file.exists() |
| 107 | + text = file.read_text(encoding="utf-8") |
| 108 | + missing = scan_file_for_missing_docs(str(file.relative_to(INCLUDE)), text) |
| 109 | + all_missing += missing |
| 110 | + |
| 111 | + if all_missing != []: |
| 112 | + s = "s" if len(all_missing) != 1 else "" |
| 113 | + print(f"-- {len(all_missing)} missing function{s} --") |
| 114 | + for name in all_missing: |
| 115 | + print(f" - {name}") |
| 116 | + print() |
| 117 | + print( |
| 118 | + "Found some undocumented C API!", |
| 119 | + "Python requires documentation on all public C API functions.", |
| 120 | + "If these function(s) were not meant to be public, please prefix " |
| 121 | + "them with a leading underscore (_PySomething_API) or move them to " |
| 122 | + "the internal C API (pycore_*.h files).", |
| 123 | + "", |
| 124 | + "In exceptional cases, certain functions can be ignored by adding " |
| 125 | + "them to Tools/c-api-docs-check/ignored_c_api.txt", |
| 126 | + "If this is a mistake and this script should not be failing, please " |
| 127 | + "create an issue and tag Peter (@ZeroIntensity) on it.", |
| 128 | + sep="\n", |
| 129 | + ) |
| 130 | + sys.exit(1) |
| 131 | + else: |
| 132 | + print("Nothing found :)") |
| 133 | + sys.exit(0) |
| 134 | + |
| 135 | + |
| 136 | +if __name__ == "__main__": |
| 137 | + main() |
0 commit comments