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))