bpo-33738: Fix macros which contradict PEP 384 (GH-7477) · python/cpython@ea62ce7
1+"""
2+pep384_macrocheck.py
3+4+This programm tries to locate errors in the relevant Python header
5+files where macros access type fields when they are reachable from
6+the limided API.
7+8+The idea is to search macros with the string "->tp_" in it.
9+When the macro name does not begin with an underscore,
10+then we have found a dormant error.
11+12+Christian Tismer
13+2018-06-02
14+"""
15+16+import sys
17+import os
18+import re
19+20+21+DEBUG = False
22+23+def dprint(*args, **kw):
24+if DEBUG:
25+print(*args, **kw)
26+27+def 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+54+def 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+68+def 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+108+def 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+137+def report(fname, nr, macro):
138+f = sys.stderr
139+print(fname + ":" + str(nr), file=f)
140+print(macro, file=f)
141+142+if __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))