| """ | |
| 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')) | |
| files.append(os.path.join(SRC_DIR, 'PC/pyconfig.h')) | |
| 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() |