(root)/
Python-3.11.7/
Tools/
scripts/
pep384_macrocheck.py
       1  """
       2  pep384_macrocheck.py
       3  
       4  This program tries to locate errors in the relevant Python header
       5  files where macros access type fields when they are reachable from
       6  the limited 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))