blob: e62218f2e86a1d21de54a17dc3c4464e57299f9c [file] [edit]
"""
Check that all macros defined by the Python C API have a name starting with
"Py". Ignore names listed by check_capi_macros_ignored.txt: macros with an
invalid name, added before this script was created.
Python C API:
* Include/*.h
* Include/cpython/*.h
* pyconfig.h.in
"""
import difflib
import glob
import os.path
import re
import sys
EXCLUDE_HEADERS = {
# Header files not included by Python.h and ignored by this script
'dynamic_annotations.h',
'errcode.h',
'opcode.h',
'opcode_ids.h',
'osdefs.h',
'pyexpat.h',
'structmember.h',
# Header files not included by Python.h but parsed by this script:
# - datetime.h
# - frameobject.h
# - marshal.h
# - py_curses.h
# - pydtrace.h
}
TOOLS_BUILD_DIR = os.path.abspath(os.path.dirname(__file__))
SRC_DIR = os.path.dirname(os.path.dirname(TOOLS_BUILD_DIR))
IGNORED_FILENAME = os.path.join(TOOLS_BUILD_DIR, 'check_capi_macros_ignored.txt')
DEFINE_REGEX = re.compile(r'\s*#\s*(?P<directive>define|undef)\s+(?P<macro>.*)')
PYTHON_PREFIX = re.compile(r'(Py|PY|_Py|_PY)')
NAME_REGEX = re.compile(r'([A-Za-z_][A-Za-z0-9_]*)\b')
CAUSES_BY_DIRECTIVE = {
"define": "defined",
"undef": "undefined",
}
def parse_file(filename, names):
with open(filename, encoding='utf8') as fp:
for lineno, line in enumerate(fp, start=1):
# Check for '#define MACRO'
match = DEFINE_REGEX.match(line)
if not match:
continue
macro = match['macro']
directive = match['directive']
cause = CAUSES_BY_DIRECTIVE.get(directive, directive)
if PYTHON_PREFIX.match(macro):
continue
match = NAME_REGEX.match(macro)
if not match:
print(f"ERROR: {filename}: Unable to parse {line!r}")
sys.exit(1)
name = match[1]
names.append((name, filename, lineno, cause))
def get_ignored_names():
ignored = []
with open(IGNORED_FILENAME, encoding='utf8') as fp:
for line in fp:
name = line.strip()
if name.startswith('#'):
# Ignore comment
continue
if name:
ignored.append(name)
return ignored
def main():
failure = False
# Parse header files
include_dir = os.path.join(SRC_DIR, 'Include')
files = glob.glob(os.path.join(include_dir, '*.h'))
files = [filename for filename in files
if os.path.basename(filename) not in EXCLUDE_HEADERS]
files.extend(glob.glob(os.path.join(include_dir, 'cpython', '*.h')))
files.append(os.path.join(SRC_DIR, 'pyconfig.h.in'))
names = [] # list of (name: str, filename: str, lineno: int, cause: str)
for filename in files:
parse_file(filename, names)
# Parse ignore list
ignored = get_ignored_names()
# Check if the sorted list has duplicated entries
if len(set(ignored)) != len(ignored):
print(f"ERROR: {IGNORED_FILENAME} list contains duplicated entries:")
print()
seen = set()
for name in ignored:
if name not in seen:
seen.add(name)
continue
print(f"- {name}")
print()
failure = True
# Check if the sorted list is sorted
ignored_sorted = sorted(ignored)
if ignored_sorted != ignored:
print(f"ERROR: {IGNORED_FILENAME} list is not sorted")
print()
diff = difflib.unified_diff(ignored, ignored_sorted,
fromfile=IGNORED_FILENAME,
tofile=IGNORED_FILENAME,
lineterm='')
for line in diff:
print(line)
print()
failure = True
# Check for outdated ignore list
names_set = {name for name, filename, lineno, cause in names}
ignored = set(ignored)
outdated = ignored - names_set
if outdated:
print(f"ERROR: {IGNORED_FILENAME} is outdated, "
"the following macros can be removed:")
print()
for name in sorted(outdated):
print(f"- {name}")
print()
print(f"Total: {len(outdated)} macros")
print()
failure = True
# Check for new macros
new_macros = names_set - ignored
if new_macros:
print('ERROR: the Python C API defines the following new macros:')
print()
count = 0
for name, filename, lineno, cause in sorted(names):
if name in ignored:
continue
print(f"- {name} {cause} at {filename}:{lineno}")
count += 1
print()
print(f"Total: {count} macros")
failure = True
if not failure:
print("OK: the ignore list is up to date and sorted")
print("OK: the Python C API only defines macros with names "
f"starting with Py (ignoring {len(ignored)} macros)")
sys.exit(0)
sys.exit(1)
if __name__ == "__main__":
main()