1""" 2pep384_macrocheck.py 3 4This program tries to locate errors in the relevant Python header 5files where macros access type fields when they are reachable from 6the limited API. 7 8The idea is to search macros with the string "->tp_" in it. 9When the macro name does not begin with an underscore, 10then we have found a dormant error. 11 12Christian Tismer 132018-06-02 14""" 15 16import sys 17import os 18import re 19 20 21DEBUG = False 22 23def dprint(*args, **kw): 24 if DEBUG: 25 print(*args, **kw) 26 27def parse_headerfiles(startpath): 28 """ 29 Scan all header files which are reachable fronm Python.h 30 """ 31 search = "Python.h" 32 name = os.path.join(startpath, search) 33 if not os.path.exists(name): 34 raise ValueError("file {} was not found in {}\n" 35 "Please give the path to Python's include directory." 36 .format(search, startpath)) 37 errors = 0 38 with open(name) as python_h: 39 while True: 40 line = python_h.readline() 41 if not line: 42 break 43 found = re.match(r'^\s*#\s*include\s*"(\w+\.h)"', line) 44 if not found: 45 continue 46 include = found.group(1) 47 dprint("Scanning", include) 48 name = os.path.join(startpath, include) 49 if not os.path.exists(name): 50 name = os.path.join(startpath, "../PC", include) 51 errors += parse_file(name) 52 return errors 53 54def ifdef_level_gen(): 55 """ 56 Scan lines for #ifdef and track the level. 57 """ 58 level = 0 59 ifdef_pattern = r"^\s*#\s*if" # covers ifdef and ifndef as well 60 endif_pattern = r"^\s*#\s*endif" 61 while True: 62 line = yield level 63 if re.match(ifdef_pattern, line): 64 level += 1 65 elif re.match(endif_pattern, line): 66 level -= 1 67 68def limited_gen(): 69 """ 70 Scan lines for Py_LIMITED_API yes(1) no(-1) or nothing (0) 71 """ 72 limited = [0] # nothing 73 unlimited_pattern = r"^\s*#\s*ifndef\s+Py_LIMITED_API" 74 limited_pattern = "|".join([ 75 r"^\s*#\s*ifdef\s+Py_LIMITED_API", 76 r"^\s*#\s*(el)?if\s+!\s*defined\s*\(\s*Py_LIMITED_API\s*\)\s*\|\|", 77 r"^\s*#\s*(el)?if\s+defined\s*\(\s*Py_LIMITED_API" 78 ]) 79 else_pattern = r"^\s*#\s*else" 80 ifdef_level = ifdef_level_gen() 81 status = next(ifdef_level) 82 wait_for = -1 83 while True: 84 line = yield limited[-1] 85 new_status = ifdef_level.send(line) 86 dir = new_status - status 87 status = new_status 88 if dir == 1: 89 if re.match(unlimited_pattern, line): 90 limited.append(-1) 91 wait_for = status - 1 92 elif re.match(limited_pattern, line): 93 limited.append(1) 94 wait_for = status - 1 95 elif dir == -1: 96 # this must have been an endif 97 if status == wait_for: 98 limited.pop() 99 wait_for = -1 100 else: 101 # it could be that we have an elif 102 if re.match(limited_pattern, line): 103 limited.append(1) 104 wait_for = status - 1 105 elif re.match(else_pattern, line): 106 limited.append(-limited.pop()) # negate top 107 108def parse_file(fname): 109 errors = 0 110 with open(fname) as f: 111 lines = f.readlines() 112 type_pattern = r"^.*?->\s*tp_" 113 define_pattern = r"^\s*#\s*define\s+(\w+)" 114 limited = limited_gen() 115 status = next(limited) 116 for nr, line in enumerate(lines): 117 status = limited.send(line) 118 line = line.rstrip() 119 dprint(fname, nr, status, line) 120 if status != -1: 121 if re.match(define_pattern, line): 122 name = re.match(define_pattern, line).group(1) 123 if not name.startswith("_"): 124 # found a candidate, check it! 125 macro = line + "\n" 126 idx = nr 127 while line.endswith("\\"): 128 idx += 1 129 line = lines[idx].rstrip() 130 macro += line + "\n" 131 if re.match(type_pattern, macro, re.DOTALL): 132 # this type field can reach the limited API 133 report(fname, nr + 1, macro) 134 errors += 1 135 return errors 136 137def report(fname, nr, macro): 138 f = sys.stderr 139 print(fname + ":" + str(nr), file=f) 140 print(macro, file=f) 141 142if __name__ == "__main__": 143 p = sys.argv[1] if sys.argv[1:] else "../../Include" 144 errors = parse_headerfiles(p) 145 if errors: 146 # somehow it makes sense to raise a TypeError :-) 147 raise TypeError("These {} locations contradict the limited API." 148 .format(errors)) 149