Source code for check_python_h_first.single_file
"""Check that Python.h is included before any stdlib headers.
May be a bit overzealous, but it should get the job done.
Originally implemented `in SciPy
<https://github.com/scipy/scipy/blob/888ca356/tools/check_python_h_first.py>`_
"""
import os.path
import re
import sys
HEADER_PATTERN = re.compile(
r'^\s*#\s*include\s*[<"]((?:\w+/)*\w+(?:\.h[hp+]{0,2})?)[>"]\s*$'
)
PYTHON_INCLUDING_HEADERS = [
"Python.h",
# This isn't all of Python.h, but it is the visibility macros
"pyconfig.h",
# NumPy
"numpy/npy_common.h",
"numpy/npy_math.h",
"numpy/arrayobject.h",
"numpy/ndarrayobject.h",
"numpy/ndarraytypes.h",
"numpy/random/distributions.h",
# Pybind
"pybind11/pybind11.h",
# Boost::Python
"boost/python.hpp",
# Pythran
"pythonic/core.hpp",
# xsf::numpy
"xsf/numpy.h",
]
LEAF_HEADERS = [
"numpy/numpyconfig.h",
"numpy/npy_os.h",
"numpy/npy_cpu.h",
"numpy/utils.h",
]
[docs]
def check_python_h_included_first(name_to_check: str) -> int:
"""Check that the passed file includes Python.h first if it does at all.
Perhaps overzealous, but that should work around concerns with
recursion.
Parameters
----------
name_to_check : str
The name of the file to check.
Returns
-------
int
The number of headers before Python.h
"""
included_python = False
included_non_python_header = []
warned_python_construct = False
basename_to_check = os.path.basename(name_to_check)
in_comment = False
includes_headers = False
with open(name_to_check) as in_file:
for i, line in enumerate(in_file, 1):
# Very basic comment parsing
# Assumes /*...*/ comments are on their own lines
if "/*" in line:
if "*/" not in line:
in_comment = True
# else-branch could use regex to remove comment and continue
continue
if in_comment:
if "*/" in line:
in_comment = False
continue
line = line.split("//", 1)[0].strip()
# Now that there's no comments, look for headers
match = HEADER_PATTERN.match(line)
if match:
includes_headers = True
this_header = match.group(1)
if this_header in PYTHON_INCLUDING_HEADERS:
if included_non_python_header and not included_python:
# Headers before python-including header
print(
f"Header before Python.h in file {name_to_check:s}\n"
f"Python.h on line {i:d}, other header(s) on line(s)"
f" {included_non_python_header}",
file=sys.stderr,
)
# else: # no headers before python-including header
included_python = True
PYTHON_INCLUDING_HEADERS.append(basename_to_check)
if os.path.dirname(name_to_check).endswith("include/numpy"):
PYTHON_INCLUDING_HEADERS.append(f"numpy/{basename_to_check:s}")
# We just found out where Python.h comes in this file
break
elif this_header in LEAF_HEADERS:
# This header is just defines, so it won't include
# the system headers that cause problems
continue
elif not included_python and (
"numpy/" in this_header
and this_header not in LEAF_HEADERS
or "python" in this_header.lower()
or "pybind" in this_header
):
print(
f"Python.h not included before python-including header "
f"in file {name_to_check:s}\n"
f"{this_header:s} on line {i:d}",
file=sys.stderr,
)
included_python = True
PYTHON_INCLUDING_HEADERS.append(basename_to_check)
elif not included_python and this_header not in LEAF_HEADERS:
included_non_python_header.append(i)
elif (
not included_python
and not warned_python_construct
and ".h" not in basename_to_check
) and ("py::" in line or "PYBIND11_" in line):
print(
"Python-including header not used before python constructs "
f"in file {name_to_check:s}\nConstruct on line {i:d}",
file=sys.stderr,
)
warned_python_construct = True
if not includes_headers:
LEAF_HEADERS.append(basename_to_check)
return included_python and len(included_non_python_header)