* feat!: drop support for Python 3.8 The minimum supported version is now Python 3.9. pybind11 v3.0 was the last release that supports Python 3.8. The deprecation note said that support goes away in 3.1. Remove the code paths that this makes dead: - the `_PyObject_Vectorcall` fallback in `cast.h` - the `frame->f_code` and `frame->f_back` fallbacks in `pytypes.h` - the `PyFrame_FastToLocals` path in `get_type_override` - the conditional `Py_VISIT(Py_TYPE(self))` in `tp_traverse` - `PYBIND11_PYCONFIG_SUPPORT_PY_VERSION_HEX`, the pre-PyConfig interpreter init, and the `widen_chars` helpers in `embed.h` Assisted-by: ClaudeCode:claude-opus-5 * ci(appveyor): use Python 3.9 The AppVeyor job set `PYTHON: 38`, which makes the path `C:\Python38`. The image gives Python 3.9.13 as `C:\Python39`. Assisted-by: ClaudeCode:claude-opus-5 * feat!: require MSVC 2019 or newer Python 3.9 is the new minimum, so MSVC 2017 is no longer needed. Raise the compile-time floor to _MSC_VER 1920 and remove the workarounds that only applied below it: std::launder, fold expressions, weak_from_this, aligned new/delete, the C4100 warning helper, and the func_handle syntax error. AppVeyor now builds with Visual Studio 2019. Assisted-by: ClaudeCode:claude-opus-5 * fix(appveyor): build against the Python that has the test packages CMake 4 has no FindPythonLibs, so pybind11 uses FindPython. FindPython reads the registry before PATH and selected `C:\Python314-x64`, but the test packages go into the Python on PATH. Pass `Python_ROOT_DIR` to name the correct one. Also set `CMAKE_ARCH`. It was never set, so the architecture came from the generator. Visual Studio 2017 defaults to Win32, but Visual Studio 2019 defaults to x64, which made this x86 job build 64-bit code. Assisted-by: ClaudeCode:claude-opus-5 * fix(appveyor): give the linker Python's libs directory The build compiled but failed to link with LNK1104 on a bare `python39.lib`. That name comes from the `#pragma comment(lib, ...)` in pyconfig.h, so the linker needs the directory. CMake 4 has no FindPythonLibs, and FindPython does not add it for this Debug x86 build. Put it on LIB instead. The directory listing is temporary, to confirm the library is present. Assisted-by: ClaudeCode:claude-opus-5 * fix(appveyor): link the release Python library in Debug The image ships python39_d.lib next to python39.lib, so FindPython picks the debug import library for a Debug build. pybind11 undefines _DEBUG around Python.h, so pyconfig.h asks for python39.lib instead and the link failed with LNK1104. Name the release library for the debug slot. Setting LIB does not work, because MSBuild replaces it from the toolset, and it would link both import libraries. Assisted-by: ClaudeCode:claude-opus-5 * chore(appveyor): print link settings to debug LNK1104 Revert the two attempted fixes. Neither changed the failure: setting LIB does not survive MSBuild, and naming the release library for Python_LIBRARY_DEBUG had no effect. Print the Python cache entries and the link settings of a generated project file instead, to see what the linker really gets. Temporary. Assisted-by: ClaudeCode:claude-opus-5 * fix(appveyor): take the release Python library in Debug The generated project file linked C:\Python39\libs\python39_d.lib in the Debug configuration, because the image ships debug binaries next to the release ones. pybind11 undefines _DEBUG around Python.h, so pyconfig.h asks for python39.lib in a #pragma comment(lib), which nothing on the link line satisfies and no library directory holds. Map Debug to the release artifacts. Assisted-by: ClaudeCode:claude-opus-5 * chore(appveyor): drop the temporary link diagnostic Assisted-by: ClaudeCode:claude-opus-5 * fix(tests): guard the unraisable warning filter for pytest < 6 The distro pytest in the Clang and GCC Docker jobs has no PytestUnraisableExceptionWarning, so an unconditional filterwarnings mark makes pytest fail with an INTERNALERROR after the tests pass. Assisted-by: ClaudeCode:claude-opus-5 Claude-Session: https://claude.ai/code/session_01Aimf6HuSz1vLRwBnbxmCTc * docs: address review items on version hints, embed docs, and a PyPy xfail Extend Python_ADDITIONAL_VERSIONS through 3.15, describe the PyConfig behavior of initialize_interpreter, and drop the stale Python 3.8 wording from the PyPy xfail reason. Assisted-by: ClaudeCode:claude-opus-5 * Update README.rst Co-authored-by: Ralf W. Grosse-Kunstleve <rwgkio@gmail.com> --------- Co-authored-by: Ralf W. Grosse-Kunstleve <rwgkio@gmail.com>
451 lines
14 KiB
Python
451 lines
14 KiB
Python
from __future__ import annotations
|
|
|
|
import contextlib
|
|
import importlib
|
|
import os
|
|
import re
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
import sysconfig
|
|
import tarfile
|
|
import zipfile
|
|
from collections.abc import Generator
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
# These tests must be run explicitly
|
|
|
|
DIR = Path(__file__).parent.resolve()
|
|
MAIN_DIR = DIR.parent.parent
|
|
|
|
FILENAME_VERSION = re.compile(r"[-_]((\d+\.\d+\.\d+)(?:[a-z]+\d*)?)(?:-|\.tar\.gz$)")
|
|
|
|
# Newer pytest has global path setting, but keeping old pytest for now
|
|
sys.path.append(str(MAIN_DIR / "tools"))
|
|
|
|
from make_global import get_global # noqa: E402
|
|
|
|
HAS_UV = shutil.which("uv") is not None
|
|
UV_ARGS = ["--installer=uv"] if HAS_UV else []
|
|
|
|
PKGCONFIG = """\
|
|
prefix=${{pcfiledir}}/../../
|
|
includedir=${{prefix}}/include
|
|
|
|
Name: pybind11
|
|
Description: Seamless operability between C++11 and Python
|
|
Version: {VERSION}
|
|
Cflags: -I${{includedir}}
|
|
"""
|
|
|
|
|
|
main_headers = {
|
|
"include/pybind11/attr.h",
|
|
"include/pybind11/buffer_info.h",
|
|
"include/pybind11/cast.h",
|
|
"include/pybind11/chrono.h",
|
|
"include/pybind11/common.h",
|
|
"include/pybind11/complex.h",
|
|
"include/pybind11/critical_section.h",
|
|
"include/pybind11/eigen.h",
|
|
"include/pybind11/embed.h",
|
|
"include/pybind11/eval.h",
|
|
"include/pybind11/functional.h",
|
|
"include/pybind11/gil.h",
|
|
"include/pybind11/gil_safe_call_once.h",
|
|
"include/pybind11/gil_simple.h",
|
|
"include/pybind11/iostream.h",
|
|
"include/pybind11/native_enum.h",
|
|
"include/pybind11/numpy.h",
|
|
"include/pybind11/operators.h",
|
|
"include/pybind11/options.h",
|
|
"include/pybind11/pybind11.h",
|
|
"include/pybind11/pytypes.h",
|
|
"include/pybind11/subinterpreter.h",
|
|
"include/pybind11/stl.h",
|
|
"include/pybind11/stl_bind.h",
|
|
"include/pybind11/trampoline_self_life_support.h",
|
|
"include/pybind11/type_caster_pyobject_ptr.h",
|
|
"include/pybind11/typing.h",
|
|
"include/pybind11/warnings.h",
|
|
}
|
|
|
|
conduit_headers = {
|
|
"include/pybind11/conduit/README.txt",
|
|
"include/pybind11/conduit/pybind11_conduit_v1.h",
|
|
"include/pybind11/conduit/pybind11_platform_abi_id.h",
|
|
"include/pybind11/conduit/wrap_include_python_h.h",
|
|
}
|
|
|
|
detail_headers = {
|
|
"include/pybind11/detail/argument_vector.h",
|
|
"include/pybind11/detail/class.h",
|
|
"include/pybind11/detail/common.h",
|
|
"include/pybind11/detail/cpp_conduit.h",
|
|
"include/pybind11/detail/descr.h",
|
|
"include/pybind11/detail/dynamic_raw_ptr_cast_if_possible.h",
|
|
"include/pybind11/detail/function_record_pyobject.h",
|
|
"include/pybind11/detail/function_ref.h",
|
|
"include/pybind11/detail/holder_caster_foreign_helpers.h",
|
|
"include/pybind11/detail/init.h",
|
|
"include/pybind11/detail/internals.h",
|
|
"include/pybind11/detail/native_enum_data.h",
|
|
"include/pybind11/detail/pybind11_namespace_macros.h",
|
|
"include/pybind11/detail/struct_smart_holder.h",
|
|
"include/pybind11/detail/type_caster_base.h",
|
|
"include/pybind11/detail/typeid.h",
|
|
"include/pybind11/detail/using_smart_holder.h",
|
|
"include/pybind11/detail/value_and_holder.h",
|
|
"include/pybind11/detail/exception_translation.h",
|
|
}
|
|
|
|
eigen_headers = {
|
|
"include/pybind11/eigen/common.h",
|
|
"include/pybind11/eigen/matrix.h",
|
|
"include/pybind11/eigen/tensor.h",
|
|
}
|
|
|
|
stl_headers = {
|
|
"include/pybind11/stl/filesystem.h",
|
|
}
|
|
|
|
cmake_files = {
|
|
"share/cmake/pybind11/FindPythonLibsNew.cmake",
|
|
"share/cmake/pybind11/pybind11Common.cmake",
|
|
"share/cmake/pybind11/pybind11Config.cmake",
|
|
"share/cmake/pybind11/pybind11ConfigVersion.cmake",
|
|
"share/cmake/pybind11/pybind11GuessPythonExtSuffix.cmake",
|
|
"share/cmake/pybind11/pybind11NewTools.cmake",
|
|
"share/cmake/pybind11/pybind11Targets.cmake",
|
|
"share/cmake/pybind11/pybind11Tools.cmake",
|
|
}
|
|
|
|
pkgconfig_files = {
|
|
"share/pkgconfig/pybind11.pc",
|
|
}
|
|
|
|
py_files = {
|
|
"__init__.py",
|
|
"__main__.py",
|
|
"_version.py",
|
|
"commands.py",
|
|
"py.typed",
|
|
"setup_helpers.py",
|
|
"share/__init__.py",
|
|
"share/pkgconfig/__init__.py",
|
|
}
|
|
|
|
headers = main_headers | conduit_headers | detail_headers | eigen_headers | stl_headers
|
|
generated_files = cmake_files | pkgconfig_files
|
|
all_files = headers | generated_files | py_files
|
|
|
|
sdist_files = {
|
|
"pyproject.toml",
|
|
"LICENSE",
|
|
"README.rst",
|
|
"PKG-INFO",
|
|
"SECURITY.md",
|
|
}
|
|
|
|
|
|
@contextlib.contextmanager
|
|
def preserve_file(filename: Path) -> Generator[str, None, None]:
|
|
old_stat = filename.stat()
|
|
old_file = filename.read_text(encoding="utf-8")
|
|
try:
|
|
yield old_file
|
|
finally:
|
|
filename.write_text(old_file, encoding="utf-8")
|
|
os.utime(filename, (old_stat.st_atime, old_stat.st_mtime))
|
|
|
|
|
|
@contextlib.contextmanager
|
|
def build_global() -> Generator[None, None, None]:
|
|
"""
|
|
Build global SDist and wheel.
|
|
"""
|
|
|
|
pyproject = MAIN_DIR / "pyproject.toml"
|
|
with preserve_file(pyproject):
|
|
newer_txt = get_global()
|
|
pyproject.write_text(newer_txt, encoding="utf-8")
|
|
yield
|
|
|
|
|
|
def read_tz_file(tar: tarfile.TarFile, name: str) -> bytes:
|
|
start = tar.getnames()[0].split("/")[0] + "/"
|
|
inner_file = tar.extractfile(tar.getmember(f"{start}{name}"))
|
|
assert inner_file
|
|
with contextlib.closing(inner_file) as f:
|
|
return f.read()
|
|
|
|
|
|
def normalize_line_endings(value: bytes) -> bytes:
|
|
return value.replace(os.linesep.encode("utf-8"), b"\n")
|
|
|
|
|
|
def test_build_sdist(monkeypatch, tmpdir):
|
|
monkeypatch.chdir(MAIN_DIR)
|
|
|
|
subprocess.run(
|
|
[sys.executable, "-m", "build", "--sdist", f"--outdir={tmpdir}", *UV_ARGS],
|
|
check=True,
|
|
)
|
|
|
|
(sdist,) = tmpdir.visit("*.tar.gz")
|
|
version = FILENAME_VERSION.search(sdist.basename).group(1)
|
|
|
|
with tarfile.open(str(sdist), "r:gz") as tar:
|
|
simpler = {n.split("/", 1)[-1] for n in tar.getnames()[1:]}
|
|
(pkg_info_path,) = (n for n in simpler if n.endswith("PKG-INFO"))
|
|
|
|
pyproject_toml = read_tz_file(tar, "pyproject.toml")
|
|
pkg_info = read_tz_file(tar, pkg_info_path).decode("utf-8")
|
|
|
|
files = headers | sdist_files
|
|
assert files <= simpler
|
|
|
|
assert b'name = "pybind11"' in pyproject_toml
|
|
assert f"Version: {version}" in pkg_info
|
|
assert "License-Expression: BSD-3-Clause" in pkg_info
|
|
assert "License-File: LICENSE" in pkg_info
|
|
assert "Provides-Extra: global" in pkg_info
|
|
assert f'Requires-Dist: pybind11-global=={version}; extra == "global"' in pkg_info
|
|
|
|
|
|
def test_build_global_dist(monkeypatch, tmpdir):
|
|
monkeypatch.chdir(MAIN_DIR)
|
|
with build_global():
|
|
subprocess.run(
|
|
[
|
|
sys.executable,
|
|
"-m",
|
|
"build",
|
|
"--sdist",
|
|
"--outdir",
|
|
str(tmpdir),
|
|
*UV_ARGS,
|
|
],
|
|
check=True,
|
|
)
|
|
|
|
(sdist,) = tmpdir.visit("*.tar.gz")
|
|
version = FILENAME_VERSION.search(sdist.basename).group(2)
|
|
|
|
with tarfile.open(str(sdist), "r:gz") as tar:
|
|
simpler = {n.split("/", 1)[-1] for n in tar.getnames()[1:]}
|
|
(pkg_info_path,) = (n for n in simpler if n.endswith("PKG-INFO"))
|
|
|
|
pyproject_toml = read_tz_file(tar, "pyproject.toml")
|
|
pkg_info = read_tz_file(tar, pkg_info_path).decode("utf-8")
|
|
|
|
files = headers | sdist_files
|
|
assert files <= simpler
|
|
|
|
assert b'name = "pybind11-global"' in pyproject_toml
|
|
assert f"Version: {version}" in pkg_info
|
|
assert "License-Expression: BSD-3-Clause" in pkg_info
|
|
assert "License-File: LICENSE" in pkg_info
|
|
assert "Provides-Extra: global" not in pkg_info
|
|
assert 'Requires-Dist: pybind11-global; extra == "global"' not in pkg_info
|
|
|
|
|
|
def tests_build_wheel(monkeypatch, tmpdir):
|
|
monkeypatch.chdir(MAIN_DIR)
|
|
|
|
subprocess.run(
|
|
[sys.executable, "-m", "build", "--wheel", "--outdir", str(tmpdir), *UV_ARGS],
|
|
check=True,
|
|
)
|
|
|
|
(wheel,) = tmpdir.visit("*.whl")
|
|
version, simple_version = FILENAME_VERSION.search(wheel.basename).groups()
|
|
|
|
files = {f"pybind11/{n}" for n in all_files}
|
|
files |= {
|
|
"dist-info/licenses/LICENSE",
|
|
"dist-info/METADATA",
|
|
"dist-info/RECORD",
|
|
"dist-info/WHEEL",
|
|
"dist-info/entry_points.txt",
|
|
}
|
|
|
|
with zipfile.ZipFile(str(wheel)) as z:
|
|
names = z.namelist()
|
|
share = zipfile.Path(z, "pybind11/share")
|
|
pkgconfig = (share / "pkgconfig/pybind11.pc").read_text(encoding="utf-8")
|
|
cmakeconfig = (share / "cmake/pybind11/pybind11Config.cmake").read_text(
|
|
encoding="utf-8"
|
|
)
|
|
(pkg_info_path,) = (n for n in names if n.endswith("METADATA"))
|
|
pkg_info = zipfile.Path(z, pkg_info_path).read_text(encoding="utf-8")
|
|
|
|
trimmed = {n for n in names if "dist-info" not in n}
|
|
trimmed |= {f"dist-info/{n.split('/', 1)[-1]}" for n in names if "dist-info" in n}
|
|
|
|
assert files == trimmed
|
|
|
|
assert 'set(pybind11_INCLUDE_DIR "${PACKAGE_PREFIX_DIR}/include")' in cmakeconfig
|
|
|
|
pkgconfig_expected = PKGCONFIG.format(VERSION=simple_version)
|
|
assert pkgconfig_expected == pkgconfig
|
|
|
|
assert f"Version: {version}" in pkg_info
|
|
assert "License-Expression: BSD-3-Clause" in pkg_info
|
|
assert "License-File: LICENSE" in pkg_info
|
|
assert "Provides-Extra: global" in pkg_info
|
|
assert f'Requires-Dist: pybind11-global=={version}; extra == "global"' in pkg_info
|
|
|
|
|
|
def tests_build_global_wheel(monkeypatch, tmpdir):
|
|
monkeypatch.chdir(MAIN_DIR)
|
|
with build_global():
|
|
subprocess.run(
|
|
[
|
|
sys.executable,
|
|
"-m",
|
|
"build",
|
|
"--wheel",
|
|
"--outdir",
|
|
str(tmpdir),
|
|
*UV_ARGS,
|
|
],
|
|
check=True,
|
|
)
|
|
|
|
(wheel,) = tmpdir.visit("*.whl")
|
|
version, simple_version = FILENAME_VERSION.search(wheel.basename).groups()
|
|
|
|
files = {f"data/data/{n}" for n in headers}
|
|
files |= {f"data/headers/{n[8:]}" for n in headers}
|
|
files |= {f"data/data/{n}" for n in generated_files}
|
|
files |= {
|
|
"dist-info/licenses/LICENSE",
|
|
"dist-info/METADATA",
|
|
"dist-info/WHEEL",
|
|
"dist-info/RECORD",
|
|
}
|
|
|
|
with zipfile.ZipFile(str(wheel)) as z:
|
|
names = z.namelist()
|
|
beginning = names[0].split("/", 1)[0].rsplit(".", 1)[0]
|
|
|
|
share = zipfile.Path(z, f"{beginning}.data/data/share")
|
|
pkgconfig = (share / "pkgconfig/pybind11.pc").read_text(encoding="utf-8")
|
|
cmakeconfig = (share / "cmake/pybind11/pybind11Config.cmake").read_text(
|
|
encoding="utf-8"
|
|
)
|
|
|
|
(pkg_info_path,) = (n for n in names if n.endswith("METADATA"))
|
|
pkg_info = zipfile.Path(z, pkg_info_path).read_text(encoding="utf-8")
|
|
|
|
assert f"Version: {version}" in pkg_info
|
|
assert "License-Expression: BSD-3-Clause" in pkg_info
|
|
assert "License-File: LICENSE" in pkg_info
|
|
assert "Provides-Extra: global" not in pkg_info
|
|
assert 'Requires-Dist: pybind11-global; extra == "global"' not in pkg_info
|
|
|
|
trimmed = {n[len(beginning) + 1 :] for n in names}
|
|
|
|
assert files == trimmed
|
|
|
|
assert 'set(pybind11_INCLUDE_DIR "${PACKAGE_PREFIX_DIR}/include")' in cmakeconfig
|
|
|
|
pkgconfig_expected = PKGCONFIG.format(VERSION=simple_version)
|
|
assert pkgconfig_expected == pkgconfig
|
|
|
|
|
|
def test_version_matches():
|
|
header = MAIN_DIR / "include/pybind11/detail/common.h"
|
|
text = header.read_text()
|
|
|
|
# Extract the relevant macro values
|
|
regex_prefix = r"#\s*define\s+PYBIND11_VERSION_"
|
|
micro = re.search(rf"{regex_prefix}MICRO\s+(\d+)\b", text).group(1)
|
|
release_level = re.search(rf"{regex_prefix}RELEASE_LEVEL\s+(\w+)\b", text).group(1)
|
|
release_serial = re.search(
|
|
rf"{regex_prefix}RELEASE_SERIAL\s+(\d+)\b",
|
|
text,
|
|
).group(1)
|
|
patch = re.search(rf"{regex_prefix}PATCH\s+([\w.-]+)\b", text).group(1)
|
|
|
|
# Map release level macro to string
|
|
level_map = {
|
|
"PY_RELEASE_LEVEL_ALPHA": "a",
|
|
"PY_RELEASE_LEVEL_BETA": "b",
|
|
"PY_RELEASE_LEVEL_GAMMA": "rc",
|
|
"PY_RELEASE_LEVEL_FINAL": "",
|
|
}
|
|
level_str = level_map[release_level]
|
|
|
|
if release_level == "PY_RELEASE_LEVEL_FINAL":
|
|
assert level_str == ""
|
|
assert release_serial == "0"
|
|
expected_patch = micro
|
|
else:
|
|
expected_patch = f"{micro}{level_str}{release_serial}"
|
|
|
|
assert patch == expected_patch
|
|
|
|
|
|
def run_command_line(*args: str) -> str:
|
|
env = os.environ.copy()
|
|
env["PYTHONPATH"] = str(MAIN_DIR)
|
|
result = subprocess.run(
|
|
[sys.executable, "-m", "pybind11", *args],
|
|
capture_output=True,
|
|
text=True,
|
|
check=True,
|
|
env=env,
|
|
)
|
|
return result.stdout
|
|
|
|
|
|
def test_cli_cflags():
|
|
out = run_command_line("--cflags")
|
|
assert "-std=c++17" in out
|
|
assert f"-I{sysconfig.get_path('include')}" in out
|
|
|
|
|
|
def test_cli_ldflags_embed():
|
|
out = run_command_line("--ldflags", "--embed")
|
|
assert "-lpython" in out
|
|
if sysconfig.get_config_var("LIBDIR"):
|
|
assert "-L" in out
|
|
|
|
|
|
@pytest.mark.skipif(os.name != "posix", reason="quote style is platform-specific")
|
|
def test_cflags_quotes_paths_with_spaces(monkeypatch):
|
|
monkeypatch.syspath_prepend(str(MAIN_DIR))
|
|
commands = importlib.import_module("pybind11.commands")
|
|
monkeypatch.setattr(sysconfig, "get_path", lambda name: f"/spa ced/{name}")
|
|
assert "'-I/spa ced/include'" in commands.get_cflags()
|
|
|
|
|
|
@pytest.mark.skipif(os.name != "posix", reason="Unix link flags only")
|
|
def test_ldflags_other_unix(monkeypatch):
|
|
monkeypatch.syspath_prepend(str(MAIN_DIR))
|
|
commands = importlib.import_module("pybind11.commands")
|
|
monkeypatch.setattr(sys, "platform", "freebsd14")
|
|
monkeypatch.setattr(commands, "_config", lambda name: "") # noqa: ARG005
|
|
out = commands.get_ldflags()
|
|
assert "-shared" in out
|
|
assert "-fPIC" in out
|
|
|
|
|
|
def test_cli_file():
|
|
out = run_command_line("--file", "example.cpp").rstrip()
|
|
ext_suffix = sysconfig.get_config_var("EXT_SUFFIX")
|
|
assert "-std=c++17" in out
|
|
assert out.index("-std=c++17") < out.index("example.cpp")
|
|
assert out.endswith(f"-o example{ext_suffix}")
|
|
if sys.platform.startswith(("linux", "darwin")):
|
|
assert out.index("example.cpp") < out.index("-shared")
|
|
|
|
|
|
def test_cli_file_embed():
|
|
out = run_command_line("--file", "example.cpp", "--embed").rstrip()
|
|
assert out.endswith("-o example")
|