Skip to content

Commit 77d1750

Browse files
committed
Add a tool for finding undocumented C API
1 parent 7739943 commit 77d1750

File tree

2 files changed

+237
-0
lines changed

2 files changed

+237
-0
lines changed
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
# descrobject.h
2+
PyClassMethodDescr_Type
3+
PyDictProxy_Type
4+
PyGetSetDescr_Type
5+
PyMemberDescr_Type
6+
PyMethodDescr_Type
7+
PyWrapperDescr_Type
8+
# pydtrace_probes.h
9+
PyDTrace_AUDIT
10+
PyDTrace_FUNCTION_ENTRY
11+
PyDTrace_FUNCTION_RETURN
12+
PyDTrace_GC_DONE
13+
PyDTrace_GC_START
14+
PyDTrace_IMPORT_FIND_LOAD_DONE
15+
PyDTrace_IMPORT_FIND_LOAD_START
16+
PyDTrace_INSTANCE_DELETE_DONE
17+
PyDTrace_INSTANCE_DELETE_START
18+
PyDTrace_INSTANCE_NEW_DONE
19+
PyDTrace_INSTANCE_NEW_START
20+
PyDTrace_LINE
21+
# fileobject.h
22+
Py_FileSystemDefaultEncodeErrors
23+
Py_FileSystemDefaultEncoding
24+
Py_HasFileSystemDefaultEncoding
25+
Py_UTF8Mode
26+
# pyhash.h
27+
Py_HASH_EXTERNAL
28+
# exports.h
29+
PyAPI_DATA
30+
Py_EXPORTED_SYMBOL
31+
Py_IMPORTED_SYMBOL
32+
Py_LOCAL_SYMBOL
33+
# modsupport.h
34+
PyABIInfo_FREETHREADING_AGNOSTIC
35+
# moduleobject.h
36+
PyModuleDef_Type
37+
# object.h
38+
Py_INVALID_SIZE
39+
Py_TPFLAGS_HAVE_VERSION_TAG
40+
Py_TPFLAGS_INLINE_VALUES
41+
Py_TPFLAGS_IS_ABSTRACT
42+
# pyexpat.h
43+
PyExpat_CAPI_MAGIC
44+
PyExpat_CAPSULE_NAME
45+
# pyport.h
46+
Py_ALIGNED
47+
Py_ARITHMETIC_RIGHT_SHIFT
48+
Py_CAN_START_THREADS
49+
Py_FORCE_EXPANSION
50+
Py_GCC_ATTRIBUTE
51+
Py_LL
52+
Py_SAFE_DOWNCAST
53+
Py_ULL
54+
Py_VA_COPY
55+
# unicodeobject.h
56+
Py_UNICODE_SIZE
57+
# cpython/methodobject.h
58+
PyCFunction_GET_CLASS
59+
# cpython/compile.h
60+
PyCF_ALLOW_INCOMPLETE_INPUT
61+
PyCF_COMPILE_MASK
62+
PyCF_DONT_IMPLY_DEDENT
63+
PyCF_IGNORE_COOKIE
64+
PyCF_MASK
65+
PyCF_MASK_OBSOLETE
66+
PyCF_SOURCE_IS_UTF8
67+
# cpython/descrobject.h
68+
PyDescr_COMMON
69+
PyDescr_NAME
70+
PyDescr_TYPE
71+
PyWrapperFlag_KEYWORDS
72+
# cpython/fileobject.h
73+
PyFile_NewStdPrinter
74+
PyStdPrinter_Type
75+
Py_UniversalNewlineFgets
76+
# cpython/setobject.h
77+
PySet_MINSIZE
78+
# cpython/ceval.h
79+
PyUnstable_CopyPerfMapFile
80+
PyUnstable_PerfTrampoline_CompileCode
81+
PyUnstable_PerfTrampoline_SetPersistAfterFork
82+
# cpython/genobject.h
83+
PyAsyncGenASend_CheckExact
84+
# cpython/longintrepr.h
85+
PyLong_BASE
86+
PyLong_MASK
87+
PyLong_SHIFT
88+
# cpython/pyerrors.h
89+
PyException_HEAD
90+
# cpython/pyframe.h
91+
PyUnstable_EXECUTABLE_KINDS
92+
PyUnstable_EXECUTABLE_KIND_BUILTIN_FUNCTION
93+
PyUnstable_EXECUTABLE_KIND_METHOD_DESCRIPTOR
94+
PyUnstable_EXECUTABLE_KIND_PY_FUNCTION
95+
PyUnstable_EXECUTABLE_KIND_SKIP
96+
# cpython/pylifecycle.h
97+
Py_FrozenMain
98+
# cpython/unicodeobject.h
99+
PyUnicode_IS_COMPACT
100+
PyUnicode_IS_COMPACT_ASCII

Tools/c-api-docs-check/main.py

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
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

Comments
 (0)