mirror of
https://github.com/pybind/pybind11.git
synced 2026-03-14 20:27:47 +00:00
* Improve performance of enum_ operators by going back to specific implementation test_enum needs a patch because ops are now overloaded and this affects their docstrings. * outline call_impl to save on code size This does cause more move constructions, as shown by the needed update to test_copy_move. Up to reviewers whether they want more code size or more moves. * add function_ref.h to PYBIND11_HEADERS. * Update test_copy_move tests with C++17 passing values just so we can see mostly-not-red tests * Remove stray TODO * fix clang-tidy * fix clang-tidy again. add function_ref.h to test_files.py * Add static assertion for function_ref lifetime safety in call_impl Add a static_assert to document and enforce that function_ref is trivially copyable, ensuring safe pass-by-value usage. This also documents the lifetime safety guarantees: function_ref is created from cap->f which lives in the capture object, and is only used synchronously within call_impl without being stored beyond its scope. * Add #undef cleanup for enum operator macros Undefine all enum operator macros after their last use to prevent macro pollution and follow the existing code pattern. This matches the cleanup pattern used for the previous enum operator macros. * Rename PYBIND11_THROW to PYBIND11_ENUM_OP_THROW_TYPE_ERROR Rename the macro to be more specific and avoid potential clashes with public macros. The new name clearly indicates it's scoped to enum operations and describes its purpose (throwing a type error). * Clarify comments in function_ref.h Replace vague comments about 'extensions to <functional>' and 'functions' with a clearer description that this is a header-only class template similar to std::function but with non-owning semantics. This makes it clear that it's template-only and requires no additional library linking. --------- Co-authored-by: Ralf W. Grosse-Kunstleve <rgrossekunst@nvidia.com>
387 lines
12 KiB
Python
387 lines
12 KiB
Python
from __future__ import annotations
|
|
|
|
import contextlib
|
|
import os
|
|
import re
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
import tarfile
|
|
import zipfile
|
|
from pathlib import Path
|
|
from typing import Generator
|
|
|
|
# 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
|